Files
oak-editor/docs/zh/facade-migration-roadmap.md
T
Mike-Solar eb634b53ef R7: pure C ABI display boundary + engine visibility closure
R7-A (Qwen 3.8 Max): oakengine/display.h rewritten to the POD contract
from r7-pure-abi-plan.md - oak_video_params everywhere, opaque
texture/frame handles with retain/free protocol (engine-heap control
blocks), OakSharedBuffer refcounted wrapper for the QVariant playback
path. All TexturePtr/FramePtr gone from app (47 sites).

R7-B (Qwen 3.8 Max): liboakengine.so exports 3486 -> 19 C++ symbols
(version script oakengine.ver: oakengine_* plus the documented
oakgl/oakvulkan dlopen plugin ABI). oakengine-obj OBJECT library feeds
both the shared lib and the test binaries (-rdynamic so dlopen'd
backends resolve engine objects).

Fix (Kimi K3): producer/consumer type mismatch - viewerdisplay
unpacks OakSharedBufferPtr but viewer.cpp pushed raw void* handles,
so no frame ever reached the display widget (all 5 vulkan viewer
tests timed out with 'never received a texture'). Producers now wrap
with oak_make_shared_frame / oak_make_shared_texture(retain).

Verified: build 0 errors, ctest 45/45, nm U _ZN5olive = 0 in
oak-editor/oak-render-worker/oak-cli, 19 exported C++ symbols in
liboakengine.so (all documented plugin ABI).
2026-07-27 00:54:46 +08:00

49 KiB
Raw Blame History

liboakengine Facade 迁移路线图

本文把"编辑器 UI 迁移到 liboakengine C ABI(facade)"这一独立工程拆解为可执行的阶段。背景与已完成的基础见各提交:28c442623(引擎拆分)、bff06e00e(Core 拆分)、以及 facade 各族提交(ipc/init/project/timeline/renderer/footage/export + 两轮编辑原语)。

终态定义

  1. liboakengine.so 只导出 oakengine_* C 符号(nm -D --defined-onlyoakengine_* 外为零,与 liboakcore 相同标准)。
  2. oak-editor 与 oak-render-worker 不再引用任何引擎 C++ 符号,全部经 facade(或其 C++ 包装层,模式同 liboakcore 的 wrapper)访问引擎。
  3. 全部现有测试(gtest + CLI ctest + 各 C ABI 测试)持续全绿,GL/Vulkan 相关用例可跳过。

现状盘点(已完成)

  • 引擎物理拆分完毕:engine/ 树 + liboakengine.so;worker 脱 UI(336MB→2.9MB)。
  • facade 13 族约 160+ C 函数:ipc/init/project/timeline/renderer/footage/export/node/keyframe/序列结构/编辑原语×2/preview/playback(在建);oak-cli 是现成的纯 C ABI 消费者(info/probe/render/transcode,已进三平台打包)。
  • 面板迁移(阶段 3)已切:Export 对话框、素材属性/项目浏览器、序列参数、时间线(三轮,含遗留命令)、节点参数面板、关键帧区;app 侧约定为直接调 C 函数。
  • 编辑器与引擎的 UI 依赖已全部反转(Core/EngineCore 拆分、handler 钩子体系、枚举下沉、布局数据纯净化)。
  • 测试基建:make_oakengine_test、GL 门控 SKIP 模式、CLI ctest、沙箱 config。

API 缺口(按 UI 需求倒推)

facade 现状覆盖:项目/序列读写、素材探测与导入、时间线查询与片段编辑、渲染、导出、IPC。缺口按优先级:

  1. 节点图族(oakengine_node_*)——最大缺口,编辑器操作的实体。
    • 节点枚举/查询:type id、label、category、输入定义(id/类型/默认值/属性)。
    • 参数读写:get/set_input(类型化值,参照 NodeValue::Type 到 C 类型映射:float/int/rational(num,den)/color(rgba)/string/bool/vec234)。
    • 图操作:add_node(factory id)、remove_node、connect(input,output)/disconnect。
    • 效果链读取:某 sequence/clip 的效果栈遍历。
  2. 关键帧族(oakengine_keyframe_*):增删改、时间/值/缓动(bezier 四点)、按参数遍历。
  3. 序列结构族:轨道高度、静音/锁定、轨道重命名/重排/删除;marker 增删改;播放头 scrub 状态。
  4. 选择/命令族:undo 命令描述(label)、按命名命令打包的编辑操作(对齐 app 命令名,便于 UI 直接映射)。
  5. 素材管理族:relink、代理状态查询/生成(ProxyManager 已有引擎实现,直接包)、footage 参数覆盖。
  6. 预览控制族:播放/暂停/倍速、音频表读数(AudioMonitor)、scrub、工作区循环。

阶段计划

阶段 0:冻结契约(1 周)

  • 将 facade 头文件与语义写进 docs/facade-api-reference.md(从各头 Javadoc 生成骨架)。
  • C++ 包装层生成器/模板定稿(照 liboakcore wrapper 模式,按族手写而非生成,规模可控)。
  • CI 增加 facade 测试目标(make_oakengine_test 已全部进 ctest,确认 Win/mac 也跑)。

阶段 1:worker 与 CLI 对齐(1-2 周)

  • oak-render-worker 内部一律改用 oliveimpl 内部头(当前已是),确认其不依赖 facade; facade 专为外部消费者。
  • oak-cli 覆盖到 proxy 生成/媒体信息全命令,作为 facade 回归基准。

阶段 2:节点图族(2-4 周)

  • oakengine_node_* 设计与落地(参数值映射表先行)。
  • 测试:程序化建图(Solid→LUT→渲染)、参数往返、连接校验、undo。
  • 里程碑:oak-cli 能 oak-cli graph <ove> 打印效果栈,oak-cli lut <ove> <lut> 给指定 clip 加 LUT。

阶段 3:编辑面迁移(4-8 周,按面板逐个)

顺序(依赖递增):

  1. Export 对话框—— facade export 族已就绪,最先切,删 UI 侧 ExportTask 直驱。
  2. Footage 属性/项目浏览器—— footage 族 + 素材管理族。
  3. 序列参数/时间标尺/工作区—— timeline 族已就绪。
  4. 时间线面板—— 需要选择族+编辑原语全集;UI 动作→facade 命令一一映射(参照 timelineundogeneral 的命名)。
  5. 节点参数面板/效果控件—— 需要节点图族+关键帧族。
  6. 预览/播放—— 预览控制族。
  7. 多机位/标记/其余面板

每切一个面板:该面板的 UI 代码改为只调 facade(或 wrapper),对应引擎类从 UI 侧 include 中移除;全量测试必须保持绿。

附 A:viewer 面板迁移映射(阶段 3.6 细化)

地基:facade 播放族(oakengine_playback_*,无头异步播放引擎)。ViewerWidget 当前自管的整条播放管线替换为 facade 播放会话;消费方式与其余面板一致(app 侧直接调 C 函数)。

