Files
oak-editor/docs/zh/c-abi-migration-handoff.md
T
Mike-Solar c486c853ff docs: C ABI migration campaign plans, handoffs and roadmap
Complete documentation set for the facade migration (B1-R6) and beyond:
facade-migration-roadmap, handoffs v3-v6, R5 guides, R6 cleanup plan,
R7 pure-ABI plan, long-term plans (riir/ai-agent/gtest/ui-redesign),
updated Chinese README draft, UI design mockups, and the Google Test +
struct-typedef rules in CONTRIBUTING.
2026-07-26 22:42:44 +08:00

335 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# liboakengine 纯 C ABI 迁移 — 交接执行计划(v3)
> 本文档是后续执行者(DeepSeek Flash 或任何接手代理)的**唯一权威执行依据**。
> 所有架构决策、边界契约、禁止事项已在本文钉死,执行时不得另行发明新方案;
> 遇到本文未覆盖的决策点,按"§8 决策兜底原则"处理,不得自由发挥。
>
> 相关文档:`docs/zh/facade-migration-roadmap.md`(各批次完成记录 + 附 D 事件机制 SOP)。
>
> v32026-07-23):K2.7 对 DeepSeek Flash 的产出做了验收,修复了 6 个真实缺陷
> (详见 §2.2,每条都附教训——**这些错误模式不得再犯**)。当前符号 39、
> 仅剩 1 个已知测试失败。剩余工作按符号逐项钉死在 §3/§5。
---
## 1. 目标与验收标准
**终态**
1. `liboakengine.so` 的动态符号表中**没有任何 `olive::` C++ 符号**(只有 `oakengine_*` C 符号 + Qt/系统符号)。
2. `oak-editor``oak-render-worker` 两个可执行文件**不 import 任何 `olive::` C++ 符号**(豁免清单见 §6.4)。
3. 全量测试通过;`engine/include/oakengine/*.h` 中**每个** `OAKENGINE_API` 声明的函数都有测试覆盖。
4. worker 端到端 harness 已完成,不要重做。
**统一度量命令**(禁止换口径):
```bash
# 总指标(当前 39,目标 = 豁免清单项数 = 6,见 §6.4)
nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive"
# 逐符号清单
nm -D cmake-build-debug/app/oak-editor | grep " U _ZN5olive" | c++filt | sed 's/.* U //' | sort
# liboakengine 侧(终态应为 0B11d 前不用管)
nm -D --defined-only cmake-build-debug/engine/liboakengine.so | grep -c " T _Z"
# facade 测试覆盖审计(终态应为空或仅 oakengine_worker_main
grep -ho "oakengine_[a-z_0-9]*" engine/include/oakengine/*.h | sort -u > /tmp/decl.txt
cat engine/tests/oakengine_*_test.cpp | grep -ho "oakengine_[a-z_0-9]*" | sort -u > /tmp/tested.txt
comm -23 /tmp/decl.txt /tmp/tested.txt
```
**构建与测试**(所有批次完成后必须全绿):
```bash
cmake --build cmake-build-debug -j$(nproc) # 构建目录已配置好,不要重新 cmake
cd cmake-build-debug && ctest --output-on-failure -j$(nproc) # 当前基线 44 个测试,约 90-140s
```
已知 flaky(历史偶发、重跑即过):`oak_cli_transcode``oakengine_export_test``olive-gtest` 各观察到过一次偶发 SEGFAULT。遇到先单独重跑;**连续两次失败才算真失败**。
---
## 2. 当前状态(v3 交接快照)
- 总符号数:**557 → 39**。
- **接手第一件事:全量构建 + 全量 ctest**,确认基线后再继续(§2.3 有当前已知的精确状态,但一切以你实测为准)。
- 未提交改动很多(所有批次都在工作区,未 commit)。**严禁任何 git 写操作**,严禁回滚任何现有未提交改动。
- app target 不直接编译任何 engine 源码;oak-editor 剩余的 `U _ZN5olive` 全部是 app 代码**调用** engine C++ 类产生的运行时导入符号。
### 2.1 当前符号清单(39,nm 实测,逐项归属在 §3)
```
4 AudioProcessor(豁免,§6.4
1 AudioWaveformCache::staticMetaObject
1 Block::staticMetaObject(豁免,§6.4
1 ColorManager::staticMetaObject
3 DynamicRendererctor / init_with_open_gl_context / load
1 Folder::staticMetaObject
5 Framector / dtor / create / allocate / set_video_params
1 NodeFactory::library
5 Nodelink / unlink / set_label / set_standard_value / staticMetaObject
2 OpenGLRendererctor / init
2 Projectname_changed / staticMetaObject
3 Renderercreate_texture / blit_color_managed / destroy
2 RenderManagerinstance_ / backend_to_string
1 SubtitleBlock::k_text_in
2 Textureupload / download
1 TrackListRippleToolCommand ctor
1 Track::staticMetaObject(豁免,§6.4
3 UndoCommandctor / redo_now / undo_now
```
### 2.2 v3 验收已修复的缺陷(DS 产出中的真实 bug,均已修复并验证)
> 这些是按"教训"写的:**每种错误模式都对应一条硬规则(§6.6),后续批次必须遵守。**
1. **B10 app 侧重复定义导致进程退出时堆损坏("corrupted double-linked list"**。DS 在 `app/common/{colorcodingapp,htmlapp,filefunctionsapp,hashstreamapp,xmlutilsapp}.cpp` 里用**与 engine 完全相同的限定名**定义了 `ColorCoding::colors``Html::k_block_tags` 等符号。可执行文件与 liboakengine.so 双定义 → ELF 符号介入合并存储 → 静态对象被**双重构造、双重析构** → double-free。这是 `timeline-tests``olive-gtest` 退出即崩的根因。**修复**`app/CMakeLists.txt` 对这 5 个文件加 `set_source_files_properties(... COMPILE_OPTIONS "-fvisibility=hidden")`(已做)。**教训见 §6.6-R1。**
2. **`app/common/nodeimpl.cpp` 是死代码**。DS 创建了它(重定义 `Node::link/unlink/copy_inputs`)但**从未注册进 `app/CMakeLists.txt`**,三个符号仍从 .so 导入。若注册而不加 hidden visibility,会造成 `oakengine_node_link → Node::link(被介入到 app 版) → oakengine_node_link` 无限递归。**教训见 §6.6-R2;处理方案钉死在 §3.4。**
3. **`SpeedDurationDialog::accept()` 的 undo 命令从未压栈**。DS 删除了 `Core::undo_stack()->push(command, name)` 但没有替代——时长修剪(BlockTrimCommand)永不执行(2 个 gtest 失败)。**修复**:末尾补 `oakengine_undo_push(command, name)`(注意:**2 个参数,全局栈,不传栈句柄**)。
4. **`ProjectViewModel::dropMimeData` 把一次移动拆成了 3 条 undo 记录**disconnect 一条、add_child 一条、空命令一条),测试 `undo_jump(-1)` 只撤销了空命令。**修复**:新增 facade 函数 `oakengine_folder_move_child(node, new_folder)``oakengine/project.h` + `src/capi/project.cpp`,单条 MultiUndoCommand 完成 detach+attach),app 改调它;测试已补进 `oakengine_footage_test.cpp::test_folder`。**教训见 §6.6-R3。**
5. **预览请求帧路径两个 linesize 错误**`engine/src/capi/preview.cpp``frame->linesize_pixels()` 填进 POD(契约是**字节**);`viewer.cpp::display_frame_from_preview``linesize_pixels()` 当字节偏移做 memcpy。**修复**POD 填 `linesize_bytes()`memcpy 用 `linesize_bytes()`。**教训见 §6.6-R4。**
6. **重建 display Frame 时 VideoParams 字段缺失**。(a)默认构造 channel_count=0 → `Frame::set_video_params` 除零崩溃;(b)默认构造 **depth=0** → Vulkan 上传 `image_size = w*h*depth*bpp = 0` → 一个字节都没上传 → 4 个 viewer NotBlack 测试黑屏。**修复**:改用四参构造 `VideoParams(width, height, format, VideoParams::k_internal_channel_count)`(该构造器 depth=1)。**教训见 §6.6-R5。**
另:`manageddisplay.cpp` 的渲染器创建曾被 DS 改成无意义的 `oakengine_renderer_init_gl(nullptr)` + 永远 OpenGLRenderer**已恢复为原来的 DynamicRenderer 创建逻辑**DynamicRenderer 的 3 个符号因此在清单里,属 §3.6 待办)。
### 2.3 当前测试状态(K2.7 实测)
- `timeline-tests`:全过(修复 #1 后)。
- `olive-gtest`:除 `Backends/ViewerRuntimeRewireTest.RewireToIndirectConnectionNotBlack/1`(Vulkan)外全过。这是**当前唯一已知失败**,线索与疑似根因见 §3.1。
- 其余 42 个 ctest 在上一次全量运行中通过,但**经过本批修复后尚未做最终全量复跑——接手第一步就是全量 ctest**。
---
## 3. 剩余工作(按符号逐项,顺序即执行顺序)
### 3.1 第零优先:修 `RewireToIndirectConnectionNotBlack/1`
**现象**viewer 正在显示 direct 链(footage→sequence),运行时插入 OpacityEffectfootage→opacity→sequence)后,画面变黑且 30s 超时内不再更新。同文件的 `RewireToDirectConnectionNotBlack`(拆节点)是过的。
**排查线索(已排除项,不要重复查)**:帧数据、纹理上传、色彩变换、VideoParams 全部已验证正常(§2.2-5/6 修复后)。问题只剩"**图变更后 viewer 的失效/重渲染触发**":插入节点后 `update_texture_from_node` 是否被触发、预览请求是否命中了旧缓存。
- 入手点:`app/widget/viewer/viewer.cpp``ConnectNodeEvent` 订阅清单 vs HEAD 原版的 connect 清单——插入节点后 texture input 变化应触发 `viewer_texture_input_changed`(事件 108)→ `update_waveform_view_from_mode`/`update_texture_from_node`。对比 HEAD 原版在该场景触发链路上是否少了什么(重点:`renderer_generated_frame`/`request_invalidate`/缓存失效事件)。
- `OpacityEffect` 插入后第一帧渲染是否失败(可在 `oakengine_preview_request_get_frame` 返回处看 has_result)。
- 修复后:该用例 + 全量 ctest 全绿才准进入 §3.2。
### 3.2 静态/杂项小点(预计 6 个符号,先做这些快的)
1. **`Project::name_changed`**grep 定位最后一个直连 connect,改事件 2 `PROJECT_NAME_CHANGED`(已存在,SOP 见 roadmap 附 D)。
2. **`SubtitleBlock::k_text_in`**:照 B4c 模式补静态字符串 getter `const char *oakengine_subtitle_text_input_id(void);`(挂 timeline.h),app 换调用。
3. **`RenderManager::backend_to_string` + `RenderManager::instance_`**:补 v2 已钉死的契约——
```c
OAKENGINE_API int oakengine_render_manager_set_aggressive_garbage_collection(int enabled);
OAKENGINE_API int oakengine_render_manager_requested_backend(void);
OAKENGINE_API int oakengine_render_manager_backend_to_string(int backend, char *buf, int buf_size);
```
`instance_` 符号随最后一个 `RenderManager::instance()` 直连点消失(manageddisplay.cpp 的 `requested_backend()` 调用点)。
4. **`AudioWaveformCache::staticMetaObject`**:grep 定位残余 moc 引用(多半是某个 connect),按事件 SOP 补事件或消除。
### 3.3 UndoCommand 3ctor / redo_now / undo_now
来源:app 直接 `new` engine 命令类并进栈(grep `new .*Command` 于 app/widget/timelinewidget、app/widget/nodeview 等)。按 v2 §5.6.5 的既定方针:逐个换 facade undoable 原语,缺的按同族模式补。**不得**为这些发明新机制。
### 3.4 Node 5 + NodeFactory 1
- **`Node::link / Node::unlink / Node::copy_inputs`**3):`app/common/nodeimpl.cpp` 死代码的两个处理方案,**钉死选方案 A**:
- **方案 A(选这个)**:删除 `app/common/nodeimpl.cpp`,把 app 侧所有 `Node::link(`、`Node::unlink(`、`Node::copy_inputs(` 调用点改为 facade 调用(`oakengine_node_link`/`oakengine_node_copy_inputs`,均已在 node.h 存在)。grep 定位调用点(预计 <10 处)。
- 方案 B(不推荐):注册 nodeimpl.cpp 且对该文件加 `-fvisibility=hidden`。只有方案 A 遇到无法改写的调用点时才用,且必须写进 roadmap 说明。
- **`Node::set_label`**1):补 `int oakengine_node_set_label(OakEngineNode *, const char *);`undoablev2 已钉死),换 app 调用点。
- **`Node::set_standard_value`**(1):grep 定位;大概率已被 `oakengine_node_set_input` 覆盖,换调用;未覆盖则补 `oakengine_node_set_standard_value`undoable,语义 = `NodeParamSetSplitStandardValueCommand`,照 `oakengine_node_set_input` 实现)。
- **`Node::staticMetaObject` + `NodeFactory::library`**2):grep 定位残余 moc/模板引用源(多为模板 connect 或 `Q_DECLARE_METATYPE`),改字符串式 connect 或 void* 透传(B8a 先例)。`NodeFactory::library` 是静态注册表,若 app 侧只剩只读枚举需求,补 `oakengine_node_factory_id_count/at`(v2 已钉死);消不掉按 §6.4 格式进豁免清单并写理由。
### 3.5 staticMetaObject 残留(ColorManager / Folder / Project / Node
逐个 grep 定位 moc 引用源(`qobject_cast`、模板 connect、`Q_DECLARE_METATYPE`、moc 生成的 metacall)。改事件机制或字符串式 connect。消不掉的按 §6.4 格式进豁免清单(必须写理由)。
### 3.6 B11b GPU/帧路径(17 个符号:Renderer 3 + OpenGLRenderer 2 + DynamicRenderer 3 + Texture 2 + Frame 5 + RenderManager 中属显示路径的部分)
**这是 DS 上次说"需要复杂 GPU 管线重构"而放弃的部分。决策已钉死,不需要重构,按薄封装做:**
现状事实(已验证):
- 显示路径(`ManagedDisplayWidget`/`ViewerDisplayWidget`)持有 C++ `Renderer* attached_renderer_`DynamicRenderer 或 OpenGLRenderer),用于 `create_texture/upload/download/blit_color_managed/destroy``Frame` 用于 CPU 帧搬运(`Frame::create/allocate/set_video_params/dtor/ctor`)。
- `manageddisplay.cpp` 的 DynamicRenderer 创建块**已恢复为 C++ 原版**(不要再动它,直到整个显示路径换完)。
**执行方案(钉死,分两步)**
1. **先 Frame5**`viewer.cpp::display_frame_from_preview` 和 viewerdisplay 的帧搬运改用——
```c
typedef struct OakEngineFrame OakEngineFrame; /* owned */
OAKENGINE_API OakEngineFrame *oakengine_frame_create(void);
OAKENGINE_API int oakengine_frame_set_video_params(OakEngineFrame *, const oak_video_params *);
OAKENGINE_API int oakengine_frame_allocate(OakEngineFrame *);
OAKENGINE_API void oakengine_frame_free(OakEngineFrame *);
```
app 侧不再直接 `new olive::Frame`。**注意**`display_frame_from_preview` 用四参构造 `VideoParams(width,height,format,k_internal_channel_count)`(§2.2-6 的修复,别回退)。
2. **再 Renderer/Texture7+3**:显示 widget 的 `attached_renderer_` 改为 facade 句柄。契约(v2 已钉死):
```c
typedef struct OakEngineTexture OakEngineTexture; /* owned */
OAKENGINE_API int oakengine_renderer_init_gl(void *qopengl_context); /* QOpenGLContext 以 void* 透传,文档注明 Qt 运行时共享例外;后端选择走 RenderManager::requested_backend 语义 */
OAKENGINE_API int oakengine_renderer_destroy(void);
OAKENGINE_API OakEngineTexture *oakengine_renderer_create_texture(const oak_video_params *, const void *data, int linesize);
OAKENGINE_API int oakengine_texture_upload(OakEngineTexture *, const void *data, int linesize);
OAKENGINE_API int oakengine_texture_download(OakEngineTexture *, void *data, int linesize);
OAKENGINE_API void oakengine_texture_free(OakEngineTexture *);
OAKENGINE_API int oakengine_renderer_blit_color_managed(const oak_color_transform *, OakEngineTexture *, const oak_video_params *);
```
facade 内部持有 DynamicRenderer/OpenGLRenderer 实例(与现在 manageddisplay 的选择逻辑相同);backend-neutral 的离屏纹理 + 下载回读路径同样走 texture 句柄。落地后**移除 B7 两个过渡桥** `oakengine_color_transform_job_set_processor`/`oakengine_color_set_display_color_processor`roadmap 补记。
**验收**5 个 Backends viewer 用例继续全过(这是该路径的现成回归测试)。
### 3.7 TrackListRippleToolCommand ctor1
v2 已钉死为遗留评估点。方案:grep 定位(timelinewidget ripple 工具),先尝试用现有 timeline 编辑原语组合替代;无法替代则设计 `oakengine_tracklist_ripple_*` 小族(参数拍平:track 列表 + per-track RippleInfo POD 数组 + 时间 + movement mode)。**这是最后一个符号,允许单独花时间;消不掉按 §6.4 进豁免清单(写理由)。**
### 3.8 B11d 收口(最后做)
1. 确认 oak-editor `U _ZN5olive` 只剩豁免清单 6 项;oak-render-worker 为 0。
2. liboakengine 符号可见性收口:`set_target_properties(oakengine PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON)` 或 version script 白名单 `oakengine_*`。验证 `nm -D --defined-only liboakengine.so | grep -c " T _Z"` → 0。liboakcore 复查不回归。
3. facade 覆盖审计为空(`oakengine_worker_main` 豁免)。
4. 终验:全量构建 + ctest 全绿;§1 四条验收逐条核对;roadmap 附 C 标记战役完成。
---
## 4. 已完成批次(不要重做)
详见 roadmap 附 C。要点:B1B8c 全部、B9aTask/Undo)、B9bConfig/AudioManager/DiskManager/ProxyManager/LUTLibrary/ProjectSerializer)、B9c(预览/渲染服务 PreviewAutoCacher/RenderTicket/RenderTicketWatcher → `oakengine_preview_cacher_*`/`oakengine_preview_request_*`)、B9dplugin)、B9egizmo POD 化 + DraggableGizmo 搬 app)、B10(工具类搬 app)、B11a 大部(Node 族、命令类、input id getter)、事件机制(ID 已分配到 143)。
**事件 ID 分配**:已用到 143141/142 playback cache、143 frame cache)。**新事件从 144 起**。
---
## 5. 每批的标准产出(SOP
1. `nm` 度量基线 → 2. grep 确认实际使用点(§3 清单仅供参考,以 grep 为准)→ 3. facade 补 C 函数(§6 契约)→ 4. app 逐文件换调用 → 5. 每个新 C 函数补单元测试(注册进 `engine/CMakeLists.txt` 的 `make_oakengine_test`)→ 6. 全量构建 + ctest 全绿 → 7. 族符号和总数双度量对比 → 8. roadmap 附 C 补记。
---
## 6. 边界契约与硬规则(钉死,不得违反)
### 6.1 C ABI 头文件规则
- 位置 `engine/include/oakengine/*.h`,实现在 `engine/src/capi/*.cpp`(注册进 `engine/src/capi/CMakeLists.txt`)。
- 每个头:GPL 版权头、`#ifdef __cplusplus extern "C"`、`OAKENGINE_API` 导出宏。
- **头文件里只允许 C 类型**:`int/int64_t/double/char*/void*`、POD struct、不透明句柄 typedef。禁止 C++ 类、模板、Qt 类型、std:: 类型、引用、默认参数、重载。
- 命名:`oakengine_<族>_<动作>`;错误码 `OAKENGINE_OK`0/ 负数 `OAKENGINE_E_*`。
- 字符串输出 buf/size 约定(返回所需长度不含 \0,`buf=NULL,buf_size=0` 查长度)。
- 线程语义:回调/事件 = Qt::DirectConnection 等价同步调用;回调内不得反调改同一对象的编辑原语。
- 所有权:create 返回 owned 句柄必须配套 free;borrowed 句柄在注释里写明。
### 6.2 undoable 编辑原语
- 所有改图操作必须 undoable,实现照 `engine/src/capi/node.cpp` 的 `push_or_run` 模式。
- **undo 粒度妥协是允许的**(facade 单命令边界导致一次用户操作产生多条 undo 记录),代码加注释说明即可。**但:用户语义上的一次操作若在 UI/测试层被当作一条 undo(如 drag&drop 移动、对话框 accept),必须用单条命令的 facade 函数**(`oakengine_folder_move_child` 是样板)。
### 6.3 事件机制(信号迁移唯一通道)
- 禁止 app 直接 `QObject::connect` engine 对象的信号。一律 `oakengine_event_subscribe` → `app/engineeventbridge` → app 连 bridge。SOP 见 roadmap 附 D。
- 新事件:events.h 加宏(**从 144 起**)、events.cpp `connect_event` 加 case、bridge 加信号 + dispatch、`oakengine_events_test.cpp` 补实测。
- **例外(钉死)**facade 自有 owned 对象(OakEnginePlayback、OakEnginePreviewRequest)的完成/数据回调用各自的 `set_*_callback`,不走事件机制。
### 6.4 豁免清单(R6 后已清空:无豁免,nm=0)
> **状态(R6 收尾)**:原"终态保留"裁决已被 R6 计划推翻并全部消除——
> `AudioProcessor`5 符号:ctor/dtor/open/close/convert)经 P5 改为 C vtable
> 接口(`oakengine/audio.h` `oakengine_audio_processor_*`app 持
> `OakEngineAudioProcessor*` 句柄);`plugin::PluginProgressReporter`4 符号:
> staticMetaObject/qt_metacast/qt_metacall/cancelled)经 P3.2 去 Q_OBJECT、
> cancelled 信号改 C 回调。`oakengine_worker_main` 为 worker 进程入口
> (非 `olive::` 符号,不计入 nm 指标)。
>
> 实测:`nm -D` 于 oak-editor 与 oak-render-worker 的 ` U _ZN5olive` 均为 **0**。
> **当前无任何豁免。**
以下为 R5 冲刺时记录的 58 符号历史分类(**R6 已全部清零,仅作存档**):
**GLM-5.2 R5 冲刺豁免清单(58 符号,分类理由)——✅ R6 已全部清零(nm 58→0)**
**A. MOC 生成 staticMetaObject9 符号)**——app 类的信号/槽参数类型
含 Node*/Project*/Sequence*/ViewerOutput*/UndoStack* 时,MOC 生成的
meta-object 代码引用 engine 类的 staticMetaObject。消除需更改所有
此类信号/槽签名为 C ABI 句柄类型(OakEngineNode* 等),工程量大。
- `Node::staticMetaObject`、`Project::staticMetaObject`、
`Sequence::staticMetaObject`、`ViewerOutput::staticMetaObject`、
`UndoStack::staticMetaObject`、`AudioWaveformCache::staticMetaObject`
- `plugin::PluginProgressReporter::staticMetaObject/qt_metacast/qt_metacall`
**B. Inline 函数拉入(8 符号)**——engine 头文件的 inline 方法引用
这些符号,app 包含头文件即产生 undefined reference。消除需创建
app 侧 handle 头(不包含 engine C++ 头)或扩 facade 覆盖所有 inline 路径。
- `Node::link`NodeLinkCommand 析构 inline 调用)
- `Node::set_standard_value`inline getter 引用)
- `Node::set_value_at_time`1 处直接调用,QVariant→POD 转换复杂)
- `UndoCommand::redo_now/undo_now/UndoCommand()`MultiUndoCommand
inline add_child/析构调用)
- `MultiCamNode::k_current_input`、`SubtitleBlock::k_text_in`
(inline 方法引用静态字符串)
**C. 实时回调边界(5 符号)**——v3 已预批。
- `AudioProcessor`ctor/dtor/open/close/convert
**D. 渲染/GPU 边界(13 符号)**——manageddisplay/viewer 的 OpenGL 路径
直接创建 OpenGLRenderer/DynamicRenderer 对象并调用虚函数。
facade 有 oakengine_renderer_* 但 app 仍用 C++ 对象。
消除需将整个渲染对象管理移入 engine。
- `Renderer`destroy/create_texture/blit_color_managed
- `DynamicRenderer`ctor/init_with_open_gl_context/load
- `OpenGLRenderer`ctor/init
- `Texture`upload/download
- `Frame`allocate/create/set_video_params
**E. 色彩管理(6 符号)**——ManagedColor/ColorProcessor 的 C++ 对象
在多个 UI 组件中使用。无 C ABI 等价物。
- `ManagedColor`ctor×2/set_color_input/set_color_output
- `ColorProcessor`create/convert_color
**F. 无 C ABI 等价物(17 符号)**——需新增 facade 函数。
- `NodeValue`4 个静态工具方法
- `VideoParams`3 个构造器重载
- `AudioWaveformSync`2 个估计算法
- `AudioSynchronizer`2 个对齐方法
- `TimelineMarker`set_time/ctor
- `ShapeNodeBase::set_rect`、`FrameHashCache::load_cache_frame`
- `RenderManager::instance_`viewer.cpp inline instance() 引用)
- `plugin::PluginProgressReporter::cancelled`(信号,需事件迁移)
### 6.5 测试规则
- facade 每个新 C 函数必须有单元测试(纯 C `assert` 风格,不依赖 GPU/QApplication;需要时 `oakengine_init(OAKENGINE_INIT_HEADLESS)`)。
- GL/Vulkan 测试必须可无 GPU 跳过(GTEST_SKIP 模式)。
- 新测试注册进 `engine/CMakeLists.txt` 的 `make_oakengine_test(...)`。
### 6.6 v3 新增硬规则(对应 §2.2 的六条教训)
- **R1ODR/符号介入)**app 侧**严禁**用与 engine 相同的限定名定义任何非 inline 符号(函数或静态数据)。确需同名本地副本(B10 模式),必须对该源文件加 `-fvisibility=hidden``app/CMakeLists.txt` 的 `set_source_files_properties` 是现成样板)。`#pragma GCC visibility` 对已被 engine 头以 default 可见性声明过的符号**无效**(GCC 取首次声明的可见性);要么用编译 flag,要么在定义处打 `__attribute__((visibility("hidden")))`。验证方法:`readelf -sW <binary> | c++filt | grep <符号>` 必须是 `HIDDEN`。
- **R2(注册检查)**:新建任何 .cpp 必须同步注册进对应 CMakeLists,并在当批验证其符号确实从 `U` 清单消失。app 侧定义的 engine 同名函数若不加 hidden,会通过 ELF 介入把 engine .so 内部调用劫持到 app 版,可能形成跨模块无限递归。
- **R3undo 语义)**`oakengine_undo_push(command, name)` **只有两个参数**(全局栈,不传栈句柄)。`Core::instance()->undo_stack()` 返回 `void*`,仅作事件订阅 handle 用。删除任何 `push` 调用时必须同步删除/替换其命令的执行路径——**命令不压栈 = 静默不执行 + 内存泄漏**。
- **R4linesize 约定)**facade POD 中的 `linesize` 一律是**字节**。`olive::Frame` 有两个值:`linesize_bytes()`(字节)和 `linesize_pixels()`(像素 = 字节/bpp)。跨边界只传字节;engine 内部(纹理上传等)按各 API 既有约定(Vulkan/OpenGL 纹理上传收**像素**)。
- **R5VideoParams 构造)**:默认构造的 `VideoParams` 是 width=0/height=0/**depth=0**/channels=0/format=invalid。凡要喂给帧分配/纹理上传的,**必须用带参构造器**(显示 RGBA 帧用四参 `VideoParams(w, h, format, VideoParams::k_internal_channel_count)`depth=1)。depth=0 不会报错,只会让上传字节数为 0(纯黑)。
- **R6(接手验证)**:任何中断/交接后,第一件事是全量构建 + 全量 ctest + 对照 §3 清单 grep 复核,不要采信上一手的完成声明(包括本文 §2.3——以你实测为准)。
---
## 7. 禁止事项(硬约束)
1. **禁止任何 git 写操作**commit / add / push / restore / checkout / stash / clean / rebase。
2. **禁止暴露 C++ ABI**:不得新建导出 C++ 类/模板/Qt 类型的头;不得往 liboakengine 导出表加 `_Z` 符号(新代码产生 C++ 弱符号时给实现类加 `visibility("hidden")`)。
3. **禁止把 Qt 类型放进 core/**liboakcore 是 Qt-free)。
4. **禁止改 worker 的 NDJSON IPC 协议**;禁止重做 §4 已完成的任何批次。
5. **禁止修改本文已钉死的签名**:各 facade 头现有函数、events.h 已分配的事件 ID(1–143)、§3 各批钉死的契约签名。
6. **禁止为追求 undo 记录合并、staticMetaObject 消除等发明新机制**——按 §6.2/§6.4 妥协条款执行。
7. **禁止降低测试标准**:新 C 函数无测试不得算完成;全量 ctest 不绿不得进入下一批。
8. **禁止重新 cmake 配置构建目录**;禁止安装/卸载系统依赖;禁止改 CI/打包文件。
9. **禁止在未验证构建状态前继续批次**(§6.6-R6)。
---
## 8. 决策兜底原则
1. roadmap 已有裁决的,从 roadmap。
2. 能搬进 app 的纯 UI/工具代码 → 搬 app(优于 facade 化)。
3. 纯数据类 → POD 化或头内联,优先于新增 facade 族。
4. 必须跨边界的 → 最小 facade 族(只包 app 实际用到的成员)。
5. GPU/帧路径、Qt 运行时耦合 → 薄封装 + `void*` 透传 + 文档注明例外。
6. 以上都拿不准的:留下不动,写进 roadmap 遗留清单,继续下一点。**不允许为单点发明新架构。**
---
## 9. 环境备忘
- 构建目录 `cmake-build-debug`Ninja + Qt6Debug);另有 cmake-build-asan / cmake-build-coverage**不要用**。
- 单文件增量验证:`rm -f cmake-build-debug/app/CMakeFiles/libolive-editor.dir/<相对路径>.o && cmake --build cmake-build-debug --target olive-editor -j$(nproc)`。
- 测试素材:`tests/demo.mp4`、`tests/img.png`、`tests/project_with_footage.ove`。
- 本机有 GPUworker/viewer 的 Vulkan 用例真实执行(OpenGL 用例 offscreen 不可绘,会 SKIP,属正常);CI 无 GPU 会 GTEST_SKIP,两者都算通过。
- 全量 ctest 44 个约 90-140solive-gtest 占 ~85s),每批必须跑完不能裁剪。
- 调试技巧(本批实测有效):teardown 堆崩溃用 `GLIBC_TUNABLES=glibc.malloc.tcache_count=0 gdb -batch -ex run -ex bt` 可拿到真实崩溃栈;帧/纹理内容验证用 `Texture::download` 回读后求和。