Files
oak-editor/docs/zh/plans/eliminate-event-bridge.md
T

9.0 KiB
Raw Blame History

消灭 EngineEventBridge:引擎纯库化计划

背景与目标

现状:app 通过 EngineEventBridgeapp/engineeventbridge.,十几个实例)+ 引擎 C 事件 engine/src/capi/events.cpp75 个 OAKENGINE_EVENT_)接收引擎回调。问题:

  • 约 60% 的事件是 app 自己调用引擎时同步产生的,app 本来就知道,不需要被"通知"。
  • 裸 C 回调(userdata=this)无生命周期追踪,已制造多起 UAF 崩溃(最近修掉 3 处,仍有 14 处靠人工 unsubscribe 保命)。
  • OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_NOTIFY(141)PLAYBACK_CACHE_INVALIDATED(141) ID 撞号,音频续推订阅实际从未生效(现存 bug)。
  • 引擎因此不是一个纯库,且后续 AI 助理等新写入方会进一步放大"变更来源"的复杂度。

目标架构(单向依赖):

app 编辑路径 ──同步调用──> engine(纯库,只保留内部 Qt 信号供自身机制使用)
app 编辑路径 ──app 内部 Qt 信号──> app 各视图(播放头、undo、结构、参数)
engine 异步事件(任务/缓存/音频)──极窄 C 通道(queue 到 GUI 线程)──> app
  • 引擎内部信号(Node::value_changed、Project::node_added 等)保留——它们是 PreviewAutoCacher/ProjectCopier 等引擎自身机制的生命线,但不许越界给 app 同步事件。
  • C ABI 事件面从 75 个收窄到 ~10 个真异步事件。
  • 消灭 EngineEventBridge 类和全部裸 oakengine_event_subscribe 调用点。

事件分类结论(来自全量盘点)

  • (a) 同步 app 发起(~60%Node 编辑族、folder、track/block 结构、marker、 workarea、viewer 参数、color、context position、group passthrough。 注意每个都有三个旁路来源:undo/redo 重放、项目加载批量、引擎内部联动—— 不能只在调用点处理,必须经"命令执行器/加载完成"统一钩子。
  • (b) 真异步(必须保留通知)TASK 族(120127)、PLAYBACK_CACHE_VALIDATED/ INVALIDATED、FRAME_CACHE_INVALIDATED、AUDIO_MANAGER_OUTPUT_PARAMS/NOTIFY。
  • (b') playhead/lengthplayhead 是 app 播放循环自己 set 的,消费者遍布 10+ 组件 ——本质是 app 内部广播,挪进 app 侧 PlaybackController,引擎零参与。 length 是引擎推导状态,改为编辑后主动查询。
  • (c) undo 驱动UNDO_INDEX_CHANGED、PROJECT_MODIFIED_CHANGED 及全部会被 undo 重放重发的结构事件——app 侧 UndoController 发粗粒度信号,视图惰性刷新。

阶段计划

P0 — 冻结分类表 + 修音频事件 ID 撞号(先行 bug fix)

  • events.h 给 AUDIO_MANAGER_OUTPUT_NOTIFY 分配新 ID(如 144),events.cpp 补 AudioManager::output_notify 的 wire-up(当前 case 141 只做 PlaybackCache cast 音频订阅永远返回 0)。
  • 产出 docs/zh/event-bridge-inventory.md75 事件 → (a)/(b)/(b')/(c) 四类的映射表 (基于本次盘点结果),作为后续迁移的对账单。

P1 — PlaybackController(消灭 playhead/length 订阅,~12 处)

  • 新建 app/playback/playbackcontroller.{h,cpp}QObjectCore 持有单例):
    • 信号:playhead_changed(oak::Node viewer, Rational time)viewer_length_changed(oak::Node, Rational)
    • ViewerWidget 播放循环/seek 的 oakengine_viewer_set_playhead 调用处统一经它发出; 其它 set_playhead 调用点(timelinewidget、timeruler、multicam)同样经它。
    • length:各 viewer 参数编辑点 + undo 后主动 viewer_output_length() 查询并广播变化。
  • 迁移订阅者:timebasedview、resizabletimelinescrollbar(部分)、 nodeparamviewwidgetbridge/keyframecontrol/connectedlabel3 个裸 C)、export dialog、 timelinewidget、viewerdisplay。
  • 收益:顺带消灭 4 个裸 C 订阅中的 3 个和播放头族全部事件。

P2 — EngineAsyncEvents(收窄 C 事件面到 ~10 个)

  • 新建 app/asyncengineevents.{h,cpp}(唯一 dispatcherCore 持有):
    • 对 TASK 族、CACHE_VALIDATED/INVALIDATED、FRAME_CACHE_INVALIDATED、 AUDIO_OUTPUT_PARAMS/NOTIFY 各建一个 C 订阅,回调里只做 QMetaObject::invokeMethod(this, ..., Qt::QueuedConnection) 统一回 GUI 线程, 再 emit 类型化 Qt 信号。
    • 明确文档化:这是引擎→app 的唯一反向通道,只覆盖真异步事件。
  • 迁移订阅者:mainstatusbar、taskmanager 面板、taskviewitem、task dialog、 timeruler 缓存条、viewer 的 cache invalidated、viewer 音频续推(接 P0 修好的 ID)。
  • engine/include/oakengine/events.h:同步事件常量标注 deprecated(先不删,P5 后清理)。

P3 — UndoController + 粗粒度模型变更信号(消灭 undo 族 + 结构类订阅)

  • 新建 app/undo/undonotifier.{h,cpp}
    • 包装所有 oakengine_undo_push/undo/redo 的 app 调用点(集中在少数几处: oakengine_undo_push_or_run 的 app 侧、Core、各面板直接 push 处); 命令执行后、undo/redo 后、项目加载完成后,emit:
      • structure_changed()(节点/边/track/block/folder 增删移动)
      • params_changed()(值/关键帧/label/flags
      • modified_changed(bool)(给 core.cpp:1108 的 setWindowModified
      • undo_index_changed(int)(给 historywidget
    • 项目加载(TaskDialog 完成)后也发一次 structure_changed + params_changed, 替代加载期批量事件驱动建视图的路径(nodeviewcontext、projectviewmodel 依赖它)。
  • 迁移结构类订阅:nodeviewcontext(节点/边增删)、nodeviewremoved_from_graph)、 timelinewidgettrack/block 结构)、projectviewmodelfolder/label)、 mainwindowViewerPanel 关闭)、trackviewitem。 迁移方式:订阅粗粒度信号 → 对应视图做惰性重建/刷新(NodeView/Timeline 本就按 context 重建,成本可控;projectviewmodel 用 model reset)。
  • historywidget 与 core.cpp 的 UNDO_INDEX_CHANGED/MODIFIED 订阅改为 UndoController 信号。