迁入 facade(从 ViewerWidget 删除):

  • playback_timer_update / playback_backup_timer_ → facade 拉取线程 + frame 回调,UI 在回调里 marshal 回主线程调 set_display_image
  • 视频 prequeue(prequeue_length_/prequeue_count_/queue_watchers_/request_next_frame_for_queue/renderer_generated_frame_for_queue)→ facade 内部 8 帧 prequeue。
  • 音频播放队列(audio_playback_queue_/audio_processor_/prequeued_audio_/k_audio_playback_interval/queue_next_audio_buffer/received_audio_buffer_for_playback/decrement_prequeued_audio)→ facade audio 回调(1/4 秒 planar float 块)。
  • 音频主时钟:facade 内部直接用 AudioManager(已在 engine/audio/)的秒时钟;UI 侧改为轮询 oakengine_playback_get_position 更新播放头。
  • ViewerPlaybackTimer(display_widget_->timer() 的音频钟→时间戳换算)整体被 get_position 取代,viewerplaybacktimer.{h,cpp} 删除。
  • finish_play_preprocess 中的 reset_output_clock/prequeued 音频推送/备份定时器(1385-1421)→ facade start 内部完成。

复用不动(帧显示路径):ViewerDisplayWidgetqueue()->append_timewise + 显示定时器整套保留——facade frame 回调 marshal 到主线程后走与今天 renderer_generated_frame_for_queue(viewer.cpp:1562)完全相同的路径;dw->play/pause 照旧。即只换帧的来源,不换帧的去向

留在 UI 侧(facade MVP 不覆盖):

  • 负倍速 / shuttle(shuttle_left/rightplayback_speed_ 变速路径)——后续再议。
  • 音频刮擦(audio_scrub_watchers_push_scrubbed_audio)——逐帧点播,非连续播放,保留现有 watcher 路径。
  • 录制/capture(arm_for_recording/disarm_recording 等)。
  • 纯 UI:gizmos、文本编辑、安全框、多屏 ViewerWindow、波形视图、右键菜单。
  • multicam 检测(detect_multicam_node)——依赖 UI 选择状态。
  • 边界与循环策略(playback_timer_update 1959-2044 的 min/max 计算、workarea in-out、StopPlaybackOnLastFrameLoop 配置、录制范围)——依赖 UI 配置与状态,保留在一个轮询 facade 位置的 UI 定时器里;到点由 UI 调 facade pause/start(循环时重锚)。

facade 播放族需随迁移做的细化:

  • pause 时应停 AudioManager 输出(engine 内部,instance 判空),否则缓冲区余音继续播;app 不再直接碰 AudioManager。
  • 音频监视器电平:facade audio 回调 → UI marshal → AudioMonitor::push_sample_buffer_on_all(AudioMonitor 是 app 侧控件)。
  • 波形监视器:AudioMonitor::start_waveform_on_all 仍在 UI 于 start 时发起(依赖 connected node 的 waveform 元数据)。

验收:播放/暂停/停止下帧画面与声音同步;时间标尺播放头跟随;全量 gtest + facade 回归(固定 9 个 + playback)绿;viewer.h 中不再出现 RenderTicketWatcher 播放队列字段。

附 C:阶段 4 耦合消减战役(2026-07 测绘,基线 559)

