9.0 KiB
9.0 KiB
消灭 EngineEventBridge:引擎纯库化计划
背景与目标
现状:app 通过 EngineEventBridge(app/engineeventbridge.,十几个实例)+ 引擎 C 事件
(engine/src/capi/events.cpp,75 个 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 族(120–127)、PLAYBACK_CACHE_VALIDATED/ INVALIDATED、FRAME_CACHE_INVALIDATED、AUDIO_MANAGER_OUTPUT_PARAMS/NOTIFY。
- (b') playhead/length:playhead 是 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.md:75 事件 → (a)/(b)/(b')/(c) 四类的映射表 (基于本次盘点结果),作为后续迁移的对账单。
P1 — PlaybackController(消灭 playhead/length 订阅,~12 处)
- 新建
app/playback/playbackcontroller.{h,cpp}(QObject,Core 持有单例):- 信号:
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/connectedlabel(3 个裸 C)、export dialog、 timelinewidget、viewerdisplay。
- 收益:顺带消灭 4 个裸 C 订阅中的 3 个和播放头族全部事件。
P2 — EngineAsyncEvents(收窄 C 事件面到 ~10 个)
- 新建
app/asyncengineevents.{h,cpp}(唯一 dispatcher,Core 持有):- 对 TASK 族、CACHE_VALIDATED/INVALIDATED、FRAME_CACHE_INVALIDATED、
AUDIO_OUTPUT_PARAMS/NOTIFY 各建一个 C 订阅,回调里只做
QMetaObject::invokeMethod(this, ..., Qt::QueuedConnection)统一回 GUI 线程, 再 emit 类型化 Qt 信号。 - 明确文档化:这是引擎→app 的唯一反向通道,只覆盖真异步事件。
- 对 TASK 族、CACHE_VALIDATED/INVALIDATED、FRAME_CACHE_INVALIDATED、
AUDIO_OUTPUT_PARAMS/NOTIFY 各建一个 C 订阅,回调里只做
- 迁移订阅者: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(节点/边增删)、nodeview(removed_from_graph)、 timelinewidget(track/block 结构)、projectviewmodel(folder/label)、 mainwindow(ViewerPanel 关闭)、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 全量重读。
- nodeviewitem(label/color/message/array size)、nodeparamviewitem/arraywidget、 connectedlabel、seekablewidget(marker/workarea)、viewerdisplay(字幕)、 multicamwidget(size/par)、manageddisplay(OCIO 配置)、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 收尾记录。
验证方案(每阶段都要过)
cmake --build cmake-build-debug -j8零错误零新警告。ctest -j4122/122。- 手工检查表(每阶段对应项):
- 播放:画面/声音正常、播放头移动、拖动播放头不崩。
- 节点图:边全部显示、增删节点/边即时刷新、切换素材不丢。
- 时间线:增删 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 的 push(code review 条目)。