P4 — 参数类订阅迁移(最大量、逐面板)

  • 原则:编辑发起处直接刷新 + UndoController::params_changed 惰性兜底。
    • nodeparamviewwidgetbridge(滑条值):SliderBase 编辑路径已知道新值(直接 set); undo/加载后靠 params_changed 触发受影响 item 重读。
    • keyframeview/keyframecontrol(关键帧按钮/曲线):编辑路径直接刷新 + params_changed 全量重读。
    • nodeviewitemlabel/color/message/array size)、nodeparamviewitem/arraywidget、 connectedlabel、seekablewidgetmarker/workarea)、viewerdisplay(字幕)、 multicamwidgetsize/par)、manageddisplayOCIO 配置)、audiowaveformview。
  • 每迁完一个面板,删除其 EngineEventBridge 成员与全部 bridge_->subscribe 调用。

P5 — 拆除与收尾

  • 删除 app/engineeventbridge.{h,cpp} 及全部引用;删除各裸 oakengine_event_subscribe 调用点(P1–P4 应已清零,grep 验证)。
  • engine/src/capi/events.cpp:同步事件订阅路径下线(保内部引擎信号); events.h 同步事件常量删除或移入内部头;C ABI 只保留 P2 的异步集合 + undo 族 undo 族是否保留视 P3 后 app 是否还有 C 调用方——AI 助理未来仍可能需要, 保留 UNDO_INDEX_CHANGED 和 MODIFIED 两个 C 事件是可接受的例外)。
  • 更新 docs/zh/event-bridge-inventory.md 为"迁移完成对照表" 更新 docs/zh/investigation-edge-display-and-playback.md 收尾记录。

验证方案(每阶段都要过)

  1. cmake --build cmake-build-debug -j8 零错误零新警告。
  2. ctest -j4 122/122。
  3. 手工检查表(每阶段对应项):
    • 播放:画面/声音正常、播放头移动、拖动播放头不崩。
    • 节点图:边全部显示、增删节点/边即时刷新、切换素材不丢。
    • 时间线:增删 clip/track 即时刷新、缓存条随渲染增长。
    • 参数面板:滑条值随编辑/undo 刷新、关键帧按钮状态正确。
    • 项目浏览器:导入素材/改名/撤销即时刷新。
    • 状态栏/任务面板:转码任务进度实时更新。
    • 历史面板:undo/redo 列表与选中行正确,标题栏修改标记正确。

风险与对策

  • 粗粒度信号导致过度刷新NodeView/Timeline 重建成本高。对策:P3 先只在 structure_changed 上做惰性(queued + 合并多次变更一次刷);profiling 不行再退回 按面板细化。
  • 漏掉旁路发射源(引擎内部联动、multicam):对策:迁移期在引擎 emit 点临时加 计数日志(复用 OAK_DEBUG_EDGES 模式),手工操作对照表逐项过一遍确认无未覆盖事件。
  • 加载期建视图路径:原来靠批量事件驱动;P3 的"加载完成后 structure_changed" 必须覆盖 nodeviewcontext、projectviewmodel、timelinewidget 三处的初始构建。
  • UndoController 覆盖不全(某些面板直接 oakengine_undo_push):grep 全部调用点 收口,禁止新增未经 UndoController 的 pushcode review 条目)。