按符号聚类的消减顺序(每步保持全绿+耦合计数下降):

  1. icon(56):注册表搬到 app(engine/ui/icons→app/ui/icons,~20 个 app 文件只改 include 路径,namespace 不变);engine 仅 4 处 data(Node::icon) 覆盖(folder/footage/sequence/node 默认),改为返回图标名字符串,projectviewmodel.cpp:185 单一消费点按名映射。不要下沉 core——liboakcore 实测 Qt-free(2026-07),QIcon 会污染它;图标本就是呈现资源,归 app。

  2. EngineCore(48) 已完成(2026-07-21):新增 oakengine/app.h + engine/src/capi/app.cpp(oakengine_app_* 族:CoreParams 启动、start/stop、open/active project、recent 列表、tool/snapping/timecode、剪贴板、footage 过滤、status bar、handler 注册,信号→OakEngineAppCallbacks 函数指针回调)。app/Core 解除对 EngineCore 的继承改为组合转发,对 app 其余代码接口不变;coreengine.h 仅把 add_open_project/add_open_project_from_task/set_active_project/add_recovery_project_from_task/get_auto_recovery_index_filename 提为 public 并新增 open_project() 只读访问器。app 侧 olive::EngineCore 未定义符号 57→0,总 U _ZN5olive 491→441。测试 oakengine_app_test(纯 C,无需 GPU)。worker 的无头 Core 继续用 C++ 不动。

  3. 节点图 UI(~103):Node 40 / ViewerOutput 20 / Track 13 / ClipBlock 12 / NodeGroup 9 / NodeKeyframe 7 / NodeTraverser 6 / MultiCamNode 6——node view、multicam 面板、曲线编辑器对节点类的直接引用,按控件逐个切。节点参数/关键帧 UI 已完成(2026-07-21,B8a):facade oakengine/node.h 扩容 ~60 个 C 函数——输入元数据(is_array/array_size/flags/is_connectable/is_keyframable/is_keyframed_ex、property 全套 typed getter 与枚举、set_input_property_string(notify 可控))、值读取(get_input_at_time/string/binary/bezier、default_value)、图查询(get_project/input_get_connected_node/get_label_and_name/get_input_name/copy_inputs)、多轨道关键帧枚举与导航(track_count/count_on_track/handle_on_track/at_time/keyframes_at_time/has/earliest/latest/closest_before/after/best_type,时间为秒有理数)、keyframe 句柄族(get_time/type/value/bezier_point/valid_bezier_point/track/element/input_id/node/has_sibling_at_time + live 非 undo 三件套 + create/dispose)、undoable 批量(remove_many/toggle_at_time/set_input_keyframing/keyframes_paste,均单条命令)、输入拖拽器 OakEngineNodeDragger(create/start/drag/end 一条 undo)。事件表新增 70-86 节点族 17 个事件(label/value_changed(带范围 ts)/connected/disconnected/flags/property/data_type/array_size/keyframe 五个/enable/context 两个/message_count),oakengine_event 扩展 c/s 字段;EngineEventBridge 同步加带类型信号。app 侧:nodeparamview 13 文件、curvewidget、keyframeview、keyframeproperties 全部切到 facade+事件桥;新增共享头 app/widget/keyframeview/keyframehandle.h(key 指针当不透明句柄的全部访问器 + TimeBasedViewSelectionManager 的 ADL 定制点,模板本体改用 selection_time/selection_set_time/selection_has_sibling_at_time/selection_time_target_parent 自由函数,TimelineMarker 实例化不受影响)。四目录解析到引擎的 olive::Node/NodeKeyframe 符号 46+21+21+5→0,总 U _ZN5olive 346→320。测试:oakengine_node_test/oakengine_keyframe_test/oakengine_events_test 各补一族纯 C 用例(handle 族/导航/toggle/keyframing/paste/dragger/属性/节点事件),35/35 绿。已知妥协:keyframe 粘贴 undo 粒度为每节点一条命令(原全局一条);k_binary 输入写路径经 string_at_time(死路径兜底);3 处 Qt6 模板 connect 改用字符串式 SIGNAL/SLOT 以规避 Node::staticMetaObject。nodeview、multicam、ViewerOutput 相关属 B8b。nodeview/NodeGroup/MultiCamNode 已完成(2026-07-21,B8b):facade oakengine/node.h 再扩 3 族——context 位置(contains/get/set_position/set_expanded(插入语义,同 C++)/count/at、oakengine_node_get_effect_input)、NodeGroup 族(is_group、add/remove_input_passthrough(直接+undoable 两版)、set/get_output_passthrough(两版)、passthrough count/at/get_id_of、resolve_input 完全解析)、MultiCam 族(is_multicam、4 个输入 id 常量、source_count、rows/cols 与 index 互换算静态数学、oakengine_clip_find_multicamoakengine_multicam_switch_source(可选 split-preserving-links + 各 linked multicam 设源,单条 MultiUndoCommand)),外加 oakengine_nodes_delete_many(NodeViewDeleteCommand 等价,节点+边数组一次提交单条 undo)。事件表新增 87/88(group passthrough added/removed,handle=内层节点,s=input id,a=element)、89(group output passthrough changed)、90(node context position changed,a/b=x/y double 位模式);EngineEventBridge 同步加 4 个带类型信号。app 侧:multicamwidget/multicamdisplay 切 OakEngineNode* 不透明句柄(Switch 走 switch_source),viewer.cpp detect_multicam_node 与 timelinewidget multicam 启用(oakengine_project_add_node 按 type id 建 multicam)切 facade;nodeview 三文件+nodeparamview 三文件+keyframeview+panel/node 的 NodeGroup 全部切 facade+bridge(resolve_input 7+ 处、get_inner 循环、passthrough 增删/枚举、84/85/90 事件订阅替代 connect,delete_selected 改收集后一次 delete_many)。olive::NodeGroup/olive::MultiCamNode app 未定义符号归零,总 U _ZN5olive 320→300。测试:oakengine_node_test 补 context-position/effect-input/group(含嵌套 resolve 与 undo/redo)/multicam/nodes_delete_many 五族,oakengine_events_test 补 87-90 实测触发(含 double 位模式解码),35/35 绿。已知妥协:group_nodes 与 timelinewidget multicam 启用由单条 MultiUndoCommand 变为多条 undo 记录。保留待 B8c:ViewerOutput 族(29 符号,timebased/viewer 系)、NodeTraverser(7,nodevaluetree/nodetableview/viewerdisplay 帧提取)、nodeview 残余 Node 调用(copy_dependency_graph、find_ways_node_arrives_here、inputs_from、外观 brush/color)、RenderManager::get_cacher()->set_multicam_node(引擎内部头)。ViewerOutput/NodeTraverser 已完成(2026-07-21,B8c,B8 系列收尾):新增 oakengine/viewer.h + engine/src/capi/viewer.cpp(oakengine_viewer_* 32 函数:from_node 类型探测(替代 dynamic_cast)、playhead get/set、length/video_length/audio_length、video/audio params(按流 index)、三类 stream count、has_enabled_streams、first_enabled_video_stream、enabled streams count+列表(替代 get_enabled_streams_as_references)、workarea POD get/set_range/set_enabled、set_default_parameters、set_parameters_from_footage、set_waveform_enabled、get_connected_waveform(const void* 透传)、5 个输入 id 常量访问器、default_sample_format、stream_enabled、subtitle count/at(返回 const Subtitle* 借用指针));新增 oakengine/traverse.h + engine/src/capi/traverse.cpp(oakengine_traverse_* 15 函数:Owned OakEngineTraverseDb(generate_database/generate_table/free + 输入/行访问器:type/source/tag/value_string/split),element_index_for_hint,以及两个 B7 式过渡桥 generate_row(就地填 app 侧 NodeValueRow)与 transform(出 QTransform 六系数));node.h 补 oakengine_node_set_value_hint(nodevaluetree 的 ValueHint 写路径)且 oak_node_value_type 追加 TEXTURE/SAMPLES/VIDEO_PARAMS/AUDIO_PARAMS 四个仅内省值;oak_video_params POD 尾部追加 video_type/premultiplied_alpha(仅 viewer 族填充)。事件表新增 100-110 viewer 族 11 事件(length/playhead/frame_rate/pixel_aspect 有理数 a=num,b=den;size a=w,b=h;interlacing/sample_rate a=值;video_params/audio_params/texture_input/connected_waveform 无载荷);EngineEventBridge 同步加 11 个带类型信号。app 侧:timebased 家族(timebasedwidget/timebasedview)、viewer 家族(viewer/audiowaveformview/footageviewer)、multicamwidget、nodeparamview 三件、export 对话框(workarea POD + playhead 事件 + sequence_has_subtitles 纯 facade)、import 工具、projectviewmodel/projectexplorer/project 面板/proxydialog、nodeview/timeruler 的 dynamic_cast、timelinewidget(set_playhead/代理生成/嵌套序列参数)、seekablewidget、mainwindow 全部切 facade+事件桥;nodetableview/nodevaluetree 重写为 traverse db 访问(新增 app/widget/viewer/vieweroutpututils.h:POD→VideoParams/AudioParams 内联互转 + 类型探测);app 五处 Q_OBJECT 信号/槽参数由 ViewerOutput* 改 OakEngineNode*(moc 不再引用 ViewerOutput 元对象),&ViewerOutput::label_changed/removed_from_graph 改 &Node:: 形式(基类信号,Node 符号属后续批次)。验收: U olive::(ViewerOutput|NodeTraverser)(含 staticMetaObject/typeinfo/k__params_input 静态)29+7→0,总 U _ZN5olive 300→271。测试:新增 oakengine_viewer_test/oakengine_traverse_test(纯 C 无 GPU,事件实测触发:playhead/length(demo.mp4 clip)/size/pixel_aspect/interlacing/sample_rate/video_params/audio_params/texture_input 均验证载荷;connected_waveform 仅订阅成功,无音频链无法触发,已注释),37/37 绿(基线 35+2)。已知妥协/遗留:PreviewAutoCacher 三函数与 plugin::set_active_viewer_provider(参数带 ViewerOutput,属附 C 第 6 项)仍在;嵌套序列经 facade set_video_params 不带 divider/color_range/video_type/音频 format;core.cpp 图层 enabled 翻转变 undoable;timeline 时间码标签方向连接改 lambda+Connection 句柄。

  4. 参数/色彩/导出(~50):VideoParams 19 / ColorManager 15 / EncodingParams 9 / ExportFormat 7——导出编解码控件、色彩管理菜单、scopes。导出面已完成(2026-07-21,B6):新增 oakengine/encoding.h(格式/编解码元数据、OakEngineEncodingParams 不透明句柄全字段读写、preset 目录与 load/save、generate_matrix、图像序列文件名辅助、oakengine_export_render_with_params、last-used 读写、音频录制启动)与 oakengine/videoparams.h(oak_video_params POD + 标准帧率/像素比/分辨率档/像素格式名等静态数据);oakengine/encoding.cpp 实现,oakengine_encoding_test 纯 C 覆盖。app 侧 export 对话框族(export、video/audio/subtitles tab、四个 codec section、format combobox、save-preset dialog)、序列对话框(参数/preset tab + standardcombos 四个组合框)、viewer 录音与 preferencesaudiotab 全部切到 C API;EncodingParams/ExportFormat/ExportCodec 未定义符号归零,总 U _ZN5olive 410→375。VideoParams 剩 9 个符号(ctor/operator==/is_valid/bytes_per_pixel 等)全部位于显示/渲染路径(viewerdisplay、manageddisplay、scopes),留 B7。色彩/显示面已完成(2026-07-21,B7):新增 oakengine/color.h + engine/src/capi/color.cpp(OakEngineColorManager 借用句柄的 config 文件名/colorspace/display/view/look 列表与默认值/luma 系数/compliant 解析,oak_color_transform POD,OakEngineColorConfig 独立 OCIO 配置句柄,OakEngineColorProcessor 属主句柄 create/free/is_valid/convert_color/id,以及两个过渡桥 oakengine_color_transform_job_set_processor/oakengine_color_set_display_color_processor);事件族新增 OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED/_REFERENCE_SPACE_CHANGED 取代 app 对 ColorManager Qt 信号的直连。engine/render/videoparams.h 的 ctor/operator==/is_valid/effective-size/bytes-per-pixel/divider 名/常量改为头内联(显示路径对 VideoParams 是值语义且要喂给 B8 范围的 renderer C++ 调用,POD 无法覆盖;POD 侧另补 oakengine_video_params_make/_equal/_is_valid/_bytes_per_pixel/_internal_channel_count 供无头消费者),ManagedColor 同样全内联。app 侧 manageddisplay(含信号订阅)、viewerdisplay、viewer、viewerbase、scopebase、waveform、vectorscope、colordialog、colorbutton、colorspacechooser、colorpreviewbox、colorswatchwidget、colorvalueswidget、projectproperties、videostreamproperties 全部切到 C API(共享辅助头 app/widget/manageddisplay/colorprocessorhandle.h)。测试:oakengine_color_test(纯 C,OCIO 缺失时跳过查询断言)、oakengine_encoding_test 增补 POD 用例。符号:olive::VideoParams/ColorTransform/ColorProcessor/ManagedColor 未定义符号归零;olive::ColorManager 仅余 staticMetaObject(来自 app target 直接编译的 engine 源码 footage.cpp/ociobase.cpp 对 ColorManager 信号的 QObject::connect,属 engine 内部连接,留后续批);总 U _ZN5olive 375→346。

  5. 工具类(~30):QtUtils 9 / FileFunctions 5 / Config 5 / MainWindowLayoutInfo 6 / olive 8——从 engine 移到 core 或 app(它们本不属于引擎)。

  6. 基础设施(~40):TaskManager/Task 14 / AudioManager 12 / DiskManager 9 / UndoStack 7 / PreviewAutoCacher 7 / RenderTicketWatcher 6 / Folder 7 / Footage 10 / Project 10 / TimelineMarker 6 / TimelineWorkArea 8——录制、刮擦、单帧刷新、preferences 等保留路径的收口,逐项判 facade 化或豁免。B4 时间线族残留清理已完成(2026-07-22,B4c)

  7. B9a Task/Undo 族收尾(2026-07-22):app 侧 Task/TaskManager/UndoStack 直驱全部切到 oakengine/task.h/undo.h C API;新增 oakengine_undo_command_create/create_multi/multi_add_child/multi_child_count/free 五个 C 函数,让 app 侧自定义 undo 命令(选择集、splitter、sequence 开/关、时间选择)不再继承 olive::UndoCommand,改由 C 回调包装。app 内 OpenSequenceCommand/CloseSequenceCommand/SetSelectionsCommand/SetSplitterSizesCommand/SetTimeCommand 五个子类移除 UndoCommand 基类;新增共享头 app/common/undowrapper.h。TaskManager|Task|UndoStack 未定义符号已清零;UndoCommand 剩余 3 个符号(redo_now/undo_now/ctor)全部来自 engine 源码被 app target 直接编译(如 Folder::RemoveElementCommand/NodeEdgeAddCommand/timeline 命令类等)以及遗留的 MultiUndoCommand 构造,需待 B11 消除 app target 直接编 engine 源码后自然消失,本批按 handoff §5.1 验收条款记录并说明。测试:engine/tests/oakengine_task_test.cpp 覆盖 task manager 空态/create/import 错误路径/load 失败路径/task 事件/undo 往返/custom+multi 命令,38/38 ctest 全绿,总数 195。:facade oakengine/timeline.h 扩容——轨道高度换算四函数(internal↔pixels、default)、oakengine_block_is_enabled、Clip 输入 id 六个字符串 getter、oakengine_clip_set_media_in(undoable)/request_invalidate/discard_cache/add_cache_passthroughmarker 句柄族(OakEngineMarkerList/OakEngineMarker:count/at/at_time/get_time/get_name/get_color/has_sibling/set_time_live/list_add/list_add_existing/remove/set_properties(一条 undo)/commit_time)、workarea 句柄族(OakEngineWorkarea:viewer 借用 oakengine_viewer_get_workarea_handleoakengine_workarea_create/free 自有、get/set live/set_range_undoable/set_enabled_undoable/reset 常量)、oakengine_sequence_add_default_nodes(独立 undo entry,原并入 import 组包,已知妥协)、oakengine_clip_get_media_range_rational(有理秒、不依赖 timebase)。事件表新增 22-24 序列轨道列表/字幕、32-35 Track(index/height/refreshed/muted)、36/37 Block(enabled/preview)、91/92 Node(links/color)、111-113 marker list、114/115 workarea;EngineEventBridge 同步加信号。app 侧:timelinewidget/trackview/trackviewitem/timelineview/ripple/transition/slip/import/pointer/add/edit/record/razor、seekablewidget/resizabletimelinescrollbar/timebasedwidget/timebasedviewselectionmanager(marker ADL,新增 app/widget/timeruler/markerhandle.h)、markerpropertiesdialog、footageviewer(override workarea 改 oakengine_workarea_create)、viewer/viewerdisplay(subtitles)、speeddurationdialog、timelinewidgetwaveformsync、core.cpp、nodeparamview、nodeviewcontext、mainwindow、trackviewsplitter 全部切换;新增共享头 app/widget/timelinewidget/trackhandle.h(is_locked/is_muted/type)与 cliphandle.h(connected node/caches/speed/loop/reverse/maintain_pitch,替代 clip.h 内联访问器对 k_* 静态成员的引用);Track::type() 改为头内联(其内联用户 to_reference()/get_track_type() 会拖拽符号)。度量:族符号(Sequence|Track|ClipBlock|TrackList|TimelineMarker|TimelineMarkerList|TimelineWorkArea|Clip|Block)75→3,总 U _ZN5olive 271→219。测试:oakengine_events_test/oakengine_timeline_edit_test 各补一族(新事件实测触发、marker/workarea/clip id/media/高度换算/add_default_nodes,含 undo/redo),37/37 绿。遗留 3 个族符号(判豁免):Block::staticMetaObject/Track::staticMetaObject(app 内部信号以 Track*/Block* 为参数,moc 的 qMetaTypeId<QObject*> 注册必然引用,需把 app 信号参数改 void* 才能消除,代价不值)、ClipBlock::ClipBlock()(add/import 工具在组包 MultiUndoCommand 里 new ClipBlock() 自建节点,ctor 注册输入无法内联,留待工具链整体 facade 化)。undo 粒度妥协(均有注释):slip 每 clip 一条、import/core 新建序列的 default nodes 独立一条、marker 删除/paste 非序列分支逐条、set in/out 点 enable+range 两条、mainwindow footage workarea 两条。已知坑:timeline_waveform_sync 等处的 clip 可能不在轨道上,ts 换算会空指针——rational 秒版 oakengine_clip_get_media_range_rational 专为此加。

  8. B9b Config(2026-07-22):新增 oakengine/config.h + engine/src/capi/config.cpp(oakengine_config_load/save/get-set_string/int/set_error_handler/report_error),用 buf/size 约定读字符串;新增 app/common/configwrapper.h,以头内联 OakConfigValue 替代 OAK_CONFIG/OAK_CONFIG_STR 宏,并把 engine/config/config.h 的宏定义加上 #ifndef 守卫,使 app 包含 wrapper 时优先走 C ABI。app 中所有直接使用 Config::load/save/set_error_handler/current/operator[] 的点改为 C API,大量 OAK_CONFIG/OAK_CONFIG_STR 使用点经 wrapper 重定向到 oakengine_config_*timelineundogeneral.h 内原本内联使用 OAK_CONFIG 的静态成员初始化移入 .cpp 并改用 C API 读取。测试:engine/tests/oakengine_config_test.cpp 覆盖 load/save 往返、string/int 读写、缺省值、error handler/report_error,39/39 ctest 全绿(含一次 oak_cli_transcode 单独重跑通过),olive::Config 未定义符号归零,总数 190。

  9. B9b DiskManager(2026-07-22):新增 oakengine/disk.h + engine/src/capi/disk.cpp,封装 DiskManager 实例生命周期、get_default_cache_path/set_default_cache_path、缓存清理、settings handler 回调、settings/change-confirmation 对话框分发、invalidate_project 信号及 get_open_folder 借用句柄。app 侧 core.cpp/projectproperties.cpp/diskcachedialog.cpp/preferencesdisktab.cpp/h 全部切到 C API;preferencesdisktab.h 移除 DiskCacheFolder* 成员,改用 QString 保存默认缓存路径。为消零符号额外补了 oakengine_disk_get_open_folder/set_default_cache_path(任务原清单未列,但 core.cpp handler 与 preferencesdisktab accept() otherwise 会残留 DiskManager::instance/get_open_folder/DiskCacheFolder::set_path 三个符号)。测试:engine/tests/oakengine_disk_test.cpp 覆盖 instance lifecycle、default cache path、open folder handle、clear_cache、settings handler round-trip、set_default_cache_path、invalidate_project;show_change_confirmation_dialog 因阻塞 QMessageBox 无法在 headless 纯 C 单测中覆盖,由 app 对话框代码间接验证。DiskManager/DiskCacheFolder 未定义符号归零,总数 179→169,41/41 ctest 全绿。

  10. B9b AudioManager(2026-07-22):新增 oakengine/audio.h + engine/src/capi/audio.cpp(oakengine_audio_create/destroy_instance/manager_handle/get_set_output_device/get_set_input_device/hard_reset/clear_buffered_output/push_to_output/stop_recording;push_to_outputOakAudioParams* + 原始字节 + 错误 buf)。事件表新增 140 OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED(handle = oakengine_audio_manager_handle(),AudioManager 单例指针),EngineEventBridge 加 audio_output_params_changed 信号。app 侧 core.cpp 生命周期、viewer.cpp 刮擦输出与事件订阅(audio_bridge_)、preferencesaudiotab 设备设置全部切到 C API。测试:engine/tests/oakengine_audio_test.cpp,40/40 ctest 全绿,olive::AudioManager 未定义符号归零,总数 190→179。

  11. B9b ProxyManager/LUTLibrary/ProjectSerializer(2026-07-22):新增三个族——oakengine/proxy.h(create/destroy_instance/params_from_config/get_state/state_to_string/get_or_start/get_working_filename,oak_proxy_result POD 含 state/filename[1024]/task 不透明 int64);oakengine/lut.h(directory_count/at/file_count/at/set_directories);oakengine/serializer.h(oakengine_serializer_check_compressed + OakEngineClipboard 句柄族 ~23 函数:set_nodes/markers/keyframes/property/copy/save_to_xml/paste/paste_with_map/free + get_loaded_* 访问器 + foreach_property/keyframe/connection 迭代器)。app 侧 proxydialog、timelinewidget 代理路径、lutfilefield、nodeparamviewwidgetbridge、preferencesluttab、main.cpp 压缩检查、keyframeview/seekablewidget/timelinewidget/nodeview/nodeparamview 复制粘贴全部切到 C API;nodeparamview.hgenerate_existing_paste_map 去 ProjectSerializer 化(改 C map 回调)。测试:oakengine_proxy_test.cpp/oakengine_lut_test.cpp/oakengine_serializer_test.cpp已知债务:serializer 测试只覆盖 4/24 函数,clipboard 族其余 ~20 函数待补(facade 覆盖审计 59),按交接文档 §5.0 列为接手第一优先。44/44 ctest 全绿,三类符号归零,总数 162。

  12. 构建修复事故记录(2026-07-22,K2.7):B9b Proxy 批次子代理执行中途因 API 配额中断,误删 timelinewidget.cpp 一个函数块(rubber-band 三件套/add/remove/set_selections/get_item_at_scene_pos/save/restore_splitter_state/add_timeline_and_track_view/SetSplitterSizesCommand::redo/undo)并把 seekablewidget.cpp 留在半迁移态(Core::undo_stack() 返回 void* 后调用点未换、resize_item_void* 后仍 dynamic_cast)。已由 K2.7 按 HEAD 恢复函数块(不含已 C API 化的 generate_existing_paste_map)、seekablewidget 改用 resize_item_kind_ + static_cast + oakengine_undo_push(command, name) 双参形式,恢复 44/44 全绿。教训已写入交接文档 §2.1。代价:seekablewidget marker resize/绘制路径残留 TimelineMarker::draw/set_time/TimelineWorkArea::set_range/MarkerChange*Command/ViewerOutput::set_playhead 共 6 个符号,归入交接文档 §5.8.1(B11c)处理。

  13. B9c/B9d/B9e/B10/B11a 大部(2026-07-22~23,DS):预览/渲染服务(PreviewAutoCacher/RenderTicket/RenderTicketWatcher → oakengine_preview_cacher_*/oakengine_preview_request_* 异步请求族)、plugin 族(oakengine_plugin_* + progress reporter 工厂回调)、gizmo(TextGizmo POD 化进 oakengine/gizmo.h,DraggableGizmo 整体搬 app)、B10 工具类(QtUtils/FileFunctions/ColorCoding/Html/xml/debug_handler/qHash/Track::Reference 流运算符全部搬 app)、B11a Node 大部(图操作原语/NodeFactory/input id getter/命令类替换)。符号 162→36。

  14. v3 验收修复(2026-07-23,K2.7):对 DS 产出验收发现 6 个真实缺陷并全部修复——(1)B10 app 侧 colorcodingapp/htmlapp/filefunctionsapp/hashstreamapp/xmlutilsapp 与 engine 同名定义造成 ELF 符号介入、静态对象双重析构,timeline-tests/olive-gtest 退出即崩("corrupted double-linked list"),修复:app/CMakeLists.txt 对这 5 个文件加 -fvisibility=hidden;(2)app/common/nodeimpl.cpp 死代码(创建未注册),钉死方案 A:删除并改调用点(见交接文档 §3.4);(3)SpeedDurationDialog::accept() undo 命令未压栈(时长修剪不执行,2 个 gtest 红),补 oakengine_undo_push(command, name);(4)dropMimeData 一次移动拆 3 条 undo,新增 oakengine_folder_move_child(单条命令)并补测试;(5)预览帧 POD linesize 误用 pixels 应为 bytes(preview.cpp + viewer.cpp 两处);(6)重建 display Frame 用 VideoParams 默认构造(channel_count=0 除零、depth=0 致 Vulkan 上传 0 字节、4 个 viewer NotBlack 黑屏),改四参构造。manageddisplay.cpp 渲染器创建恢复 DynamicRenderer 原版。当前符号 39(36+恢复的 DynamicRenderer 3),仅剩 Backends/ViewerRuntimeRewireTest.RewireToIndirectConnectionNotBlack/1 一个已知失败(交接文档 §3.1 有排查线索)。六条教训固化为交接文档 §6.6 硬规则 R1-R6。

