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

519 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 已证),
彻底清理为低优先级长项。