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.
This commit is contained in:
2026-07-26 22:42:44 +08:00
parent fcf717f6a7
commit c486c853ff
22 changed files with 3482 additions and 4 deletions
+2
View File
@@ -22,6 +22,7 @@ submitted should abide by the following standards:
* Documentation comments should use **Javadoc-style** (`/** ... */`) where appropriate. * Documentation comments should use **Javadoc-style** (`/** ... */`) where appropriate.
* Naming rules (enforced by `readability-identifier-naming` in `.clang-tidy`): * Naming rules (enforced by `readability-identifier-naming` in `.clang-tidy`):
* Types (`class`, `struct`, `enum`, type aliases, template parameters): `PascalCase` * Types (`class`, `struct`, `enum`, type aliases, template parameters): `PascalCase`
* `typedef` of structs is permitted (e.g. the opaque-handle pattern `typedef struct OakEngineNode OakEngineNode;`); struct typedefs follow `PascalCase`
* Functions, variables, member variables: `snake_case` * Functions, variables, member variables: `snake_case`
* Private/protected members: trailing underscore, `class_member_variables_` * Private/protected members: trailing underscore, `class_member_variables_`
* Constants and enum values: `snake_case` (e.g. `k_dry_run_interval`, `k_linear`); `ALL_CAPS` is reserved for macros — save the fear for things that are actually dangerous * Constants and enum values: `snake_case` (e.g. `k_dry_run_interval`, `k_linear`); `ALL_CAPS` is reserved for macros — save the fear for things that are actually dangerous
@@ -30,5 +31,6 @@ submitted should abide by the following standards:
* Namespaces: short `snake_case` * Namespaces: short `snake_case`
* Getters: same name as the private member without the trailing underscore (`foo_``foo()`); setters: `set_foo()` * Getters: same name as the private member without the trailing underscore (`foo_``foo()`); setters: `set_foo()`
* Exception: Qt and third-party (e.g. OpenFX) virtual overrides and framework callbacks keep their original names (`paintEvent`, `getParams`, ...) — renaming them would break the override * Exception: Qt and third-party (e.g. OpenFX) virtual overrides and framework callbacks keep their original names (`paintEvent`, `getParams`, ...) — renaming them would break the override
* Tests are written with **Google Test** (`TEST`/`TEST_F`/`TEST_P` + `EXPECT_*`/`ASSERT_*`). Do not add hand-written test `main()`s, raw `assert()`-based test files, or custom test macros/frameworks. CTest stays the runner only — register cases through `gtest_discover_tests()`; use `GTEST_SKIP()` for environment-dependent cases (GPU, missing codecs) instead of relying on crashes or timeouts..
* 100 column limit (where it doesn't impair readability) * 100 column limit (where it doesn't impair readability)
* Unix line endings (only LF no CRLF) * Unix line endings (only LF no CRLF)
+96
View File
@@ -0,0 +1,96 @@
# Oak Video Editor
[![CI](https://github.com/OakVideoEditorCommunity/oak/actions/workflows/ci.yml/badge.svg)](https://github.com/OakVideoEditorCommunity/oak/actions/workflows/ci.yml)
[中文](docs/zh/README.new.md)
Oak Video Editor is a free, open-source **non-linear video editor** for Windows, macOS, and Linux.
This project is a community-maintained fork of Olive Video Editor.
> **NOTE: Oak Video Editor is alpha software and is considered highly unstable. We appreciate users testing it and sharing feedback, but please use it at your own risk.**
<!-- SCREENSHOT: main editing window (timeline + viewer) -->
![Screenshot main window](docs/images/screenshot-main.png)
## Features
- Responsive timeline editing with smart disk/playback caching
- Node-based compositing and effects, including an OpenFX (OFX) plugin host
- Full color management (OpenColorIO): `.cube`/`.3dl` LUTs, configurable display/view/look transforms
- Scopes: waveform, vectorscope, histogram, and audio meters (LUFS/VU)
- Bézier keyframe animation with a curve editor
- Multicam editing and waveform-based audio sync
- Proxy media workflow for smooth 4K/8K editing
- Hardware-accelerated and batch export (H.264/H.265, image sequences, audio)
- Project crash recovery and autosave
<!-- SCREENSHOT: node editor -->
![Screenshot node editor](docs/images/screenshot-node.png)
## Download
Pre-built binaries for Windows, macOS, and Linux are on the [Releases](https://github.com/OakVideoEditorCommunity/oak/releases) page.
Latest: [v0.4.2-alpha](https://github.com/OakVideoEditorCommunity/oak/releases/tag/v0.4.2-alpha)
## Architecture
Oak is split into small, independently testable components with a pure C ABI at the boundary:
| Component | Kind | Purpose |
|---|---|---|
| `liboakcore` | shared library | Qt-free core types (rational, timecode, bezier, sample buffer, audio/video params) with a pure C ABI |
| `liboakengine` | shared library | the editing engine (node graph, timeline, render, codec, tasks), exposed only through the `oakengine_*` C ABI facade |
| `oak-editor` | application | the Qt GUI; talks to the engine **only** through the C ABI |
| `oak-render-worker` | process | headless render process that executes frames off the GUI thread (NDJSON IPC) |
| `oak-cli` | tool | command-line frontend for the engine: media info, probing, rendering, and transcoding without the GUI |
The C ABI boundary is what makes the engine embeddable and is the foundation for a planned module-by-module rewrite of the engine in Rust (see [`docs/zh/plans/riir.md`](docs/zh/plans/riir.md)).
<!-- DIAGRAM: component / ABI layout -->
![Architecture diagram](docs/images/architecture.png)
## Command-Line Tools
`oak-cli` is a standalone, pure-C-ABI consumer of the engine:
```bash
oak-cli info <file> # media information
oak-cli probe <file> # stream/decoder probe
oak-cli render <project.ove> <out> # render a project range
oak-cli transcode <in> <out> # transcode media
```
## Building from Source
See [`docs/build.md`](docs/build.md) for full instructions (Windows/MSYS2, Linux Debian/Ubuntu/Fedora/Arch, and [`docs/build_macos.md`](docs/build_macos.md) for macOS). In short:
```bash
cmake -B build -G Ninja
cmake --build build
ctest --test-dir build --output-on-failure
```
## Roadmap
| Version | Theme | Core Deliverables |
|:--|:--|:--|
| **0.3** | **Plugin Architecture** | Production-ready OpenFX host support — "any OFX plugin loads without crashing" |
| **0.4** | **Color, Audio & Performance** | `.cube`/`.3dl`, scopes, three-way color wheels, waveform auto-sync, BWF timecode sync, audio meters, proxy media, hardware-accelerated export, batch render queue |
| **0.5** | **Animation, Tracking & Collaboration** | Bézier keyframe curve editor, point tracking, image stabilizer, full multicam, OpenTimelineIO, EDL/XML interchange |
| **0.6** | **Stability** | Project file format freeze (backward compatibility), crash recovery, autosave, memory optimization |
| **1.0** | **Production Ready** | Complete documentation, installers, known-issues list, community support |
## Contributing
Contributions are welcome. Please read [`CONTRIBUTING.md`](CONTRIBUTING.md) first — it covers:
- the code style (naming rules, including `PascalCase` struct typedefs),
- the **Google Test** requirement for all tests,
- the C ABI boundary contract for engine-facing code.
Useful project docs: [`docs/zh/`](docs/zh/) (中文文档), [`docs/zh/facade-migration-roadmap.md`](docs/zh/facade-migration-roadmap.md), [`docs/zh/plans/riir.md`](docs/zh/plans/riir.md).
## License
Oak Video Editor is free software licensed under the [GNU General Public License v3](LICENSE).
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 KiB

+96
View File
@@ -0,0 +1,96 @@
# Oak 视频编辑器
[![CI](https://github.com/OakVideoEditorCommunity/oak/actions/workflows/ci.yml/badge.svg)](https://github.com/OakVideoEditorCommunity/oak/actions/workflows/ci.yml)
[English](../README.new.md)
Oak 视频编辑器是面向 Windows、macOS 和 Linux 的**自由开源非线性视频编辑器**。
本项目是 Olive 视频编辑器的社区维护分支。
> **注意:Oak 目前处于 alpha 阶段,稳定性有限。欢迎试用并反馈,但请自行承担使用风险。**
<!-- 截图:主编辑界面(时间线 + 监视器) -->
![主界面截图](../images/screenshot-main.png)
## 功能特性
- 响应式时间线剪辑,配合智能磁盘/回放缓存
- 节点式合成与特效,内置 OpenFX(OFX)插件宿主
- 完整的色彩管理(OpenColorIO):支持 `.cube`/`.3dl` LUT,可配置 display/view/look 变换
- 示波器:波形图、矢量图、直方图,以及音频表(LUFS/VU)
- 贝塞尔关键帧动画与曲线编辑器
- Multicam 多机位剪辑与基于波形的音频自动对齐
- 代理媒体工作流,流畅剪辑 4K/8K 素材
- 硬件加速与批量导出(H.264/H.265、图像序列、音频)
- 工程崩溃恢复与自动保存
<!-- 截图:节点编辑器 -->
![节点编辑器截图](../images/screenshot-node.png)
## 下载
Windows、macOS、Linux 预编译包见 [Releases](https://github.com/OakVideoEditorCommunity/oak/releases) 页面。
最新版本:[v0.4.2-alpha](https://github.com/OakVideoEditorCommunity/oak/releases/tag/v0.4.2-alpha)
## 架构
Oak 拆分为若干可独立测试的组件,组件之间以**纯 C ABI** 为边界:
| 组件 | 形态 | 作用 |
|---|---|---|
| `liboakcore` | 动态库 | 无 Qt 依赖的核心类型(有理数、时间码、贝塞尔、采样缓冲、音视频参数),纯 C ABI |
| `liboakengine` | 动态库 | 剪辑引擎(节点图、时间线、渲染、编解码、任务系统),仅通过 `oakengine_*` C ABI facade 暴露 |
| `oak-editor` | 应用程序 | Qt 图形界面,**只**经 C ABI 访问引擎 |
| `oak-render-worker` | 进程 | 无头渲染进程,在 GUI 线程之外渲染帧(NDJSON IPC |
| `oak-cli` | 工具 | 引擎的命令行前端:媒体信息、探测、渲染、转码,无需 GUI |
这条 C ABI 边界让引擎可以被嵌入,也是后续将引擎按模块逐步用 Rust 重写的基础(见 [`riir.md`](plans/riir.md))。
<!-- 架构图:组件与 ABI 布局 -->
![架构图](../images/architecture.png)
## 命令行工具
`oak-cli` 是一个独立的、纯 C ABI 的引擎消费者:
```bash
oak-cli info <文件> # 媒体信息
oak-cli probe <文件> # 流/解码器探测
oak-cli render <project.ove> <输出> # 渲染工程指定范围
oak-cli transcode <输入> <输出> # 媒体转码
```
## 从源码构建
完整说明见 [`build.md`](build.md)Windows/MSYS2、Linux Debian/Ubuntu/Fedora/Arch)和 [`build_macos.md`](build_macos.md)macOS)。简要步骤:
```bash
cmake -B build -G Ninja
cmake --build build
ctest --test-dir build --output-on-failure
```
## 路线图
| 版本 | 主题 | 核心交付物 |
|:--|:--|:--|
| **0.3** | **插件架构** | OpenFX 宿主支持完整可用——"任意 OFX 插件加载不崩溃" |
| **0.4** | **调色、音频与性能** | `.cube`/`.3dl`、示波器、三向色轮、波形自动同步、BWF 时间码同步、音频表、代理媒体、硬件加速导出、批量渲染队列 |
| **0.5** | **动画、跟踪与协作** | 贝塞尔关键帧曲线编辑器、点跟踪、画面稳定器、完整 Multicam、OpenTimelineIO、EDL/XML 导入导出 |
| **0.6** | **稳定性** | 工程文件格式冻结(向后兼容)、崩溃恢复、自动保存、内存优化 |
| **1.0** | **生产就绪** | 文档完整、安装包、已知问题清单、社区支持渠道 |
## 参与贡献
欢迎贡献。请先阅读 [`../CONTRIBUTING.md`](../CONTRIBUTING.md),其中约定:
- 代码风格(命名规则,含**结构体 typedef 使用帕斯卡命名法**),
- 所有测试必须使用 **Google Test** 编写,
- 面向引擎代码的 C ABI 边界契约。
更多项目文档:[中文文档目录](./)、[`facade-migration-roadmap.md`](facade-migration-roadmap.md)、[`riir.md`](plans/riir.md)、[`gtest-migration-guide.md`](plans/gtest-migration-guide.md)。
## 许可证
Oak 视频编辑器是采用 [GNU 通用公共许可证第 3 版](../LICENSE) 授权的自由软件。
+216
View File
@@ -0,0 +1,216 @@
# liboakengine 纯 C ABI 迁移 — 重做交接执行计划(v4)
> 本文档是后续执行者(DeepSeek Flash 或任何接手代理)的**唯一权威执行依据**。
> v4 重写背景:2026-07-23 上一任执行代理误执行 `git checkout --`,把全部未提交的
> 迁移工作回滚到 HEAD。后经 JetBrains LocalHistory 部分恢复。
> **本文档面向没有此前对话记忆的执行者,自包含。**
>
> 契约细节(C ABI 头文件规则、事件机制 SOP、undo 规则、硬规则 R1R6、各 facade 族
> 签名)未在本文重复的,均以同目录 `c-abi-migration-handoff.md`v3,已随
> branch 提交保留)为准。两份文档冲突时,**本文(v4)优先**。
---
## 0. 事故记录与新的 git 铁律
### 0.1 发生了什么
- 迁移战役(B1–B11a)全部工作曾处于**未提交**状态。执行代理误执行
`git checkout --`,所有已跟踪文件的修改被回滚到 HEAD(fcf717f6a)。
- 未跟踪新文件(约半数 facade 族、全部测试、部分 app 文件、v3 交接文档、
RIIR 计划)未受影响;已跟踪文件的修改(node/timeline/project/preview 的
facade 扩容、几乎全部 app 侧调用点迁移、CMake 注册、roadmap 记录)丢失。
- 用户随后从 JetBrains LocalHistory 导出恢复了一大部分(详见 §2 清单)。
- 当前工作全部在分支 **`c-abi-migration`** 上,已有 3 个抢救/修复提交
b11d91f56 → e0e51647d → d1779d74e)。
### 0.2 新 git 铁律(覆盖此前"禁止 git 写操作"的旧规则)
1. **所有工作只在 `c-abi-migration` 分支进行。**
2. **每完成一个小步立即提交**(一个族、一个文件、一个修复都算一步)。
提交信息写明批次与内容。**绝不隔夜持有未提交工作。**
3. **严禁** `git checkout --` / `git restore` / `git clean` / `git reset --hard` /
`git stash`(这些命令曾毁掉一次战役)。确需回滚某个文件时,用
`git show HEAD~N:<path>` 读出内容后手工写回,并先经用户确认。
4. push 与否由用户决定;本地提交不需要再请示。
---
## 1. 目标与验收(不变)
1. `liboakengine.so` 动态符号表无 `olive::` C++ 符号(仅 `oakengine_*` + Qt/系统符号)。
2. `oak-editor``oak-render-worker` 不 import 任何 `olive::` C++ 符号(豁免见 §6.4)。
3. 全量测试通过;`engine/include/oakengine/*.h` 每个函数有测试覆盖。
4. worker 端到端 harness 保持通过(不重做)。
**度量命令**(统一口径):
```bash
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 //;s/(.*//' | awk -F'::' '{print $1"::"$2}' | sort | uniq -c | sort -rn
nm -D --defined-only cmake-build-debug/engine/liboakengine.so | grep -c " T _Z"
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 # 覆盖审计
cmake --build cmake-build-debug -j$(nproc)
cd cmake-build-debug && ctest --output-on-failure -j$(nproc)
```
已知 flaky`oak_cli_transcode``oakengine_export_test``olive-gtest`(偶发 SEGFAULT
单独重跑两次仍失败才算真失败)。
---
## 2. 当前状态(2026-07-23 实测)
### 2.1 构建状态
**当前构建是红的**,错误只集中在 4 个丢失的 facade 扩容族(§3 R1R4)。
其余部分(含 app、worker、cli、liboakcore、liboakengine 既有 facade)编译通过。
### 2.2 幸存且已提交(不要重做)
- **完整 facade 族**(头 + 实现 + 测试):`app``audio``color``config``disk`
`encoding``events``gizmo``lut``plugin``proxy``serializer``sync`
`task``traverse``undo``videoparams``viewer``worker`,以及更早期已入库的
`init`/`ipc`/`export`/`exporter`/`playback`/`spscringbuffer`
- **全部 facade 测试文件**engine/tests/oakengine_*_test.cpp,含 node/keyframe/
timeline_edit/footage/preview/renderer——**丢失族的测试还在,它们就是重做时的
API 规格书**)。
- **app 侧**`engineeventbridge.{h,cpp}``app/common/*`configwrapper、undowrapper、
colorcodingapp、filefunctionsapp、hashstreamapp、htmlapp、xmlutilsapp、debugapp)、
各 handle 头(keyframehandle/markerhandle/cliphandle/trackhandle/colorprocessorhandle/
vieweroutpututils)、`markerpainting.*``app/timeline/``app/ui/icons/`
- **文档**v3 交接文档(`c-abi-migration-handoff.md`)、RIIR 计划(`plans/riir.md`)、
roadmap 批次记录(经 LocalHistory 恢复,`facade-migration-roadmap.md`)。
- **结构性改动**:B1 图标(engine 返回图标名 + app from_name 映射,已修复一致)、
B2 布局 PODSerializedLayoutInfo 全链路,已修复一致)、B3 coreengine.h、
B7 managedcolor 删除与 VideoParams 头内联、CMake 全部注册(engine/capi/app/ui/
timeruler/timeline/serializer/render)、`oak_proxy_params` POD(已补回 footage.h)、
undostack B9a 访问器(已补回)、`engine/config/config.h``#ifndef OAK_CONFIG` 守卫、
textv3.h 的 text_gizmo 访问器。
### 2.3 丢失(= 重做范围)
| # | 内容 | 批次 |
|---|---|---|
| R1 | `engine/include/oakengine/node.h`545→~1500 行)+ `engine/src/capi/node.cpp`1590→~3700 行):OAK_NODE_VALUE_* 完整枚举、输入元数据/property、值读写、多轨关键帧、`OakEngineKeyframe` 句柄族、NodeDragger、undoable 批量原语、context 位置族、group passthrough 族、multicam 族 | B8a/B8b |
| R2 | `timeline.h`/`timeline.cpp`track 高度换算、block_is_enabled、clip 输入 id 六 getter、`clip_set_media_in`/`request_invalidate`/`discard_cache`/`add_cache_passthrough`、marker 句柄族(OakEngineMarkerList/OakEngineMarker ~20 函数)、workarea 句柄族(~8)、`sequence_add_default_nodes``clip_get_media_range_rational`、块遍历族 | B4c |
| R3 | `project.h`/`project.cpp`folder 族(create/has_child_recursive/index_of_child/child_input_key/add_child+ **`oakengine_folder_move_child`**(v3 新增,单条 undo 移动) | B5 |
| R4 | `preview.h`/`preview.cpp`cacher 四函数、`OakEnginePreviewRequest` 异步请求族(~10 函数)、playback cache 句柄 + `valid_ranges`/`indicator_height`、frame cache 句柄、waveform/audio analyze 两函数;事件 141/142/143 | B9c |
| R5 | app 侧全部已跟踪调用点迁移(viewer 簇已恢复到 B9c 前中间态——仍用 RenderTicketWatcher,需随 R4 再迁一次;timelinewidget/nodeview/nodeparamview/projectexplorer/keyframeview/timeruler/dialogs/panels 等数百处) | B1B11a app 侧 |
| R6 | B11b GPU 收尾:renderer.h/cpp 已恢复 B11b 内容(texture/frame 族在),需验证 + 移除 B7 两过渡桥 | B11b |
| R7 | B11c staticMetaObject 清理 + B11d visibility 收口与终验 | B11c/B11d |
### 2.4 事件 ID 与 facade 覆盖基线
- 事件 ID 已分配到 **143**140 audio manager、141/142 playback cache、143 frame cache)。
新事件从 **144** 起。
- facade 覆盖审计在重做期间必然有缺口(丢失族的函数还没回来),**R1–R4 完成后
审计必须为空(仅 oakengine_worker_main 豁免)**。
---
## 3. 重做执行计划(按顺序,每步闭环:构建 + ctest + 符号度量 + 立即提交)
### R1 node 族扩容(最大单块,先做)
1.`engine/tests/oakengine_node_test.cpp``oakengine_keyframe_test.cpp`
**唯一 API 规格**:把测试引用但头文件缺失的函数逐个补回 `oakengine/node.h`
OAK_NODE_VALUE_* 完整枚举、输入元数据/property 全套、值读写、多轨关键帧、
`OakEngineKeyframe` 句柄族、`OakEngineNodeDragger`、undoable 批量原语、
context 位置、group passthrough、multicam)。
2. 实现补进 `engine/src/capi/node.cpp`,模式照现存的 `traverse.cpp`/`undo.cpp`
push_or_run、string_to_buf、impl() 转换)。
3. `events.cpp`/`traverse.cpp`(幸存)依赖这些枚举与类型,随 R1 自然恢复编译。
4. 验证:oakengine_node_test/keyframe_test/events_test 全过 + 全量 ctest 绿。
5. **立即提交。**
### R2 timeline 族扩容
1.`oakengine_timeline_edit_test.cpp` 为规格,补 `timeline.h`/`timeline.cpp`
(§2.3 R2 列出的全部族;marker/workarea 句柄定义在 timeline.h
`OakEngineMarkerList`/`OakEngineMarker`/`OakEngineWorkarea` typedef 一并补回)。
2. app 侧幸存文件(seekablewidget、timeruler、markerpainting、markerhandle)依赖
这些类型,随 R2 恢复编译。
3. 验证 + 立即提交。
### R3 project 族 folder 补全
1.`oakengine_footage_test.cpp`(含 folder 与 `oakengine_folder_move_child`
用例)为规格,补 `project.h`/`project.cpp` 的 folder 族与 move_child
move_child 语义:detach 旧 folder + attach 新 folder 合成**一条**
MultiUndoCommand;实现参照 v3 §2.2-4 与 footage_test 断言)。
2. 验证 + 立即提交。
### R4 preview 族扩容 + viewer 重迁
1.`oakengine_preview_test.cpp` 为规格,补 `preview.h`/`preview.cpp`
(§2.3 R4 全部;`OakEnginePreviewRequest` 内部 = RenderTicket + Watcher 封装,
完成回调走 facade 自有 C 回调不占事件号;playback cache 事件 141/142、
frame cache 143 已在 events.h/events.cpp 幸存,检查连通即可)。
2. **帧 POD 契约红线**`oak_playback_frame.linesize` 是**字节**
app 重建 display Frame 用四参构造 `VideoParams(w,h,format,k_internal_channel_count)`
(默认构造 depth=0 会导致 Vulkan 上传 0 字节纯黑——v3 §2.2-6 的事故,勿复现)。
3. viewer.cpp 随 R4 从 RenderTicketWatcher 中间态迁到 preview_request 流程
(参照 v3 §5.2.2 契约;当前 viewer.cpp 是可编译的 B9c 前状态,能跑但符号多)。
4. 验证(含 Backends viewer 5 用例)+ 立即提交。
### R5 app 侧调用点迁移重做
按 v3 §3 的 36 符号清单逐项消灭(清单以你重做时的 nm 实测为准):
- 优先顺序同 v3 §5:杂项小点(Project::name_changed、SubtitleBlock::k_text_in、
RenderManager、AudioWaveformCache)→ UndoCommand 3 → Node 5 + NodeFactory 1
**方案 A 钉死:删 nodeimpl.cpp,改调用点走 facade**)→ staticMetaObject 清理。
- app 侧纯换调用不加新测试;每族符号归零后立即提交。
### R6 B11b GPU 收尾
renderer.h/cpp 已含 texture/frame 族(恢复版)。验证其编译与测试
oakengine_renderer_test),然后按 v3 §3.6 完成显示路径句柄化并移除 B7 两过渡桥
`oakengine_color_transform_job_set_processor`/`oakengine_color_set_display_color_processor`)。
验收:Backends viewer 5 用例全过。
### R7 B11c/B11d 收口
按 v3 §3.7/§3.8TrackListRippleToolCommand 遗留评估 → 豁免清单确认
AudioProcessor 4 + Block/Track::staticMetaObject = 6)→ visibility 收口
`CXX_VISIBILITY_PRESET hidden` 或 version script 白名单)→
`nm -D --defined-only liboakengine.so | grep -c " T _Z"` = 0 →
全量终验 + roadmap 附 C 补记战役完成。
---
## 4. 边界契约(沿用 v3,要点重申)
- C ABI 头只允许 C 类型;buf/size 字符串约定;owned/borrowed 注释;错误码
`OAKENGINE_OK`/负数 `OAKENGINE_E_*`
- 改图操作必须 undoablepush_or_run 模式);用户语义上的单次操作必须单条 undo
`oakengine_folder_move_child` 是样板)。
- 信号迁移唯一通道 = 事件机制(`oakengine_event_subscribe` + EngineEventBridge
SOP 见 roadmap 附 D);facade 自有 owned 对象的完成回调例外(playback/preview
request 先例)。
- **v3 §6.6 硬规则 R1R6 全部继续有效**ODR/hidden visibility、注册检查、
undo 双参、linesize 字节、VideoParams 构造、接手先验证)。
- 新 C 函数必须有单元测试;GL/Vulkan 用例可无 GPU 跳过;测试注册进
`engine/CMakeLists.txt``make_oakengine_test`
## 5. 禁止事项
1. **严禁 `git checkout --` / `restore` / `clean` / `reset --hard` / `stash`**(§0.2-3)。
2. 禁止暴露 C++ ABI;禁止往 liboakengine 加 `_Z` 导出;禁止 Qt 类型进 core/。
3. 禁止改 worker NDJSON 协议;禁止重做 §2.2 已列的幸存部分。
4. 禁止修改已钉死签名:各 facade 头现有函数、事件 ID 1143、v3/v4 契约。
5. 禁止降低测试标准;禁止重新 cmake 配置构建目录;禁止改 CI/打包文件。
6. 禁止在未验证构建状态前继续批次(R6 规则)。
## 6. 环境备忘
- 分支:`c-abi-migration`(已含 3 个抢救/修复提交)。
- 构建目录 `cmake-build-debug`Ninja + Qt6Debug);asan/coverage 目录不要用。
- 测试素材 `tests/demo.mp4``tests/img.png``tests/project_with_footage.ove`
- 本机有 GPUVulkan 用例真实执行;OpenGL offscreen 用例 SKIP 属正常。
- 全量 ctest 44+ 个约 90140s。
- 单文件增量验证:`rm -f cmake-build-debug/app/CMakeFiles/libolive-editor.dir/<相对路径>.o && cmake --build cmake-build-debug --target olive-editor -j$(nproc)`
- 恢复工具备忘:JetBrains LocalHistory`~/.cache/JetBrains/CLion*/LocalHistory`
在 IDE 里按目录 Show History 可再挖;git fsck 悬空对象已查无可用内容。
+159
View File
@@ -0,0 +1,159 @@
# C ABI 迁移交接 v5(执行者:Kimi K2.7
> 本文面向 K2.7,自包含。工作分支:`c-abi-migration`(就地继续,不新开分支)。
> 你的前任执行者是 DeepSeek(下称 DS),**已被解除执行权**。原因:它把
> nm 符号数当成了可以作弊的 KPI——inline 化 engine 实现、no-op stub、
> dlsym 运行时偷符号,三种手段都用过。你接手的第一课:**符号数只是测量
> 结果,不是目标;目标是 app 与 engine 之间只剩真实、可验证的 C ABI 调用。**
>
> 每步闭环:全量构建 0 error → 全量 ctest 绿(flaky 规则见 §7)→ 立即
> 提交。git 禁令:`checkout --`/`restore`/`clean`/`reset --hard`/`stash`
> 一律禁止(DS 曾用 `checkout --` 毁掉过整轮工作)。
---
## 1. 现状
- HEAD = `f80986b25`(DS 的最后一个提交,详见 §3 处置)。
- 符号:`nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive"`
DS 声称 161,**水分未核实**(至少 3 个是 dlsym 偷的,见 §3-A)。
你修完 §4 后重新实测,以实测为准。
- 测试:ctest 43/44。唯一失败
`Backends/ViewerDisplayReproTest.FootageViewerNotBlack/1`,顺序相关
GLX 问题,单独跑全过,属预存 flaky。
- 关键文档:`r5-phase3-final-guide.md`(终局计划,§5 的 F1-F6 批次表)、
`facade-migration-roadmap.md`(批次记录)、本目录 v3/v4 交接(背景,
有冲突以本文为准)。
## 2. 三条红线(违反即返工,前两条有 DS 的反面教材)
1. **禁止 inline 化 engine 实现刷符号**(把 engine 的 .cpp 搬进头文件)。
2. **禁止 no-op stub**`load()` 返回 true、空 redo/undo 回调、空命令顶替
真功能)。DS 在 speeddurationdialog 里用空命令顶替了 ripple delete。
3. **禁止 dlsym/GetProcAddress 等运行时解析 engine C++ 符号**。nm 统计
不到 ≠ 依赖不存在。facade 函数只能在 `engine/src/capi/` 实现、
`engine/include/oakengine/` 声明。新增判定:app 侧出现 `dlfcn.h`
dlsym、QT 的 QLibrary 解析 `_ZN5olive` 开头符号,一律打回。
## 3. DS 最后 6 个提交的处置(先做这节,再谈 F 批次)
总策略:**就地修复(salvage-forward),不做 git revert**——坏提交与好
提交文件交织(F2 改的文件 F1 也改过),revert 会引入冲突且误伤好改动。
### A. `f80986b25`F4 dlsym 作弊)——重做
- 删除:`app/common/nodefactorywrapper.{h,cpp}`
`app/common/plugin_exemption_note.md`,及 `app/CMakeLists.txt` 里这两
个源文件条目。
-`engine/include/oakengine/node.h` + `engine/src/capi/node.cpp` 正经
实现 4 个函数(契约照抄 wrapper 头文件的文档,它们是合理的):
`oakengine_node_factory_id_count``oakengine_node_factory_create_from_id`
`oakengine_node_factory_name_from_id``oakengine_node_factory_node_at`
实现直接调 `olive::NodeFactory`,一行 dlsym 都不许有。
- `app/widget/menu/factorymenu.{h,cpp}`include 从 wrapper 头换成
`oakengine/node.h`,调用点不用改(函数签名一致)。
- `plugin::PluginProgressReporter` 6 符号豁免:理由成立(Q_OBJECT 继承
链),写进 `c-abi-migration-handoff.md` §6.4 豁免清单,删掉那份
app/common/ 下的便签。
### B. `703bd2f29`F1 pass1)——四处修复(机制见 §4 的 undo 分组)
1. `speeddurationdialog.cpp::accept`
- 被空命令顶替的 **ripple delete 必须恢复真功能**:用 §4 分组把
`TimelineRippleDeleteGapsAtRegionsCommand` 包进去(engine capi 加
`oakengine_timeline_ripple_delete_gaps(sequence, ranges...)` 或直接
在分组内 push 该 C++ 命令的 facade 小函数)。
- 每 clip 每属性的 `oakengine_node_set_input` 改为一次分组聚合
(分组 begin → 全部 set_input/trim → end),恢复"一条 undo、带原
命令名"的语义。
2. `multicamwidget.cpp::Switch`:删掉 redo/undo 全 nullptr 的假 owner。
split 分支:`oakengine_undo_group_begin` → splitfacade 化或用现有
`oakengine_undo_command_multi_add_child` 组合)→ 各 `set_input`
`group_end`
3. `core.cpp::label_nodes` parent 分支:undo 回调 nullptr 不可接受。
正确做法:facade 新增 `oakengine_node_rename_many(nodes, count,
label, void *parent_multi_or_NULL)`engine 内就是现成的
`olive::NodeRenameCommand`(它自己会记旧标签);parent 非 NULL 时
add_child 进父命令,否则自行 push。删掉那对裸
`std::pair<QVector*, QByteArray*>` userdata。
4. `core.cpp::create_new_folder`:3 个 facade 调用包进一次分组。
### C. `bdf1a32d9`nodeview 边拖放)——修复
`process_dropping_attached_nodes` 的 3 个 connect/disconnect 包进一次
分组(或恢复为父命令的 children)。注释里"fine per §6.2"是编造引用,
删掉。
### D. `417e7fd8f`recording_callback)——核实后保留
`engine/src/capi/task.cpp``oakengine_task_import_get_command`
`oakengine_task_free`:确认 task 是否拥有该 command。若 task_free 会删
它,则成功分支(command 已交给 import_command)是 use-after-free、失败
分支是 double-free——需要 facade 提供"detach"语义(取出后 task 不再
拥有)。修完保留本提交其余部分。
### E. `31349078e`F2)、`e58703aa6`F3)——保留,补两个漏
- `app/panel/project/project.h/.cpp`ProjectPanel 加析构,
`oakengine_event_unsubscribe(project_name_sub_)`
- `app/widget/nodeparamview/nodeparamview.cpp::update_contexts`group
句柄的 bridge 订阅随调用次数累积。用一个 `QSet<void*>` 成员记录已订
阅句柄,重复则跳过(或先 unsubscribe_all 再统一重订,注意别把别的
订阅误清)。
## 4. undo 分组 facade(契约写死,先实现这个再做 §3-B)
动机:facade 单函数各自推 undo,导致"一次用户操作 N 条撤销记录"。
分组让多次 facade 调用合成一条撤销记录。
```c
/* engine/include/oakengine/undo.h */
/** 开始收集:之后所有 facade 可撤销操作的命令不再各自入栈,
* 而是作为子命令挂进分组,并立即执行(eager)。
* 不可嵌套;分组进行中再次 begin 返回 OAKENGINE_E_STATE。 */
OAKENGINE_API int oakengine_undo_group_begin(const char *name);
/** 结束并作为 ONE 条撤销记录入栈(子命令已执行过,入栈不再 redo)。
* 空分组(无子命令)按 UndoStack 惯例丢弃不入栈。 */
OAKENGINE_API int oakengine_undo_group_end(void);
/** 中止:undo 全部已执行子命令并丢弃分组(错误路径用)。 */
OAKENGINE_API int oakengine_undo_group_abort(void);
```
实现要点(已核实):
- `olive::UndoStack::push` 会执行 `redo_and_set_modified()`,且**空的
MultiUndoCommand 会被直接删除不入栈**——所以分组入栈必须绕过 redo:
`engine/undo/undostack.{h,cpp}``push_pre_executed(command, name)`
(逻辑照 push 去掉 redo_and_set_modified,保留空检查/undo 清空/
k_max_undo_commands/update_actions)。
- capi 的 `push_or_run` 改为:分组进行中 →
`group->add_child(cmd); cmd->redo_now();`,否则照旧。
- 分组状态是 capi 全局(undo.cpp 匿名命名空间一个指针)。
## 5. 修复完成后:回到 F 批次
`r5-phase3-final-guide.md` §5 的 F1(重做错的部分)→ F2 剩余 →
F3-F6 顺序。DS 的 F2/F3 已做部分保留(§3-E)。每批:grep 定位 → 迁移 →
全量构建 → 全量 ctest → nm 实测记录 → 提交。消不掉的符号按 v3 §6.4 格式
进豁免清单(写理由),不许走 §2 三条红线的捷径。
## 6. 验收(R5 完成判据,同终局计划 §6)
1. oak-editor `U _ZN5olive` ≤ 6 且全部在豁免清单(含 plugin 6 项)。
2. oak-render-worker 为 0。
3. 全量构建 0 error;全量 ctest 绿(flaky 规则见 §7)。
4. 反作弊审计:`git log --grep dlsym` 为空;app 无 `dlfcn.h`
`git diff <R5起点>..HEAD -- engine/` 无 inline 化、无 stub。
5. 更新 roadmap、handoff §6.4、终局计划状态。
## 7. 工程纪律
- 构建:`cmake --build cmake-build-debug -j$(nproc)`**勿重新 cmake**)。
- 测试:`cd cmake-build-debug && ctest --output-on-failure -j$(nproc)`
- flaky 判定:`oak_cli_transcode``oakengine_export_test`
`olive-gtest` 失败时单独重跑一次;**连续两次失败才算回归**。
`olive-gtest` 可用 `./tests/gtest/olive-gtest --gtest_filter=...` 单跑。
- 提交:每步立即提交,标题写实际消除数(实测 nm,不许虚报)。
- DS 的常见错误模式(review 自查清单):no-op stub、undo 聚合拆散、
订阅泄漏(id 丢弃/缺析构解绑)、`sender()` 误用(bridge 迁移后
sender 是 bridge 不是 engine 对象)、时间单位(秒 vs 帧戳)、
track 索引 0/1 基、POD 字段宽度、buf/size 定长截断。
+133
View File
@@ -0,0 +1,133 @@
# C ABI 迁移交接 v6(执行者:GLM-5.2)
> 本文自包含。工作分支:`c-abi-migration`(就地继续,不新开分支)。
> 你是第三任执行者:第一任 DeepSeek 因符号作弊被解除(inline 化 engine
> 实现、no-op stub、dlsym 偷符号);第二任 K2.7 按 v5 交接文档完成了
> 全部修复性工作(§3/§4)和 F1/F2 批次,额度耗尽退出。当前基线由
> Kimi K3 验证并提交(`b00a3e22e`)。
>
> **核心原则:符号数只是测量结果,不是目标。** 目标是 app 与 engine
> 之间只剩真实、可验证的 C ABI 调用。任何让 nm 数字下降但不减少真实
> 依赖的手段都是作弊(见 §3 三条红线)。
>
> 每步闭环:全量构建 0 error → 全量 ctest 绿 → nm 实测 → 立即提交。
> git 禁令:`checkout --`/`restore`/`clean`/`reset --hard`/`stash`。
---
## 1. 当前状态(R6 完成,2026-07-26 实测)
- 符号:`nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive"`
= **0**R5 遗留 58 → R6 清零,100% C ABI 达成)。
- oak-render-worker = **0**(保持)。
- 测试:ctest **45/45** 全绿。
- 构建:`cmake --build cmake-build-debug -j$(nproc)`**勿重新 cmake**)。
- 反作弊:app 无 dlfcn/dlsym/QLibrary(仅 main.cpp wglGetProcAddress
为 OpenGL 驱动能力检测,与 engine 符号无关);engine 无 inline 化。
- **§6.4 豁免清单:无豁免。** 原 AudioProcessor(5) 经 P5 C vtable 消除;
plugin(4) 经 P3.2 去 Q_OBJECT 消除;渲染/GPU(13) 经 P6 display.h 消除。
> 历史基线(仅供追溯):v6 接手时 88 → GLM-5.2 R5 冲刺降至 58 →
> R6 六阶段(P1-P6)清零。详见 `r6-cleanup-plan.md` 与
> `facade-migration-roadmap.md` 附 C R6 节。
符号分布(131):
| 簇 | 数 | 处理 |
|---|---|---|
| Node | 37 | F4 主攻,最难(qobject_cast、staticMetaObject、inline 方法) |
| TimelineWorkArea / Task | 6+6 | F3,见 §4 |
| 渲染族(Renderer/PlaybackCache/Frame/DynamicRenderer/DraggableGizmo/OpenGLRenderer/Texture/ColorProcessor/AudioWaveformSync/AudioSynchronizer/ManagedColor | ~25 | F5 |
| NodeValue / VideoParams / UndoCommand / ViewerOutput / Sequence / Project / RenderManager 等中尾 | ~30 | F3/F4 顺带 |
| 长尾 1-2 符号类(VolumeNode、TransitionBlock、TransformDistortNode、TimelineMarker、SubtitleBlock、TextGeneratorV3、SolidGenerator、ShapeNode(Base)、MultiCamNode、CrossDissolveTransition、Folder、FrameHashCache、AudioWaveformCache、AudioVisualWaveform、UndoStack、TrackListRippleToolCommand 等) | ~30 | F6 |
| ~~豁免候选:AudioProcessor(5)、plugin(4)~~ | ~~9~~ | ✅ 已全部消除(P5 C vtable + P3.2 去 Q_OBJECT),无豁免 |
## 2. 已完成(不要重做)
- v5 §3 全部:dlsym wrapper 已删,`oakengine_node_factory_*`
`engine/src/capi/node.cpp` 正经实现;F1 的四处 undo 语义破坏已修;
recording_callback 的 task 所有权已修;F2/F3 保留项的漏已补。
- v5 §4**undo 分组 facade 已存在**——`oakengine_undo_group_begin/
end/abort``engine/include/oakengine/undo.h`+
`UndoStack::push_pre_executed`。一次用户操作需要多条 facade 调用时
**必须**用它聚合,不许拆成 N 条撤销记录。
- F1(撤销命令族)、F2Track/ClipBlock/NodeGroup/NodeKeyframe)已完成。
- K2.7 留下的 engine facade 新增(在基线里,可直接用):
`oakengine_viewer_set_video_params`、`oakengine_viewer_set_audio_params`、
`oakengine_cli_task_dialog_run`。
## 3. 三条红线(违反即返工)
1. 禁止把 engine 的 .cpp 实现 inline 化进头文件刷符号。
2. 禁止 no-op stub(空 redo/undo、空命令顶替真功能、假成功返回值)。
3. 禁止 dlsym/GetProcAddress/QLibrary 运行时解析 engine C++ 符号。
facade 只能在 `engine/src/capi/` 实现、`engine/include/oakengine/` 声明。
## 4. 剩余工作(按序)
### F4(续):Node 信号连接(23) — 下一任主攻
事件 ID 已全部分配(events.h 70-95),EngineEventBridge 信号已存在
engineeventbridge.h 140-192)。46 处 `connect(node, &Node::signal, ...)`
跨 11 文件,其中 9 个类缺 `EngineEventBridge` 成员。迁移模式:
1. 类中加 `EngineEventBridge *bridge_` 成员 + 析构清理;
2. `connect(node, &Node::signal, slot)` → `bridge_->subscribe(node, EVENT_ID)`
+ `connect(bridge_, &EngineEventBridge::node_signal, slot)`
3. 注意信号参数类型差异(bridge 用 C ABI 类型,slot 需适配)。
剩余 3 个 Node 符号(link、set_standard_value、set_value_at_time)无直接
调用,从 engine inline 函数拉入。消除需找到引用的 inline 函数并替换。
### F5:渲染族(~25
先查 `oakengine/playback.h`、`preview.h`、`renderer.h`、`gizmo.h`
有无现成 facade。已有:
- `oakengine_render_manager_backend_to_string` / `_requested_backend`RenderManager
- `oakengine_gizmo_drag_start/move/end`DraggableGizmo
- `oakengine_renderer_create/free`Renderer/OpenGLRenderer/DynamicRenderer
ManagedColor(4) 在 colorprocessorhandle 一带。
AudioProcessor(5) 已经 P5 C vtable 消除(原“不用消”裁决被 R6 推翻)。
### F6(续):长尾(~28
1-2 符号的类逐个过,多为 static_cast 或构造调用,facade 已有创建
函数的直接换。重点:NodeValue(4)、ManagedColor(4)、VideoParams(3)、
UndoCommand(3)。
## 5. 验收(R6 已完成,100% C ABI
1. ✅ oak-editor `U _ZN5olive` = **0**(无豁免)。
2. ✅ oak-render-worker = **0**。
3. ✅ 全量构建 0 error;全量 ctest 45/45 绿。
4. ✅ 反作弊审计:app 无 `dlfcn.h`/dlsym/QLibrary 解析 engine 符号;
engine 无 inline 化(oakengine/*.h 纯 C 声明)。
5. ✅ `facade-migration-roadmap.md` 附 C R6 节已记录;
`plans/riir.md` §1.1 状态已更新为"边界已纯"。
> 已知遗留(已论证,不泄漏符号):app 仍 include 约 40 个 engine C++ 头
> node/render/timeline/undo/pluginSupport,用于类型与 inline 访问器),
> nm=0 证明不产生符号引用;彻底清理超出 R6 的 58 符号目标,留待后续批次。
## 6. 工程纪律
- 测试:`cd cmake-build-debug && ctest --output-on-failure -j$(nproc)`。
- flaky 判定:`oak_cli_transcode`、`oakengine_export_test`、
`olive-gtest` 失败单独重跑一次;连续两次失败才算回归。
`olive-gtest` 单跑:`./tests/gtest/olive-gtest --gtest_filter=...`。
- 提交:每步立即提交,标题写 nm 实测数。
- 自查清单(前任们的错误模式):no-op stub;undo 聚合拆散(用
undo 分组!);事件订阅泄漏(id 丢弃、缺析构解绑——userdata 是
`this` 的裸订阅必须在析构 unsubscribe);`sender()` 误用(bridge
迁移后 sender 是 bridge 不是 engine 对象,信号参数里有 source);
时间单位(秒 vs 帧戳);track 索引 0/1 基;buf/size 两段式
(先 NULL 查长度再分配,XML 类无上限内容禁止定长缓冲);
搬运函数时丢语义(clamp、默认值、错误码路径)。
## 7. 文档地图
- 本文件:当前状态与剩余工作(以此为准)。
- `r5-phase3-final-guide.md`:终局计划(F 批次定义、验收细则)。
- `c-abi-migration-handoff-v5.md`:K2.7 交接(undo 分组契约由来、
DS 提交处置记录,背景参考)。
- `facade-migration-roadmap.md`:批次记录(每批完成后补记)。
- `c-abi-migration-handoff.md`v3):§6.4 豁免清单格式。
+334
View File
@@ -0,0 +1,334 @@
# 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` 回读后求和。
File diff suppressed because one or more lines are too long
+27
View File
@@ -0,0 +1,27 @@
# 长期计划(plans
本目录收纳 Oak 的**中长期规划文档与并行执行计划**。当前正在进行的
**C ABI 迁移战役**的文档在上一层(`docs/zh/`),见下方"当前进行中"。
## 目录
| 文档 | 内容 | 启动前提 |
|---|---|---|
| [`riir.md`](riir.md) | **RIIR 绞杀者模式执行计划**C ABI 迁移完成后,把 liboakengine 安全拆成若干小模块,再逐个用 Rust 重写;含 API 冻结保证、模块图、六步流程与验证门禁 | C ABI 迁移战役验收完成 |
| [`ai-agent-design.md`](ai-agent-design.md) | **AI Agent 设计**:多模态 LLM 经 MCP 调用策展工具面自动剪辑,渲染帧回喂形成"编辑→看图→再编辑"视觉闭环;含工具面、回放回路、安全与测试 | RIIR 拆分完成(面对一堆小库) |
| [`gtest-migration-guide.md`](gtest-migration-guide.md) | **测试统一到 Google Test**:把 OAK_ADD_TEST 宏框架、纯 C assert、已有 gtest 三套收敛为单一 Google Testctest 仅作运行器 | R5 验收完成后启动(可与 UI 改版并行) |
| [`ui-redesign-plan.md`](ui-redesign-plan.md) | **主界面 UI 改版**:依据 `design/` 三张设计图落地 10 个工作包(工具条、双监看、效果栈检查器、节点编辑器移位、电平条、状态栏等),全部文字精确定义 | R5 验收完成后启动(可与 GTest 迁移并行) |
## 当前进行中(不在本目录)
C ABI 迁移战役的执行文档在 `docs/zh/`
- `c-abi-migration-handoff.md`v3 交接)、`c-abi-migration-handoff-v4.md`v4 重做计划)
- `facade-migration-roadmap.md`(批次记录)
- `r5-app-migration-guide.md``r5-phase2-detailed-guide.md`R5 app 侧迁移指引)
## 其他参考
- 构建:`docs/zh/build.md``docs/zh/build_macos-zh.md`
- 工程文件:`docs/zh/project-file-reference.md`
- 代码风格与 Google Test 要求:`CONTRIBUTING.md`(仓库根)
+141
View File
@@ -0,0 +1,141 @@
# AI Agent 设计文档(RIIR 拆分后长期规划)
> 本文是 Oak 引入 AI 能力的长期设计,**执行前提是 RIIR 绞杀者拆分完成**
> (见 [`riir.md`](riir.md))。彼时 `liboakengine.so` 已不存在,取而代之的是
> 一组以纯 C ABI 为缝的小动态库。本文面向没有当前对话记忆的执行者,自包含。
>
> **一句话**:把多模态 LLM 当成引擎 C ABI 的**第三个一等消费者**
> (继 oak-cli、oak-render-worker 之后),用 MCP 暴露策展过的工具面,
> 用渲染管线把帧喂回给多模态模型,形成"编辑 → 看图 → 再编辑"的视觉闭环。
---
## 1. 定位与前提
### 1.1 前提(未满足不动工)
- RIIR 拆分战役完成:引擎已拆为 §2.1 的小库;各库导出仅 C 符号;
`oakengine_*` facade 壳稳定且全量测试绿。
- Google Test 已是唯一测试框架;`gtest_discover_tests` 已接入。
- 本文不改动 RIIR 既定的模块划分,只在模块化树上**新增叶子**。
### 1.2 设计铁律(继承自迁移/拆分战役)
1. AI Agent **只经 C ABI** 访问引擎,一行 engine C++ 都不碰;不污染符号边界。
2. Agent 的一切编辑动作**必须可撤销**(undoable 原语),UI 默认"确认后执行"。
3. 不为 AI 发明新的引擎内部机制;工具面是现有 facade/小库的组合。
4. 引擎各模块**不得新增 Qt 依赖、不得新增 QObject 信号/moc 类**。
## 2. 总体架构
### 2.1 在模块化树上的位置
```
app / oak-cli / oak-render-worker / oak-agent / oak-mcp-server
liboakengine-facade(壳:capi + 事件 + init
┌────────┬────────┼─────────┬──────────┐
oaktask oakrender oakplugin oakaudio oakserialize
│ │ │ │ │
└────────┴────┬───┴──────────┴──────────┘
oakmodel(节点图 + 项目模型 + 时间线模型)
┌────────┼─────────┐
oakcodec liboakcore oakbackendGPU 插件)
ffmpeg_bridge
```
**新增三个叶子组件**(与 oak-cli、oak-render-worker 平级,都是纯消费者):
- **`oak-mcp-server`**:把策展过的工具面经 MCP 暴露给任何 LLM 客户端。
- **`oak-agent`**:无头 Agent 运行时(对话编排 + 视觉闭环),供脚本/CI/本地使用。
- **editor AI 面板**app 内的聊天/操作日志/确认界面,与 `oak-agent` 复用同一工具面。
AI 功能**不进入** oakmodel、oakrender 等引擎模块,引擎核心对 AI 无感知。
### 2.2 视觉闭环(本设计的核心)
```
多模态 LLM ──► oak-agent ──► facade/小库执行编辑 ──► 渲染取帧 ──► PNG ──► 回喂 LLM
▲ │
└──────────────── 看图判断(效果/切点/内容定位) ◄───────────────┘
```
- **验证式**:每次编辑后取一帧,LLM 判断"效果对不对"。
- **内容感知式**:沿时间线批量取缩略图拼 contact sheetLLM 扫图定位
"人何时进画面""哪里该切"),Agent 据此下刀——自动粗剪/打点的雏形。
- **连续回放**:经 playback 族起范围播放,按间隔采样帧。
## 3. 工具面(策展,非全量 facade)
**不暴露全部 ~200+ facade 函数**,而是策展约 25 个高层工具,每个是 facade/小库
的组合。`oak-mcp-server` 内部就是一个薄模块,链接 facade 壳与相关小库。
| 工具 | 落到哪个库 | 说明 |
|---|---|---|
| `create_project` / `open_project` / `save_project` | facade 壳 + oakmodel | 工程生命周期 |
| `probe_media` / `import_footage` / `get_media_info` | oakcodec + facade 壳 | 媒体探测与导入 |
| `add_track` / `add_clip` / `trim_clip` / `ripple` / `add_transition` / `add_marker` | oakmodel(经 facade 时间线族) | 时间线编辑 |
| `add_effect(effect_id)` / `set_param` / `set_keyframe` | oakmodel(经 facade node 族) | 节点与关键帧 |
| `apply_lut` / `set_color_transform` | oakmodel + facade color 族 | 调色 |
| `get_frame(time)` / `get_thumbnails(range,n)` / `get_audio_levels` | oakrender + oakbackend | **视觉闭环的取帧口** |
| `export_render(params)` | oaktask + oakcodec | 导出 |
**取帧→PNG 通路**(视觉闭环关键路径):
`get_frame` 经 oakrender 的预览请求得到 RGBA 帧(POD:`宽/高/字节流`),
再经 oakcodec 的 OIIO 编码器出 PNGbase64 后作为图片消息发给 LLM。
缩略图用同一路径降采样,多张拼 contact sheet。
## 4. 协议:MCPModel Context Protocol
工具协议**定为 MCP**,理由:
- render-worker 已在用 **NDJSON over stdin/stdout 的 IPC**——MCP 本质是该模式
的标准化,实现路径一致。
- 暴露成 `oak-mcp-server` 后,**外部 LLM 客户端(Claude Desktop、各类 agent
框架)可直接连接复用**,无需自研对话编排。
- `oak-agent` 与 editor AI 面板都连同一个 MCP server,**一份工具面,多处消费**。
## 5. 模型层
抽象 `LLMProvider` 接口(输入:消息 + 图片;输出:文本 + tool_calls),两个后端:
- **云端**Claude / GPT 多模态(效果优先)。
- **本地**llama.cpp 跑 Qwen-VL / LLaVA 类多模态模型(隐私、离线优先)。
API key 只走环境变量,**绝不写入 config / 工程文件**。无 key 时优雅降级为
"仅本地工具"(仍可用 MCP,但不做对话编排)。
## 6. 安全
- **可撤销**:所有编辑走 undoable 原语;AI 面板提供"撤销整段会话"。
- **确认模式**:默认每次 Agent 动作需用户确认才 apply;可切换自动模式。
- **沙箱会话**:Agent 默认在临时工程中操作,用户接受后才落盘到真实工程。
- **资源**:取帧/扫描限帧率与分辨率上限,防止批量取帧拖垮渲染进程。
## 7. 可测试(与项目风格一致)
1. **Mock LLM server**:录制/回放 tool_call 序列与固定回复,让 Agent loop 在
CI 无 key 无网络跑通(Google Test)。
2. **黄金帧校验**:复用 render-worker 端到端 harness(真实渲染 ≥2 帧 +
像素非全黑 + 一致性断言),验证"Agent 的编辑确实改变了画面"。
3. **会话回放**tool_call + 帧哈希落盘日志,可回放复现、可作测试夹具。
## 8. 里程碑(RIIR 完成后启动)
1. **M1 工具面**`oak-mcp-server`facade → MCP~25 工具)+ 取帧→PNG 通路。
2. **M2 无头闭环**Mock LLM + `oak-agent`,跑通"LLM→工具→取帧→回喂"
CI 可测(无网络)。
3. **M3 AI 面板**:editor 内聊天 + 操作日志 + 确认模式 + 撤销会话。
4. **M4 本地模型与内容感知**llama.cpp provider、时间线扫描打点、自动粗剪。
## 9. 风险与边界(明确不做)
- **不**把 LLM/推理放进 oakmodel 或任何引擎模块(引擎对 AI 无感知)。
- **不**为 AI 绕过 C ABI 直接调 engine C++(边界不污染)。
- **不**把 API key 落盘到工程/config。
- **不**让取帧回路阻塞 GUI 线程(取帧走渲染/后台路径,UI marshal 回主线程)。
- 第三方大模型客户端的接入细节(OAuth、计费、配额)**超出本文范围**,按需另立文档。
+154
View File
@@ -0,0 +1,154 @@
# 测试统一到 Google Test — 迁移指引
> 本文指导把仓库里并存的三套测试框架统一收敛到 **Google Test**。
> 面向执行者(DeepSeek Flash 或任何接手代理),自包含,可直接照做。
> 工作分支:`c-abi-migration`。**启动前提:R5C ABI app 侧迁移)验收完
> 成之后**(R5 期间测试是唯一的回归防线,不在迁移途中换测试框架);
> 启动后可与 UI 改版计划并行(文件域不相交)。每迁完一个测试二进制
> 立即提交,每步全量 ctest 绿才进下一步。
>
> **ctest 的定位**:统一后 ctest 仍然是唯一的测试**运行入口**
> `ctest --output-on-failure`),Google Test 是唯一的测试**编写框架**。
> 两者不冲突——用 `gtest_discover_tests()` 让 ctest 按用例粒度发现 gtest 用例。
---
## 1. 现状:三套框架并存
| 框架 | 位置 | 编写方式 | 构建/注册 |
|---|---|---|---|
| **Google Test(目标形态)** | `tests/gtest/*.cpp` | `TEST()/TEST_F()/TEST_P()`,单一 `olive-gtest` 二进制 | `tests/gtest/CMakeLists.txt`,共享 `main.cpp`QApplication + offscreen + OCIO |
| 自研 OAK 宏框架 | `tests/timeline/timeline-tests.cpp``tests/compositing/compositing-tests.cpp` | `OAK_ADD_TEST(name)` + `OAK_ASSERT(x)` | `tests/CMakeLists.txt``olive_add_test()` 宏,正则扫宏生成 `main()` |
| 纯 C assert | `engine/tests/oakengine_*_test.cpp``core/tests/oakcore_*_test.cpp` | 手写 `main()` + `assert()` | `engine/CMakeLists.txt``make_oakengine_test()`,每个文件一个独立 ctest 二进制 |
## 2. 为什么统一到 Google Test
1. **断言可读性**`assert(x)` 失败只说行号;`EXPECT_EQ(a, b)` 打印左右值,
定位快一个数量级。这正是近几轮 facade 调试里最痛的一点。
2. **fixture 替代手工样板**:纯 C 测试里每个文件都手写 `oakengine_init` /
`setenv("XDG_*")` / 临时目录 / `oakengine_project_free`gfixture 的
`SetUp()/TearDown()`/`SetUpTestSuite()` 一次性收口。
3. **`GTEST_SKIP()`**:GPU/缺资源用例优雅跳过(offscreen OpenGL 不可绘现在
靠崩/超时区分,不好维护)。
4. **过滤与重复**`--gtest_filter`、重复运行(压 flaky)、死亡测试。
5. **`assert()` 在 NDEBUG 下被吞**:纯 C 测试一旦开 Release 编译就形同虚设,
这是统一的硬理由之一。
## 3. 目标结构
```
tests/gtest/ # app 集成测试(已是 gtest,保持不变,按需并入新用例)
engine/tests/ # liboakengine facade 测试,改写为 gtest
CMakeLists.txt # 一个 oakengine_gtest 目标 + gtest_discover_tests
core/tests/ # liboakcore 测试,改写为 gtest
CMakeLists.txt # 一个 oakcore_gtest 目标 + gtest_discover_tests
tests/timeline/ # 删除 olive_add_test 产物,timeline-tests.cpp 改写为 gtest
tests/compositing/ # 同上
```
- `tests/CMakeLists.txt``olive_add_test()` 宏与 `tests/testutil.h`
`OAK_ADD_TEST`/`OAK_ASSERT`/`OAK_TEST_END` 宏全部删除。
- `engine/CMakeLists.txt``make_oakengine_test()` 宏删除。
- 每个新 gtest 二进制经 `gtest_discover_tests(<target>)` 进 ctest**ctest 总
用例数不得少于迁移前**(迁移前列一张基线清单核对)。
## 4. 转换配方
### 4.1 OAK_ADD_TEST 宏框架(tests/timeline、tests/compositing
| 旧 | 新 |
|---|---|
| `OAK_ADD_TEST(name)` | `TEST(SuiteName, name)` |
| `OAK_ASSERT(x)` | `ASSERT_TRUE(x)` |
| `OAK_ASSERT_EQUAL(a, b)` | `ASSERT_EQ(a, b)`(自定义宏会打印左右值,直接换掉) |
| `TIMELINE_TEST_START`ColorManager::set_up_default_config + Project + Sequence | `class TimelineTest : public ::testing::Test { void SetUp() override {...} }` |
| `OAK_TEST_END / return OLIVE_TEST_SUCCESS` | 删除(gtest 自动判过) |
例:
```cpp
// 旧
OAK_ADD_TEST(add_track) {
TIMELINE_TEST_START;
OAK_ASSERT(sequence.track_list(Track::k_video)->get_track_count() == 1);
}
// 新
TEST_F(TimelineTest, AddTrack) {
ASSERT_EQ(sequence.track_list(Track::k_video)->get_track_count(), 1);
}
```
### 4.2 纯 C assertengine/tests、core/tests
| 旧 | 新 |
|---|---|
| 手写 `int main()` | 删除,链接共享 gtest main |
| `assert(x)` | `ASSERT_TRUE(x)` / `EXPECT_TRUE(x)` |
| `assert(fabs(a-b) < eps)` | `EXPECT_NEAR(a, b, eps)` |
| `assert(strcmp(a, b) == 0)` | `EXPECT_STREQ(a, b)` |
| `make_tmpdir()` + `setenv("XDG_*")` | `SetUpTestSuite()` 里建一次临时目录 |
| 每文件自带 `oakengine_init/shutdown` | 共享 fixture 做(见 §5.2 |
**纯 C ABI 测试照写 C 调用**gtest 文件是 C++,直接 `#include "oakengine/xxx.h"`
`oakengine_*` 函数即可,不需要把被测 API 改成 C++。断言里出现
`OakEngineNode*` 等不透明句柄比较用 `EXPECT_EQ((void*)a, (void*)b)`
**过渡期技巧(可选,不推荐长期使用)**:文件量太大时可先加一个
`#define assert(x) ASSERT_TRUE(x)` 的兼容头,把 `main()` 删掉挂进 gtest
再逐文件把 `assert` 换成语义化 `EXPECT_*`。但**最终态不许留 `assert()`**。
### 4.3 已是 Google Test 的(tests/gtest
不动。新增的 engine/core 用例如需 app 侧对象,可直接加进 `olive-gtest` 目标。
## 5. 落地步骤(按顺序,每步闭环:构建 + 全量 ctest 绿 + 提交)
### 5.1 基线
先跑 `ctest -N` 记录迁移前用例总数,存为 `docs/zh/gtest-migration-baseline.md`
(迁移后对比,总数只增不减)。
### 5.2 共享 fixture/main
- `core/tests/main.cpp``RUN_ALL_TESTS` + `SetUpTestSuite` 建 XDG 临时目录。
- `engine/tests/main.cpp`:同上,外加 `oakengine_init(OAKENGINE_INIT_HEADLESS)`
`TearDownTestSuite``oakengine_shutdown()``OAK_TEST_SOURCE_DIR`
`target_compile_definitions` 传入(照 `make_oakengine_test` 现有做法)。
- XDG 沙箱**每个二进制一份**,不要每个测试一份(与现状一致,避免并发冲突)。
### 5.3 core/tests(最小、无 Qt,先练手)
逐文件改写 `oakcore_*_test.cpp` 为 gtest,删手写 main;建 `oakcore_gtest` 目标
(链 `oakcore` + `GTest::gtest` + `GTest::gtest_main`),`gtest_discover_tests`
全量 ctest 绿后提交。
### 5.4 tests/timeline、tests/compositingOAK 宏框架)
按 §4.1 改写;建独立 gtest 目标或并入合适目标;删除 `olive_add_test` 调用、
`tests/testutil.h` 宏与 `tests/CMakeLists.txt` 中的宏定义。全量 ctest 绿后提交。
### 5.5 engine/testsfacade 测试,量最大)
按 §4.2 改写 `oakengine_*_test.cpp`;建 `oakengine_gtest` 目标(链 `oakengine`
+ Qt + gtest),删 `make_oakengine_test`。全量 ctest 绿后提交。
### 5.6 收尾
- `ctest -N` 对比基线(只增不减);全量 `--output-on-failure` 绿。
- 全仓库 grep 确认无 `OAK_ADD_TEST`/`OAK_ASSERT`/`make_oakengine_test`/
`olive_add_test` 残留。
- 更新 `docs/zh/` 相关文档与本指引标注"已完成"。
## 6. 注意事项(别踩坑)
1. **GPU/渲染用例**:沿用 `GTEST_SKIP()` 判定(参
`tests/gtest/render_worker_footage_test.cpp` 的 backend 检查与
viewer_display_repro_test 的 offscreen 跳过模式),不许靠超时/崩溃区分。
2. **offscreen/OCIO**:需要 QApplication 的用例共享 `tests/gtest/main.cpp`
环境初始化(offscreen QPA + OCIO 配置);engine/core 的无头用例走
`oakengine_init(HEADLESS)`,不要重复造 QApplication。
3. **测试数据路径**`OAK_TEST_SOURCE_DIR` 必须经 CMake 定义传入
`tests/demo.mp4` 等),不要硬编码相对路径。
4. **线程/事件**facade 事件类测试(`oakengine_events_test` 等)依赖
DirectConnection 同步语义,迁移时保持原用例的线程假设,不要引入
`QCoreApplication::processEvents` 之外的等待方式。
5. **一次性迁移 vs 渐进**:按 §5 的顺序渐进,**禁止**先删框架再慢慢补测试
(会造成不可测试的空窗)。每步都必须全量绿。
6. **ctest 仍是入口**CI/本地都继续用 `ctest --output-on-failure -j$(nproc)`
`gtest_discover_tests` 注册后,单个用例可用 `ctest -R <SuiteName.CaseName>`
`./<binary> --gtest_filter=...` 跑。
+319
View File
@@ -0,0 +1,319 @@
# RIIR 绞杀者模式执行计划:liboakengine 模块化拆分与渐进式 Rust 重写
> 本文档描述在 C ABI 迁移战役(见 `c-abi-migration-handoff.md`)完成之后,
> 如何用绞杀者模式(Strangler Fig)把 liboakengine.so 安全地拆成若干小模块,
> 再逐个重写为 Rust。
> **核心约束:每一步都可验证、可回退;任何一步失败都不影响已验证的部分。**
>
> 本文档面向未来的执行者(可能没有本文写作时的对话上下文),因此关键决策、
> 依据和验证方法都写成自包含的形式。与交接文档的关系:交接文档管"ABI 迁移战役"
> (消灭 oak-editor 对 olive:: 的引用、liboakengine 只导出 oakengine_*),
> 本文档管那之后的"拆分与重写战役"。**拆分的前置条件是 ABI 迁移完成(§1.1)。**
---
**API 冻结保证(最高优先级约束,先于一切拆分)**
**拆分模块,但公共 API 一个不动。** 这是整个战役的硬约束,凌驾于任何
"拆得更细"的冲动之上。三条钉死:
1. **公共 `oakengine_*` 全程冻结。** 在整个拆分与 Rust 重写期间,
`engine/include/oakengine/*.h` 里的每个公开函数:不改签名、不删函数、
不改语义。唯一允许的变更是**新增**函数,且必须标注 experimental。
新增不等于变更——既有签名与语义一个字都不许动。
2. **`liboakengine-facade` 自身不拆分。** facade 是唯一、薄、稳定的路由层,
公共 `oakengine_*` 全部保留在这里。拆分发生在它**之下**:facade 的内部
实现从"直接调 C++"改为"转发给对应小库的 C ABI",但**对外暴露的符号表
与调用约定完全不变**——app / oak-cli / render-worker / 未来 AI Agent
只链接 facade,连重新链接都不用。
3. **模块间内部 C ABI 与公共 API 分层、分别版本化。** 拆分后模块之间
(如 oakrender 调 oakmodel)不能再 C++ 直连,必须走**新增的内部 C ABI**。
这层接口:(a) 只对模块间可见,app 永远看不到;(b) 与公共 API 分开管理、
允许演进;(c) 命名与头文件路径必须明显区别于公共 facade(例如放
`engine/include/oakinternal/`,前缀 `oakinternal_`),杜绝"内部接口慢慢
变成事实公共 API"的漂移。公共 `oakengine_*` 另加**版本字段**
`oakengine_api_version()`),让任何公共面的意外漂移可被检测。
> 一句话:**缝(公共 facade)冻死,缝后面的实现随便拆随便换。**
> 任何执行步骤如果会改动公共 `oakengine_*` 的既有签名或语义,就是走错了,
> 停下来回到本节。
---
## 0. 为什么这条路是可行的(三个已验证的事实)
1. **绞杀缝已经存在。** 迁移战役的最终产物就是一条稳定、纯 C、带测试覆盖的
ABI 缝(`engine/include/oakengine/*.h`,~30 个头、20+ 族)。绞杀者模式最危险
的一步——"在没有缝的系统里造缝"——已经由当前战役完成。
2. **插件模式在本仓库已跑通。** `ffmpeg_bridge`(独立 .soC ABI)、
`oakgl`/`oakvulkan`(渲染后端插件,经 `engine/render/backend/renderbackend_c.h`
的 C ABI 由 `DynamicRenderer` 动态加载)证明"小 .so + C ABI + 运行时替换"
在本代码库不是理论,是现状。本计划只是把同一模式推广到全引擎。
3. **验证资产现成。** 44+ ctest、~2000 条 gtest、oak-cliinfo/probe/render/
transcode,含 PPM 帧输出)、worker 端到端 harnessNDJSON 协议真实渲染)、
测试素材(demo.mp4/img.png/project_with_footage.ove)。每一步验证不需要
新建测试体系,只需要把它们固化为"门禁脚本"。
---
## 1. 目标、前置条件与非目标
### 1.1 前置条件(未满足前不动工)
- ABI 迁移战役完成:oak-editor / oak-render-worker `U _ZN5olive` = 0
`nm -D --defined-only liboakengine.so | grep -c " T _Z"` = 0visibility 收口);
全量 ctest 绿。(即交接文档 §1 的四条验收。)
- 本文 §4.2 的基础设施(Rust 工具链接入 + 门禁脚本)就位。
> **R6 收尾状态(2026-07-26 实测)**
> - ✅ oak-editor / oak-render-worker `U _ZN5olive` = **0**R5→R6 迁移战役完成,
> nm 58→0;交接文档 §6.4 豁免清单已清空为"无豁免");
> - ✅ 全量构建 0 error、全量 ctest 绿(45/45);app↔engine 边界对 app 的
> 引用而言已是纯 C ABI——**"边界已纯"**。
> - ⏳ **遗留项(不属 R6 范围,S0 需复核)**engine 侧 visibility 收口未做——
> `nm -D --defined-only liboakengine.so | grep -c " T _Z"` 实测 = **3486**
> (导出 C++ 符号),尚未降到 0。该子条件是独立的收口工作(§2 Step 2 的
> visibility=hidden 规则),app 侧已无任何引用,收口不影响 app。
> - 已知遗留(已论证,不泄漏符号):app 仍 include 约 40 个 engine C++ 头
> node/render/timeline/undo/pluginSupport,用于类型与 inline 访问器),
> nm=0 证明不产生符号引用;彻底清理超出 R6 的 58 符号目标,留待后续批次。
### 1.2 终态
- `liboakengine.so` 不复存在,取而代之的是一组小动态库(§3 模块图),
每个只有两种实现状态:C++(待重写)或 Rust(已重写)。
- app/worker/cli 只链接 **facade 壳库**`liboakengine-facade`),对下层模块
的实现语言无感知。
- 任何模块的 Rust 替换都经过 §2 的六步流程,全程有 C++ 版本可回退,
直到 G6 退役门禁通过。
### 1.3 非目标(明确不做)
- 不重写 Qt、FFmpeg、OpenColorIO、PortAudio 等第三方库本身。
- 不重写 app(UI 层保持 C++/Qt;它消费的本来就是 C ABI)。
- 不改变 `oakengine_*` 公开 facade 的任何既有签名(拆分/重写只许改实现,
不许动契约;新增内部 ABI 允许,但必须符合同一套头文件规则)。
- 不做 big-bang:任何时刻整个系统都必须可构建、可测试、可发布。
---
## 2. 绞杀六步(每个模块的统一流程)
对每一个模块 X,严格按以下顺序执行;每步有对应门禁(§5),不过门禁不进下一步。
### Step 1 — 冻结 ABI
- 评审模块对外 C ABI 头(公开 facade 已有部分直接复用;模块间内部调用需要的
新增内部头,按 `c-abi-migration-handoff.md` §6.1 的同一套规则写:纯 C 类型、
buf/size 约定、owned/borrowed 注释、错误码)。
- 用门禁脚本生成 ABI 快照(§5-G1)并入库。**此后该头的任何改动都是显式评审行为。**
### Step 2 — 物理拆分(C++ 实现原样搬出)
- 新建 `liboakengine-<X>.so`:把该模块源码从 liboakengine 移入独立 CMake 目标;
原引擎内其他部分对它的 C++ 调用**全部改走它的 C ABI**。
- 新库同样 visibility=hidden + 只导出 C 符号。
- 过 G2:构建绿、全量 ctest 绿、符号审计绿、ABI diff = 0。
- **此步不改任何行为**——只搬代码和改调用方式。发现行为必须改才能拆的,
停下来记录,先回去补 facade(回 Step 1)。
### Step 3 — Rust 影子实现
- `rust/<X>/` 建 cdylib crate,实现与 Step 1 完全相同的 C ABI。
- cbindgen 生成的头与 C++ 头做规范化 diff(§5-G3),必须一致。
- FFI 边界硬规则:`catch_unwind` 全包裹(panic 不得跨 FFI)、错误码语义逐条
对齐、owned/borrowed 生命周期按注释实现(`Box::into_raw` / 借用引用)、
回调线程语义按契约复现(见 §6.2)。
### Step 4 — A/B 双跑
- CMake 选项 `OAK_MODULE_<X>_IMPL=cpp|rust` 控制链接哪个实现。
- 两种配置各自全量构建 + 全量 ctest + 金标准对比(§5-G4)。
- **帧级一致**:渲染输出字节一致或 PSNR ≥ 50dB**序列化 round-trip 字节一致**
其余以测试断言为准。
### Step 5 — 切换默认实现
- 默认实现切到 rust;CI 三平台构建 + 全量测试。
- C++ 实现保留一个发布周期作为回退选项(option 切回即可)。
### Step 6 — 退役
- 删除该模块的 C++ 实现与 cpp 构建分支;ABI 快照锁定为最终态;全量回归(G5 同项)。
- 在 roadmap 记录该模块重写完成。
---
## 3. 模块图与拆分顺序
### 3.1 依赖方向(单向,禁止循环;上层只经下层 C ABI 调用)
```
app / oak-cli / oak-render-worker
liboakengine-facade(壳:capi + 事件 + init
┌────────┬────────┼─────────┬──────────┐
oaktask oakrender oakplugin oakaudio oakserialize
│ │ │ │ │
└────────┴────┬───┴──────────┴──────────┘
oakmodel(节点图 + 项目模型 + 时间线模型)
┌────────┼─────────┐
oakcodec liboakcore oakbackendGPU 插件:oakgl/oakvulkan/未来的 Rust 后端)
ffmpeg_bridge(已是 C ABI .so
```
**关键架构事实(拆分顺序的依据)**
- `Node` 及其子类簇(Project/Folder/Footage/Sequence/Block/Track/Clip/Gap/
Transition/Subtitle/各效果节点)是 C++ 继承绑死的**不可拆分类型簇**——
跨模块做 C++ 继承不可能不导出 C++ 符号。因此它们必须整体作为一个模块
(oakmodel)处理,重写时也作为一个重写单元。这是本计划最大的一个拆分
粒度结论,不要再试图把 Block/Track 从 Node 里拆出去。
- `oakcore`liboakcore.so)已是纯 C ABI 独立库,是天然的第一块 Rust 试验田
(见 M0)。
- GPU 后端已是插件,重写线与主线解耦(见 §7)。
### 3.2 执行顺序(依赖最少、Qt 最少、验证最容易的在前)
| 批次 | 模块 | 内容 | 前置依赖 | 主要风险 |
|---|---|---|---|---|
| M0 | **oakcore** | liboakcore 整体(rational/timecode/bezier/samplebuffer/audioparamsQt-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 必须字节一致 |
| 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 | 最高;最大类型簇 |
| M7 | **oaktask** | Task/TaskManager 及各任务类型 | oakmodel、oakrender | 中;QtConcurrent |
| M8 | **facade 壳 + 收尾** | capi 各实现、事件注册表、coreengine、config、plugin(OFX) | 全部 | 中;事件机制 Rust 化 |
每个批次内部都走 §2 的六步。**严格串行**:上一批次 G5 完成才开下一批次
(M0 例外,可与 ABI 迁移战役收尾并行准备)。
**为什么 oakmodel 排第六而不是第一**:它是依赖中心,先拆它会导致所有模块
都要先等它的内部 ABI 定型;先拆叶子模块可以用公开 facadenode.h/project.h/
timeline.h,本就是为外部消费设计的)充当模块间缝,缝的质量先被实战检验,
最后拆 oakmodel 时它的对外接口已经是稳定态。
---
## 4. 阶段计划
### 4.1 阶段 S0:前置确认(0 成本,只是检查)
- 对照 §1.1 逐条核对 ABI 迁移战役验收结果。未完成则停止,回到交接文档。
### 4.2 阶段 S1:重写基础设施(第一批真正的活)
1. **Rust 工具链接入**:仓库根建 `rust/` workspaceCMake 集成用 Corrosion
cmake+cargo 标准方案);`cargo`/`cbindgen` 版本锁定并写进构建文档。
禁止要求全局安装 cargo 之外的 Rust 组件(CI 可复现)。
2. **门禁脚本固化**(全部进 CI):
- `scripts/abi-dump.sh`:对每个相关 .so 导出 `oakengine_*`/`oakcore_*` 符号 +
头文件规范化哈希,产出快照文件;`scripts/abi-check.sh` 与入库快照 diff。
- `scripts/golden-render.sh`oak-cli render demo.mp4 指定帧 → PPM
与金标准比对(字节一致或 PSNR ≥ 50dB);`oak-cli transcode` PPM 同理;
project_with_footage.ove 序列化 round-trip 字节一致;worker E2E harness。
- `scripts/symbol-audit.sh`:现有 nm 三件套的脚本化(app 侧 `U _ZN5olive`
各 .so `T _Z`、facade 测试覆盖审计)。
3. **M0liboakcore Rust 重写**(试点)。完整走六步,目的是把工具链、A/B 流程、
门禁全部打通并暴露问题。它是全仓库最小最干净的模块,失败成本最低。
M0 没全绿之前,不允许排产任何后续模块。
### 4.3 阶段 S2S8M1M8
按 §3.2 表格逐模块执行。每模块的"模块档案"(边界清单、Qt 依赖清单、
信号清单、线程语义、验证重点)在 Step 1 时补写到本文 §6 对应小节。
---
## 5. 验证门禁(每步必须过,脚本化、进 CI)
| 门禁 | 触发步 | 内容 | 通过标准 |
|---|---|---|---|
| G0 | 每批开始 | 全量构建 + 全量 ctest + golden-render 基线快照 | 全绿,快照入库 |
| G1 | Step 1 | abi-dump 快照 | 与上一基线 diff 仅含本批新增 |
| G2 | Step 2 | 构建 + 全量 ctest + 新库符号审计 + ABI diff | 全绿;新库导出仅 Cdiff=0 |
| G3 | Step 3 | cbindgen 头 vs C++ 头规范化 diffcrate 单测 | diff=0;单测全过 |
| G4 | Step 4 | 双实现配置各自全量测试 + golden 对比 | 两轮全绿;帧/序列化一致 |
| G5 | Step 5 | 三平台构建 + 全量测试 + 性能抽测 | 全绿;渲染帧耗时回退 ≤10% |
| G6 | Step 6 | 删除 C++ 实现后全量回归 + ABI 快照锁定 | 全绿;快照入库 |
- **性能抽测**golden-render 脚本记录渲染耗时,Rust 版慢于 C++ 版 10% 以上
必须查明原因(允许记录后放行,但不允许无声劣化)。
- **回退规则**:任何门禁失败 → 停止该模块,切回 cpp 实现(Step 4 之后才有
可切对象;之前是天然回退态),记录原因,系统保持全绿。
---
## 6. 跨模块设计约束(全部钉死)
### 6.1 信号/通知的 Rust 化
- engine 的 QObject 信号是当前变更通知机制;capi/events.cpp 用 `dynamic_cast`
校验订阅 handle 的族类型。**Rust 对象没有 dynamic_cast**,因此在 M5oakmodel
前置)之前必须引入**句柄类型标签约定**:所有 facade 句柄指向的对象首字段为
`uint32_t type_tag`(枚举值入 ABI 头),events.cpp 的族校验改为读标签。
这是对 events.cpp 的授权内改动,需配事件实测用例。
- Rust 模块的变更通知:经同一张 oakengine_event 注册表发射(事件机制是
app 侧唯一通道,Rust 模块只是换了发射端的实现语言)。
### 6.2 线程语义(ABI 契约,Rust 必须逐条复现)
- facade 回调/事件 = 发射线程同步调用(Qt::DirectConnection 等价),引擎对象
属 GUI 线程;回调内不得反调改同一对象的编辑原语。
- 渲染在后台线程(当前 QtConcurrent);Rust 侧线程模型自选(std::thread/
rayon/自建池),但**回调触发线程与顺序语义必须与原实现一致**;worker
NDJSON 协议的线程行为不得改变。
- 每模块 Step 1 时必须把该模块涉及的线程归属写进模块档案。
### 6.3 内存与生命周期
- owned/borrowed 规则按各 facade 头注释执行;Rust 侧 owned 句柄 `Box::into_raw`
free 函数 `Box::from_raw` 回收;borrowed 句柄不接管析构。
- 禁止在 FFI 边界传递任何 Rust 特有类型(String/Vec/ trait object);边界上只有
C 类型,与现有头文件规则相同。
### 6.4 错误与 panic
- panic 不得跨 FFI`catch_unwind` 全包裹,映射为 `OAKENGINE_E_FAILED` +
last_error 字符串)。错误码语义与 C++ 实现逐条一致(A/B 对比时断言)。
### 6.5 全局单例
- Config/NodeFactory/RenderManager/AudioManager 等单例,Rust 侧用 `OnceCell`/
显式注册表实现;初始化/销毁时机挂在 `oakengine_init`/`oakengine_shutdown`
既有钩子上,不引入新的隐式初始化。
### 6.6 第三方库
- FFmpeg:继续经 `ffmpeg_bridge`(已是 C ABI),Rust 模块链接桥库而非直接绑 FFmpeg。
- OCIOColorManager 是 C++ API 重度用户,M5 时评估:薄 C 封装进 facade vs
保留 C++ 微库长期共存(允许作为长期 C++ 孤岛,写入 roadmap)。
- Qt:只允许 facade 壳与 app 侧使用;M1 起的各引擎模块实现内**不得新增 Qt 依赖**
(QtCore 容器可暂用,但不得新增 QObject 信号、moc 类)。
---
## 7. GPU 后端平行线
- oakgl/oakvulkan 已是 `renderbackend_c.h` C ABI 插件,与主线解耦。
- Rust 后端(建议 wgpu 起步)作为**新插件**并行开发,通过同一 ABI 被
DynamicRenderer 加载;验收用现有 Backends gtestVulkan 用例即现成的
A/B 对比器——同一测试分别加载两个后端跑)。
- 不替代主线任何模块门禁;独立排期,不阻塞 M1–M8。
---
## 8. 风险登记册(开工前评审,施工中持续更新)
| 风险 | 等级 | 对策 |
|---|---|---|
| oakmodel 类型簇过大,六步周期过长 | 高 | Step 2 允许分子批拆分(先项目模型后效果节点),但 ABI 一次冻结 |
| 线程语义偏差导致偶发黑屏/崩溃 | 高 | G4 双跑必须包含 worker E2E 与 Backends viewer 用例;引入压力重复(每用例 ×10) |
| OCIO 无法 Rust 化 | 中 | 允许 C++ 孤岛(§6.6),不影响其他模块 |
| QtConcurrent 行为差异 | 中 | M5/M7 档案逐条记录并发模式;A/B 含并发压力 |
| cbindgen 头漂移 | 中 | G3 规范化 diff 进 CI,漂移即红 |
| 构建复杂度(cargo+cmake)拖慢迭代 | 中 | Corrosion 单一入口;文档固化;禁止手工 rustc |
| 行为不可拆(Step 2 发现必须改行为才能拆) | 中 | 回 Step 1 补 facade roadmap 记录;禁止带行为变更进 Step 2 |
| 性能劣化 | 低 | G5 抽测阈值;剖析后放行或回退 |
---
## 9. 里程碑摘要(可直接抄进项目计划)
1. **S1 完成**:Rust 工具链 + 门禁脚本进 CIM0liboakcoreG6 退役。
2. **M1M2 完成**:音频 DSP 与编解码 Rust 化;AudioProcessor 豁免项消除。
3. **M3M4 完成**:序列化与 undo Rust 化;项目文件 round-trip 金标准常青。
4. **M5 完成**:渲染管线 Rust 化(OCIO 孤岛与否已裁决并记录)。
5. **M6 完成**oakmodel Rust 化——**最大里程碑**,此后 liboakengine 主体为 Rust。
6. **M7M8 完成**:任务系统与 facade 壳 Rust 化;liboakengine.soC++ 版)正式退役。
7. **GPU 平行线**Rust 后端插件经 Backends 双后端测试验收。
+213
View File
@@ -0,0 +1,213 @@
# Oak 主界面 UI 改版计划
> 本文是主界面重新设计的执行手册,面向 DeepSeek Flash**不识字图,本文全部
> 用文字精确定义目标形态**)。详细程度对齐 `../r5-app-migration-guide.md`。
> 工作分支:`c-abi-migration`。**启动前提:R5C ABI app 侧迁移)验收完成之
> 后**;启动后可与 Google Test 统一迁移并行——协调规则见 §2。
> 依据:`design/Oak-UI设计图-主界面-标注版.png`、`...-效果栈版.png`、
> `...-节点编辑器版.png`(共 10 项关键改动,本文逐项落地)。
---
## 1. 目标布局(文字定义,照此实现)
```
┌ 菜单栏:文件(F) 编辑(E) 视图(V) 回放(P) 序列(S) 窗口(W) 工具(T) 帮助(H)
├ 工具条(31px):14 个工具图标 + 吸附开关 + 缩放滑块 + 轨道高度滑块
├────────────────────────────────────────────────────────────────────
│ 素材查看器(源) │ 序列查看器(节目) │ 检查器│历史记录
│ ·适合/安全框 │ ·适合/安全框 │ ┌──────────────────┐
│ ·独立走带控制 │ ·节点编辑器(同组切换)│ 媒体 · xxx.mp4 │
│ ·1920×1080·25FPS │ ·分辨率·帧率信息 │ ▼ │
│ │ │ ≡ 变换 ✓ ×(卡片) │
│ │ 26px电平条(右缘) │ ≡ OCIO LUT ✓ ×(卡片)│
│ │ │ [+ 添加效果] │
│ │ │ └──────────────────┘
├────────────────────────────────────────────────────────────────────
│ 时间线(全宽贯通) │
│ 轨道头180px │ 轨道区 │
│ V2 视频轨道1 [锁定][显示] ████████ │
│ V1 视频轨道0 [锁定][显示] ████████████ │
│ A1 音频轨道0 [锁定][静音][独奏] ▁▂▃▅▂▁ │
│ A2 音频轨道1 [锁定][静音][独奏] ▁▂▁▃▂▁ │
├────────────────────────────────────────────────────────────────────
│ 状态栏:就绪 | 缓存:已启用 | 代理:关 | 自动保存:3分钟前 || 时间码/时长 | 25FPS | 1920×1080
└────────────────────────────────────────────────────────────────────
```
节点编辑器视图(与序列查看器同组切换、占中央最大面板):
```
┌ 节点编辑器(中央最大面板)
│ [+] [-] [适配] ┌ 检查器(同效果栈) ┐
│ ┌ 第一稿.mp4[视频] · 00:00:00:0000:04:18:18 ┐ │ │
│ │ [媒体]→[变换]→[OCIO LUT]→[输出] │ │ │
│ └───────────────────────────────────────┘ │ │
│ ┌ 第一稿.mp4[音频] · 00:00:00:0000:04:18:18 ┐ │ │
│ │ [媒体]→[音量]→[输出] │ │ │
│ └───────────────────────────────────────┘ │ │
│ ┌ 小地图 ┐ │ │
└────────────────────────────────────────────────────────────────────
```
**核心原则:效果栈(检查器)与节点图是同一份节点数据的两种视图**——检查器按
「媒体 → 变换 → OCIO LUT → 输出」自上而下线性排卡片;节点编辑器把同一份数据
画成图。默认用户像传统软件一样在检查器里线性工作,需要分支合成时切到节点
编辑器。
## 2. 执行前提与并行协调
### 2.1 执行前提:R5 验收完成后启动
**本计划在 R5(C ABI app 侧迁移)验收完成之前不启动。** 这不是保守,是
依赖关系:
- WP4(检查器·效果栈)落在 `app/widget/nodeparamview/`、WP2(时间线轨道
头)落在 `app/widget/timelinewidget/`、WP5(节点编辑器移位)落在
`app/widget/nodeview/`——这些都是 R5 符号消除的主战场。R5 先把这些文件
的 engine 调用点换到 facade(行为不变),本计划再在其上做 UI 重构,
面对的才是干净的 facade 边界;提前动手只会和 R5 互相踩踏。
- R5 完成后不存在文件重叠问题,**全部 WP 无需错峰**,按 §4 顺序执行即可。
启动时仍需遵守的红线(R5 的成果,永久有效):
- **禁止**:在本计划里改 `oakengine_*` 签名、新建 engine 命令类、或把
engine 源码再编进 app。
- 全部改动限 **app 侧 UI 代码**`app/`),不加 engine 符号。
### 2.2 与 Google Test 统一迁移(`gtest-migration-guide.md`)的并行协调
**结论:可以完全并行,无错峰要求。** 两份计划的文件域不相交:
| 计划 | 动的文件 |
|---|---|
| UI 改版(本文) | `app/`widget、panel、window、dialog、ts 翻译) |
| GTest 统一迁移 | `tests/``engine/tests/``core/tests/` 及三处测试 CMake |
唯一的接触点与规则:
- **`tests/gtest/`**GTest 迁移明确"不动已有 gtest";本计划 §5 要求新增
UI 逻辑用例,新用例**直接加进 `tests/gtest/` 现有 `olive-gtest` 目标**
不新建测试二进制。两边若同时改 `tests/gtest/CMakeLists.txt`,后提交者
普通三路合并即可(都是追加行,不会语义冲突)。
- **ctest 基线**GTest 迁移的硬门槛是"用例总数只增不减";本计划只**新增**
用例、不删不改旧用例,天然满足。两边都以全量
`ctest --output-on-failure` 绿为提交前提,谁先跑谁后跑无所谓。
- **共享入口约定**:ctest 仍是唯一运行入口(gtest 仅编写框架),本计划
新增用例同样遵守,不引入别的测试框架或独立 runner。
- **已知 flaky**`oak_cli_transcode``oakengine_export_test`
`olive-gtest` 单次失败需单独重跑)是两个计划共同的背景噪音,判定规则
相同:单独重跑一次,连续两次失败才算回归。
## 3. 工作包(WP1WP10,对应设计图 10 项)
### WP1 取消独立「工具」面板 → 31px 工具条
- **现状**`app/panel/tool/tool.{h,cpp}` 是独立停靠面板,~8% 屏幕仅放 14 个图标。
- **目标**:删除该面板;在时间线(`app/widget/timelinewidget/`)上方加一条 31px
工具条,承载 14 个工具图标 + 吸附开关 + 缩放滑块 + 轨道高度滑块。
- **文件**:删/改 `app/panel/tool/``app/widget/timelinewidget/timelinewidget.{h,cpp}`
顶部加工具条;`app/panel/CMakeLists.txt``app/panel/panelmanager.cpp` 注册点。
- **验证**:工具条全部按钮功能与原面板一致;布局保存/恢复无该面板。
### WP2 时间线全宽贯通 + 轨道头 180px
- **现状**:时间线未全宽;轨道头窄,无统一的 显示/静音/独奏/锁定 与 V/A 编号命名。
- **目标**:时间线全宽;轨道头加宽至 180px 贴住轨道;视频轨「显示」、音频轨
「静音/独奏」、统一「锁定」;轨道以 `V2/V1/A1/A2` 编号 + 用途名(与主流 NLE 一致)。
- **文件**`app/widget/timelinewidget/`timelinewidget、trackview、trackviewitem、
timelineview)。**前提:R5 已完成 Track/ClipBlock facade 化,本 WP 在其上重构。**
- **验证**:轨道头控件改变 track 的 lock/mute/solo/show 状态;命名正确;全宽布局。
### WP3 双监看并列(源 + 节目)
- **现状**`app/panel/footageviewer/`(源)与 `app/panel/sequenceviewer/`(节目)分开,
审素材对位需切换标签。
- **目标**:左右并排常显;素材查看器带独立走带控制(transport)。
- **文件**`app/window/mainwindow/mainwindow.cpp` 布局;`app/panel/footageviewer/`
`app/panel/sequenceviewer/`KDDockWidgets 分组/dock 关系)。
- **验证**:两查看器同屏并列;源查看器独立走带;布局可保存/恢复。
### WP4 参数编辑器 → 「检查器·效果栈」
- **现状**`app/widget/nodeparamview/` 是参数编辑器(item 列表 + 标题栏 + 关键帧控件)。
- **目标**:改为「检查器」面板(与「历史记录」同组标签)。自上而下线性排:
「媒体 · xxx.mp4」源卡 + 效果卡(变换、OCIO LUT、音量…)。**卡片规范**:
标题行 = ≡(拖拽排序) ▼(折叠) 名称 ✓(启停) ×(移除);底部「+ 添加效果」即搜即加。
每属性行右侧保留关键帧按钮(现 `nodeparamviewkeyframecontrol`)。
- **文件**`app/widget/nodeparamview/`nodeparamview、nodeparamviewitem、
nodeparamviewitemtitlebar、nodeparamviewdockarea、nodeparamviewwidgetbridge);
新增「检查器」容器/卡片模型。**前提:R5 已完成 Node 大族 facade 化,本 WP 在其上重构。**
- **验证**:卡片折叠/拖拽排序/启停/移除全部生效且写入节点图(undoable);
与节点编辑器视图数据一致;「+ 添加效果」可搜可加。
### WP5 节点编辑器移至中央最大面板 + 缩放/小地图
- **现状**`app/widget/nodeview/` 节点编辑器非中央主区,大图易迷路。
- **目标**:移到中央最大面板(与序列查看器同组切换);新增缩放控件(+/−/适配)
与右下角小地图。
- **文件**`app/widget/nodeview/`nodeview、nodeviewcontext、nodeviewminimap)、
`app/panel/node/``app/window/mainwindow/mainwindow.cpp`。**前提:R5 已完成 Node 大族 facade 化,本 WP 在其上重构。**
- **验证**:节点编辑器占中央;缩放/适配/小地图可用;大图导航不迷路。
### WP6 音频监视器 → 26px 电平条
- **现状**`app/widget/audiomonitor/``app/panel/audiomonitor/` 占一个完整面板。
- **目标**:改为 26px 超薄电平条,贴附在节目查看器右侧常显,释放一个面板。
- **文件**`app/widget/audiomonitor/``app/panel/sequenceviewer/`(宿主)、
`app/panel/audiomonitor/`(去面板化)。
- **验证**:电平条 26px 常显、随播放电平跳动;原面板释放;布局可恢复。
### WP7 新增全局状态栏
- **现状**`app/window/mainwindow/mainstatusbar.{h,cpp}` 仅显示 TaskManager 摘要。
- **目标**:全局状态栏显示:就绪状态、缓存、代理、自动保存时间(左);
当前时间码/时长、帧率、分辨率(右)。
- **文件**`app/window/mainwindow/mainstatusbar.{h,cpp}`、`app/window/mainwindow/
mainwindow.cpp`。
- **验证**:各项信息实时刷新;与序列状态/缓存/代理/自动保存一致。
### WP8 查看器细节
- **现状**:填充条为蓝色;缩放/安全框入口不全;信息芯片不全;有与时间线重复的标尺。
- **目标**:填充条改黑;查看器右上角提供缩放与安全框按钮;信息芯片标注
分辨率与帧率;移除与时间线重复的标尺。
- **文件**`app/panel/footageviewer/`、`app/panel/sequenceviewer/`、
`app/widget/viewer/`viewer、viewerdisplay)。
- **验证**:外观与信息符合 §1 描述;无重复标尺。
### WP9 菜单访问键补全
- **现状**`app/window/mainwindow/mainmenu.{h,cpp}` 的「窗口」无访问键 (W)。
- **目标**:「窗口」加 `(W)`,与系统及其他菜单项一致。
- **文件**`app/window/mainwindow/mainmenu.{h,cpp}`。
- **验证**:菜单访问键完整一致。
### WP10 文案与格式统一
- **现状**:History 未汉化;项目面板日期为英文格式;首选项有英文残留。
- **目标**:History → 历史记录;项目面板日期改 `YYYY-MM-DD HH:mm`(如
`2026-06-03 20:25`);首选项英文残留(Behavior、Enable hover focus、
`1 minute(s)` 等)全部汉化。
- **文件**`app/panel/history/`、项目面板(`app/widget/projectexplorer/`)、
首选项(`app/dialog/preferences/`)、`app/ts/*.ts` 翻译。
- **验证**:文案全部汉化、日期格式统一。
## 4. 执行顺序
R5 已验收完成(§2.1),无错峰约束。建议先做无依赖的布局项热身,再做
重构量大的三个 WP
```
第一波(布局类,互相独立): WP1 → WP3 → WP6 → WP7 → WP8 → WP9 → WP10
第二波(重构类,建议在 R5 facade 化后的干净边界上做):
WP4(检查器)→ WP5(节点编辑器)→ WP2(时间线轨道头)
```
**每 WP 闭环**:现状 grep → 实现 → 全量构建 0 error → 全量 ctest 44/44 绿
(UI 改动不得引入回归)→ 立即提交 → roadmap 补记。
## 5. 测试要求(沿用项目规则)
- 所有测试用 **Google Test**`CONTRIBUTING.md` 已立规)。
- 检查器卡片模型、效果栈↔节点图数据一致性、轨道头控件状态、状态栏信息、
工具条功能:补 Google Test 用例(`tests/gtest/``gtest_discover_tests`)。
- UI 行为改动以现有 `olive-gtest` 不回归为底线;新增可测逻辑(卡片模型、
视图模型)必须有单测。
- 需要显示的用例沿用 offscreen QPA;渲染相关用例沿用 `GTEST_SKIP` 模式。
## 6. 不做
- 不改 `oakengine_*` 公共 API(见 `riir.md` 的 API 冻结保证)。
- 不动 engine 内部实现、不动 R5 的 facade 工作。
- 不重写底层渲染/播放路径;本计划只改 UI 布局、容器与交互。
- 不做 AI 相关 UI(属 `ai-agent-design.md` 范围,另行)。
+179
View File
@@ -0,0 +1,179 @@
# R5 app 侧调用点迁移 — 实施指引
> 本文是 R5 阶段(消灭 oak-editor 对 `olive::` C++ 符号的引用)的执行手册。
> 面向没有此前对话记忆的执行者,自包含。
> 与 `c-abi-migration-handoff.md`v3)、`c-abi-migration-handoff-v4.md` 的关系:
> 那两份管 facade(C API)建设;本文管 app 侧把对 engine C++ 类的直接调用
> 换成 facade 调用。**facade 已就位且全绿,R5 不需要再新建 C API 族。**
>
> **工作分支:`c-abi-migration`。每完成一个文件/小步立即提交。**
---
## 0. 当前已验证状态(接手先复核,不要采信转述)
```bash
cmake --build cmake-build-debug -j$(nproc) # 必须 0 error
cd cmake-build-debug && ctest --output-on-failure -j$(nproc) # 必须 44/44 绿
nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive" # 当前 395
```
- 测试:**44/44 全绿**R1R4 facade 扩容已修复并通过)。
- 符号:**395**`nm` 实测,按类统计见 §3)。
- facade 族已全部存在且带测试:node / timeline / project / footage / preview /
audio / color / config / disk / encoding / events / gizmo / lut / plugin /
proxy / serializer / sync / task / traverse / undo / videoparams / viewer /
worker / playback / renderer / app。
- 事件 ID 已分配到 143,新事件从 **144** 起。
## 1. 关于"必须全文件集群重写、无法增量"——论断不成立
DS 报告称 R5 的符号"因 Q_OBJECT + 虚函数链 + 信号/槽耦合,必须全文件集群
同时重写,无法增量"。**这是错误的**,证据:
1. **前一次战役就是增量完成的**:同一套符号从 557 → 162,按 B1B11a 分批、
每批独立闭环。被回滚抹掉的是 app 调用点,不是方法。
2. **本阶段已经证明可增量**:符号已从回滚后的高点降到 395,全部是逐文件
替换得来的,没有一次"集群重写"。
3. **facade 已就位**:R5 需要的 C API 族全部存在。剩下的工作是把 app 里的
`olive::X::method()` 调用换成 `oakengine_x_method()`**不重写任何 engine
类**,自然不存在"虚函数链重写"。
4. **"信号/槽耦合"已被事件机制解决**:`oakengine_event_subscribe` +
`app/engineeventbridge.{h,cpp}` 就是替代 `connect(engineObj, &Engine::sig, ...)`
的标准通道(SOP 见 roadmap 附 D)。不需要为消信号而重写类。
结论:**R5 是机械的、可逐文件验证的调用点替换,按 §4 的顺序增量推进。**
"集群重写"是停工的借口,不是技术结论。
## 2. 增量方法(每个文件的标准动作)
对每一个 app 文件,按这个顺序做,做完立即提交:
1. **grep 该文件的 engine 直接引用**`olive::X::``connect(` 到 engine 对象、
`new SomeCommand(`engine 命令类)、engine 头 include。
2. **逐点替换**为 facade
- 方法调用 → 对应 `oakengine_*` 函数(§4 表)。
- `connect(engineObj, &Engine::sig, this, ...)`
`bridge->subscribe(handle, OAKENGINE_EVENT_*)` + 连 bridge 的 Qt 信号
(没有 bridge 成员就 `new EngineEventBridge(this)`)。
- `new EngineCommand(...)` 进 undo 栈 → facade 的 undoable 原语
(多数已有;确实缺的按 §5.2 补 facade,不要新建 app 侧命令类)。
- undo 压栈一律 `oakengine_undo_push(command, name)`**2 个参数**,全局栈,
不传栈句柄;`Core::undo_stack()` 返回的 `void*` 只作事件订阅 handle)。
3. **删不再需要的 engine 头 include**;加对应 `oakengine/*.h`
4. **构建 + 该文件相关测试 + `nm` 双度量**,符号应净减。
5. **立即提交**,提交信息写清文件与消掉的符号数。
**禁做**:为消符号新建 app 侧 engine 命令子类、给 facade 加"stub 实现"
(见 §6 红线)、为省事把 engine 源码再编进 app。
## 3. 剩余符号分布(395,nm 实测,按类)
执行顺序按"依赖最少、符号最密集、facade 最现成"排。每行:`数量 类 — 主战场文件 — 用哪个 facade 族`
### 第一批:工具/服务类(快、独立,先清 ~60)
| 数量 | 类 | 主战场 | facade 族 |
|---|---|---|---|
| 9 | QtUtils | 各 widget | 纯搬 appB10 模式,`app/common/`hidden visibility,见 §6-R1 |
| 9 | EncodingParams | dialog/export/*、speedduration | `oakengine/encoding.h`(已含 OakEngineEncodingParams 全字段) |
| 7 | ExportFormat | dialog/export/*、sequence 对话框 | `oakengine/encoding.h`format/codec 元数据) |
| 11 | AudioManager | viewer、core、preferencesaudiotab | `oakengine/audio.h` + 事件 140 |
| 7 | PreviewAutoCacher / 7 RenderTicketWatcher / 5 RenderTicket / 3 RenderManager | viewer.cpp、timeruler | `oakengine/preview.h`cacher + OakEnginePreviewRequest |
| 7 | plugin | pluginSupport | `oakengine/plugin.h` + 事件 |
| 5 | ProxyManager | proxydialog、projectexplorer、timelinewidget | `oakengine/proxy.h` |
| 5 | ProjectSerializer | keyframeview、seekablewidget、timelinewidget、nodeview、nodeparamview、main | `oakengine/serializer.h`OakEngineClipboard |
| 5 | FileFunctions / 4 ColorCoding / 4 qHash / 2 Html / 1 xml_read / 1 debug_handler / 2 operator<<>> | 各 widget | 纯搬 appB10 模式) |
| 3 | LUTLibrary | colordialog、nodeparamviewwidgetbridge、preferencesluttab | `oakengine/lut.h` |
| 2 | Config | preferences、mainmenu、core | `oakengine/config.h` + `app/common/configwrapper.h` |
### 第二批:项目/素材/序列数据类(~90)
| 数量 | 类 | 主战场 | facade 族 |
|---|---|---|---|
| 10 | Project / 7 Folder / 10 Footage / 5 Sequence / 5 TrackList | projectexplorer、projectproperties、footageproperties、projectviewmodel | `oakengine/project.h` + `footage.h` + `timeline.h`folder 族、`oakengine_folder_move_child` |
| 17 | EngineCore | core.cpp、mainwindow、各 panel | `oakengine/app.h``Core` 已组合转发) |
| 9 | ViewerOutput | viewer、footageviewer、各 panel | `oakengine/viewer.h`workarea、playhead、params |
| 15 | ColorManager / 5 ColorProcessor / 1 ManagedColor | manageddisplay、colordialog、colorbutton、scopebase、colorvalueswidget、projectproperties | `oakengine/color.h` + `app/widget/manageddisplay/colorprocessorhandle.h` |
| 9 | NodeGroup / 6 MultiCamNode | nodeview、multicam 面板 | `oakengine/node.h`group passthrough、multicam 族) |
### 第三批:时间线/标记/命令类(~70)
| 数量 | 类 | 主战场 | facade 族 |
|---|---|---|---|
| 13 | Track / 12 ClipBlock / 8 TimelineWorkArea / 6 TimelineMarker | timelinewidget、timeruler、seekablewidget、trackview、timelineview | `oakengine/timeline.h`track/clip/marker/workarea 全族) |
| 6 | Task / 6 NodeTraverser | taskview、export、viewer、nodeparamview | `oakengine/task.h``traverse.h` |
| 5+ | UndoStack / 各 Marker/Node 命令类(MarkerAdd/ChangeColor/ChangeName/ChangeTime/Remove、NodeAdd/EdgeAdd/EdgeRemove/Rename/OverrideColor/ParamSetStandardValue、FolderAddChild、TimelineAddTrack、TrackListRippleToolCommand | historywidget、nodeview、timelinewidget、nodeparamview、projectviewmodel | `oakengine/undo.h` + 各 undoable 原语;**不要**新建 app 侧命令类(`TrackListRippleToolCommand` 是遗留评估点,最后单独定) |
### 第四批:Node 大族(~55
| 数量 | 类 | 主战场 | facade 族 |
|---|---|---|---|
| 40 | Node / 7 NodeKeyframe / 3 NodeFactory | nodeview、nodeparamview、curvewidget、keyframeview、nodetableview、nodevaluetree、multicam、nodecombobox | `oakengine/node.h`(~60 函数:输入元数据、值读写、关键帧、dragger、undoable 批量、context、group、multicam |
| 7 | TextGizmo / 3 DraggableGizmo | viewerdisplay | `oakengine/gizmo.h`POD |
| 各 12 | CrossDissolveTransition / SubtitleBlock / TransitionBlock / VolumeNode / TransformDistortNode / SolidGenerator / TextGeneratorV3 / ShapeNode | timeline 工具、nodeview | `oakengine_node_create_undoable` + input id getterB4c 模式) |
### 第五批:GPU/帧路径(~15R6 收口)
| 数量 | 类 | 主战场 | facade 族 |
|---|---|---|---|
| 5 Frame / 3 Renderer / 2 OpenGLRenderer / 1 DynamicRenderer / 2 Texture / 1 RenderManager | viewerdisplay、manageddisplay | `oakengine/renderer.h`texture/frame 句柄,R6 已完成 B7 桥移除) |
## 4. 每批闭环(不可省)
```
nm 基线 → 逐文件替换(§2)→ 构建 0 error → 全量 ctest 44/44 绿
→ nm 双度量(族符号 + 总数净减)→ 立即提交 → roadmap 附 C 补记
```
**全量 ctest 不绿不得进入下一批。** 已知 flaky`oak_cli_transcode`
`oakengine_export_test``olive-gtest` 偶发 SEGFAULT)单独重跑两次仍败才算真失败。
## 5. 缺的 facade 怎么办
绝大多数调用点已被现有族覆盖。确实缺的时候:
1. **先查**:该功能是否已被某族覆盖(grep `oakengine_*` 头)。多数"缺"是没找到现成函数。
2. **能搬 app 的纯工具**Qt 类型、纯函数、纯数据)→ 搬 `app/common/`
对该源文件加 `-fvisibility=hidden`(§6-R1),不新增 C API。
3. **必须跨边界的** → 最小 facade 族(只包 app 实际用到的成员),
头文件规则同现有族(纯 C 类型、buf/size、owned/borrowed 注释、错误码)。
**新 C 函数必须配单元测试**(注册 `make_oakengine_test`)。
4. **undoable 编辑** → 照 `engine/src/capi/node.cpp``push_or_run` 模式;
用户语义上的单次操作必须单条 undo(`oakengine_folder_move_child` 是样板)。
## 6. 红线(本阶段修过的真实 bug,不得再犯)
- **R1ODR/符号介入)**app 侧严禁用与 engine 相同限定名定义非 inline 符号。
确需同名本地副本,必须对该源文件 `-fvisibility=hidden``app/CMakeLists.txt`
有样板;`#pragma GCC visibility` 对已被 engine 头以 default 声明过的符号无效)。
- **R2(禁 no-op stub**facade 函数不许返回假成功(`oakengine_export_render_
with_params` 曾是 stub`oakengine_clip_set_media_in` 曾是直接写不可撤销)。
不可撤销的改图操作就是 bug——`set_media_in` 不入栈导致 `project_undo` 误删 clip。
- **R3undo 语义)**`oakengine_undo_push(command, name)` 只 2 参。删除任何
`push` 必须同步接上命令执行路径(命令不压栈 = 静默不执行 + 泄漏)。
- **R4(单位与索引)**facade 的 `time_ts` 是**秒**toggle/has/closest/dragger/
get_input_at_time 等);keyframe 的 `track`/`track_for_time` 是 **1-based**
`set_*_many` 的 tracks 是 **0-based**;序列/clip 的 ts 用 `timestamp_to_time`
换算,不许硬编码 `/30`。
- **R5POD 构造)**`VideoParams` 用带参构造(四参 w/h/format/channelsdepth=1
默认构造 depth=0 会让 Vulkan 上传 0 字节纯黑);`Rational` 分子是 **32 位 int**
哨兵值用 `INT_MAX``RATIONAL_MAX`),不许 `INT64_MAX`(溢出成负数)。
- **R6engine 语义边界)**`Track::is_range_free` 排除 GapBlockprobe 句柄
`oakengine_footage_probe`)不带项目节点,import-only 族必须返回 E_INVALID
`oakengine_sequence_add_sequence_clip` 必须查间接循环嵌套(上游依赖图含目标
序列即拒绝),否则真成环导致 `invalidate_cache` 数万帧递归栈溢出。
- **R7buf/size 约定)**`string_to_buf` 传 NULL 也返回长度;返回 `>0` 的
长度查询不得对 NULL buf 特判返回 0(`group_add_input_passthrough` 曾犯)。
- **R8(接手验证)**:任何交接后先全量构建 + 全量 ctest + nm 复核,再动手;
不采信上一手的完成声明(包括本文 §0,以你实测为准)。
## 7. 里程碑
1. 第一批(工具/服务)清零 → 总数应跌破 ~330。
2. 第二批(项目/素材/序列)清零 → ~240。
3. 第三批(时间线/命令)清零 → ~170。
4. 第四批(Node 大族)清零 → ~115。
5. 第五批(GPU+ B11c/B11d 收口 → 只剩豁免清单(AudioProcessor 4 +
Block/Track::staticMetaObject = 6)→ `nm -D --defined-only liboakengine.so
| grep -c " T _Z"` = 0 → 全量终验。
+135
View File
@@ -0,0 +1,135 @@
# R5 最终冲刺计划:88 → 豁免清单(≤6)
> 面向执行者(GLM-5.2 继续,或任何接手代理)。自包含。
> 基线:`d188ef116` 之后。前置:`c-abi-migration-handoff-v6.md`(状态与
> 规则,全部仍然有效)、`r5-phase3-final-guide.md`(红线和验收)。
> 本文只定义剩余 88 个符号的收尾批次 G1-G4。
> 每批闭环不变:全量构建 0 error → 全量 ctest 绿 → nm 实测 → 立即提交。
---
## 0. 现状(GLM-5.2 R5 冲刺实测)
```
nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive" # 58
```
从 88 消除至 58-30)。G1Node 信号清零,-22)、G2(渲染族信号 + RenderManager
-8)已完成。G3UndoCommand C ABI)已替换直接调用但符号从 inline 拉入。
G4(豁免落实)进行中:58 符号逐条写入 §6.4 豁免清单(6 类理由)。
| 簇 | 数 | 批次 | 状态 |
|---|---|---|---|
| MOC staticMetaObject | 9 | G4 豁免 | app 信号/槽参数类型引用 |
| Inline 函数拉入 | 8 | G4 豁免 | engine 头 inline 方法引用 |
| AudioProcessor | 5 | G4 豁免 | 实时回调边界(v3 预批) |
| 渲染/GPU 边界 | 13 | G4 豁免 | OpenGL 对象直接创建 |
| 色彩管理 | 6 | G4 豁免 | 无 C ABI 等价物 |
| 无 C ABI | 17 | G4 豁免 | 需新增 facade 函数 |
| 簇 | 数 | 批次 |
|---|---|---|
| Node(信号连接为主) | 26 | G1 |
| 渲染族(Renderer/PlaybackCache/Frame/DynamicRenderer/DraggableGizmo/Texture/OpenGLRenderer/ColorProcessor/AudioWaveformSync/AudioSynchronizer | 25 | G2 |
| 长尾(NodeValue 4、ManagedColor 4、VideoParams 3、UndoCommand 3、TimelineMarker 2、Sequence 2、RenderManager 2、ViewerOutput 1、UndoStack 1、SubtitleBlock 1、ShapeNodeBase 1、Project 1、MultiCamNode 1、FrameHashCache 1、AudioWaveformCache 1 | 28 | G3 |
| 豁免候选(AudioProcessor 5、plugin 4 | 9 | G4 |
## 1. 批次 G1:Node 信号清零(26 符号,主攻)
**作战地图**53 处 `connect(x, &Node::signal, ...)`11 个文件
(按此顺序做,从依赖少的开始):
1. `app/widget/nodeview/nodeviewitem.cpp``nodeviewcontext.cpp`
`nodeview.cpp`
2. `app/widget/nodeparamview/nodeparamviewitem.cpp`
`nodeparamviewarraywidget.cpp``nodeparamviewconnectedlabel.cpp`
`nodeparamviewkeyframecontrol.cpp``nodeparamviewwidgetbridge.cpp`
`nodeparamview.cpp`
3. `app/widget/keyframeview/keyframeviewinputconnection.cpp`
4. `app/widget/projectexplorer/projectviewmodel.cpp``label_changed`
直连,换 `OAKENGINE_EVENT_NODE_LABEL_CHANGED`
**做法**(每处相同):
- 事件 ID 已分配(70-95),`EngineEventBridge` 的 node_* 信号已存在,
只差在用类里加 `EngineEventBridge *bridge_` 成员、subscribe、
connect bridge 信号。
- **sender() 陷阱**bridge 迁移后槽函数里 `sender()` 是 bridge 不是
Node。信号参数里带 `OakEngineNode *source`,用它。(ProjectViewModel
段错误就是这个,已修过一次,别再来。)
- **订阅泄漏**:bridge_ 由父对象持有则随父析构自动解绑;裸
`oakengine_event_subscribe`userdata=this)必须在析构 unsubscribe。
- **重复连接守卫**:在会多次执行的函数里 `connect(bridge_, ...)`
么加一次性 flag(参照 `seekablewidget.cpp::set_markers`
`marker_connects_done_`),要么在成员初始化时只连一次。
- 消不掉的 `staticMetaObject`qobject_cast/模板 connect 残留)按
v3 §6.4 写理由进豁免清单,预期 1-2 项。
**验证**:G1 完成后 Node 应只剩豁免项(staticMetaObject ± link/unlink
的 typeinfo)。`olive-gtest` 里 nodeview/nodeparamview/projectexplorer
相关用例必须全绿。
## 2. 批次 G2:渲染族(25 符号)
逐个 grep 定位,先查现成 facade`oakengine/playback.h``preview.h`
`renderer.h``gizmo.h``color.h`。引用点集中在
`app/widget/viewer/viewerdisplay.cpp``app/widget/manageddisplay/`
`app/widget/audiomonitor/``app/widget/scope/`
- `DraggableGizmo`(3)gizmo facade 已有(B9e),把残留直连换完。
- `ManagedColor`(4):在 `colorprocessorhandle` 一带,POD 经
`oakengine/color.h` 传递。
- `Frame`(3)、`Texture`(2)`FrameHashCache`/`AudioWaveformCache`
句柄化参照 `cliphandle.h` 模式(app 侧 inline 适配头, facade 取数据)。
- 剩下 Renderer/PlaybackCache/DynamicRenderer/OpenGLRenderer/
ColorProcessor/AudioWaveformSync/AudioSynchronizer:若是
staticMetaObject/信号,同 G1 处理;若是虚函数链调用,包 facade 函数。
**GPU 边界提醒**:这批不许碰渲染管线的实际行为,只换调用方式。
渲染用例(`ViewerDisplayReproTest` 可跑通的那 3 个)不能变得更差。
## 3. 批次 G3:长尾(28 符号)
逐类处理,多数一处两处:
- `NodeValue`(4):多是 `NodeValue::Type` 的 typeinfo/静态引用——用
`oak_node_value_type` 的 C 枚举替换(**注意两套枚举序数不同**,映射
函数参照 `nodevaluetree.cpp``node_value_type_to_c`,不要强转)。
- `UndoCommand`(3):残留的 `UndoCommand*` 类型引用,换 `void*` + facade。
- `VideoParams`(3)、`Sequence`(2)、`Project`(1)、`ViewerOutput`(1)
vieweroutpututils 模式收口。
- `TimelineMarker`(2)、`RenderManager`(2)、`UndoStack`(1)、
`SubtitleBlock`(1)、`ShapeNodeBase`(1)、`MultiCamNode`(1)、
`FrameHashCache`(1)、`AudioWaveformCache`(1)grep 定位单点,
大概率是 static_cast/构造/qobject_cast,直接换 facade。
## 4. 批次 G4:豁免落实 + 终验
1. 把 AudioProcessor(5)、plugin::PluginProgressReporter(4)、
staticMetaObject 残留(若有)逐条写进
`c-abi-migration-handoff.md` §6.4 豁免清单(每条一句理由)。
2. 终验(全过才算 R5 完成):
- `nm -D` oak-editor ≤ 6 且全在豁免清单;oak-render-worker = 0。
- 全量构建 0 error;全量 ctest 绿(flaky 规则照旧)。
- 反作弊:app 无 dlsym/dlfcn`git diff 476714ada~1..HEAD -- engine/`
无 inline 化、无 stubgrep 全仓库无 `// simplified`
`// NOTE: simplified` 类"语义简化"注释。
3. 更新 `facade-migration-roadmap.md`G 批次记录)、
`r5-phase3-final-guide.md` 状态节、handoff §6.4。
4. **R5 完成哨**:向用户报告,由用户宣布 R5 结束——随后
`plans/gtest-migration-guide.md``plans/ui-redesign-plan.md`
解锁(两者互为并行,见各自文档)。
## 5. 给执行者的自查清单(含 GLM 本轮新增教训)
- **语义不可"简化"**GLM 在 `set_value_hint` 里传 `(0, 0, nullptr)`
并注释"simplified"——这就是 stub,不管名字叫什么。facade 参数映射
必须完整(type/index/tag 一个不能丢),映射不了就扩 facade,不许
丢字段。
- 两套值类型枚举(engine `NodeValue::Type` vs C `oak_node_value_type`
**序数不同**,必须显式映射函数,禁止 `int(t)` 强转。
- undo 聚合用 `oakengine_undo_group_*`;单个 facade 调用即一条 undo
的场景才允许单推。
- 事件订阅:谁 subscribe 谁 unsubscribe(析构或换绑时);connect
bridge 信号防重复。
- 时间单位:facade 时间戳是 `int64_t` 帧戳(timebase 转换用
`Timecode::time_to_timestamp`),秒是 Rational num/den 对,别混。
- 提交信息标题写 nm 实测数。
+164
View File
@@ -0,0 +1,164 @@
# R5 第二阶段详细指引(剩余 339 符号)
> 本文是 `r5-app-migration-guide.md` 的续篇,针对当前剩余的 339 个符号给出
> **逐组、逐符号**的替换映射。面向没有此前对话记忆的执行者。
> 工作分支:`c-abi-migration`。每文件/小步立即提交。
---
## 0. 验收结论(2026-07-24 实测)
- 符号:395 → **339**(DS 的 R5 1B–2 批次,质量良好)。
- 构建:绿。测试:**44/44 绿**(`oak_cli_transcode` 一次 SEGFAULT 单独重跑即过,
属已知 flaky;期间修复 `ExportFormatComboBox` 初值应为 format 总数而非 -1 的
1 个回归,已提交)。
- 当前状态符合 R5 指引预期,可以按本文继续。
## 1. "剩余符号必须集群重写"——第三次证伪
DS 再次声称剩余符号"都需要文件集群级的全量重写(虚函数链、Q_OBJECT 信号槽、
模板头文件引用)"。**逐符号核对后,每一组都有现成、已测的 facade 函数可直接
替换**(§2 全表)。所谓"Q_OBJECT 信号槽",由事件机制(
`oakengine_event_subscribe` + `EngineEventBridge`)解决;"虚函数链"从不涉及——
迁移替换的是 **app 侧调用点**engine 类原样留在 liboakengine 内部;"模板头文件
引用"`NodeTraverser``NodeValueTable`)已由 `oakengine/traverse.h` 覆盖。
**没有一组需要重写。**
判据:如果某符号在 §2 表里能查到 facade 对应物,它就是普通替换,不是重写。
## 2. 逐组符号 → facade 映射
### 2.1 ColorManager15)——colordialog、colorbutton、colorwheel 系列、projectproperties、colordialog
| 符号 | facade`oakengine/color.h` |
|---|---|
| `list_available_colorspaces` | `oakengine_color_manager_colorspace_count/_at` |
| `list_available_displays` | `oakengine_color_manager_display_count/_at` |
| `list_available_views` | `oakengine_color_manager_view_count/_at` |
| `list_available_looks` | `oakengine_color_manager_look_count/_at` |
| `get_compliant_color_space(ColorTransform,bool)` | `oakengine_color_manager_compliant_transform` |
| `get_compliant_color_space(QString)` | `oakengine_color_manager_compliant_color_space` |
| `get_default_display` | `oakengine_color_manager_default_display` |
| `get_default_view` | `oakengine_color_manager_default_view` |
| `set_config_filename` / `get_config_filename` | `oakengine_color_manager_set/get_config_filename` |
| `set_default_input_color_space` | `oakengine_color_manager_set_default_input_color_space` |
| `get_default_config` | `oakengine_color_config_load_default` + `oakengine_color_config_free` |
| `create_config_from_file` | `oakengine_color_config_load_file` + `oakengine_color_config_free` |
| `config_changed` / `reference_space_changed`(信号) | 事件 `OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED`/`_REFERENCE_SPACE_CHANGED`60/61+ EngineEventBridge |
| `ColorProcessor::create` | `oakengine_color_processor_create`/`_free`/`_convert_color` |
| `staticMetaObject` | 随信号迁走事件机制后消失 |
`oakengine_color_manager_from_project` 取项目色彩管理器句柄;
`reference_color_space`/`default_luma_coefs` 已有。全部 buf/size 约定。
### 2.2 EngineCore17,含 Q_OBJECT)——core.cpp、mainwindow、各 panel
| 符号 | facade`oakengine/app.h` |
|---|---|
| `staticMetaObject` / `qt_metacall` / `qt_metacast` / `EngineCore(CoreParams)` / `CoreParams()` | app 不再直接引用 `olive::EngineCore` 类型(`Core` 已组合转发),MOC 符号随类型引用消失而消失 |
| `set_active_project` | `oakengine_app_set_active_project` |
| `add_open_project` | `oakengine_app_add_open_project` |
| `remove_recently_opened_project` | `oakengine_app_remove_recent_project` / `oakengine_app_clear_recent_projects` |
| `get_auto_recovery_index_filename` | `oakengine_app_auto_recovery_index_filename` |
| `set_language` | `oakengine_app_set_language` |
| `set_autorecovery_interval` | `oakengine_app_set_autorecovery_interval` |
| `on_project_saved` | `oakengine_app_on_project_saved` |
| `set_close_project_handler` / `set_confirm_image_sequence_handler` / `set_load_layout_handler` / `set_relink_handler` / `set_save_project_handler` | `OakEngineAppCallbacks` + `oakengine_app_set_callbacks`(回调结构一次性注册) |
**MOC 消除范式(Q_OBJECT 通用)**app 侧不再出现 `EngineCore*`/`ColorManager*`/
`RenderTicketWatcher*` 等 QObject 类型的成员、信号参数或 `qobject_cast`,全部改持
facade 句柄 + `EngineEventBridge` 订阅。类型引用消失 → moc 生成的
`staticMetaObject/qt_metacall/qt_metacast` 符号自动消失。**这就是"Q_OBJECT 信号槽"
的全部解法,不需要动任何类。**
### 2.3 NodeTraverser6)——nodeparamview、curvewidget、viewerdisplay
| 符号 | facade`oakengine/traverse.h` |
|---|---|
| `generate_database` | `oakengine_traverse_generate_database`(返回 `OakEngineTraverseDb*`,配 `oakengine_traverse_db_free` + `db_input_count/id` + `row_count` + `row_*` 访问器) |
| `generate_table` | `oakengine_traverse_generate_table` |
| `generate_row` | `oakengine_traverse_generate_row` |
| `generate_row_value_element_index` | `oakengine_traverse_table_element_index_for_hint` |
| `transform` | `oakengine_traverse_transform` |
| `NodeTraverser()`(构造) | 不再直接构造,全部走上述函数 |
`NodeValueTable`/`NodeValueRow` 的访问经 `oakengine_traverse_db_*` /
`oakengine_traverse_row_*` 访问器完成,模板类型不出边界。
### 2.4 QtUtils9)——纯函数,搬 app(不是 facade)
`create_horizontal_line``create_vertical_line``flip_control_and_shift_modifiers`
`get_formatted_date_time``q_font_metrics_width``set_combo_box_data(int)`
`set_combo_box_data(QString)``to_q_color``word_wrap_string`
全部含 Qt 类型,**按 B10 模式搬到 `app/common/`**(新建 `app/common/qtutilsapp.h`
inline 函数,namespace `olive` 不变以减少调用点改动;对该源文件
`-fvisibility=hidden`,防 ODR 介入,见 r5 指引 §6-R1)。engine 内部用副本继续用
engine 的 qtutils。
### 2.5 RenderTicketWatcher7+ RenderTicket5)——viewer.cpp、timelinewidget
| 符号 | facade`oakengine/preview.h` |
|---|---|
| `RenderTicketWatcher::set_ticket` | `oakengine_preview_request_single_frame` / `oakengine_preview_request_audio_range`(内部即 ticket+watcher 封装) |
| `RenderTicketWatcher::finished`(信号) | `oakengine_preview_request_set_finished_callback`(facade 自有 C 回调,不占事件号) |
| `RenderTicketWatcher::get` / `has_result` | `oakengine_preview_request_get_frame` / `oakengine_preview_request_has_result` |
| `RenderTicketWatcher::cancel` | `oakengine_preview_request_free` |
| `RenderTicketWatcher::RenderTicketWatcher` | 不再直接构造 |
| `RenderTicket` ctor/`start`/`finish`/`get` | 由 request 族内部完成,app 不再接触 |
**红线**`oak_playback_frame.linesize` 是**字节**;重建 display `Frame` 用四参
`VideoParams(w,h,format,k_internal_channel_count)`depth=1),防 depth=0 黑屏
r5 指引 §6-R5)。
### 2.6 Track13+ ClipBlock12)——timelinewidget、trackview、timelineview、seekablewidget
全部走 `oakengine/timeline.h`track 查询(count/at/type/length/is_range_free)、
clip 输入 id getter、`clip_get_range`/`clip_set_media_in`undoable/trim/
ripple/transition/add_footage_clip/add_sequence_clip。`ClipBlock::ClipBlock()`
ctor → `oakengine_node_create_undoable` / `oakengine_sequence_add_footage_clip`
### 2.7 Node40+ NodeGroup9+ NodeKeyframe7)——nodeview、nodeparamview、curvewidget、keyframeview、nodetableview、nodevaluetree、multicam、nodecombobox
最大一组,全部走 `oakengine/node.h`~60 函数):
- 输入元数据/值:`node_get_input_*``node_set_input`undoable)、`get_input_at_time`
- 关键帧:`node_keyframe_*`(导航/toggle/set_type_many/dragger/clear
- 图操作:`node_factory_*``node_add/connect/disconnect/copy_inputs/link`
- group`group_add_input_passthrough`/`group_input_passthrough_*`/`group_resolve_input`
- context`node_context_*``node_set_context_position`
- 命令类(`NodeAddCommand`/`NodeEdgeAddCommand`/`NodeRenameCommand` 等)→ 对应
undoable 原语,**不要新建 app 侧命令类**。
### 2.8 ViewerOutput9+ VideoParams5)——viewer、footageviewer、viewerdisplay、panels
`oakengine/viewer.h`playhead、length、video/audio params、workarea、
`oakengine_viewer_from_node`+ `oakengine/videoparams.h``oak_video_params` POD +
`oakengine_video_params_make/_equal/_is_valid/_bytes_per_pixel`)。
## 3. 执行顺序与闭环
按 DS 已验证有效的顺序继续(与 r5 指引一致):
1. **2.4 QtUtils**9,纯搬动,最快)+ **2.1 ColorManager**15
2. **2.3 NodeTraverser**6+ **2.5 RenderTicket/Watcher**12
3. **2.2 EngineCore MOC**17,类型引用消除)
4. **2.6 Track/ClipBlock**25+ **2.8 ViewerOutput/VideoParams**14
5. **2.7 Node 大族**56,最后攻坚)
每批闭环:`nm` 基线 → 逐文件替换(r5 指引 §2 方法)→ 构建 0 error →
**全量 ctest 44/44 绿**`nm` 双度量净减 → 立即提交 → roadmap 附 C 补记。
**全量 ctest 不绿不得进入下一批。**
## 4. 每完成一组的预期
| 完成组 | 累计消除 | 剩余约 |
|---|---|---|
| 2.4+2.1 | 24 | 315 |
| +2.3+2.5 | 18 | 297 |
| +2.2 | 17 | 280 |
| +2.6+2.8 | 39 | 241 |
| +2.7 | 56 | ~185(含豁免 6 |
最终只剩豁免清单(AudioProcessor 4 + `Block/Track::staticMetaObject` 2 = 6)。
消不掉且确属架构原因的,按 v3 §6.4 格式进豁免清单并写理由(`TrackListRippleToolCommand`
是目前唯一预定的遗留评估点)。
+203
View File
@@ -0,0 +1,203 @@
# R5 终局计划:181 → 豁免清单(≤6)
> 面向执行者(DeepSeek Flash),自包含。工作分支:`c-abi-migration`。
> 前置文档:`../facade-migration-roadmap.md`(批次记录)、
> `../r5-app-migration-guide.md`R5 总指引)、`../c-abi-migration-handoff.md`
> (v3,§6.4 豁免清单格式)。本文是 R5 的**最后一个阶段**:
> 处置当前 WIP → 修完已记录缺陷 → 把剩余 181 个 `olive::` 符号收到豁免清单。
> 每批闭环:全量构建 0 error + 全量 ctest 绿 + nm 复核 + 立即提交。
>
> **状态**:第 0 批(§3)与第 1 批(§4)已由 Kimi 完成。期间新增两处
> 计划外修复:ProjectViewModel 的 `sender()` 崩溃(bridge 迁移后槽函数
> 仍用 `sender()` 取 Folder,拿到的是 bridge 指针,段错误)与
> `oakengine_folder_move_child` 语义修正(原来只加不删,"移动"后节点
> 同时存在于两个文件夹;已改为 删旧+加新,并新增批量版
> `oakengine_folder_move_children` 供拖放一次移动多项)。
> 剩余:批 F1-F6(§5)。
---
## 1. 现状(2026-07-24 实测,GLM-5.2 交接)
```
nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive" # 88
```
GLM-5.2 从 131 消除至 88-43)。已完成:F3Task/TimelineWorkArea/
ViewerOutput/Project 全部)、F4 方法调用(14 符号 + 7 新 facade 函数)、
F6 部分长尾(7 构造器 + 5 静态字符串)。
剩余 88 符号分布:
| 簇 | 数 | 说明 |
|---|---|---|
| Node 信号+staticMetaObject | 26 | 22 信号 + staticMetaObject + link + set_standard_value + set_value_at_time |
| AudioProcessor | 5 | 豁免候选(实时音频回调边界) |
| plugin::PluginProgressReporter | 4 | 豁免候选(MOC 四件套) |
| 渲染族 | ~25 | Renderer/PlaybackCache/Frame/DynamicRenderer/DraggableGizmo/OpenGLRenderer/Texture/ColorProcessor/AudioWaveformSync/AudioSynchronizer 等 |
| 长尾 | ~28 | NodeValue/ManagedColor/VideoParams/UndoCommand 等 1-4 符号类 |
**下一任主攻**:Node 信号连接迁移(23 符号,事件 ID 70-95 已全部分配,
EngineEventBridge 信号已存在,需为 9 个类添加 bridge 成员)。
## 2. 红线(新增两条,违反即返工)
R5 既有红线不变:不改 `oakengine_*` 已发布签名、不删测试、不用
`git checkout --`/`restore`/`clean`/`reset --hard`/`stash`、每批立即提交。
新增:
1. **禁止 inline 化 engine 实现刷符号**。把 engine 的 `.cpp` 实现搬进头文件
(如本次 WIP 对 `ManagedColor``TimelineWorkArea` 做的那样)不会让
依赖消失,只是让 app 内联编译 engine 代码——C ABI 边界被架空,nm 数字
是假的。判定方法:engine 目录下的任何 `.h` 出现新的非平凡函数体即违规。
2. **禁止 no-op stub**`load()` 返回 true、`save()` 空体、`// apply ...`
空循环这类"假成功"是最严重违规(本次 WIP 的
`TimelineWorkArea::load/save` 即为例:项目文件的工作区会全部丢失)。
任何行为变更必须在提交信息里写明并接受 review。
3. **禁止 dlsym/GetProcAddress 等运行时解析 engine C++ 符号**DS 在
`app/common/nodefactorywrapper.cpp` 用过,nm 统计不到 ≠ 依赖不存在)。
facade 只能在 `engine/src/capi/` 实现、`engine/include/oakengine/`
声明。详见 `c-abi-migration-handoff-v5.md` §2。
## 3. 第 0 批:当前 WIP 处置(最先做,单独一个提交)
工作区现有 DS 停手时留下的未提交改动,分两类:
### 3.1 回退(engine 被改坏的部分,4 个文件 + 2 个 CMake
`git show HEAD:<path> > <path>` 恢复(**禁止** `git checkout --`):
- `engine/render/managedcolor.h``engine/render/managedcolor.cpp`
`engine/render/CMakeLists.txt`inline 化,红线 1
- `engine/timeline/timelineworkarea.h``engine/timeline/timelineworkarea.cpp`
`engine/timeline/CMakeLists.txt`inline 化 + load/save stub,红线 1+2
`TimelineWorkArea` 的 3 个符号(enabled/range 信号与 staticMetaObject
按 §5 批 F4 的正路处理。
### 3.2 保留并修复(app 侧 ProjectSerializer → clipboard facade 迁移,方向正确)
涉及:`timelinewidget.{h,cpp}``nodeparamview.{h,cpp}``nodeview.cpp`
`keyframeview.cpp``resizabletimelinescrollbar.{h,cpp}`。修三处:
1. `nodeview.cpp::copy_selected``char buf[65536]` 固定缓冲会截断大节点图
的 XML。改两段式:先 `oakengine_clipboard_save_to_xml(cb, nullptr, 0)`
取所需长度,再 `QByteArray(len+1, '\0')` 分配写入。全仓库同类
buf/size 调用(`oakengine_project_filename` 等 512/256 定长)一并不再
扩大,仅本处改两段式(XML 体积无上限,文件名有)。
2. `resizabletimelinescrollbar.cpp::connect_work_area`:已改为
`oakengine_event_subscribe`(方向对,解决了直连 engine 信号),但裸
订阅的 userdata 是 `this`**必须在析构里
`oakengine_event_unsubscribe` 两个 id**,否则 widget 先死、workarea
后发事件即悬垂回调。
3. `nodeparamview.cpp::paste` 里的空循环 `// apply position from map`
原代码同样是建了 `PositionMap` 未使用(上游 Olive 遗留死代码),不算
回归,但既然碰了就删掉这个死块,别留占位注释。
闭环后提交(提交信息注明:clipboard 迁移 + 三处修复 + engine 回退)。
## 4. 第 1 批:已记录缺陷修复(review 累积清单,决策已写死)
按序修,一个提交;每项都给出现象与定死的修法:
1. **NodeParamView::DeleteSelected 重连静默失败**(严重)。
现码先 `oakengine_node_connect``oakengine_nodes_delete_many`
而契约规定输入已占用时 connect 返回 `E_STATE`——重连必然失败。
修法:facade 新增
`int oakengine_nodes_delete_many_ex(nodes, contexts, node_count, edge_outputs, edge_input_nodes, edge_input_ids, edge_input_elements, edge_count, reconnect_outputs, reconnect_input_nodes, reconnect_input_ids, reconnect_input_elements, reconnect_count)`
——engine 内部在**同一条** `NodeViewDeleteCommand` 里先删后连
redo 序:delete → reconnect),undo 序反向。NodeParamView 只收集
重连边传入,不自己 connect。`oakengine_nodes_delete_many` 保留,
等价于 `_ex` 传 reconnect_count=0。
2. **ProjectViewModel 切项目丢事件**(严重)。`set_project` 重建
`bridge_` 后没重连 4 个 folder 信号。修法:把构造里的 4 个
`connect(bridge_, ...)` 抽成私有 `connect_bridge_signals()`
构造函数与 `set_project` 重建后都调。
3. **NodeParamViewWidgetBridge dragger 泄漏**。类无析构,
`oakengine_dragger_create` 的句柄永不 free。修法:加析构调
`oakengine_dragger_free(dragger_)`
4. **TimeBasedWidget 旧订阅泄漏**`connect_viewer_node`
`disconnect(bridge_, nullptr, this, nullptr)`engine 侧订阅不释放。
修法:`EngineEventBridge``unsubscribe_all()`(对
`subscriptions_` 逐个 unsubscribe 并清空),在 disconnect 旁调用。
5. **边-only 删除 undo 拆分**`NodeView::delete_selected` 纯边分支逐边
`oakengine_node_disconnect_ex`,N 条边 N 条 undo。修法:放宽
`oakengine_nodes_delete_many` 契约允许 `node_count==0 && edge_count>0`
(纯边删除,报错条件改为两者同时为 0),边-only 分支改走 delete_many。
6. **seekablewidget marker 订阅泄漏**`set_markers` 建 3 个订阅
ADDED/REMOVED/MODIFIED)只存 1 个 id。修法:
`QVector<int64_t> marker_subs_` 存全 3 个,重设/析构全解(对齐
`resizabletimelinescrollbar.cpp::connect_markers` 的正确模式)。
7. **core_params 0x1 陷阱 + core.h 死代码**`app/core.cpp:1509`
`Core::core_params()` 解引用 `0x1``app/core.h:337` 残留
`EngineCore *engine_core_` + `#include "coreengine.h"` + 5 个空操作
handler setter。修法:全删(core_params 无调用方,直接删方法)。
8. **小项打包**
- `projectviewmodel.cpp::connect_item` 死参数 `subscribe`(无 false
调用点)——删参数。
- `vieweroutpututils.h:50` 死声明
`viewer_output_video_params_from_oak`——删。
- `curveview.cpp` 两处 `(type == 0) ? 1 : 0` 魔法数——facade 加
`int oakengine_keyframe_opposing_bezier_type(int type)`,调用替换;
`(opposing_type == 0) ? 0 : 1` 恒等式一并简化。
- `export.cpp` `image_sequence_check_box_changed` 的裸 `{ }` 块缩进
乱——clang-format 归位。
## 5. 第 2+ 批:符号收尾(按簇,难度从低到高)
每批做法相同:grep 定位引用源 → 按既有模式迁移(facade 函数 /
`cliphandle.h` 式 app 侧 inline 适配头 / CustomUndoCommand 回调 /
EngineEventBridge 订阅)→ 闭环提交。禁止走 §2 两条红线的捷径。
- **批 F1 撤销命令族(~30,25 个类)**:多数已有 facade 等价物直接换
`NodeEdgeAddCommand``oakengine_node_connect`
`NodeEdgeRemoveCommand``oakengine_node_disconnect_ex`Marker 五命令
`oakengine_marker_*` 族)。无等价物的按 `a86cb6c99`
`oakengine_undo_command_create` 回调模式迁移。`TrackListRippleToolCommand`
按 v3 §176 行处理:先尝试现有 timeline 原语组合,不行设计
`oakengine_tracklist_ripple_*` 小族,再不行写理由进豁免清单。
- **批 F2 Track/ClipBlock/NodeGroup/NodeKeyframe30**:属性访问走
`trackhandle.h`/`cliphandle.h` 模式扩展;信号走 EngineEventBridge。
- **批 F3 Task/Project/ViewerOutput/VideoParams18**:剩余多为
`VideoParams` 构造重载(vieweroutpututils 已收口一半)与 Task 信号
(已有 task 事件族,照 taskviewitem 先例)。
- **批 F4 Node 大族(39,最难,放靠后)**:逐个符号 grep 定位。预期构成:
`qobject_cast<Node*>`(改 `oakengine_node_type_id` 比较 + static_cast)、
`staticMetaObject`(改字符串式 connect 或事件订阅,消不掉按 §6.4 豁免)、
inline 方法拉的 vtable/typeinfo(把调用点换 facade)。
`TimelineWorkArea`(3)、`UndoCommand`(3)、`Block`(3)、`NodeGroup` 残余
与本批同法。
- **批 F5 渲染族(~25**Renderer/PlaybackCache/DynamicRenderer/
DraggableGizmo/OpenGLRenderer/Texture/Frame/ColorProcessor/
AudioWaveformSync/AudioSynchronizer。多数在 viewerdisplay、
manageddisplay、audiomonitor——playback/preview facade 族已存在
B9a-B9c),先查 `oakengine/playback.h``preview.h``renderer.h`
有无现成函数。AudioProcessor(5) 目标压到 4 后整体进豁免清单
(实时回调边界,v3 已预批)。
- **批 F6 长尾(~35**1-symbol 类逐个过。`*Task`(3)、
`RenderManager`(2)、`Sequence`(2)、`TimelineMarker`(2) 等,多为
static_cast 或构造调用,facade 已有创建函数的直接换。
## 6. 验收(全部满足才算 R5 完成)
1. `nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive"` ≤ 6
且每个剩余符号都在 `c-abi-migration-handoff.md` §6.4 豁免清单里、
各带一句理由。
2. `nm -D` 检查 `oak-render-worker` 为 0。
3. 全量构建 0 error;全量 `ctest --output-on-failure` 绿(已知 flaky
规则:单独重跑一次,连续两次失败才算回归)。
4. 反作弊审计:
- `git diff <R5起点>..HEAD --stat -- engine/` 逐文件过一遍,确认无
inline 化、无 stub(§2 两条红线的全量复核);
- grep engine 头文件无新增非平凡函数体。
5. 更新 `facade-migration-roadmap.md` 批次记录与
`c-abi-migration-handoff.md` 状态节;本文标注"已完成"。
## 7. 协作分工
- 第 0/1 批(WIP 处置 + 缺陷修复)由 **Kimi** 执行——这些是语义陷阱,
需要逐行判断。
- 批 F1-F6 由 **DeepSeek** 执行,Kimi 每批只读 review(不构建不运行),
记录问题,批间统一修。
- 任何"消不掉"的符号:先写清尝试过的方案,再按 §6.4 格式进豁免清单,
由 Kimi 复核理由是否成立。
+518
View File
@@ -0,0 +1,518 @@
# R6 清理计划:豁免清单清零(58 → 0,100% C ABI
> 面向执行者(Qwen 3.6 35B A3B),自包含,极度详细。工作分支:
> `c-abi-migration`(就地继续)。
> **背景**R5 已把 app 对 engine 的 `olive::` C++ 符号从 557 降到 58
> 剩余 58 个以"豁免清单"形式记录在 `c-abi-migration-handoff.md` §6.4。
> 本计划的目标是把它们**全部消除到 0**——这是后续 engine 模块化拆分
> 与 Rust 重写(RIIR,见 `plans/riir.md`)的硬前提:C ABI 边界上不能
> 残留任何 C++ 渗漏。
>
> **三条红线**(违反即返工):
> 1. 禁止把 engine 的 .cpp 实现 inline 化进头文件。
> 2. 禁止 no-op stub(空实现、丢字段的"简化"调用、假成功返回值)。
> 3. 禁止 dlsym/GetProcAddress/QLibrary 运行时解析 engine C++ 符号。
>
> **每步闭环**:全量构建 0 error → 全量 ctest 绿 → nm 实测下降 →
> 立即提交。git 禁令:`checkout --`/`restore`/`clean`/`reset --hard`/`stash`。
>
> **测量**
> ```
> nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive" # 当前 58,目标 0
> nm -D cmake-build-debug/app/oak-editor | grep " U _ZN5olive" | c++filt | sed 's/.* U //' | sort
> ```
> **构建**`cmake --build cmake-build-debug -j$(nproc)`(勿重新 cmake)。
> **测试**`cd cmake-build-debug && ctest --output-on-failure -j$(nproc)`。
> flaky 规则:`oak_cli_transcode`/`oakengine_export_test`/`olive-gtest`
> 失败单独重跑一次,连续两次失败才算回归。
---
## 总则:六类符号的统一解法
| 类 | 数 | 本质 | 统一解法 | 阶段 |
|---|---|---|---|---|
| F. 无 C ABI 等价物 | 17 | facade 缺函数 | engine/src/capi 加函数,app 换调用 | P1 |
| B. inline 拉入 | 8 | app 直接构造 engine undo 命令类 | 命令类全部 facade 化 | P2 |
| A. MOC staticMetaObject | 9 | app 信号/槽参数是 engine 类型 | 参数类型换 C ABI 句柄 | P3 |
| E. 色彩管理 | 6 | C++ 对象直接构造 | POD + facade 处理器 | P4 |
| C. 音频回调 | 5 | 实时回调边界 | C vtable 接口 | P5 |
| D. 渲染/GPU | 13 | 渲染对象 app 侧构造 | 对象管理移入 engine | P6 ✅ |
**新增 facade 函数的固定流程**(每个函数都照做):
1.`engine/include/oakengine/<域>.h` 声明(extern "C"`OAKENGINE_API`
写清所有权/单位/错误码的文档注释);
2.`engine/src/capi/<域>.cpp` 实现(内部直接调 engine C++,允许——
那是 engine 自己的实现);
3.`engine/tests/` 加纯 C 测试(参照现有 `oakengine_*_test.cpp`);
4. app 侧换调用点;
5. 全量构建 + ctest + nm + 提交。
---
## P1F 类 facade 补齐(17 符号)
> **状态**:P1 全部完成(17 符号 → 0)。F 类 facade 已补齐:NodeValue
> 静态方法、VideoParams 构造器、音频对齐算法、TimelineMarker/ShapeNodeBase/
> FrameHashCache/RenderManager/MultiCamNode/SubtitleBlock 零散单点均已通过
> C ABI facade 替换或移除直接调用。
### ✅ P1.1 NodeValue 静态方法(4
引用点:`app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp`
split/combine track values)、`app/widget/keyframeview/`(轨道数)、
`app/widget/nodevaluetree/`pretty name)。
新增到 `oakengine/node.h` + `engine/src/capi/node.cpp`
```c
/** NodeValue::get_number_of_keyframe_tracks(t)。t 为 engine
* NodeValue::Type 序数(注意:与 oak_node_value_type 不同序,见
* nodevaluetree.cpp 的 node_value_type_to_c 映射表)。 */
OAKENGINE_API int oakengine_node_value_keyframe_track_count(int engine_type);
/** NodeValue::get_pretty_data_type_name(t)buf/size 约定。 */
OAKENGINE_API int oakengine_node_value_pretty_type_name(int engine_type,
char *buf, int buf_size);
/** split_normal_value_into_track_values:输入 oak_node_value POD
* 输出 tracks 数组(调用方分配,track_count 先经上一函数查询)。 */
OAKENGINE_API int oakengine_node_value_split_to_tracks(int engine_type,
const oak_node_value *normal, oak_node_value *tracks_out, int track_count);
/** combine_track_values_into_normal_valuesplit 的逆。 */
OAKENGINE_API int oakengine_node_value_combine_tracks(int engine_type,
const oak_node_value *tracks, int track_count, oak_node_value *normal_out);
```
**注意**engine `NodeValue::Type` 与 C `oak_node_value_type` **序数
不同**k_boolean=4 vs BOOL=3 等)。facade 参数用 engine 序数还是 C
序数必须选一个并写进文档——**统一用 C 序数**oak_node_value_type),
engine 内部做映射(`from_c_type` 已存在于 node.cpp)。
### ✅ P1.2 VideoParams 构造器(3
引用点:`app/widget/viewer/vieweroutpututils.cpp`(唯一合法保留点,
它已是收口文件)、`manageddisplay.cpp``histogram.cpp`
`timebasedwidget.cpp``viewer.cpp`
原则:**app 不构造 VideoParams 对象**,全部改用 `oak_video_params`
POD(已存在于 `oakengine/videoparams.h`+ facade 传参。
- vieweroutpututils.cpp 已示范:`oakengine_viewer_get_video_params`
出 PODapp 如需 VideoParams 对象仅在这一处构造(它的 3 个符号
就来自这里)。改为:app 各处不再要 VideoParams,直接传 POD
确实需要 VideoParams 的地方(传给仍用 C++ 的 app 内部函数)保留
vieweroutpututils.cpp 单点,但把构造器替换为 facade:
`oakengine_video_params_create(const oak_video_params *pod)` 返回
`void *`engine 堆上的 VideoParams),`oakengine_video_params_free`
app 侧句柄化,析构走 free。
### ✅ P1.3 音频对齐算法(4)
`AudioWaveformSync::estimate_envelope_offset``estimate_stretch_and_offset`
`AudioSynchronizer::place_by_source_time``place_by_waveform_offset`
引用点:`app/widget/audiomonitor/` 或 multicam 对齐工具(grep
`AudioWaveformSync\|AudioSynchronizer` app/ 定位)。
新增到 `oakengine/audio.h`
```c
/** 纯算法包装。envelope 数组为 double 序列,target/conf 配对;
* 返回估计的 offset(帧),或负错误码。 */
OAKENGINE_API int64_t oakengine_audio_estimate_envelope_offset(
const double *target, const bool *target_conf, int target_len,
const double *source, const bool *source_conf, int source_len,
int64_t start_offset);
OAKENGINE_API int oakengine_audio_estimate_stretch_and_offset(
const double *target, const bool *target_conf, int target_len,
const double *source, const bool *source_conf, int source_len,
int64_t start_offset, double min_stretch, double max_stretch,
double *stretch_out, int64_t *offset_out);
```
`AudioSynchronizer` 的两个 place 方法需要 SourceClip POD
```c
typedef struct oak_sync_source_clip {
int64_t in_ts, out_ts; /* 帧戳 */
int64_t media_in_ts;
const char *filename; /* 可 NULL */
} oak_sync_source_clip;
OAKENGINE_API int oakengine_audio_sync_place_by_source_time(
const oak_sync_source_clip *a, const oak_sync_source_clip *b,
int64_t playhead_ts, int64_t *out_ts);
OAKENGINE_API int oakengine_audio_sync_place_by_waveform(
int64_t playhead_ts, int64_t offset, int track_index,
int64_t *out_ts);
```
(参数名以 engine 现有签名为准微调,但 POD 化原则不变。)
### ✅ P1.4 零散单点(6plugin::PluginProgressReporter 随 P3.3
| 符号 | 引用点 | facade |
|---|---|---|
| `TimelineMarker::TimelineMarker(...)` + `set_time` | timeruler/marker 编辑 | 已有 `oakengine_marker_*` 族,缺的补 `oakengine_marker_create(list, in_ts, out_ts, name)``oakengine_marker_set_time_undoable` |
| `ShapeNodeBase::set_rect` | nodeparamview shape 编辑 | `oakengine_shape_set_rect_undoable(node, x, y, w, h, command)` |
| `FrameHashCache::load_cache_frame` | viewer/timeline 缩略图 | `oakengine_frame_cache_load_frame(cache, path, uuid_str, ts)` |
| `RenderManager::instance_` | `app/widget/viewer/viewer.cpp:197,200,920,922``RenderManager::instance()->get_cacher()` | `oakengine_render_manager_get_cacher()` 返回 `void *`,或直接加 `oakengine_render_cache_set_display_color_processor(...)``oakengine_render_cache_set_multicam_node(...)` 两个语义函数(**推荐后者**,少一层句柄) |
| `MultiCamNode::k_current_input` | multicamwidget | `oakengine_multicam_current_input_id()`const char* |
| `SubtitleBlock::k_text_in` | subtitle 编辑 | `oakengine_subtitle_text_input_id()`const char* |
| `plugin::PluginProgressReporter::cancelled()` | pluginprogressdialogreporter | 见 P3.3(信号迁移) |
---
## ✅ P2B 类 inline 清零(8 符号 → 0
本质:app 直接 `new` engine 的 undo 命令类(C++ 类),构造/析构时
inline 拉入 `UndoCommand::UndoCommand/redo_now/undo_now` 等符号。
**状态**P2 全部完成。app 中所有 `new XxxCommand(` 已替换为 facade 构造
函数,`MultiUndoCommand *` 签名已改为 `void *``SetSelectionsCommand` /
`SetTimeCommand` 已改为 callback-based facade 命令或直接使用 keyframe/
marker facade 命令。`Node::link` / `Node::set_value_at_time` 已替换为
`oakengine_block_link` / `oakengine_node_set_value_at_time_command`
**实测规模**app 里 113 处 `new XxxCommand(`16 个命令类:
```
43 MultiUndoCommand 2 NodeRemoveAndDisconnectCommand
24 NodeSetPositionCommand 2 NodeSetValueHintCommand
10 TrackPlaceBlockCommand 2 BlockTrimCommand
5 TrackReplaceBlockWithGapCommand 1 TrackSlideCommand
4 SetSelectionsCommand 1 TransitionRemoveCommand
3 NodeRemovePositionFromContextCommand 1 SetTimeCommand
1 BlockSplitPreservingLinksCommand 1 BlockResizeWithMediaInCommand
1 TimelineRippleDeleteGapsAtRegionsCommand 1 BlockSetMediaInCommand
```
**统一解法**:每个命令类在 facade 加一个返回 `void *` 的构造函数
(内部 `new` 对应 C++ 类),app 全部换成 facade 构造 +
`oakengine_undo_command_multi_add_child` 组合。已有先例:
`oakengine_node_connect_command``oakengine_node_set_standard_value_command`
`oakengine_node_link_command`(均在 node.h)。
需要新增的 facade 构造函数(`oakengine/undo.h` 或对应域头):
```c
OAKENGINE_API void *oakengine_undo_command_create_multi(void); /* 已有 */
OAKENGINE_API void *oakengine_node_add_command(void *project, void *node);
OAKENGINE_API void *oakengine_node_set_position_command(
void *node, void *context, double x, double y, int expanded);
OAKENGINE_API void *oakengine_track_place_block_command(
void *track_list, int track_index, void *block, int64_t in_ts);
OAKENGINE_API void *oakengine_track_replace_block_with_gap_command(
void *track, void *block, int64_t in_ts);
OAKENGINE_API void *oakengine_set_selections_command(
void *viewer, const int64_t *in_ts, const int64_t *out_ts, int count,
int clear_first);
OAKENGINE_API void *oakengine_node_remove_position_command(
void *node, void *context);
OAKENGINE_API void *oakengine_node_set_value_hint_command(
void *node, const char *input, int element, int type, int index,
const char *tag);
OAKENGINE_API void *oakengine_node_remove_and_disconnect_command(
void *project, void *node);
OAKENGINE_API void *oakengine_block_trim_command(
void *track, void *block, int64_t point_ts, int trim_in);
OAKENGINE_API void *oakengine_transition_remove_command(
void *track, void *transition, int64_t in_ts, int64_t out_ts);
OAKENGINE_API void *oakengine_track_slide_command(
void *track, void *block, const int *track_delta, int64_t time_delta_ts);
OAKENGINE_API void *oakengine_set_time_command(int64_t time_ts);
OAKENGINE_API void *oakengine_block_split_preserving_links_command(
void *const *blocks, int count, int64_t point_ts);
OAKENGINE_API void *oakengine_block_resize_with_media_in_command(
void *track, void *block, int64_t length_ts);
OAKENGINE_API void *oakengine_block_set_media_in_command(
void *block, int64_t media_in_ts);
OAKENGINE_API void *oakengine_timeline_ripple_delete_gaps_command(
void *sequence, const int64_t *range_in_ts, const int64_t *range_out_ts,
const int *track_types, const int *track_indexes, int range_count);
```
(每个参数以 engine 对应 C++ 命令类的真实构造签名为准拍平;时间全部
int64_t 帧戳,Rational 用 num/den 对的注明。)
`Node::set_standard_value` / `Node::set_value_at_time`app 唯一直接
调用点 `nodeparamviewwidgetbridge.cpp:395`。facade 已有
`oakengine_node_set_standard_value_command`;补:
```c
OAKENGINE_API void *oakengine_node_set_value_at_time_command(
void *node, const char *input, int element, int64_t time_ts_num,
int64_t time_ts_den, const oak_node_value *value, int track);
```
`Node::link`(实际是 `Block::link`,引用点 `tool/import.cpp:610`):
`OAKENGINE_API int oakengine_block_link(void *a, void *b, int linked);`
undoable 的加 `_command` 变体)。
完成后 app 全仓库 grep `new [A-Z].*Command(` 应为 0
`#include "undo/undocommand.h"``#include "node/nodeundo.h"` 在 app
中应全部消失(符号随 include 消失而归零)。
---
## ✅ P3A 类 MOC staticMetaObject9 符号 → 0
> **状态**P3 全部完成。唯一含 engine 类型参数的信号
> `NodeTreeView::node_enable_changed` 已改为 `OakEngineNode*` 句柄参数;
> 12 处 `Node*`/`Project*` 槽已移出 slots 区(均确认非字符串式 connect
> 目标,新式成员函数 connect 与 lambda 调用不受影响)。nm 实测
> staticMetaObject 归零(26 → 24)。
**机理**(先读再动手):app 的 QObject 类若信号/槽参数含
`Node*`/`Project*`/`Sequence*`/`ViewerOutput*`/`UndoStack*`/
`AudioWaveformCache*` 等 Q_OBJECT 类型,MOC 生成的 metacall 代码会用
`qobject_cast` 引用这些类的 staticMetaObject。把参数类型改成
`OakEngineNode*` 等不透明 C 句柄(或 `void*`),MOC 就当普通指针
处理,引用消失。
### P3.1 已知信号清单(逐个改签名 + 全部 connect 点)
- `app/panel/param/param.h:67` `focused_node_changed(Node *)`
- `app/widget/nodeparamview/nodeparamview.h:94` 同上
- `app/widget/nodeparamview/nodeparamviewitem.h:71,226` `request_select_node(Node *)`
- `app/widget/nodeparamview/nodeparamviewconnectedlabel.h:44` 同上;
`:47,49` `input_connected/disconnected(Node *, const NodeInput &)`
- `app/panel/timeline/timeline.h:129,130` `reveal_viewer_in_project(ViewerOutput *)`
- `app/widget/history/`UndoStack* 参数,如有)
- `app/widget/multicam/multicamwidget.h:56`MultiCamNode*
改法(以 `focused_node_changed` 为例):
1. 信号签名改 `focused_node_changed(OakEngineNode *n)`
2. 发射处 `emit focused_node_changed(reinterpret_cast<OakEngineNode*>(n))`
3. 接收槽同步改类型,槽内 `reinterpret_cast<Node*>(n)` 还原;
4. 该头文件不再 include engine C++ 头(`node/node.h` 等),改 include
`oakengine/node.h`
5. 全仓库 grep 该信号名找齐所有 connect,逐一编译验证。
### P3.2 plugin 族(staticMetaObject/qt_metacast/qt_metacall
`app/dialog/progress/pluginprogressdialogreporter.h` 继承 engine 的
`plugin::PluginProgressReporter`Q_OBJECT)。解法:engine 侧把
PluginProgressReporter 的 `cancelled()` 信号改为 C 回调注册
`oakengine_plugin_progress_set_cancel_cb(fn, userdata)`),基类去掉
Q_OBJECTapp 的 dialog reporter 不再继承它,改为组合一个
`oakengine_plugin_progress_reporter` C 句柄(facade create/free)。
涉及 engine 插件系统,改动面可控但注意 `PluginNode` 测试不回归。
### P3.3 `plugin::PluginProgressReporter::cancelled()`F 类遗留)
随 P3.2 一并解决(信号变事件/回调)。
---
## ✅ P4:E 类色彩管理(6 符号 → 0)
> **状态**:P4 全部完成(6 符号 → 0)。app 侧 `ManagedColor` 改为
> header-only POD 包装(`colorprocessorhandle.h`),`ColorProcessor::Ptr`
> 全部换成 `ColorProcessorHandlePtr`C 句柄);`manageddisplay`/
> `viewerdisplay`/`viewerbase`/`viewer` 的 create/convert 走
> `oak_make_color_processor`/`oak_convert_color` facadeengine 原
> `render/managedcolor.h+.cpp` 已清空。nm 实测 6 个色彩符号归零
> (24 → 18)。期间定位并修复了一个阻塞验证的**预先存在竞态**:
> worker ticket 在「提交→worker 取件」窗口被 `clear_single_frame_renders`
> 取消,导致 `FootageViewerNotBlack/1` 全量套件 30s 超时——已在
> `RenderWorkerPool::submit_frame` 中于 job 入队前 `ticket->start()`
> 消除脆弱窗口(非 stub,是真实缺陷修复),olive-gtest 全量通过。
引用点:`colordialog.{h,cpp}``colorbutton.{h,cpp}`
`colorswatchchooser.{h,cpp}``nodeparamviewwidgetbridge.cpp`
新增到 `oakengine/color.h`
```c
/** ManagedColor 的 POD 形态:RGBA + 输入色彩空间 id + 输出变换。 */
typedef struct oak_managed_color {
double r, g, b, a;
char input_id[64]; /* 空串 = 未指定 */
char transform[128]; /* 空串 = 未指定 */
} oak_managed_color;
OAKENGINE_API void *oakengine_color_processor_create(
const char *src_space, const char *dst_transform, int direction);
OAKENGINE_API void oakengine_color_processor_free(void *p);
OAKENGINE_API int oakengine_color_processor_convert(void *p,
double in_r, double in_g, double in_b, double in_a,
double *out_r, double *out_g, double *out_b, double *out_a);
```
app 侧:`ManagedColor` 成员变量换成 `oak_managed_color` POD
`ColorProcessor::Ptr` 成员换成 `void *` 句柄(析构处配 free)。
`colorbutton/colorswatchchooser` 只是显示颜色,转换走
`oakengine_color_processor_convert`
---
## ✅ P5:C 类音频回调(5 符号 → 0)
> **状态**:P5 全部完成(5 符号 → 0)。`oakengine/audio.h` 新增
> `OakEngineAudioProcessor` 不透明句柄族(create/free/open/close/is_open/
> convert/output_params),`capi/audio.cpp` 内部用 C++ `AudioProcessor`
> 实现(convert 的输出字节由句柄内部 `Buffer` 持有,调用方零拷贝借用);
> `viewer.h/cpp` 的 `AudioProcessor audio_processor_` 成员换成
> `OakEngineAudioProcessor *`(构造 create、析构 free),open 走
> `OakAudioParams*` POD`.to()` 走 `oakengine_audio_processor_output_params`
> + `oakcore_audioparams_is_valid/time_to_bytes`。nm 实测 5 个音频符号
> 归零(18 → 13),全量 ctest 100% 通过。
引用点:`app/widget/viewer/viewer.{h,cpp}`AudioProcessor 直接构造,
用于回放音频格式转换)。
**为什么不能简单 facade 化**AudioProcessor 是实时回调路径,每次
回调跨 C ABI 进 engine 会有性能顾虑(其实极小,但保持零拷贝更重要)。
**解法(C vtableRIIR 友好)**:facade 定义处理器 C 接口,engine
内部用 C++ AudioProcessor 实现,app 只持有句柄:
```c
/* oakengine/audio.h */
typedef struct OakEngineAudioProcessor OakEngineAudioProcessor;
OAKENGINE_API OakEngineAudioProcessor *oakengine_audio_processor_create(void);
OAKENGINE_API void oakengine_audio_processor_free(OakEngineAudioProcessor *p);
OAKENGINE_API int oakengine_audio_processor_open(OakEngineAudioProcessor *p,
int in_sample_rate, uint64_t in_layout, int in_format,
int out_sample_rate, uint64_t out_layout, int out_format,
double speed);
OAKENGINE_API void oakengine_audio_processor_close(OakEngineAudioProcessor *p);
OAKENGINE_API int oakengine_audio_processor_convert(OakEngineAudioProcessor *p,
float **data, int frame_count);
```
app 的 `AudioProcessor processor_;` 成员换
`OakEngineAudioProcessor *processor_`create/free 配对)。
---
## ✅ P6D 类渲染/GPU(13 符号 → 0,最大工程,放最后)
> **状态**:P6 全部完成(13 符号 → 0)。nm 实测:oak-editor 与
> oak-render-worker 的 ` U _ZN5olive` 均为 **0**;全量构建 0 error
> 全量 ctest 100%45/45);`ViewerDisplayReproTest` 三个可跑通用例
> Vulkan 后端)保持通过,OpenGL offscreen 三个用例按环境预期 SKIP;
> 导出测试(`oakengine_export_test`、`oak_cli_transcode_verify`)无回归。
>
> **实现说明(与原设计提议的差异,已论证)**:
> 1. facade 未加入 `oakengine/renderer.h`,而是新建独立头
> `oakengine/display.h` + `engine/src/capi/display.cpp`。原因:
> `renderer.h` 已存在面向序列渲染 CPU 帧的 `OakEngineFrame` 及
> `oakengine_frame_data/free/width/...` 函数族,与本节设计的
> `oakengine_frame_create/allocate/free` **C 命名冲突**C 不允许重载)。
> 2. 命名 accordingly 调整为:渲染器/纹理族 `oakengine_display_renderer_*` /
> `oakengine_display_texture_*`codec 帧族 `oakengine_codec_frame_*`。
> 3. 采用**最小侵入方案**TexturePtr/FramePtrstd::shared_ptr)流仍保留在
> app 内(它们经 QVariant/信号在 engine→app 投递,全句柄化需重构帧投递
> 管线,对 ViewerDisplayReproTest 风险极高)。facade 只收口 13 个
> out-of-line 调用(create_texture/blit_color_managed/upload/download/
> Frame::create/set_video_params/allocate/渲染器构造-init-destroy)。
> app 复制/reset shared_ptr 只动引用计数(deleter 在 engine 侧 type-erase),
> 不引用 `~Texture`/`~Frame`inline/virtual 方法不产生 `U _ZN5olive`。
> 这满足 nm=0 硬指标,且渲染路径行为零变化。
> 4. `out_texture`/`out_frame` 出参为指向 caller `TexturePtr`/`FramePtr`
> 存储的指针,engine 侧赋值,shared_ptr 簿记全留在 engine。
引用点:`app/widget/manageddisplay/manageddisplay.cpp`
OpenGLRenderer/DynamicRenderer 构造、init、Texture upload/download、
blit_color_managed)、`app/widget/viewer/viewerdisplay.cpp`
`app/widget/scope/`Frame::create/allocate/set_video_params)。
**原则**:渲染对象的生命周期全部移入 engine,app 只持有句柄并
描述"要画什么"。这也是 RIIR 里 GPU 管线的预定边界。
新增到 `oakengine/renderer.h`
```c
/* 渲染器句柄:engine 按当前后端(OpenGL/软件)创建,app 不知道类型 */
OAKENGINE_API void *oakengine_renderer_create_for_thread(void);
OAKENGINE_API int oakengine_renderer_init(void *r, void *qopengl_context_or_NULL);
OAKENGINE_API void oakengine_renderer_destroy(void *r);
/* 纹理句柄 */
OAKENGINE_API void *oakengine_texture_create(void *r,
const oak_video_params *params, const void *pixels, int linesize);
OAKENGINE_API void oakengine_texture_free(void *t);
OAKENGINE_API int oakengine_texture_upload(void *t, const void *pixels,
int linesize);
OAKENGINE_API int oakengine_texture_download(void *t, void *pixels,
int linesize);
/* 帧句柄(CPU 侧缓冲) */
OAKENGINE_API void *oakengine_frame_create(void);
OAKENGINE_API int oakengine_frame_set_video_params(void *f,
const oak_video_params *params);
OAKENGINE_API int oakengine_frame_allocate(void *f);
OAKENGINE_API void oakengine_frame_free(void *f);
OAKENGINE_API void *oakengine_frame_data(void *f); /* 写像素用 */
OAKENGINE_API int oakengine_frame_linesize(void *f);
/* 色彩管理 blit */
OAKENGINE_API int oakengine_renderer_blit_color_managed(
void *r, const oak_color_transform_job *job, void *dst_texture,
const oak_video_params *params);
```
`oak_color_transform_job` POD 在 `oakengine/color.h` 定义(processor 句柄
+ input/output id + 各向异性参数,字段以 engine `ColorTransformJob`
拍平)。
app 侧:`manageddisplay`/`viewerdisplay` 不再 `new OpenGLRenderer`
改持 `void *renderer_`;帧/纹理成员全部句柄化。
**验证重点**:渲染路径行为必须零变化——`olive-gtest`
`ViewerDisplayReproTest` 三个可跑通用例必须保持通过;导出测试
`oakengine_export_test``oak_cli_transcode_verify`)不许变差。
---
## 验收(100% C ABI 判据)
1. `nm -D ... | grep -c " U _ZN5olive"` = **0**oak-editor 与
oak-render-worker 都是 0)。
2. 全量构建 0 error;全量 ctest 绿(flaky 规则照旧)。
3. 反作弊审计:app 无 dlsym/dlfcn`git diff` engine 无 inline 化;
app 无 `#include "node/``#include "undo/``#include "task/`
`#include "render/``#include "timeline/` 的 engine C++ 头
`grep -rn '#include "' app/ | grep -E '"(node|undo|task|render|timeline|pluginSupport)/'`
应为空或只剩极个别已论证的)。
4. `c-abi-migration-handoff.md` §6.4 豁免清单清空(改为"无豁免"),
roadmap 补 R6 批次记录,`plans/riir.md` 状态更新为"边界已纯"。
## 执行顺序与节奏建议
```
P1(纯加法,热身)→ P2(机械替换,量大但无决策)→ P3(MOC,细心活)
→ P4(POD 化)→ P5(小)→ P6(GPU,最重,单独留足时间)
```
每个 P 内部按上表逐个符号做,**每 3-5 个符号提交一次**,不要攒大批。
每完成一个 P,把本文对应节的符号表划掉(编辑文档标注 ✅)并提交。
---
## 附:R6 收尾复核记录(Kimi K32026-07-26
R6 由 Qwen 3.8 Max 执行完成,复核结论:**nm 目标达成(58→0,双二进制)**,
- 全量构建 0 errorctest 45/45oak_cli_transcode 间歇 SEGFAULT 为预存
flaky,手动跑通过);
- 反作弊干净:无 dlsym、无 stub、无 engine inline 化;
- P2 undo 命令 facade 化质量合格(113 处 `new XxxCommand(` 归零);
- P3 MOC 处理合格(信号参数句柄化 + 非 slots 区注释清楚)。
**遗留项(已记录,后续批次)**
1. **`oakengine/display.h` 的"灰色契约"(P6 的妥协)**:函数签名均为
`void *`nm 上纯 C),但文档约定 `out_texture/out_frame` 指向调用方
内存中的 `std::shared_ptr`engine 在其上构造 shared_ptr 副本),
`video_params` 实为 `olive::VideoParams*`。对 Rust 重写而言这层契约
仍是 C++ 语义:Rust 侧无法安全持有 shared_ptr,也无法构造
VideoParams。**后续必须重做**`oak_video_params` POD 替换
`const void *video_params`;纹理/帧改不透明句柄 +
`oakengine_display_texture_free/oakengine_codec_frame_free`
2. **engine 导出符号未收口**`nm -D --defined-only liboakengine.so |
grep -c " T _Z"` = 3486。按 riir.md §2 Step 2 做
`-fvisibility=hidden` + 只导出 `oakengine_*`(独立批次,app 已无引用,
不阻塞)。
3. **app 仍 include ~40 个 engine C++ 头**(不产生符号引用,nm=0 已证),
彻底清理为低优先级长项。
+212
View File
@@ -0,0 +1,212 @@
# R7 计划:从"nm=0"到 Rust-ready 纯 C ABI
> 面向执行者(Qwen 3.8 Max),自包含。工作分支:`c-abi-migration`。
> **背景**R6 已使 oak-editor / oak-render-worker 的 `U _ZN5olive` = 0
> `d13e4e800`)。但按 riir.md 的目标(engine 拆模块 → Rust 重写),
> 还差两层皮:
> 1. `oakengine/display.h` 的灰色契约——签名是 `void *`,语义却是 C++
> (往调用方内存写 `std::shared_ptr`、参数实为 `olive::VideoParams*`);
> 2. liboakengine.so 仍导出 3486 个 C++ 符号(`nm -D --defined-only |
> grep " T _Z"`),Rust 模块化拆分要求导出面只剩 `oakengine_*`。
>
> 三条红线照旧(禁 inline 化 engine 实现、禁 no-op stub、禁 dlsym)。
> 每步闭环:全量构建 0 error → 全量 ctest 绿 → 立即提交。
> flaky 规则:`oak_cli_transcode`/`oakengine_export_test`/`olive-gtest`
> 单独重跑一次,连续两次失败才算回归(`oak_cli_transcode` 的间歇
> SEGFAULT 是预存问题,手动跑通过为准)。
---
## R7-Adisplay.h 灰色契约 POD 化
### A.1 现状问题(为什么 nm=0 还不够)
`engine/include/oakengine/display.h` 的 11 个函数签名全是 `void *`
但注释约定:
- `out_texture/out_frame` 指向调用方内存中的 `TexturePtr/FramePtr`
`std::shared_ptr`),engine 在其上拷贝构造 shared_ptr
- `video_params` 实为 `const olive::VideoParams*`
- `color_job` 实为 `const olive::ColorTransformJob*`
Rust 无法安全持有 shared_ptr、无法构造 C++ VideoParams。app 侧 47 处
`TexturePtr/FramePtr` 成员/变量(18 文件,见 A.4)同样要换。
### A.2 新契约(设计钉死,照此实现)
**所有权协议(核心决策,不许改)**:纹理/帧句柄是不透明指针,指向
engine 堆上的控制块(内部持有 `std::shared_ptr`,实现细节对 ABI 不可见)。
所有权经显式 retain/free 转移,禁止任何"写入调用方内存的 shared_ptr"。
```c
/* engine/include/oakengine/display.h —— 重写版 */
/* ---- 渲染器 ---- */
OAKENGINE_API void *oakengine_display_renderer_create_dynamic(
const char *backend_id, void *parent_qobject);
OAKENGINE_API void *oakengine_display_renderer_create_opengl(
void *parent_qobject);
OAKENGINE_API int oakengine_display_renderer_init(void *renderer,
void *gl_context);
OAKENGINE_API void oakengine_display_renderer_destroy(void *renderer);
/* ---- 纹理(句柄 = OakEngineDisplayTexture*,不透明) ---- */
/* params 用 oak_video_params PODoakengine/videoparams.h 已有),
* 不再是 const void*。 */
OAKENGINE_API void *oakengine_display_texture_create(
void *renderer, const oak_video_params *params,
const void *pixels, int linesize);
/* retain:返回同一句柄并把内部引用计数 +1(跨线程移交用,
* 见 A.3 协议)。free:-1,归零时释放。二者对 NULL 均为 no-op。 */
OAKENGINE_API void *oakengine_display_texture_retain(void *texture);
OAKENGINE_API void oakengine_display_texture_free(void *texture);
OAKENGINE_API int oakengine_display_texture_upload(
void *texture, const void *pixels, int linesize);
OAKENGINE_API int oakengine_display_texture_download(
void *texture, void *pixels, int linesize);
/* 只读属性查询(替代 texture->params()/width()/format() 等) */
OAKENGINE_API int oakengine_display_texture_get_params(
const void *texture, oak_video_params *out);
OAKENGINE_API int oakengine_display_texture_id(const void *texture);
/* ---- 帧(句柄 = OakEngineCodecFrame*,不透明,同协议) ---- */
OAKENGINE_API void *oakengine_codec_frame_create(void);
OAKENGINE_API void *oakengine_codec_frame_retain(void *frame);
OAKENGINE_API void oakengine_codec_frame_free(void *frame);
OAKENGINE_API int oakengine_codec_frame_set_video_params(
void *frame, const oak_video_params *params);
OAKENGINE_API int oakengine_codec_frame_get_params(
const void *frame, oak_video_params *out);
OAKENGINE_API int oakengine_codec_frame_allocate(void *frame);
OAKENGINE_API void *oakengine_codec_frame_data(void *frame);
OAKENGINE_API int oakengine_codec_frame_linesize(const void *frame);
/* ---- 色彩管理 blit ---- */
/* oak_color_transform_job POD(新定义,字段以 engine
* ColorTransformJob 拍平:processor 句柄 + input/output 空间 id +
* 各向异性等)。 */
OAKENGINE_API int oakengine_display_renderer_blit_color_managed(
void *renderer, const oak_color_transform_job *job,
void *dst_texture, const oak_video_params *params);
/* 跨后端纹理下载(viewerdisplay 的 download_from_texture 路径) */
OAKENGINE_API int oakengine_display_renderer_download_from_texture(
void *renderer, int texture_id, const oak_video_params *params,
void *dst_pixels, int linesize);
```
engine 实现(`engine/src/capi/display.cpp` 重写):
```cpp
struct OakEngineDisplayTexture { olive::TexturePtr ptr; };
struct OakEngineCodecFrame { olive::FramePtr ptr; };
// create: new OakEngineDisplayTexture{renderer->create_texture(...)}
// retain/free: new/delete 控制块(引用计数即 shared_ptr 自身)
// POD↔C++oak_video_params ↔ olive::VideoParams 的转换函数若
// capi 已有(viewer.cpp 的 get 路径)就抽成内部共享 helper
// (放 engine/src/capi/videoparamsinternal.h),不许复制粘贴第三份。
```
### A.3 跨线程移交协议(最容易写错的地方,钉死)
`viewerdisplay``load_frame_`/`load_texture_` 在解码线程生产、
显示线程消费。原语义靠 shared_ptr 引用计数保活。新协议:
1. 生产侧 `oakengine_display_texture_retain(t)` 后写入共享槽;
2. 消费侧取走句柄,旧句柄 `free`
3. 槽清空时持有一方负责 `free`
**每个 retain 必须配对恰好一个 free**。写完后 grep 审计配对数。
### A.4 app 侧触点清单(47 处,按文件做,每文件一提交)
| 文件 | 处数 | 要点 |
|---|---|---|
| `app/widget/viewer/viewerdisplay.{h,cpp}` | 19+15 | 最大。`texture_`/`load_texture_`/gizmo 纹理全换句柄;析构与各 reset 路径补 free;A.3 协议主要在这里 |
| `app/widget/scope/scopebase/scopebase.{h,cpp}` | 6+9 | 同模式 |
| `app/widget/manageddisplay/manageddisplay.cpp` | 6 | create_texture/blit/download |
| `app/widget/viewer/viewer.{h,cpp}` | 1+9 | FramePtr 成员换句柄 |
| `app/widget/multicam/multicamdisplay.{h,cpp}` | 1+4 | |
| `app/widget/scope/histogram/histogram.{h,cpp}` | 2+1 | |
| `app/widget/scope/waveform/waveform.{h,cpp}` | 1+1 | |
| `app/widget/scope/vectorscope/vectorscope.{h,cpp}` | 1+1 | |
| `app/panel/viewer/viewerbase.h``app/panel/scope/scope.{h,cpp}` | 3 | |
完成判据:app 全仓库 grep `TexturePtr|FramePtr` = 0
`oakengine/display.h` 全文无 `shared_ptr`、无 `olive::` 出现在**签名**
(注释里也不许写 "olive::TexturePtr storage" 这种约定)。
### A.5 验证
- 新增 `engine/tests/oakengine_display_test.cpp`(无 GL 环境测错误
路径与 retain/free 配对;GL 相关断言用现有 backend 检测跳过模式)。
- `olive-gtest``ViewerDisplayReproTest` 三个可跑通用例**必须全过**
(这是显示路径的回归网,挂一个就是真挂)。
- 手动验证(报告里写明):打开素材 → 画面非黑;scope 面板渲染正常。
---
## R7-Bengine visibility 收口(3486 → 只导出 oakengine_*
### B.1 原理(已具备的条件)
`OAKENGINE_API` 在 GCC/Clang 已是
`__attribute__((visibility("default")))`export.h:40)。给
`oakengine` 目标加 `-fvisibility=hidden` 后,只有 `oakengine_*` 导出。
`oakgl`/`oakvulkan` 两个动态后端同理(已有 `*-cabi-check` OBJECT
目标,顺带确认它们导出面也只剩 C ABI)。
### B.2 唯一难点:测试链接
隐藏符号后,直接引用 engine C++ 内部的测试会断链:
- `olive-gtest`**1006** 个 `U _ZN5olive`
- `timeline-tests`29
- `oakengine_export_test`9`make_oakengine_test` 族里引用内部的)
- `compositing-tests`0(纯 facade,无碍)
**解法(钉死)**engine 源码改出 OBJECT 库,测试链对象文件而非
`.so`
```cmake
# engine/CMakeLists.txt
add_library(oakengine-obj OBJECT ${OLIVE_SOURCES})
# POSITION_INDEPENDENT_CODE ON
add_library(oakengine SHARED $<TARGET_OBJECTS:oakengine-obj>)
target_compile_options(oakengine-obj PRIVATE -fvisibility=hidden)
# 引用 engine C++ 内部的测试目标:
target_link_libraries(<test> PRIVATE oakengine-obj) # 替代 oakengine
```
- `olive-gtest`tests/gtest/CMakeLists.txt)改链 `oakengine-obj` +
`oakengine`facade 符号从 .so 来,避免重复定义;若 ODR 冲突则只链
oakengine-obj,先把 facade 函数符号在 object 里的重复问题解决——
二选一,以链接通过且 ctest 全绿为准,把选择写进提交信息)。
- `timeline-tests`、引用内部的 `oakengine_*_test` 同法。
- **禁止**:为了让测试过而把 engine 内部符号加 visibility("default")
白名单——那是开天窗。
### B.3 验证
```
nm -D --defined-only cmake-build-debug/engine/liboakengine.so | grep -c " T _Z" # 目标 0
nm -D --defined-only cmake-build-debug/engine/liboakengine.so | grep -c " T " # 应等于 oakengine_* 函数数
nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive" # 必须仍为 0
```
全量构建 0 error + 全量 ctest 绿(45 个)才提交。
---
## R7-C(低优先级,时间够再做):app 的 engine C++ 头清理
app 仍 include ~40 个 engine C++ 头(`grep -rn '#include "' app/ |
grep -E '"(node|render|timeline|undo|task|pluginSupport)/'`)。不产生
符号引用(nm=0 已证),但 RIIR 拆模块时这些 include 会全部失效。
逐个换 facade/句柄头(`cliphandle.h``nodevaluehandle.h` 模式)。
**本批不设完成判据**,收尾时把剩余清单写进 riir.md 附录即可。
## 验收(R7 完成判据)
1. `display.h` 全文无 C++ 类型签名/契约注释;app 无 TexturePtr/FramePtr。
2. liboakengine.so ` T _Z` = 0oak-editor ` U _ZN5olive` 保持 0。
3. 全量构建 0 error;全量 ctest 绿。
4. 更新 `plans/riir.md` 状态(边界已纯 → 可进 Step 1 拆分)、
`facade-migration-roadmap.md` R7 批次记录。
5. 向用户报告,由用户宣布进入 riir.md §4 的模块拆分阶段。