豁免原则:纯 UI 呈现类(不触引擎执行)可经 C++ 包装层引用——但包装层本身也是 C++ 符号引用,故阶段 4 的"0"实际指直接 olive:: 符号;包装类应放进 liboakengine 的 wrapper 头(符号由 wrapper 内联消解,不进动态符号表)。

阶段 4:隐藏 C++ 符号

  • app/worker 的引擎符号引用清零后:visibility=hidden + version script(oakengine_* 白名单)。
  • 验收:nm -Doakengine_*;全部测试(含 CLI)绿;三平台打包复验。

R7 实施记录(2026-07-26 完成)

R7-Aapp 层 C ABI facade 迁移)display.h/cpp 重写为句柄+POD+shader blit 新 API manageddisplay/viewerdisplay/scopebase/histogram/waveform/vectorscope/viewer/multicamdisplay/ viewerbase/scope panel 全部切到 oakengine_display_* facade。app 侧 U _ZN5olive = 0。

R7-BOBJECT 库重构 + 版本脚本)

  • engine/CMakeLists.txt 重构为 oakengine-objOBJECT+ oakengineSHARED)双层架构。 OBJECT 库编译一次,SHARED 库用其对象 + 版本脚本(oakengine.ver)限制导出。
  • 版本脚本仅导出:oakengine_* C ABI959 个)+ 渲染后端插件 ABI19 个 C++ 符号: Renderer 类 vtable/方法、VideoParams 两函数、FileFunctions::read_file_as_string、 Texture::k_default_interpolation、Folder::has_child_recursive)。 C++ 导出从 2048 → 19
  • 内部消费者(app、tests)直接链 oakengine-obj 绕过版本脚本; 外部消费者(cli、worker)链 oakengine SHARED(纯 C ABI)。
  • engine/common/CMakeLists.txtengine/pluginSupport/CMakeLists.txttarget_sources 改为 oakengine-obj
  • app 侧 6 个 wrapper 文件(htmlapp/filefunctionsapp/colorcodingapp/xmlutilsapp/ hashstreamapp/qtutilsapp)从构建移除(不再需要,oakengine-obj 提供全部符号)。
  • 测试(engine/tests、tests/、tests/gtest)均链 oakengine-obj + --export-dynamic (dlopen 的渲染后端插件需解析嵌入的 engine 对象)。

