Move the finished migration campaign docs (handoffs v3-v6, roadmap, R5 guides, R6 cleanup, R7 pure-ABI) from docs/zh/ into docs/zh/plans/completed/ with an archive README; fix all cross-references; refresh plans/README.md index (active plans now marked unlocked).
26 KiB
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,见../riir.md)的硬前提:C ABI 边界上不能 残留任何 C++ 渗漏。三条红线(违反即返工):
- 禁止把 engine 的 .cpp 实现 inline 化进头文件。
- 禁止 no-op stub(空实现、丢字段的"简化"调用、假成功返回值)。
- 禁止 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 函数的固定流程(每个函数都照做):
- 在
engine/include/oakengine/<域>.h声明(extern "C",OAKENGINE_API, 写清所有权/单位/错误码的文档注释); - 在
engine/src/capi/<域>.cpp实现(内部直接调 engine C++,允许—— 那是 engine 自己的实现); - 在
engine/tests/加纯 C 测试(参照现有oakengine_*_test.cpp); - app 侧换调用点;
- 全量构建 + ctest + nm + 提交。
P1:F 类 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:
/** 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_value:split 的逆。 */
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出 POD,app 如需 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:
/** 纯算法包装。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:
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 零散单点(6,plugin::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(信号迁移) |
✅ P2:B 类 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 或对应域头):
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;补:
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 消失而归零)。
✅ P3:A 类 MOC staticMetaObject(9 符号 → 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:67focused_node_changed(Node *)app/widget/nodeparamview/nodeparamview.h:94同上app/widget/nodeparamview/nodeparamviewitem.h:71,226request_select_node(Node *)app/widget/nodeparamview/nodeparamviewconnectedlabel.h:44同上;:47,49input_connected/disconnected(Node *, const NodeInput &)app/panel/timeline/timeline.h:129,130reveal_viewer_in_project(ViewerOutput *)等app/widget/history/(UndoStack* 参数,如有)app/widget/multicam/multicamwidget.h:56(MultiCamNode*)
改法(以 focused_node_changed 为例):
- 信号签名改
focused_node_changed(OakEngineNode *n); - 发射处
emit focused_node_changed(reinterpret_cast<OakEngineNode*>(n)); - 接收槽同步改类型,槽内
reinterpret_cast<Node*>(n)还原; - 该头文件不再 include engine C++ 头(
node/node.h等),改 includeoakengine/node.h。 - 全仓库 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_OBJECT;app 的 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_colorfacade;engine 原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:
/** 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 vtable,RIIR 友好):facade 定义处理器 C 接口,engine 内部用 C++ AudioProcessor 实现,app 只持有句柄:
/* 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 配对)。
✅ P6:D 类渲染/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)无回归。实现说明(与原设计提议的差异,已论证):
- 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/freeC 命名冲突(C 不允许重载)。- 命名 accordingly 调整为:渲染器/纹理族
oakengine_display_renderer_*/oakengine_display_texture_*;codec 帧族oakengine_codec_frame_*。- 采用最小侵入方案:TexturePtr/FramePtr(std::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 硬指标,且渲染路径行为零变化。out_texture/out_frame出参为指向 callerTexturePtr/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:
/* 渲染器句柄: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 判据)
nm -D ... | grep -c " U _ZN5olive"= 0(oak-editor 与 oak-render-worker 都是 0)。- 全量构建 0 error;全量 ctest 绿(flaky 规则照旧)。
- 反作弊审计:app 无 dlsym/dlfcn;
git diffengine 无 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)/'应为空或只剩极个别已论证的)。 c-abi-migration-handoff.md§6.4 豁免清单清空(改为"无豁免"), roadmap 补 R6 批次记录,../riir.md状态更新为"边界已纯"。
执行顺序与节奏建议
P1(纯加法,热身)→ P2(机械替换,量大但无决策)→ P3(MOC,细心活)
→ P4(POD 化)→ P5(小)→ P6(GPU,最重,单独留足时间)
每个 P 内部按上表逐个符号做,每 3-5 个符号提交一次,不要攒大批。 每完成一个 P,把本文对应节的符号表划掉(编辑文档标注 ✅)并提交。
附:R6 收尾复核记录(Kimi K3,2026-07-26)
R6 由 Qwen 3.8 Max 执行完成,复核结论:nm 目标达成(58→0,双二进制),
- 全量构建 0 error;ctest 45/45(oak_cli_transcode 间歇 SEGFAULT 为预存 flaky,手动跑通过);
- 反作弊干净:无 dlsym、无 stub、无 engine inline 化;
- P2 undo 命令 facade 化质量合格(113 处
new XxxCommand(归零); - P3 MOC 处理合格(信号参数句柄化 + 非 slots 区注释清楚)。
遗留项(已记录,后续批次):
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_paramsPOD 替换const void *video_params;纹理/帧改不透明句柄 +oakengine_display_texture_free/oakengine_codec_frame_free。- engine 导出符号未收口:
nm -D --defined-only liboakengine.so | grep -c " T _Z"= 3486。按 riir.md §2 Step 2 做-fvisibility=hidden+ 只导出oakengine_*(独立批次,app 已无引用, 不阻塞)。 - app 仍 include ~40 个 engine C++ 头(不产生符号引用,nm=0 已证), 彻底清理为低优先级长项。