验收:构建 0 errornm -D --defined-only liboakengine.so | grep " T _Z" | wc -l = 19 ctest 44/45olive-gtest 超时为预存问题,非 R7 引入)。

附 B:复合场景 facade 化取舍(2026-07 评估)

  • nest(序列嵌套):补一个小原语 oakengine_sequence_add_sequence_clip(与 add_footage_clip 对称)。拖序列上时间线的执行路径由此可迁。
  • multicam:不补专门族。MultiCamNode 是节点,节点图族(add_node/connect/set_input)已覆盖建线与切机位;multicam widget 的交互状态留 UI。
  • waveform-sync(波形对齐):补一个小族(estimate_offset / estimate_stretch_offset 两函数),把 timelinewidget 里最后两处直接调引擎算法的执行路径(timelinewidget.cpp:1117/1133)迁掉;偏移量的应用走已有编辑原语。
  • import place_at:不补。import_footage + add_track + add_footage_clip 组合已够,文件夹递归/静帧时长/轨道定位是 UI 策略,留在 app。

附 D:变更通知事件机制(oakengine/events.h,2026-07-21 落地)

app 直接 connect 引擎 QObject 信号是 B4/B5 后最大的遗留耦合(~30 个连接点)。本批建立了通用替代机制:引擎侧 C 订阅 API + app 侧 Qt 信号桥

机制

  • oakengine_event_subscribe(void *handle, int32_t event_id, oakengine_event_fn fn, void *userdata) -> int64_t:按事件族传对应 facade handle(Project/Sequence/Track/Node,handle 即引擎对象指针,与既有约定一致)。返回订阅 id(>0),失败返回 0(handle NULL、事件 id 未知、或 handle 类型与事件族不匹配——内部用 dynamic_castQObject* 校验)。
  • oakengine_event_unsubscribe(int64_t id):退订;id 已失效(对象已销毁)时返回 OAKENGINE_E_NOT_FOUND,无害。
  • 回调签名为 void (*)(const oakengine_event *event, void *userdata);oakengine_event 是纯 POD:{id, a, b, source, handle}(a/b 为 int64:标志位、index 或帧时间戳;source 为被订阅 handle;handle 为事件相关对象,均为借用指针,仅回调期间有效)。
  • 线程语义:与原 Qt direct connection 完全一致——回调在发射线程上同步调用(内部 Qt::DirectConnection),先于引擎自身发射返回。引擎对象都在 GUI 线程,回调即在 GUI 线程。回调不得在持锁点反向调用修改同一对象的编辑原语。
  • 生命周期:被观察对象销毁时引擎侧自动注销(Qt destroyed),绝不会有悬空回调;userdata 归订阅方管理,退订前需自行保证有效。

事件 ID 表

ID 订阅 handle 载荷
1 PROJECT_MODIFIED_CHANGED OakEngineProject* a = modified 0/1
2 PROJECT_NAME_CHANGED OakEngineProject*
10/11 FOLDER_BEGIN/END_INSERT_ITEM OakEngineNode*(folder) handle = 子节点, a = index
12/13 FOLDER_BEGIN/END_REMOVE_ITEM OakEngineNode*(folder) handle = 子节点, a = index
20/21 SEQUENCE_TRACK_ADDED/REMOVED OakEngineSequence* handle = OakEngineTrack*, a = track type
30/31 TRACK_BLOCK_ADDED/REMOVED OakEngineTrack* handle = OakEngineBlock*, a/b = in/out(ts)
40/41/42 SEQUENCE_MARKER_ADDED/REMOVED/MODIFIED OakEngineSequence* a = marker 时间(ts)
50/51 SEQUENCE_WORKAREA_RANGE_CHANGED/ENABLED_CHANGED OakEngineSequence* a/b = in/out(ts) 或 a = enabled
60/61 COLOR_MANAGER_CONFIG_CHANGED/REFERENCE_SPACE_CHANGED OakEngineColorManager*
70 NODE_LABEL_CHANGED OakEngineNode* s = 新 label
71 NODE_INPUT_VALUE_CHANGED OakEngineNode* s = input id, a = element, b/c = 范围 in/out(ts)
72/73 NODE_INPUT_CONNECTED/DISCONNECTED OakEngineNode* handle = 对端节点, s = input id, a = element
74 NODE_INPUT_FLAGS_CHANGED OakEngineNode* s = input id, a = flags
75 NODE_INPUT_PROPERTY_CHANGED OakEngineNode* s = input id(key/value 省略,用 getter 重读)
76 NODE_INPUT_DATA_TYPE_CHANGED OakEngineNode* s = input id, a = oak_node_value_type
77 NODE_INPUT_ARRAY_SIZE_CHANGED OakEngineNode* s = input id, a/b = 旧/新 size
78 NODE_KEYFRAME_ENABLE_CHANGED OakEngineNode* s = input id, a = element, b = enabled
79/80 NODE_KEYFRAME_ADDED/REMOVED OakEngineNode* handle = keyframe, s = input id, a = element, b = track
81/82/83 NODE_KEYFRAME_TIME/TYPE/VALUE_CHANGED OakEngineNode* handle = keyframe
84/85 NODE_NODE_ADDED/REMOVED_TO_CONTEXT OakEngineNode*(context) handle = 子节点
86 NODE_MESSAGE_COUNT_CHANGED OakEngineNode*
87/88 GROUP_INPUT_PASSTHROUGH_ADDED/REMOVED OakEngineNode*(group) handle = 内层节点, s = input id, a = element
89 GROUP_OUTPUT_PASSTHROUGH_CHANGED OakEngineNode*(group) handle = 新输出节点
90 NODE_CONTEXT_POSITION_CHANGED OakEngineNode*(context) handle = 子节点, a/b = x/y(double 位模式)
100/101/102/104 VIEWER_LENGTH/PLAYHEAD/FRAME_RATE/PIXEL_ASPECT_CHANGED OakEngineNode*(viewer) a/b = 秒有理数 num/den
103 VIEWER_SIZE_CHANGED OakEngineNode*(viewer) a = width, b = height
105/109 VIEWER_INTERLACING_CHANGED/SAMPLE_RATE_CHANGED OakEngineNode*(viewer) a = interlacing 枚举 / 采样率
106/107/108/110 VIEWER_VIDEO_PARAMS/AUDIO_PARAMS/TEXTURE_INPUT/CONNECTED_WAVEFORM_CHANGED OakEngineNode*(viewer)
22/23/24 SEQUENCE_TRACK_LIST_CHANGED/TRACK_HEIGHT_CHANGED/SUBTITLES_CHANGED OakEngineSequence* a = track type;23 带 handle = track、b = 像素高;24 a/b = ts 范围
32/33/34/35 TRACK_INDEX/HEIGHT/BLOCKS_REFRESHED/MUTED_CHANGED OakEngineTrack* 32 a/b = 旧/新 index;33 a = double 位模式;35 a = 0/1
36/37 BLOCK_ENABLED/PREVIEW_CHANGED OakEngineBlock*
91/92 NODE_LINKS/COLOR_CHANGED OakEngineNode*
111/112/113 MARKER_LIST_MARKER_ADDED/REMOVED/MODIFIED OakEngineMarkerList* handle = OakEngineMarker*
114/115 WORKAREA_RANGE/ENABLED_CHANGED OakEngineWorkarea* 114 无载荷(重读 oakengine_workarea_get);115 a = 0/1

(oakengine_event 在 B8a 扩展了 c(第三整数载荷)与 s(字符串载荷,仅回调期间有效)两个字段;宏均带 OAKENGINE_EVENT_ 前缀。)

app 侧:EngineEventBridge

app/engineeventbridge.{h,cpp}:一个 QObject,subscribe(handle, event_id) 注册 C 回调并把事件按 id 分发为带类型的 Qt 信号(如 folder_begin_insert_item(OakEngineNode*, OakEngineNode*, int))。桥拥有订阅,析构时全退订;被观察对象死亡时引擎侧自动注销,双向都安全。

已迁的代表性连接点

  • app/core.cpp on_active_project_changed:Project::modified_changed → bridge 订阅 + 信号接 QMainWindow::setWindowModified
  • app/widget/projectexplorer/projectviewmodel.cpp:Folder 的 begin/end insert/remove 四个信号 → folder_bridge_ + folder_subscriptions_(QHash<Folder*, ids>);槽函数改为显式传 Folder*(原 sender() 语义由事件的 source 字段承担)。

后续批次迁移连接点的标准操作步骤

  1. 确认目标信号已在事件 ID 表内;不在则:在 events.h 加宏(新 id)、events.cppconnect_event 加 case(dynamic_cast 校验 + DirectConnection + POD 载荷)、EngineEventBridge 加对应信号和 dispatch case、在 oakengine_events_test 补一条实测。
  2. app 侧:在原来 connect(engineObj, &EngineClass::sig, ...) 处改为 bridge->subscribe(reinterpret_cast<OakEngineXxx*>(obj), OAKENGINE_EVENT_...),保存返回 id;对象失效或换绑时 unsubscribe(id)(引擎对象销毁会自动注销,重复 unsubscribe 无害)。
  3. 若原槽函数用 sender(),改为从事件的 source/handle 字段显式传入(见 projectviewmodel 改法)。
  4. 构造期一次性 connect(bridge, &EngineEventBridge::xxx, this, ...);回调语义与原 direct connection 相同,无需改线程假设。
  5. 全量构建 + ctest 全绿,nm -D app/oak-editor | grep -c " U _ZN5olive" 应下降。

Track 块遍历族(同批落地)

oakengine_track_block_at_time / nearest_block_before(_or_at) / nearest_block_after(_or_at) / block_count + oakengine_block_next/prev/is_gap/get_range(timeline.h,OakEngineBlock 不透明句柄,含 gap;clip 句柄与 OakEngineClip 同指针)。已迁 razor(nearest_block_before)与 trackselect(链式遍历)两个代表点;ripple/transition/timelineview 的同构用法照此替换即可。

风险与对策

  • 节点参数类型膨胀:NodeValue 有 ~20 种类型。先支持编辑器最常用的 8 种(float/int/bool/string/rational/color/vec/combo),其余按面板需要逐个加。
  • 性能:参数级 facade 调用频率低(用户操作粒度),不构成问题;渲染/帧路径已有同步封装。
  • Qt 对象生命周期:节点/序列/clip 均为 borrowed handle 随 project;文档已有约定,迁移时沿用。
  • undo 语义:所有编辑原语必须经全局 UndoStack(facade 已强制),UI 侧不得再直接 new 命令。
  • icon 归属(2026-07 实测):app→引擎剩余耦合中最大单类是 olive::icon(56 个符号,节点图标元数据留在引擎)。它不属于任何面板,阶段 4 隐藏符号前需要专门处理:补 icon 查询 facade 族(按节点 type id 取图标),或把图标注册表下沉到 core。倾向后者,icon 是纯资源元数据。
  • 架构事实(同次实测,防止误判):liboakengine 对 liboakcore 只经 C ABI(134 个 oakcore_* 全为 undefined 引用);EngineCore 不在 liboakcore 内,进程里只有 liboakengine 一份,无双单例问题。

验收清单(阶段 4 完成时)

剩余耦合度可量化复测(在构建目录下执行;阶段 3 推进期间应单调下降,2026-07 基线为 565):

nm -u app/oak-editor | awk '$1=="U"{print $2}' | grep '^_ZN5olive' | sort -u > /tmp/u.txt
comm -12 <(nm -D --defined-only engine/liboakengine.so | awk '{print $3}' | sort -u) /tmp/u.txt | wc -l
  • 上述耦合计数降为 0
  • nm -D --defined-only liboakengine.sooakengine_* + 19 个渲染后端插件 ABI 符号
  • oak-editor/oak-render-worker ldd 正常,全部启动
  • 1986+ gtest 全绿,CLI ctest 全绿,5+ C ABI 测试全绿
  • 三平台打包含 liboakengine(liboakengine.so/dylib/oakengine.dll)Linux 位于标准 libdir

附 CR5 批次记录

F3Task/TimelineWorkArea/ViewerOutput/Project)— GLM-5.2 完成

  • Task(6)+CLITaskDialog(1)+ProjectLoadTask(1)+ProjectSaveTask(1)+ProjectImportTask(1)=10 符号 TaskDialog/TaskViewItem/TaskView/TaskManagerPanel 改用 OakEngineTask* Core 改用 oakengine_task_create_project_load/save/import/otio + 访问器; 删除 FacadeExportTask/FacadeProxyTaskengine 自有等价物); oakengine_cli_task_dialog_run 替代 CLITaskDialog
  • ViewerOutput k_*_params_input(3) + Project(2) 35 处 inline get_*_params() 调用替换为 viewer_output_video/audio_params 助手和 C ABI Project::get_project_from_object 替换为 oakengine_project_from_object
  • TimelineWorkArea(6) oakengine_workarea_create/set_range/set_enabled 替代构造和方法调用; 信号连接改事件订阅 OAKENGINE_EVENT_WORKAREA_* WorkareaSetEnabled/RangeCommand 替换为 oakengine_workarea_set_*_undoable

F4Node 方法调用)— GLM-5.2 完成

  • 14 符号:新增 7 个 facade 函数(oakengine_node_enabled_input_id_category_name_link_command_copy_in_graph_copy_dependency_graph_connect_command_string_transform_time_to); 15 处 Node:: 方法调用替换为 C ABI。
  • Node 信号连接(23)未完成46 处 connect(node, &Node::signal, ...) 跨 11 文件; 事件 ID 已全部分配(70-95),EngineEventBridge 信号已存在, 但 9 个类缺少 EngineEventBridge 成员——需逐类添加。

F6(长尾部分)— GLM-5.2 完成

  • 7 个节点构造器 替换为 oakengine_node_factory_create_from_id VolumeNode、TransformDistortNode、SubtitleBlock、ShapeNode、SolidGenerator、 TextGeneratorV3、CrossDissolveTransition。
  • 5 个静态字符串 替换为 C ABI 访问器: VolumeNode::k_samples_input、TransformDistortNode::k_texture_input、 TransitionBlock::k_in/out_block_input、AudioVisualWaveform::k_maximum_sample_rate。

当前状态(GLM-5.2 R5 冲刺交接)

  • nm U _ZN5olive = 58(从 131 降下来,GLM-5.2 共消除 73 个)。
  • oak-render-worker = 0。
  • 构建 0 errorctest 43/44flaky 不计)。
  • 反作弊:app 无 dlfcnengine 改动仅 engine/include/oakengine/ + engine/src/capi/

G1Node 信号清零(88→66-22

53 处 connect(node, &Node::signal, ...) 跨 13 文件迁移为 bridge_->subscribe() + connect(bridge_, &EngineEventBridge::node_*, ...)。 22 个 Node 信号符号全部消除。剩余 4 个 Node 符号(link/set_standard_value/ set_value_at_time/staticMetaObject)从 inline 函数拉入,进豁免清单。

G2:渲染族信号 + RenderManager66→58-8

  • PlaybackCache invalidated/validated 迁事件订阅(timeruler.cpp
  • Sequence::subtitles_changed 迁事件订阅(viewerdisplay.cpp
  • RenderManager::backend_to_string + instance() 换 C ABImanageddisplay.cpp

G3UndoCommand C ABI58 不变)

  • oakengine_undo_command_redo_now/undo_now 声明补入 undo.h
  • 6 处直接调用替换;符号仍从 MultiUndoCommand inline 引用

R6:豁免清单清零(58 → 0,100% C ABI)— 完成

详见 docs/zh/r6-cleanup-plan.md(各 P 节已标 )。目标:把 R5 遗留的 58 个豁免符号全部消除到 0,为 engine 模块化拆分与 RIIR 打地基。

  • P1F 类 facade 补齐,17NodeValue 静态方法、VideoParams 构造器、 音频对齐算法、TimelineMarker/ShapeNodeBase/FrameHashCache/RenderManager/ MultiCamNode/SubtitleBlock 零散单点,全部新增 C facade 替换。
  • P2B 类 inline 清零,8app 中 113 处 new XxxCommand((16 个命令类) 替换为 facade 构造;Node::link/set_value_at_time 换 C ABI。
  • P3A 类 MOC staticMetaObject9+1:app 信号/槽参数类型由 engine C++ 类 改 C ABI 句柄(OakEngineNode* 等);plugin::PluginProgressReporter 去 Q_OBJECT 改 C 回调(推翻原"终态保留"裁决)。
  • P4E 类色彩管理,6ManagedColor 整体迁出 engine 至 app colorprocessorhandle.h,纯 UI 值类型);ColorProcessor create/convert_color 换 C ABI。nm 24→18。
  • P5C 类音频回调,5AudioProcessor 改 C vtable 接口 oakengine_audio_processor_*,推翻原"终态保留"裁决)。nm 18→13。
  • P6D 类渲染/GPU13:新增 oakengine/display.h + engine/src/capi/display.cpp oakengine_display_renderer_*/oakengine_display_texture_*/ oakengine_codec_frame_* 共 11 函数);manageddisplay/viewerdisplay/scopebase/ viewer/multicamdisplay/histogram 的渲染器构造-init-destroy、create_texture、 blit_color_managed、upload/download、Frame::create/set_video_params/allocate 全部收口到 facade。nm 13→0。

验收nm U _ZN5olive = 0oak-editor 与 oak-render-worker 均为 0); 全量构建 0 error;全量 ctest 100%45/45);ViewerDisplayReproTest 三个可跑通 用例(Vulkan)保持通过;导出测试无回归。反作弊:app 无 dlsym/dlfcn/QLibrary (仅 main.cpp 的 wglGetProcAddress 为 OpenGL 驱动能力检测,与 engine 符号无关); engine 无 inline 化(oakengine/*.h 纯 C 声明,ManagedColor 为类整体迁出非 inline 化)。 handoff §6.4 豁免清单已清空为"无豁免"。