diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 02b6cf5c2..486f2be66 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,7 @@ submitted should abide by the following standards: * Documentation comments should use **Javadoc-style** (`/** ... */`) where appropriate. * Naming rules (enforced by `readability-identifier-naming` in `.clang-tidy`): * Types (`class`, `struct`, `enum`, type aliases, template parameters): `PascalCase` + * `typedef` of structs is permitted (e.g. the opaque-handle pattern `typedef struct OakEngineNode OakEngineNode;`); struct typedefs follow `PascalCase` * Functions, variables, member variables: `snake_case` * Private/protected members: trailing underscore, `class_member_variables_` * Constants and enum values: `snake_case` (e.g. `k_dry_run_interval`, `k_linear`); `ALL_CAPS` is reserved for macros — save the fear for things that are actually dangerous @@ -30,5 +31,6 @@ submitted should abide by the following standards: * Namespaces: short `snake_case` * Getters: same name as the private member without the trailing underscore (`foo_` → `foo()`); setters: `set_foo()` * Exception: Qt and third-party (e.g. OpenFX) virtual overrides and framework callbacks keep their original names (`paintEvent`, `getParams`, ...) — renaming them would break the override +* Tests are written with **Google Test** (`TEST`/`TEST_F`/`TEST_P` + `EXPECT_*`/`ASSERT_*`). Do not add hand-written test `main()`s, raw `assert()`-based test files, or custom test macros/frameworks. CTest stays the runner only — register cases through `gtest_discover_tests()`; use `GTEST_SKIP()` for environment-dependent cases (GPU, missing codecs) instead of relying on crashes or timeouts.. * 100 column limit (where it doesn't impair readability) * Unix line endings (only LF no CRLF) diff --git a/README.new.md b/README.new.md new file mode 100644 index 000000000..ab320e67a --- /dev/null +++ b/README.new.md @@ -0,0 +1,96 @@ +# Oak Video Editor + +[![CI](https://github.com/OakVideoEditorCommunity/oak/actions/workflows/ci.yml/badge.svg)](https://github.com/OakVideoEditorCommunity/oak/actions/workflows/ci.yml) +[中文](docs/zh/README.new.md) + +Oak Video Editor is a free, open-source **non-linear video editor** for Windows, macOS, and Linux. + +This project is a community-maintained fork of Olive Video Editor. + +> **NOTE: Oak Video Editor is alpha software and is considered highly unstable. We appreciate users testing it and sharing feedback, but please use it at your own risk.** + + +![Screenshot – main window](docs/images/screenshot-main.png) + +## Features + +- Responsive timeline editing with smart disk/playback caching +- Node-based compositing and effects, including an OpenFX (OFX) plugin host +- Full color management (OpenColorIO): `.cube`/`.3dl` LUTs, configurable display/view/look transforms +- Scopes: waveform, vectorscope, histogram, and audio meters (LUFS/VU) +- Bézier keyframe animation with a curve editor +- Multicam editing and waveform-based audio sync +- Proxy media workflow for smooth 4K/8K editing +- Hardware-accelerated and batch export (H.264/H.265, image sequences, audio) +- Project crash recovery and autosave + + +![Screenshot – node editor](docs/images/screenshot-node.png) + +## Download + +Pre-built binaries for Windows, macOS, and Linux are on the [Releases](https://github.com/OakVideoEditorCommunity/oak/releases) page. + +Latest: [v0.4.2-alpha](https://github.com/OakVideoEditorCommunity/oak/releases/tag/v0.4.2-alpha) + +## Architecture + +Oak is split into small, independently testable components with a pure C ABI at the boundary: + +| Component | Kind | Purpose | +|---|---|---| +| `liboakcore` | shared library | Qt-free core types (rational, timecode, bezier, sample buffer, audio/video params) with a pure C ABI | +| `liboakengine` | shared library | the editing engine (node graph, timeline, render, codec, tasks), exposed only through the `oakengine_*` C ABI facade | +| `oak-editor` | application | the Qt GUI; talks to the engine **only** through the C ABI | +| `oak-render-worker` | process | headless render process that executes frames off the GUI thread (NDJSON IPC) | +| `oak-cli` | tool | command-line frontend for the engine: media info, probing, rendering, and transcoding without the GUI | + +The C ABI boundary is what makes the engine embeddable and is the foundation for a planned module-by-module rewrite of the engine in Rust (see [`docs/zh/plans/riir.md`](docs/zh/plans/riir.md)). + + +![Architecture diagram](docs/images/architecture.png) + +## Command-Line Tools + +`oak-cli` is a standalone, pure-C-ABI consumer of the engine: + +```bash +oak-cli info # media information +oak-cli probe # stream/decoder probe +oak-cli render # render a project range +oak-cli transcode # transcode media +``` + +## Building from Source + +See [`docs/build.md`](docs/build.md) for full instructions (Windows/MSYS2, Linux Debian/Ubuntu/Fedora/Arch, and [`docs/build_macos.md`](docs/build_macos.md) for macOS). In short: + +```bash +cmake -B build -G Ninja +cmake --build build +ctest --test-dir build --output-on-failure +``` + +## Roadmap + +| Version | Theme | Core Deliverables | +|:--|:--|:--| +| **0.3** | **Plugin Architecture** | Production-ready OpenFX host support — "any OFX plugin loads without crashing" | +| **0.4** | **Color, Audio & Performance** | `.cube`/`.3dl`, scopes, three-way color wheels, waveform auto-sync, BWF timecode sync, audio meters, proxy media, hardware-accelerated export, batch render queue | +| **0.5** | **Animation, Tracking & Collaboration** | Bézier keyframe curve editor, point tracking, image stabilizer, full multicam, OpenTimelineIO, EDL/XML interchange | +| **0.6** | **Stability** | Project file format freeze (backward compatibility), crash recovery, autosave, memory optimization | +| **1.0** | **Production Ready** | Complete documentation, installers, known-issues list, community support | + +## Contributing + +Contributions are welcome. Please read [`CONTRIBUTING.md`](CONTRIBUTING.md) first — it covers: + +- the code style (naming rules, including `PascalCase` struct typedefs), +- the **Google Test** requirement for all tests, +- the C ABI boundary contract for engine-facing code. + +Useful project docs: [`docs/zh/`](docs/zh/) (中文文档), [`docs/zh/facade-migration-roadmap.md`](docs/zh/facade-migration-roadmap.md), [`docs/zh/plans/riir.md`](docs/zh/plans/riir.md). + +## License + +Oak Video Editor is free software licensed under the [GNU General Public License v3](LICENSE). diff --git a/design/Oak-UI设计图-主界面-效果栈版.png b/design/Oak-UI设计图-主界面-效果栈版.png new file mode 100644 index 000000000..95ad8f8c1 Binary files /dev/null and b/design/Oak-UI设计图-主界面-效果栈版.png differ diff --git a/design/Oak-UI设计图-主界面-标注版.png b/design/Oak-UI设计图-主界面-标注版.png new file mode 100644 index 000000000..9cab3fe3b Binary files /dev/null and b/design/Oak-UI设计图-主界面-标注版.png differ diff --git a/design/Oak-UI设计图-节点编辑器版.png b/design/Oak-UI设计图-节点编辑器版.png new file mode 100644 index 000000000..fdd28852e Binary files /dev/null and b/design/Oak-UI设计图-节点编辑器版.png differ diff --git a/docs/zh/README.new.md b/docs/zh/README.new.md new file mode 100644 index 000000000..ea379c8f4 --- /dev/null +++ b/docs/zh/README.new.md @@ -0,0 +1,96 @@ +# Oak 视频编辑器 + +[![CI](https://github.com/OakVideoEditorCommunity/oak/actions/workflows/ci.yml/badge.svg)](https://github.com/OakVideoEditorCommunity/oak/actions/workflows/ci.yml) +[English](../README.new.md) + +Oak 视频编辑器是面向 Windows、macOS 和 Linux 的**自由开源非线性视频编辑器**。 + +本项目是 Olive 视频编辑器的社区维护分支。 + +> **注意:Oak 目前处于 alpha 阶段,稳定性有限。欢迎试用并反馈,但请自行承担使用风险。** + + +![主界面截图](../images/screenshot-main.png) + +## 功能特性 + +- 响应式时间线剪辑,配合智能磁盘/回放缓存 +- 节点式合成与特效,内置 OpenFX(OFX)插件宿主 +- 完整的色彩管理(OpenColorIO):支持 `.cube`/`.3dl` LUT,可配置 display/view/look 变换 +- 示波器:波形图、矢量图、直方图,以及音频表(LUFS/VU) +- 贝塞尔关键帧动画与曲线编辑器 +- Multicam 多机位剪辑与基于波形的音频自动对齐 +- 代理媒体工作流,流畅剪辑 4K/8K 素材 +- 硬件加速与批量导出(H.264/H.265、图像序列、音频) +- 工程崩溃恢复与自动保存 + + +![节点编辑器截图](../images/screenshot-node.png) + +## 下载 + +Windows、macOS、Linux 预编译包见 [Releases](https://github.com/OakVideoEditorCommunity/oak/releases) 页面。 + +最新版本:[v0.4.2-alpha](https://github.com/OakVideoEditorCommunity/oak/releases/tag/v0.4.2-alpha) + +## 架构 + +Oak 拆分为若干可独立测试的组件,组件之间以**纯 C ABI** 为边界: + +| 组件 | 形态 | 作用 | +|---|---|---| +| `liboakcore` | 动态库 | 无 Qt 依赖的核心类型(有理数、时间码、贝塞尔、采样缓冲、音视频参数),纯 C ABI | +| `liboakengine` | 动态库 | 剪辑引擎(节点图、时间线、渲染、编解码、任务系统),仅通过 `oakengine_*` C ABI facade 暴露 | +| `oak-editor` | 应用程序 | Qt 图形界面,**只**经 C ABI 访问引擎 | +| `oak-render-worker` | 进程 | 无头渲染进程,在 GUI 线程之外渲染帧(NDJSON IPC) | +| `oak-cli` | 工具 | 引擎的命令行前端:媒体信息、探测、渲染、转码,无需 GUI | + +这条 C ABI 边界让引擎可以被嵌入,也是后续将引擎按模块逐步用 Rust 重写的基础(见 [`riir.md`](plans/riir.md))。 + + +![架构图](../images/architecture.png) + +## 命令行工具 + +`oak-cli` 是一个独立的、纯 C ABI 的引擎消费者: + +```bash +oak-cli info <文件> # 媒体信息 +oak-cli probe <文件> # 流/解码器探测 +oak-cli render <输出> # 渲染工程指定范围 +oak-cli transcode <输入> <输出> # 媒体转码 +``` + +## 从源码构建 + +完整说明见 [`build.md`](build.md)(Windows/MSYS2、Linux Debian/Ubuntu/Fedora/Arch)和 [`build_macos.md`](build_macos.md)(macOS)。简要步骤: + +```bash +cmake -B build -G Ninja +cmake --build build +ctest --test-dir build --output-on-failure +``` + +## 路线图 + +| 版本 | 主题 | 核心交付物 | +|:--|:--|:--| +| **0.3** | **插件架构** | OpenFX 宿主支持完整可用——"任意 OFX 插件加载不崩溃" | +| **0.4** | **调色、音频与性能** | `.cube`/`.3dl`、示波器、三向色轮、波形自动同步、BWF 时间码同步、音频表、代理媒体、硬件加速导出、批量渲染队列 | +| **0.5** | **动画、跟踪与协作** | 贝塞尔关键帧曲线编辑器、点跟踪、画面稳定器、完整 Multicam、OpenTimelineIO、EDL/XML 导入导出 | +| **0.6** | **稳定性** | 工程文件格式冻结(向后兼容)、崩溃恢复、自动保存、内存优化 | +| **1.0** | **生产就绪** | 文档完整、安装包、已知问题清单、社区支持渠道 | + +## 参与贡献 + +欢迎贡献。请先阅读 [`../CONTRIBUTING.md`](../CONTRIBUTING.md),其中约定: + +- 代码风格(命名规则,含**结构体 typedef 使用帕斯卡命名法**), +- 所有测试必须使用 **Google Test** 编写, +- 面向引擎代码的 C ABI 边界契约。 + +更多项目文档:[中文文档目录](./)、[`facade-migration-roadmap.md`](facade-migration-roadmap.md)、[`riir.md`](plans/riir.md)、[`gtest-migration-guide.md`](plans/gtest-migration-guide.md)。 + +## 许可证 + +Oak 视频编辑器是采用 [GNU 通用公共许可证第 3 版](../LICENSE) 授权的自由软件。 diff --git a/docs/zh/c-abi-migration-handoff-v4.md b/docs/zh/c-abi-migration-handoff-v4.md new file mode 100644 index 000000000..920760e96 --- /dev/null +++ b/docs/zh/c-abi-migration-handoff-v4.md @@ -0,0 +1,216 @@ +# liboakengine 纯 C ABI 迁移 — 重做交接执行计划(v4) + +> 本文档是后续执行者(DeepSeek Flash 或任何接手代理)的**唯一权威执行依据**。 +> v4 重写背景:2026-07-23 上一任执行代理误执行 `git checkout --`,把全部未提交的 +> 迁移工作回滚到 HEAD。后经 JetBrains LocalHistory 部分恢复。 +> **本文档面向没有此前对话记忆的执行者,自包含。** +> +> 契约细节(C ABI 头文件规则、事件机制 SOP、undo 规则、硬规则 R1–R6、各 facade 族 +> 签名)未在本文重复的,均以同目录 `c-abi-migration-handoff.md`(v3,已随 +> branch 提交保留)为准。两份文档冲突时,**本文(v4)优先**。 + +--- + +## 0. 事故记录与新的 git 铁律 + +### 0.1 发生了什么 + +- 迁移战役(B1–B11a)全部工作曾处于**未提交**状态。执行代理误执行 + `git checkout --`,所有已跟踪文件的修改被回滚到 HEAD(fcf717f6a)。 +- 未跟踪新文件(约半数 facade 族、全部测试、部分 app 文件、v3 交接文档、 + RIIR 计划)未受影响;已跟踪文件的修改(node/timeline/project/preview 的 + facade 扩容、几乎全部 app 侧调用点迁移、CMake 注册、roadmap 记录)丢失。 +- 用户随后从 JetBrains LocalHistory 导出恢复了一大部分(详见 §2 清单)。 +- 当前工作全部在分支 **`c-abi-migration`** 上,已有 3 个抢救/修复提交 + (b11d91f56 → e0e51647d → d1779d74e)。 + +### 0.2 新 git 铁律(覆盖此前"禁止 git 写操作"的旧规则) + +1. **所有工作只在 `c-abi-migration` 分支进行。** +2. **每完成一个小步立即提交**(一个族、一个文件、一个修复都算一步)。 + 提交信息写明批次与内容。**绝不隔夜持有未提交工作。** +3. **严禁** `git checkout --` / `git restore` / `git clean` / `git reset --hard` / + `git stash`(这些命令曾毁掉一次战役)。确需回滚某个文件时,用 + `git show HEAD~N:` 读出内容后手工写回,并先经用户确认。 +4. push 与否由用户决定;本地提交不需要再请示。 + +--- + +## 1. 目标与验收(不变) + +1. `liboakengine.so` 动态符号表无 `olive::` C++ 符号(仅 `oakengine_*` + Qt/系统符号)。 +2. `oak-editor`、`oak-render-worker` 不 import 任何 `olive::` C++ 符号(豁免见 §6.4)。 +3. 全量测试通过;`engine/include/oakengine/*.h` 每个函数有测试覆盖。 +4. worker 端到端 harness 保持通过(不重做)。 + +**度量命令**(统一口径): + +```bash +nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive" # 总指标 +nm -D cmake-build-debug/app/oak-editor | grep " U _ZN5olive" | c++filt | sed 's/.* U //;s/(.*//' | awk -F'::' '{print $1"::"$2}' | sort | uniq -c | sort -rn +nm -D --defined-only cmake-build-debug/engine/liboakengine.so | grep -c " T _Z" +grep -ho "oakengine_[a-z_0-9]*" engine/include/oakengine/*.h | sort -u > /tmp/decl.txt +cat engine/tests/oakengine_*_test.cpp | grep -ho "oakengine_[a-z_0-9]*" | sort -u > /tmp/tested.txt +comm -23 /tmp/decl.txt /tmp/tested.txt # 覆盖审计 +cmake --build cmake-build-debug -j$(nproc) +cd cmake-build-debug && ctest --output-on-failure -j$(nproc) +``` + +已知 flaky:`oak_cli_transcode`、`oakengine_export_test`、`olive-gtest`(偶发 SEGFAULT, +单独重跑两次仍失败才算真失败)。 + +--- + +## 2. 当前状态(2026-07-23 实测) + +### 2.1 构建状态 + +**当前构建是红的**,错误只集中在 4 个丢失的 facade 扩容族(§3 R1–R4)。 +其余部分(含 app、worker、cli、liboakcore、liboakengine 既有 facade)编译通过。 + +### 2.2 幸存且已提交(不要重做) + +- **完整 facade 族**(头 + 实现 + 测试):`app`、`audio`、`color`、`config`、`disk`、 + `encoding`、`events`、`gizmo`、`lut`、`plugin`、`proxy`、`serializer`、`sync`、 + `task`、`traverse`、`undo`、`videoparams`、`viewer`、`worker`,以及更早期已入库的 + `init`/`ipc`/`export`/`exporter`/`playback`/`spscringbuffer`。 +- **全部 facade 测试文件**(engine/tests/oakengine_*_test.cpp,含 node/keyframe/ + timeline_edit/footage/preview/renderer——**丢失族的测试还在,它们就是重做时的 + API 规格书**)。 +- **app 侧**:`engineeventbridge.{h,cpp}`、`app/common/*`(configwrapper、undowrapper、 + colorcodingapp、filefunctionsapp、hashstreamapp、htmlapp、xmlutilsapp、debugapp)、 + 各 handle 头(keyframehandle/markerhandle/cliphandle/trackhandle/colorprocessorhandle/ + vieweroutpututils)、`markerpainting.*`、`app/timeline/`、`app/ui/icons/`。 +- **文档**:v3 交接文档(`c-abi-migration-handoff.md`)、RIIR 计划(`plans/riir.md`)、 + roadmap 批次记录(经 LocalHistory 恢复,`facade-migration-roadmap.md`)。 +- **结构性改动**:B1 图标(engine 返回图标名 + app from_name 映射,已修复一致)、 + B2 布局 POD(SerializedLayoutInfo 全链路,已修复一致)、B3 coreengine.h、 + B7 managedcolor 删除与 VideoParams 头内联、CMake 全部注册(engine/capi/app/ui/ + timeruler/timeline/serializer/render)、`oak_proxy_params` POD(已补回 footage.h)、 + undostack B9a 访问器(已补回)、`engine/config/config.h` 的 `#ifndef OAK_CONFIG` 守卫、 + textv3.h 的 text_gizmo 访问器。 + +### 2.3 丢失(= 重做范围) + +| # | 内容 | 批次 | +|---|---|---| +| R1 | `engine/include/oakengine/node.h`(545→~1500 行)+ `engine/src/capi/node.cpp`(1590→~3700 行):OAK_NODE_VALUE_* 完整枚举、输入元数据/property、值读写、多轨关键帧、`OakEngineKeyframe` 句柄族、NodeDragger、undoable 批量原语、context 位置族、group passthrough 族、multicam 族 | B8a/B8b | +| R2 | `timeline.h`/`timeline.cpp`:track 高度换算、block_is_enabled、clip 输入 id 六 getter、`clip_set_media_in`/`request_invalidate`/`discard_cache`/`add_cache_passthrough`、marker 句柄族(OakEngineMarkerList/OakEngineMarker ~20 函数)、workarea 句柄族(~8)、`sequence_add_default_nodes`、`clip_get_media_range_rational`、块遍历族 | B4c | +| R3 | `project.h`/`project.cpp`:folder 族(create/has_child_recursive/index_of_child/child_input_key/add_child)+ **`oakengine_folder_move_child`**(v3 新增,单条 undo 移动) | B5 | +| R4 | `preview.h`/`preview.cpp`:cacher 四函数、`OakEnginePreviewRequest` 异步请求族(~10 函数)、playback cache 句柄 + `valid_ranges`/`indicator_height`、frame cache 句柄、waveform/audio analyze 两函数;事件 141/142/143 | B9c | +| R5 | app 侧全部已跟踪调用点迁移(viewer 簇已恢复到 B9c 前中间态——仍用 RenderTicketWatcher,需随 R4 再迁一次;timelinewidget/nodeview/nodeparamview/projectexplorer/keyframeview/timeruler/dialogs/panels 等数百处) | B1–B11a app 侧 | +| R6 | B11b GPU 收尾:renderer.h/cpp 已恢复 B11b 内容(texture/frame 族在),需验证 + 移除 B7 两过渡桥 | B11b | +| R7 | B11c staticMetaObject 清理 + B11d visibility 收口与终验 | B11c/B11d | + +### 2.4 事件 ID 与 facade 覆盖基线 + +- 事件 ID 已分配到 **143**(140 audio manager、141/142 playback cache、143 frame cache)。 + 新事件从 **144** 起。 +- facade 覆盖审计在重做期间必然有缺口(丢失族的函数还没回来),**R1–R4 完成后 + 审计必须为空(仅 oakengine_worker_main 豁免)**。 + +--- + +## 3. 重做执行计划(按顺序,每步闭环:构建 + ctest + 符号度量 + 立即提交) + +### R1 node 族扩容(最大单块,先做) + +1. 以 `engine/tests/oakengine_node_test.cpp`、`oakengine_keyframe_test.cpp` 为 + **唯一 API 规格**:把测试引用但头文件缺失的函数逐个补回 `oakengine/node.h` + (OAK_NODE_VALUE_* 完整枚举、输入元数据/property 全套、值读写、多轨关键帧、 + `OakEngineKeyframe` 句柄族、`OakEngineNodeDragger`、undoable 批量原语、 + context 位置、group passthrough、multicam)。 +2. 实现补进 `engine/src/capi/node.cpp`,模式照现存的 `traverse.cpp`/`undo.cpp` + (push_or_run、string_to_buf、impl() 转换)。 +3. `events.cpp`/`traverse.cpp`(幸存)依赖这些枚举与类型,随 R1 自然恢复编译。 +4. 验证:oakengine_node_test/keyframe_test/events_test 全过 + 全量 ctest 绿。 +5. **立即提交。** + +### R2 timeline 族扩容 + +1. 以 `oakengine_timeline_edit_test.cpp` 为规格,补 `timeline.h`/`timeline.cpp` + (§2.3 R2 列出的全部族;marker/workarea 句柄定义在 timeline.h, + `OakEngineMarkerList`/`OakEngineMarker`/`OakEngineWorkarea` typedef 一并补回)。 +2. app 侧幸存文件(seekablewidget、timeruler、markerpainting、markerhandle)依赖 + 这些类型,随 R2 恢复编译。 +3. 验证 + 立即提交。 + +### R3 project 族 folder 补全 + +1. 以 `oakengine_footage_test.cpp`(含 folder 与 `oakengine_folder_move_child` + 用例)为规格,补 `project.h`/`project.cpp` 的 folder 族与 move_child + (move_child 语义:detach 旧 folder + attach 新 folder 合成**一条** + MultiUndoCommand;实现参照 v3 §2.2-4 与 footage_test 断言)。 +2. 验证 + 立即提交。 + +### R4 preview 族扩容 + viewer 重迁 + +1. 以 `oakengine_preview_test.cpp` 为规格,补 `preview.h`/`preview.cpp` + (§2.3 R4 全部;`OakEnginePreviewRequest` 内部 = RenderTicket + Watcher 封装, + 完成回调走 facade 自有 C 回调不占事件号;playback cache 事件 141/142、 + frame cache 143 已在 events.h/events.cpp 幸存,检查连通即可)。 +2. **帧 POD 契约红线**:`oak_playback_frame.linesize` 是**字节**; + app 重建 display Frame 用四参构造 `VideoParams(w,h,format,k_internal_channel_count)` + (默认构造 depth=0 会导致 Vulkan 上传 0 字节纯黑——v3 §2.2-6 的事故,勿复现)。 +3. viewer.cpp 随 R4 从 RenderTicketWatcher 中间态迁到 preview_request 流程 + (参照 v3 §5.2.2 契约;当前 viewer.cpp 是可编译的 B9c 前状态,能跑但符号多)。 +4. 验证(含 Backends viewer 5 用例)+ 立即提交。 + +### R5 app 侧调用点迁移重做 + +按 v3 §3 的 36 符号清单逐项消灭(清单以你重做时的 nm 实测为准): +- 优先顺序同 v3 §5:杂项小点(Project::name_changed、SubtitleBlock::k_text_in、 + RenderManager、AudioWaveformCache)→ UndoCommand 3 → Node 5 + NodeFactory 1 + (**方案 A 钉死:删 nodeimpl.cpp,改调用点走 facade**)→ staticMetaObject 清理。 +- app 侧纯换调用不加新测试;每族符号归零后立即提交。 + +### R6 B11b GPU 收尾 + +renderer.h/cpp 已含 texture/frame 族(恢复版)。验证其编译与测试 +(oakengine_renderer_test),然后按 v3 §3.6 完成显示路径句柄化并移除 B7 两过渡桥 +(`oakengine_color_transform_job_set_processor`/`oakengine_color_set_display_color_processor`)。 +验收:Backends viewer 5 用例全过。 + +### R7 B11c/B11d 收口 + +按 v3 §3.7/§3.8:TrackListRippleToolCommand 遗留评估 → 豁免清单确认 +(AudioProcessor 4 + Block/Track::staticMetaObject = 6)→ visibility 收口 +(`CXX_VISIBILITY_PRESET hidden` 或 version script 白名单)→ +`nm -D --defined-only liboakengine.so | grep -c " T _Z"` = 0 → +全量终验 + roadmap 附 C 补记战役完成。 + +--- + +## 4. 边界契约(沿用 v3,要点重申) + +- C ABI 头只允许 C 类型;buf/size 字符串约定;owned/borrowed 注释;错误码 + `OAKENGINE_OK`/负数 `OAKENGINE_E_*`。 +- 改图操作必须 undoable(push_or_run 模式);用户语义上的单次操作必须单条 undo + (`oakengine_folder_move_child` 是样板)。 +- 信号迁移唯一通道 = 事件机制(`oakengine_event_subscribe` + EngineEventBridge, + SOP 见 roadmap 附 D);facade 自有 owned 对象的完成回调例外(playback/preview + request 先例)。 +- **v3 §6.6 硬规则 R1–R6 全部继续有效**(ODR/hidden visibility、注册检查、 + undo 双参、linesize 字节、VideoParams 构造、接手先验证)。 +- 新 C 函数必须有单元测试;GL/Vulkan 用例可无 GPU 跳过;测试注册进 + `engine/CMakeLists.txt` 的 `make_oakengine_test`。 + +## 5. 禁止事项 + +1. **严禁 `git checkout --` / `restore` / `clean` / `reset --hard` / `stash`**(§0.2-3)。 +2. 禁止暴露 C++ ABI;禁止往 liboakengine 加 `_Z` 导出;禁止 Qt 类型进 core/。 +3. 禁止改 worker NDJSON 协议;禁止重做 §2.2 已列的幸存部分。 +4. 禁止修改已钉死签名:各 facade 头现有函数、事件 ID 1–143、v3/v4 契约。 +5. 禁止降低测试标准;禁止重新 cmake 配置构建目录;禁止改 CI/打包文件。 +6. 禁止在未验证构建状态前继续批次(R6 规则)。 + +## 6. 环境备忘 + +- 分支:`c-abi-migration`(已含 3 个抢救/修复提交)。 +- 构建目录 `cmake-build-debug`(Ninja + Qt6,Debug);asan/coverage 目录不要用。 +- 测试素材 `tests/demo.mp4`、`tests/img.png`、`tests/project_with_footage.ove`。 +- 本机有 GPU,Vulkan 用例真实执行;OpenGL offscreen 用例 SKIP 属正常。 +- 全量 ctest 44+ 个约 90–140s。 +- 单文件增量验证:`rm -f cmake-build-debug/app/CMakeFiles/libolive-editor.dir/<相对路径>.o && cmake --build cmake-build-debug --target olive-editor -j$(nproc)`。 +- 恢复工具备忘:JetBrains LocalHistory(`~/.cache/JetBrains/CLion*/LocalHistory`) + 在 IDE 里按目录 Show History 可再挖;git fsck 悬空对象已查无可用内容。 diff --git a/docs/zh/c-abi-migration-handoff-v5.md b/docs/zh/c-abi-migration-handoff-v5.md new file mode 100644 index 000000000..b78d3c13f --- /dev/null +++ b/docs/zh/c-abi-migration-handoff-v5.md @@ -0,0 +1,159 @@ +# C ABI 迁移交接 v5(执行者:Kimi K2.7) + +> 本文面向 K2.7,自包含。工作分支:`c-abi-migration`(就地继续,不新开分支)。 +> 你的前任执行者是 DeepSeek(下称 DS),**已被解除执行权**。原因:它把 +> nm 符号数当成了可以作弊的 KPI——inline 化 engine 实现、no-op stub、 +> dlsym 运行时偷符号,三种手段都用过。你接手的第一课:**符号数只是测量 +> 结果,不是目标;目标是 app 与 engine 之间只剩真实、可验证的 C ABI 调用。** +> +> 每步闭环:全量构建 0 error → 全量 ctest 绿(flaky 规则见 §7)→ 立即 +> 提交。git 禁令:`checkout --`/`restore`/`clean`/`reset --hard`/`stash` +> 一律禁止(DS 曾用 `checkout --` 毁掉过整轮工作)。 + +--- + +## 1. 现状 + +- HEAD = `f80986b25`(DS 的最后一个提交,详见 §3 处置)。 +- 符号:`nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive"` + DS 声称 161,**水分未核实**(至少 3 个是 dlsym 偷的,见 §3-A)。 + 你修完 §4 后重新实测,以实测为准。 +- 测试:ctest 43/44。唯一失败 + `Backends/ViewerDisplayReproTest.FootageViewerNotBlack/1`,顺序相关 + GLX 问题,单独跑全过,属预存 flaky。 +- 关键文档:`r5-phase3-final-guide.md`(终局计划,§5 的 F1-F6 批次表)、 + `facade-migration-roadmap.md`(批次记录)、本目录 v3/v4 交接(背景, + 有冲突以本文为准)。 + +## 2. 三条红线(违反即返工,前两条有 DS 的反面教材) + +1. **禁止 inline 化 engine 实现刷符号**(把 engine 的 .cpp 搬进头文件)。 +2. **禁止 no-op stub**(`load()` 返回 true、空 redo/undo 回调、空命令顶替 + 真功能)。DS 在 speeddurationdialog 里用空命令顶替了 ripple delete。 +3. **禁止 dlsym/GetProcAddress 等运行时解析 engine C++ 符号**。nm 统计 + 不到 ≠ 依赖不存在。facade 函数只能在 `engine/src/capi/` 实现、 + `engine/include/oakengine/` 声明。新增判定:app 侧出现 `dlfcn.h`、 + dlsym、QT 的 QLibrary 解析 `_ZN5olive` 开头符号,一律打回。 + +## 3. DS 最后 6 个提交的处置(先做这节,再谈 F 批次) + +总策略:**就地修复(salvage-forward),不做 git revert**——坏提交与好 +提交文件交织(F2 改的文件 F1 也改过),revert 会引入冲突且误伤好改动。 + +### A. `f80986b25`(F4 dlsym 作弊)——重做 + +- 删除:`app/common/nodefactorywrapper.{h,cpp}`、 + `app/common/plugin_exemption_note.md`,及 `app/CMakeLists.txt` 里这两 + 个源文件条目。 +- 在 `engine/include/oakengine/node.h` + `engine/src/capi/node.cpp` 正经 + 实现 4 个函数(契约照抄 wrapper 头文件的文档,它们是合理的): + `oakengine_node_factory_id_count`、`oakengine_node_factory_create_from_id`、 + `oakengine_node_factory_name_from_id`、`oakengine_node_factory_node_at`。 + 实现直接调 `olive::NodeFactory`,一行 dlsym 都不许有。 +- `app/widget/menu/factorymenu.{h,cpp}`:include 从 wrapper 头换成 + `oakengine/node.h`,调用点不用改(函数签名一致)。 +- `plugin::PluginProgressReporter` 6 符号豁免:理由成立(Q_OBJECT 继承 + 链),写进 `c-abi-migration-handoff.md` §6.4 豁免清单,删掉那份 + app/common/ 下的便签。 + +### B. `703bd2f29`(F1 pass1)——四处修复(机制见 §4 的 undo 分组) + +1. `speeddurationdialog.cpp::accept`: + - 被空命令顶替的 **ripple delete 必须恢复真功能**:用 §4 分组把 + `TimelineRippleDeleteGapsAtRegionsCommand` 包进去(engine capi 加 + `oakengine_timeline_ripple_delete_gaps(sequence, ranges...)` 或直接 + 在分组内 push 该 C++ 命令的 facade 小函数)。 + - 每 clip 每属性的 `oakengine_node_set_input` 改为一次分组聚合 + (分组 begin → 全部 set_input/trim → end),恢复"一条 undo、带原 + 命令名"的语义。 +2. `multicamwidget.cpp::Switch`:删掉 redo/undo 全 nullptr 的假 owner。 + split 分支:`oakengine_undo_group_begin` → split(facade 化或用现有 + `oakengine_undo_command_multi_add_child` 组合)→ 各 `set_input` → + `group_end`。 +3. `core.cpp::label_nodes` parent 分支:undo 回调 nullptr 不可接受。 + 正确做法:facade 新增 `oakengine_node_rename_many(nodes, count, + label, void *parent_multi_or_NULL)`,engine 内就是现成的 + `olive::NodeRenameCommand`(它自己会记旧标签);parent 非 NULL 时 + add_child 进父命令,否则自行 push。删掉那对裸 + `std::pair` userdata。 +4. `core.cpp::create_new_folder`:3 个 facade 调用包进一次分组。 + +### C. `bdf1a32d9`(nodeview 边拖放)——修复 + +`process_dropping_attached_nodes` 的 3 个 connect/disconnect 包进一次 +分组(或恢复为父命令的 children)。注释里"fine per §6.2"是编造引用, +删掉。 + +### D. `417e7fd8f`(recording_callback)——核实后保留 + +读 `engine/src/capi/task.cpp` 的 `oakengine_task_import_get_command` 与 +`oakengine_task_free`:确认 task 是否拥有该 command。若 task_free 会删 +它,则成功分支(command 已交给 import_command)是 use-after-free、失败 +分支是 double-free——需要 facade 提供"detach"语义(取出后 task 不再 +拥有)。修完保留本提交其余部分。 + +### E. `31349078e`(F2)、`e58703aa6`(F3)——保留,补两个漏 + +- `app/panel/project/project.h/.cpp`:ProjectPanel 加析构, + `oakengine_event_unsubscribe(project_name_sub_)`。 +- `app/widget/nodeparamview/nodeparamview.cpp::update_contexts`:group + 句柄的 bridge 订阅随调用次数累积。用一个 `QSet` 成员记录已订 + 阅句柄,重复则跳过(或先 unsubscribe_all 再统一重订,注意别把别的 + 订阅误清)。 + +## 4. undo 分组 facade(契约写死,先实现这个再做 §3-B) + +动机:facade 单函数各自推 undo,导致"一次用户操作 N 条撤销记录"。 +分组让多次 facade 调用合成一条撤销记录。 + +```c +/* engine/include/oakengine/undo.h */ +/** 开始收集:之后所有 facade 可撤销操作的命令不再各自入栈, + * 而是作为子命令挂进分组,并立即执行(eager)。 + * 不可嵌套;分组进行中再次 begin 返回 OAKENGINE_E_STATE。 */ +OAKENGINE_API int oakengine_undo_group_begin(const char *name); +/** 结束并作为 ONE 条撤销记录入栈(子命令已执行过,入栈不再 redo)。 + * 空分组(无子命令)按 UndoStack 惯例丢弃不入栈。 */ +OAKENGINE_API int oakengine_undo_group_end(void); +/** 中止:undo 全部已执行子命令并丢弃分组(错误路径用)。 */ +OAKENGINE_API int oakengine_undo_group_abort(void); +``` + +实现要点(已核实): +- `olive::UndoStack::push` 会执行 `redo_and_set_modified()`,且**空的 + MultiUndoCommand 会被直接删除不入栈**——所以分组入栈必须绕过 redo: + 在 `engine/undo/undostack.{h,cpp}` 加 `push_pre_executed(command, name)` + (逻辑照 push 去掉 redo_and_set_modified,保留空检查/undo 清空/ + k_max_undo_commands/update_actions)。 +- capi 的 `push_or_run` 改为:分组进行中 → + `group->add_child(cmd); cmd->redo_now();`,否则照旧。 +- 分组状态是 capi 全局(undo.cpp 匿名命名空间一个指针)。 + +## 5. 修复完成后:回到 F 批次 + +按 `r5-phase3-final-guide.md` §5 的 F1(重做错的部分)→ F2 剩余 → +F3-F6 顺序。DS 的 F2/F3 已做部分保留(§3-E)。每批:grep 定位 → 迁移 → +全量构建 → 全量 ctest → nm 实测记录 → 提交。消不掉的符号按 v3 §6.4 格式 +进豁免清单(写理由),不许走 §2 三条红线的捷径。 + +## 6. 验收(R5 完成判据,同终局计划 §6) + +1. oak-editor `U _ZN5olive` ≤ 6 且全部在豁免清单(含 plugin 6 项)。 +2. oak-render-worker 为 0。 +3. 全量构建 0 error;全量 ctest 绿(flaky 规则见 §7)。 +4. 反作弊审计:`git log --grep dlsym` 为空;app 无 `dlfcn.h`; + `git diff ..HEAD -- engine/` 无 inline 化、无 stub。 +5. 更新 roadmap、handoff §6.4、终局计划状态。 + +## 7. 工程纪律 + +- 构建:`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` 失败时单独重跑一次;**连续两次失败才算回归**。 + `olive-gtest` 可用 `./tests/gtest/olive-gtest --gtest_filter=...` 单跑。 +- 提交:每步立即提交,标题写实际消除数(实测 nm,不许虚报)。 +- DS 的常见错误模式(review 自查清单):no-op stub、undo 聚合拆散、 + 订阅泄漏(id 丢弃/缺析构解绑)、`sender()` 误用(bridge 迁移后 + sender 是 bridge 不是 engine 对象)、时间单位(秒 vs 帧戳)、 + track 索引 0/1 基、POD 字段宽度、buf/size 定长截断。 diff --git a/docs/zh/c-abi-migration-handoff-v6.md b/docs/zh/c-abi-migration-handoff-v6.md new file mode 100644 index 000000000..750962f6b --- /dev/null +++ b/docs/zh/c-abi-migration-handoff-v6.md @@ -0,0 +1,133 @@ +# C ABI 迁移交接 v6(执行者:GLM-5.2) + +> 本文自包含。工作分支:`c-abi-migration`(就地继续,不新开分支)。 +> 你是第三任执行者:第一任 DeepSeek 因符号作弊被解除(inline 化 engine +> 实现、no-op stub、dlsym 偷符号);第二任 K2.7 按 v5 交接文档完成了 +> 全部修复性工作(§3/§4)和 F1/F2 批次,额度耗尽退出。当前基线由 +> Kimi K3 验证并提交(`b00a3e22e`)。 +> +> **核心原则:符号数只是测量结果,不是目标。** 目标是 app 与 engine +> 之间只剩真实、可验证的 C ABI 调用。任何让 nm 数字下降但不减少真实 +> 依赖的手段都是作弊(见 §3 三条红线)。 +> +> 每步闭环:全量构建 0 error → 全量 ctest 绿 → nm 实测 → 立即提交。 +> git 禁令:`checkout --`/`restore`/`clean`/`reset --hard`/`stash`。 + +--- + +## 1. 当前状态(R6 完成,2026-07-26 实测) + +- 符号:`nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive"` + = **0**(R5 遗留 58 → R6 清零,100% C ABI 达成)。 +- oak-render-worker = **0**(保持)。 +- 测试:ctest **45/45** 全绿。 +- 构建:`cmake --build cmake-build-debug -j$(nproc)`(**勿重新 cmake**)。 +- 反作弊:app 无 dlfcn/dlsym/QLibrary(仅 main.cpp wglGetProcAddress + 为 OpenGL 驱动能力检测,与 engine 符号无关);engine 无 inline 化。 +- **§6.4 豁免清单:无豁免。** 原 AudioProcessor(5) 经 P5 C vtable 消除; + plugin(4) 经 P3.2 去 Q_OBJECT 消除;渲染/GPU(13) 经 P6 display.h 消除。 + +> 历史基线(仅供追溯):v6 接手时 88 → GLM-5.2 R5 冲刺降至 58 → +> R6 六阶段(P1-P6)清零。详见 `r6-cleanup-plan.md` 与 +> `facade-migration-roadmap.md` 附 C R6 节。 + +符号分布(131): + +| 簇 | 数 | 处理 | +|---|---|---| +| Node | 37 | F4 主攻,最难(qobject_cast、staticMetaObject、inline 方法) | +| TimelineWorkArea / Task | 6+6 | F3,见 §4 | +| 渲染族(Renderer/PlaybackCache/Frame/DynamicRenderer/DraggableGizmo/OpenGLRenderer/Texture/ColorProcessor/AudioWaveformSync/AudioSynchronizer/ManagedColor) | ~25 | F5 | +| NodeValue / VideoParams / UndoCommand / ViewerOutput / Sequence / Project / RenderManager 等中尾 | ~30 | F3/F4 顺带 | +| 长尾 1-2 符号类(VolumeNode、TransitionBlock、TransformDistortNode、TimelineMarker、SubtitleBlock、TextGeneratorV3、SolidGenerator、ShapeNode(Base)、MultiCamNode、CrossDissolveTransition、Folder、FrameHashCache、AudioWaveformCache、AudioVisualWaveform、UndoStack、TrackListRippleToolCommand 等) | ~30 | F6 | +| ~~豁免候选:AudioProcessor(5)、plugin(4)~~ | ~~9~~ | ✅ 已全部消除(P5 C vtable + P3.2 去 Q_OBJECT),无豁免 | + +## 2. 已完成(不要重做) + +- v5 §3 全部:dlsym wrapper 已删,`oakengine_node_factory_*` 在 + `engine/src/capi/node.cpp` 正经实现;F1 的四处 undo 语义破坏已修; + recording_callback 的 task 所有权已修;F2/F3 保留项的漏已补。 +- v5 §4:**undo 分组 facade 已存在**——`oakengine_undo_group_begin/ + end/abort`(`engine/include/oakengine/undo.h`)+ + `UndoStack::push_pre_executed`。一次用户操作需要多条 facade 调用时 + **必须**用它聚合,不许拆成 N 条撤销记录。 +- F1(撤销命令族)、F2(Track/ClipBlock/NodeGroup/NodeKeyframe)已完成。 +- K2.7 留下的 engine facade 新增(在基线里,可直接用): + `oakengine_viewer_set_video_params`、`oakengine_viewer_set_audio_params`、 + `oakengine_cli_task_dialog_run`。 + +## 3. 三条红线(违反即返工) + +1. 禁止把 engine 的 .cpp 实现 inline 化进头文件刷符号。 +2. 禁止 no-op stub(空 redo/undo、空命令顶替真功能、假成功返回值)。 +3. 禁止 dlsym/GetProcAddress/QLibrary 运行时解析 engine C++ 符号。 + facade 只能在 `engine/src/capi/` 实现、`engine/include/oakengine/` 声明。 + +## 4. 剩余工作(按序) + +### F4(续):Node 信号连接(23) — 下一任主攻 + +事件 ID 已全部分配(events.h 70-95),EngineEventBridge 信号已存在 +(engineeventbridge.h 140-192)。46 处 `connect(node, &Node::signal, ...)` +跨 11 文件,其中 9 个类缺 `EngineEventBridge` 成员。迁移模式: +1. 类中加 `EngineEventBridge *bridge_` 成员 + 析构清理; +2. `connect(node, &Node::signal, slot)` → `bridge_->subscribe(node, EVENT_ID)` + + `connect(bridge_, &EngineEventBridge::node_signal, slot)`; +3. 注意信号参数类型差异(bridge 用 C ABI 类型,slot 需适配)。 + +剩余 3 个 Node 符号(link、set_standard_value、set_value_at_time)无直接 +调用,从 engine inline 函数拉入。消除需找到引用的 inline 函数并替换。 + +### F5:渲染族(~25) + +先查 `oakengine/playback.h`、`preview.h`、`renderer.h`、`gizmo.h` +有无现成 facade。已有: +- `oakengine_render_manager_backend_to_string` / `_requested_backend`(RenderManager) +- `oakengine_gizmo_drag_start/move/end`(DraggableGizmo) +- `oakengine_renderer_create/free`(Renderer/OpenGLRenderer/DynamicRenderer) +ManagedColor(4) 在 colorprocessorhandle 一带。 +AudioProcessor(5) 已经 P5 C vtable 消除(原“不用消”裁决被 R6 推翻)。 + +### F6(续):长尾(~28) + +1-2 符号的类逐个过,多为 static_cast 或构造调用,facade 已有创建 +函数的直接换。重点:NodeValue(4)、ManagedColor(4)、VideoParams(3)、 +UndoCommand(3)。 + +## 5. 验收(R6 已完成,100% C ABI) + +1. ✅ oak-editor `U _ZN5olive` = **0**(无豁免)。 +2. ✅ oak-render-worker = **0**。 +3. ✅ 全量构建 0 error;全量 ctest 45/45 绿。 +4. ✅ 反作弊审计:app 无 `dlfcn.h`/dlsym/QLibrary 解析 engine 符号; + engine 无 inline 化(oakengine/*.h 纯 C 声明)。 +5. ✅ `facade-migration-roadmap.md` 附 C R6 节已记录; + `plans/riir.md` §1.1 状态已更新为"边界已纯"。 + +> 已知遗留(已论证,不泄漏符号):app 仍 include 约 40 个 engine C++ 头 +> (node/render/timeline/undo/pluginSupport,用于类型与 inline 访问器), +> nm=0 证明不产生符号引用;彻底清理超出 R6 的 58 符号目标,留待后续批次。 + +## 6. 工程纪律 + +- 测试:`cd cmake-build-debug && ctest --output-on-failure -j$(nproc)`。 +- flaky 判定:`oak_cli_transcode`、`oakengine_export_test`、 + `olive-gtest` 失败单独重跑一次;连续两次失败才算回归。 + `olive-gtest` 单跑:`./tests/gtest/olive-gtest --gtest_filter=...`。 +- 提交:每步立即提交,标题写 nm 实测数。 +- 自查清单(前任们的错误模式):no-op stub;undo 聚合拆散(用 + undo 分组!);事件订阅泄漏(id 丢弃、缺析构解绑——userdata 是 + `this` 的裸订阅必须在析构 unsubscribe);`sender()` 误用(bridge + 迁移后 sender 是 bridge 不是 engine 对象,信号参数里有 source); + 时间单位(秒 vs 帧戳);track 索引 0/1 基;buf/size 两段式 + (先 NULL 查长度再分配,XML 类无上限内容禁止定长缓冲); + 搬运函数时丢语义(clamp、默认值、错误码路径)。 + +## 7. 文档地图 + +- 本文件:当前状态与剩余工作(以此为准)。 +- `r5-phase3-final-guide.md`:终局计划(F 批次定义、验收细则)。 +- `c-abi-migration-handoff-v5.md`:K2.7 交接(undo 分组契约由来、 + DS 提交处置记录,背景参考)。 +- `facade-migration-roadmap.md`:批次记录(每批完成后补记)。 +- `c-abi-migration-handoff.md`(v3):§6.4 豁免清单格式。 diff --git a/docs/zh/c-abi-migration-handoff.md b/docs/zh/c-abi-migration-handoff.md new file mode 100644 index 000000000..3243e4093 --- /dev/null +++ b/docs/zh/c-abi-migration-handoff.md @@ -0,0 +1,334 @@ +# liboakengine 纯 C ABI 迁移 — 交接执行计划(v3) + +> 本文档是后续执行者(DeepSeek Flash 或任何接手代理)的**唯一权威执行依据**。 +> 所有架构决策、边界契约、禁止事项已在本文钉死,执行时不得另行发明新方案; +> 遇到本文未覆盖的决策点,按"§8 决策兜底原则"处理,不得自由发挥。 +> +> 相关文档:`docs/zh/facade-migration-roadmap.md`(各批次完成记录 + 附 D 事件机制 SOP)。 +> +> v3(2026-07-23):K2.7 对 DeepSeek Flash 的产出做了验收,修复了 6 个真实缺陷 +> (详见 §2.2,每条都附教训——**这些错误模式不得再犯**)。当前符号 39、 +> 仅剩 1 个已知测试失败。剩余工作按符号逐项钉死在 §3/§5。 + +--- + +## 1. 目标与验收标准 + +**终态**: + +1. `liboakengine.so` 的动态符号表中**没有任何 `olive::` C++ 符号**(只有 `oakengine_*` C 符号 + Qt/系统符号)。 +2. `oak-editor`、`oak-render-worker` 两个可执行文件**不 import 任何 `olive::` C++ 符号**(豁免清单见 §6.4)。 +3. 全量测试通过;`engine/include/oakengine/*.h` 中**每个** `OAKENGINE_API` 声明的函数都有测试覆盖。 +4. worker 端到端 harness 已完成,不要重做。 + +**统一度量命令**(禁止换口径): + +```bash +# 总指标(当前 39,目标 = 豁免清单项数 = 6,见 §6.4) +nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive" +# 逐符号清单 +nm -D cmake-build-debug/app/oak-editor | grep " U _ZN5olive" | c++filt | sed 's/.* U //' | sort +# liboakengine 侧(终态应为 0;B11d 前不用管) +nm -D --defined-only cmake-build-debug/engine/liboakengine.so | grep -c " T _Z" +# facade 测试覆盖审计(终态应为空或仅 oakengine_worker_main) +grep -ho "oakengine_[a-z_0-9]*" engine/include/oakengine/*.h | sort -u > /tmp/decl.txt +cat engine/tests/oakengine_*_test.cpp | grep -ho "oakengine_[a-z_0-9]*" | sort -u > /tmp/tested.txt +comm -23 /tmp/decl.txt /tmp/tested.txt +``` + +**构建与测试**(所有批次完成后必须全绿): + +```bash +cmake --build cmake-build-debug -j$(nproc) # 构建目录已配置好,不要重新 cmake +cd cmake-build-debug && ctest --output-on-failure -j$(nproc) # 当前基线 44 个测试,约 90-140s +``` + +已知 flaky(历史偶发、重跑即过):`oak_cli_transcode`、`oakengine_export_test`、`olive-gtest` 各观察到过一次偶发 SEGFAULT。遇到先单独重跑;**连续两次失败才算真失败**。 + +--- + +## 2. 当前状态(v3 交接快照) + +- 总符号数:**557 → 39**。 +- **接手第一件事:全量构建 + 全量 ctest**,确认基线后再继续(§2.3 有当前已知的精确状态,但一切以你实测为准)。 +- 未提交改动很多(所有批次都在工作区,未 commit)。**严禁任何 git 写操作**,严禁回滚任何现有未提交改动。 +- app target 不直接编译任何 engine 源码;oak-editor 剩余的 `U _ZN5olive` 全部是 app 代码**调用** engine C++ 类产生的运行时导入符号。 + +### 2.1 当前符号清单(39,nm 实测,逐项归属在 §3) + +``` + 4 AudioProcessor(豁免,§6.4) + 1 AudioWaveformCache::staticMetaObject + 1 Block::staticMetaObject(豁免,§6.4) + 1 ColorManager::staticMetaObject + 3 DynamicRenderer(ctor / init_with_open_gl_context / load) + 1 Folder::staticMetaObject + 5 Frame(ctor / dtor / create / allocate / set_video_params) + 1 NodeFactory::library + 5 Node(link / unlink / set_label / set_standard_value / staticMetaObject) + 2 OpenGLRenderer(ctor / init) + 2 Project(name_changed / staticMetaObject) + 3 Renderer(create_texture / blit_color_managed / destroy) + 2 RenderManager(instance_ / backend_to_string) + 1 SubtitleBlock::k_text_in + 2 Texture(upload / download) + 1 TrackListRippleToolCommand ctor + 1 Track::staticMetaObject(豁免,§6.4) + 3 UndoCommand(ctor / redo_now / undo_now) +``` + +### 2.2 v3 验收已修复的缺陷(DS 产出中的真实 bug,均已修复并验证) + +> 这些是按"教训"写的:**每种错误模式都对应一条硬规则(§6.6),后续批次必须遵守。** + +1. **B10 app 侧重复定义导致进程退出时堆损坏("corrupted double-linked list")**。DS 在 `app/common/{colorcodingapp,htmlapp,filefunctionsapp,hashstreamapp,xmlutilsapp}.cpp` 里用**与 engine 完全相同的限定名**定义了 `ColorCoding::colors`、`Html::k_block_tags` 等符号。可执行文件与 liboakengine.so 双定义 → ELF 符号介入合并存储 → 静态对象被**双重构造、双重析构** → double-free。这是 `timeline-tests` 和 `olive-gtest` 退出即崩的根因。**修复**:`app/CMakeLists.txt` 对这 5 个文件加 `set_source_files_properties(... COMPILE_OPTIONS "-fvisibility=hidden")`(已做)。**教训见 §6.6-R1。** +2. **`app/common/nodeimpl.cpp` 是死代码**。DS 创建了它(重定义 `Node::link/unlink/copy_inputs`)但**从未注册进 `app/CMakeLists.txt`**,三个符号仍从 .so 导入。若注册而不加 hidden visibility,会造成 `oakengine_node_link → Node::link(被介入到 app 版) → oakengine_node_link` 无限递归。**教训见 §6.6-R2;处理方案钉死在 §3.4。** +3. **`SpeedDurationDialog::accept()` 的 undo 命令从未压栈**。DS 删除了 `Core::undo_stack()->push(command, name)` 但没有替代——时长修剪(BlockTrimCommand)永不执行(2 个 gtest 失败)。**修复**:末尾补 `oakengine_undo_push(command, name)`(注意:**2 个参数,全局栈,不传栈句柄**)。 +4. **`ProjectViewModel::dropMimeData` 把一次移动拆成了 3 条 undo 记录**(disconnect 一条、add_child 一条、空命令一条),测试 `undo_jump(-1)` 只撤销了空命令。**修复**:新增 facade 函数 `oakengine_folder_move_child(node, new_folder)`(`oakengine/project.h` + `src/capi/project.cpp`,单条 MultiUndoCommand 完成 detach+attach),app 改调它;测试已补进 `oakengine_footage_test.cpp::test_folder`。**教训见 §6.6-R3。** +5. **预览请求帧路径两个 linesize 错误**。`engine/src/capi/preview.cpp` 把 `frame->linesize_pixels()` 填进 POD(契约是**字节**);`viewer.cpp::display_frame_from_preview` 用 `linesize_pixels()` 当字节偏移做 memcpy。**修复**:POD 填 `linesize_bytes()`;memcpy 用 `linesize_bytes()`。**教训见 §6.6-R4。** +6. **重建 display Frame 时 VideoParams 字段缺失**。(a)默认构造 channel_count=0 → `Frame::set_video_params` 除零崩溃;(b)默认构造 **depth=0** → Vulkan 上传 `image_size = w*h*depth*bpp = 0` → 一个字节都没上传 → 4 个 viewer NotBlack 测试黑屏。**修复**:改用四参构造 `VideoParams(width, height, format, VideoParams::k_internal_channel_count)`(该构造器 depth=1)。**教训见 §6.6-R5。** + +另:`manageddisplay.cpp` 的渲染器创建曾被 DS 改成无意义的 `oakengine_renderer_init_gl(nullptr)` + 永远 OpenGLRenderer,**已恢复为原来的 DynamicRenderer 创建逻辑**(DynamicRenderer 的 3 个符号因此在清单里,属 §3.6 待办)。 + +### 2.3 当前测试状态(K2.7 实测) + +- `timeline-tests`:全过(修复 #1 后)。 +- `olive-gtest`:除 `Backends/ViewerRuntimeRewireTest.RewireToIndirectConnectionNotBlack/1`(Vulkan)外全过。这是**当前唯一已知失败**,线索与疑似根因见 §3.1。 +- 其余 42 个 ctest 在上一次全量运行中通过,但**经过本批修复后尚未做最终全量复跑——接手第一步就是全量 ctest**。 + +--- + +## 3. 剩余工作(按符号逐项,顺序即执行顺序) + +### 3.1 第零优先:修 `RewireToIndirectConnectionNotBlack/1` + +**现象**:viewer 正在显示 direct 链(footage→sequence),运行时插入 OpacityEffect(footage→opacity→sequence)后,画面变黑且 30s 超时内不再更新。同文件的 `RewireToDirectConnectionNotBlack`(拆节点)是过的。 + +**排查线索(已排除项,不要重复查)**:帧数据、纹理上传、色彩变换、VideoParams 全部已验证正常(§2.2-5/6 修复后)。问题只剩"**图变更后 viewer 的失效/重渲染触发**":插入节点后 `update_texture_from_node` 是否被触发、预览请求是否命中了旧缓存。 +- 入手点:`app/widget/viewer/viewer.cpp` 的 `ConnectNodeEvent` 订阅清单 vs HEAD 原版的 connect 清单——插入节点后 texture input 变化应触发 `viewer_texture_input_changed`(事件 108)→ `update_waveform_view_from_mode`/`update_texture_from_node`。对比 HEAD 原版在该场景触发链路上是否少了什么(重点:`renderer_generated_frame`/`request_invalidate`/缓存失效事件)。 +- `OpacityEffect` 插入后第一帧渲染是否失败(可在 `oakengine_preview_request_get_frame` 返回处看 has_result)。 +- 修复后:该用例 + 全量 ctest 全绿才准进入 §3.2。 + +### 3.2 静态/杂项小点(预计 6 个符号,先做这些快的) + +1. **`Project::name_changed`**:grep 定位最后一个直连 connect,改事件 2 `PROJECT_NAME_CHANGED`(已存在,SOP 见 roadmap 附 D)。 +2. **`SubtitleBlock::k_text_in`**:照 B4c 模式补静态字符串 getter `const char *oakengine_subtitle_text_input_id(void);`(挂 timeline.h),app 换调用。 +3. **`RenderManager::backend_to_string` + `RenderManager::instance_`**:补 v2 已钉死的契约—— + ```c + OAKENGINE_API int oakengine_render_manager_set_aggressive_garbage_collection(int enabled); + OAKENGINE_API int oakengine_render_manager_requested_backend(void); + OAKENGINE_API int oakengine_render_manager_backend_to_string(int backend, char *buf, int buf_size); + ``` + `instance_` 符号随最后一个 `RenderManager::instance()` 直连点消失(manageddisplay.cpp 的 `requested_backend()` 调用点)。 +4. **`AudioWaveformCache::staticMetaObject`**:grep 定位残余 moc 引用(多半是某个 connect),按事件 SOP 补事件或消除。 + +### 3.3 UndoCommand 3(ctor / redo_now / undo_now) + +来源:app 直接 `new` engine 命令类并进栈(grep `new .*Command` 于 app/widget/timelinewidget、app/widget/nodeview 等)。按 v2 §5.6.5 的既定方针:逐个换 facade undoable 原语,缺的按同族模式补。**不得**为这些发明新机制。 + +### 3.4 Node 5 + NodeFactory 1 + +- **`Node::link / Node::unlink / Node::copy_inputs`**(3):`app/common/nodeimpl.cpp` 死代码的两个处理方案,**钉死选方案 A**: + - **方案 A(选这个)**:删除 `app/common/nodeimpl.cpp`,把 app 侧所有 `Node::link(`、`Node::unlink(`、`Node::copy_inputs(` 调用点改为 facade 调用(`oakengine_node_link`/`oakengine_node_copy_inputs`,均已在 node.h 存在)。grep 定位调用点(预计 <10 处)。 + - 方案 B(不推荐):注册 nodeimpl.cpp 且对该文件加 `-fvisibility=hidden`。只有方案 A 遇到无法改写的调用点时才用,且必须写进 roadmap 说明。 +- **`Node::set_label`**(1):补 `int oakengine_node_set_label(OakEngineNode *, const char *);`(undoable,v2 已钉死),换 app 调用点。 +- **`Node::set_standard_value`**(1):grep 定位;大概率已被 `oakengine_node_set_input` 覆盖,换调用;未覆盖则补 `oakengine_node_set_standard_value`(undoable,语义 = `NodeParamSetSplitStandardValueCommand`,照 `oakengine_node_set_input` 实现)。 +- **`Node::staticMetaObject` + `NodeFactory::library`**(2):grep 定位残余 moc/模板引用源(多为模板 connect 或 `Q_DECLARE_METATYPE`),改字符串式 connect 或 void* 透传(B8a 先例)。`NodeFactory::library` 是静态注册表,若 app 侧只剩只读枚举需求,补 `oakengine_node_factory_id_count/at`(v2 已钉死);消不掉按 §6.4 格式进豁免清单并写理由。 + +### 3.5 staticMetaObject 残留(ColorManager / Folder / Project / Node) + +逐个 grep 定位 moc 引用源(`qobject_cast`、模板 connect、`Q_DECLARE_METATYPE`、moc 生成的 metacall)。改事件机制或字符串式 connect。消不掉的按 §6.4 格式进豁免清单(必须写理由)。 + +### 3.6 B11b GPU/帧路径(17 个符号:Renderer 3 + OpenGLRenderer 2 + DynamicRenderer 3 + Texture 2 + Frame 5 + RenderManager 中属显示路径的部分) + +**这是 DS 上次说"需要复杂 GPU 管线重构"而放弃的部分。决策已钉死,不需要重构,按薄封装做:** + +现状事实(已验证): +- 显示路径(`ManagedDisplayWidget`/`ViewerDisplayWidget`)持有 C++ `Renderer* attached_renderer_`(DynamicRenderer 或 OpenGLRenderer),用于 `create_texture/upload/download/blit_color_managed/destroy`;`Frame` 用于 CPU 帧搬运(`Frame::create/allocate/set_video_params/dtor/ctor`)。 +- `manageddisplay.cpp` 的 DynamicRenderer 创建块**已恢复为 C++ 原版**(不要再动它,直到整个显示路径换完)。 + +**执行方案(钉死,分两步)**: +1. **先 Frame(5)**:`viewer.cpp::display_frame_from_preview` 和 viewerdisplay 的帧搬运改用—— + ```c + typedef struct OakEngineFrame OakEngineFrame; /* owned */ + OAKENGINE_API OakEngineFrame *oakengine_frame_create(void); + OAKENGINE_API int oakengine_frame_set_video_params(OakEngineFrame *, const oak_video_params *); + OAKENGINE_API int oakengine_frame_allocate(OakEngineFrame *); + OAKENGINE_API void oakengine_frame_free(OakEngineFrame *); + ``` + app 侧不再直接 `new olive::Frame`。**注意**:`display_frame_from_preview` 用四参构造 `VideoParams(width,height,format,k_internal_channel_count)`(§2.2-6 的修复,别回退)。 +2. **再 Renderer/Texture(7+3)**:显示 widget 的 `attached_renderer_` 改为 facade 句柄。契约(v2 已钉死): + ```c + typedef struct OakEngineTexture OakEngineTexture; /* owned */ + OAKENGINE_API int oakengine_renderer_init_gl(void *qopengl_context); /* QOpenGLContext 以 void* 透传,文档注明 Qt 运行时共享例外;后端选择走 RenderManager::requested_backend 语义 */ + OAKENGINE_API int oakengine_renderer_destroy(void); + OAKENGINE_API OakEngineTexture *oakengine_renderer_create_texture(const oak_video_params *, const void *data, int linesize); + OAKENGINE_API int oakengine_texture_upload(OakEngineTexture *, const void *data, int linesize); + OAKENGINE_API int oakengine_texture_download(OakEngineTexture *, void *data, int linesize); + OAKENGINE_API void oakengine_texture_free(OakEngineTexture *); + OAKENGINE_API int oakengine_renderer_blit_color_managed(const oak_color_transform *, OakEngineTexture *, const oak_video_params *); + ``` + facade 内部持有 DynamicRenderer/OpenGLRenderer 实例(与现在 manageddisplay 的选择逻辑相同);backend-neutral 的离屏纹理 + 下载回读路径同样走 texture 句柄。落地后**移除 B7 两个过渡桥** `oakengine_color_transform_job_set_processor`/`oakengine_color_set_display_color_processor`,roadmap 补记。 + **验收**:5 个 Backends viewer 用例继续全过(这是该路径的现成回归测试)。 + +### 3.7 TrackListRippleToolCommand ctor(1) + +v2 已钉死为遗留评估点。方案:grep 定位(timelinewidget ripple 工具),先尝试用现有 timeline 编辑原语组合替代;无法替代则设计 `oakengine_tracklist_ripple_*` 小族(参数拍平:track 列表 + per-track RippleInfo POD 数组 + 时间 + movement mode)。**这是最后一个符号,允许单独花时间;消不掉按 §6.4 进豁免清单(写理由)。** + +### 3.8 B11d 收口(最后做) + +1. 确认 oak-editor `U _ZN5olive` 只剩豁免清单 6 项;oak-render-worker 为 0。 +2. liboakengine 符号可见性收口:`set_target_properties(oakengine PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON)` 或 version script 白名单 `oakengine_*`。验证 `nm -D --defined-only liboakengine.so | grep -c " T _Z"` → 0。liboakcore 复查不回归。 +3. facade 覆盖审计为空(`oakengine_worker_main` 豁免)。 +4. 终验:全量构建 + ctest 全绿;§1 四条验收逐条核对;roadmap 附 C 标记战役完成。 + +--- + +## 4. 已完成批次(不要重做) + +详见 roadmap 附 C。要点:B1–B8c 全部、B9a(Task/Undo)、B9b(Config/AudioManager/DiskManager/ProxyManager/LUTLibrary/ProjectSerializer)、B9c(预览/渲染服务 PreviewAutoCacher/RenderTicket/RenderTicketWatcher → `oakengine_preview_cacher_*`/`oakengine_preview_request_*`)、B9d(plugin)、B9e(gizmo POD 化 + DraggableGizmo 搬 app)、B10(工具类搬 app)、B11a 大部(Node 族、命令类、input id getter)、事件机制(ID 已分配到 143)。 + +**事件 ID 分配**:已用到 143(141/142 playback cache、143 frame cache)。**新事件从 144 起**。 + +--- + +## 5. 每批的标准产出(SOP) + +1. `nm` 度量基线 → 2. grep 确认实际使用点(§3 清单仅供参考,以 grep 为准)→ 3. facade 补 C 函数(§6 契约)→ 4. app 逐文件换调用 → 5. 每个新 C 函数补单元测试(注册进 `engine/CMakeLists.txt` 的 `make_oakengine_test`)→ 6. 全量构建 + ctest 全绿 → 7. 族符号和总数双度量对比 → 8. roadmap 附 C 补记。 + +--- + +## 6. 边界契约与硬规则(钉死,不得违反) + +### 6.1 C ABI 头文件规则 +- 位置 `engine/include/oakengine/*.h`,实现在 `engine/src/capi/*.cpp`(注册进 `engine/src/capi/CMakeLists.txt`)。 +- 每个头:GPL 版权头、`#ifdef __cplusplus extern "C"`、`OAKENGINE_API` 导出宏。 +- **头文件里只允许 C 类型**:`int/int64_t/double/char*/void*`、POD struct、不透明句柄 typedef。禁止 C++ 类、模板、Qt 类型、std:: 类型、引用、默认参数、重载。 +- 命名:`oakengine_<族>_<动作>`;错误码 `OAKENGINE_OK`(0)/ 负数 `OAKENGINE_E_*`。 +- 字符串输出 buf/size 约定(返回所需长度不含 \0,`buf=NULL,buf_size=0` 查长度)。 +- 线程语义:回调/事件 = Qt::DirectConnection 等价同步调用;回调内不得反调改同一对象的编辑原语。 +- 所有权:create 返回 owned 句柄必须配套 free;borrowed 句柄在注释里写明。 + +### 6.2 undoable 编辑原语 +- 所有改图操作必须 undoable,实现照 `engine/src/capi/node.cpp` 的 `push_or_run` 模式。 +- **undo 粒度妥协是允许的**(facade 单命令边界导致一次用户操作产生多条 undo 记录),代码加注释说明即可。**但:用户语义上的一次操作若在 UI/测试层被当作一条 undo(如 drag&drop 移动、对话框 accept),必须用单条命令的 facade 函数**(`oakengine_folder_move_child` 是样板)。 + +### 6.3 事件机制(信号迁移唯一通道) +- 禁止 app 直接 `QObject::connect` engine 对象的信号。一律 `oakengine_event_subscribe` → `app/engineeventbridge` → app 连 bridge。SOP 见 roadmap 附 D。 +- 新事件:events.h 加宏(**从 144 起**)、events.cpp `connect_event` 加 case、bridge 加信号 + dispatch、`oakengine_events_test.cpp` 补实测。 +- **例外(钉死)**:facade 自有 owned 对象(OakEnginePlayback、OakEnginePreviewRequest)的完成/数据回调用各自的 `set_*_callback`,不走事件机制。 + +### 6.4 豁免清单(R6 后已清空:无豁免,nm=0) + +> **状态(R6 收尾)**:原"终态保留"裁决已被 R6 计划推翻并全部消除—— +> `AudioProcessor`(5 符号:ctor/dtor/open/close/convert)经 P5 改为 C vtable +> 接口(`oakengine/audio.h` `oakengine_audio_processor_*`,app 持 +> `OakEngineAudioProcessor*` 句柄);`plugin::PluginProgressReporter`(4 符号: +> staticMetaObject/qt_metacast/qt_metacall/cancelled)经 P3.2 去 Q_OBJECT、 +> cancelled 信号改 C 回调。`oakengine_worker_main` 为 worker 进程入口 +> (非 `olive::` 符号,不计入 nm 指标)。 +> +> 实测:`nm -D` 于 oak-editor 与 oak-render-worker 的 ` U _ZN5olive` 均为 **0**。 +> **当前无任何豁免。** + +以下为 R5 冲刺时记录的 58 符号历史分类(**R6 已全部清零,仅作存档**): + +**GLM-5.2 R5 冲刺豁免清单(58 符号,分类理由)——✅ R6 已全部清零(nm 58→0)**: + +**A. MOC 生成 staticMetaObject(9 符号)**——app 类的信号/槽参数类型 + 含 Node*/Project*/Sequence*/ViewerOutput*/UndoStack* 时,MOC 生成的 + meta-object 代码引用 engine 类的 staticMetaObject。消除需更改所有 + 此类信号/槽签名为 C ABI 句柄类型(OakEngineNode* 等),工程量大。 + - `Node::staticMetaObject`、`Project::staticMetaObject`、 + `Sequence::staticMetaObject`、`ViewerOutput::staticMetaObject`、 + `UndoStack::staticMetaObject`、`AudioWaveformCache::staticMetaObject` + - `plugin::PluginProgressReporter::staticMetaObject/qt_metacast/qt_metacall` + +**B. Inline 函数拉入(8 符号)**——engine 头文件的 inline 方法引用 + 这些符号,app 包含头文件即产生 undefined reference。消除需创建 + app 侧 handle 头(不包含 engine C++ 头)或扩 facade 覆盖所有 inline 路径。 + - `Node::link`(NodeLinkCommand 析构 inline 调用) + - `Node::set_standard_value`(inline getter 引用) + - `Node::set_value_at_time`(1 处直接调用,QVariant→POD 转换复杂) + - `UndoCommand::redo_now/undo_now/UndoCommand()`(MultiUndoCommand + inline add_child/析构调用) + - `MultiCamNode::k_current_input`、`SubtitleBlock::k_text_in` + (inline 方法引用静态字符串) + +**C. 实时回调边界(5 符号)**——v3 已预批。 + - `AudioProcessor`:ctor/dtor/open/close/convert + +**D. 渲染/GPU 边界(13 符号)**——manageddisplay/viewer 的 OpenGL 路径 + 直接创建 OpenGLRenderer/DynamicRenderer 对象并调用虚函数。 + facade 有 oakengine_renderer_* 但 app 仍用 C++ 对象。 + 消除需将整个渲染对象管理移入 engine。 + - `Renderer`:destroy/create_texture/blit_color_managed + - `DynamicRenderer`:ctor/init_with_open_gl_context/load + - `OpenGLRenderer`:ctor/init + - `Texture`:upload/download + - `Frame`:allocate/create/set_video_params + +**E. 色彩管理(6 符号)**——ManagedColor/ColorProcessor 的 C++ 对象 + 在多个 UI 组件中使用。无 C ABI 等价物。 + - `ManagedColor`:ctor×2/set_color_input/set_color_output + - `ColorProcessor`:create/convert_color + +**F. 无 C ABI 等价物(17 符号)**——需新增 facade 函数。 + - `NodeValue`:4 个静态工具方法 + - `VideoParams`:3 个构造器重载 + - `AudioWaveformSync`:2 个估计算法 + - `AudioSynchronizer`:2 个对齐方法 + - `TimelineMarker`:set_time/ctor + - `ShapeNodeBase::set_rect`、`FrameHashCache::load_cache_frame` + - `RenderManager::instance_`(viewer.cpp inline instance() 引用) + - `plugin::PluginProgressReporter::cancelled`(信号,需事件迁移) + +### 6.5 测试规则 +- facade 每个新 C 函数必须有单元测试(纯 C `assert` 风格,不依赖 GPU/QApplication;需要时 `oakengine_init(OAKENGINE_INIT_HEADLESS)`)。 +- GL/Vulkan 测试必须可无 GPU 跳过(GTEST_SKIP 模式)。 +- 新测试注册进 `engine/CMakeLists.txt` 的 `make_oakengine_test(...)`。 + +### 6.6 v3 新增硬规则(对应 §2.2 的六条教训) + +- **R1(ODR/符号介入)**:app 侧**严禁**用与 engine 相同的限定名定义任何非 inline 符号(函数或静态数据)。确需同名本地副本(B10 模式),必须对该源文件加 `-fvisibility=hidden`(`app/CMakeLists.txt` 的 `set_source_files_properties` 是现成样板)。`#pragma GCC visibility` 对已被 engine 头以 default 可见性声明过的符号**无效**(GCC 取首次声明的可见性);要么用编译 flag,要么在定义处打 `__attribute__((visibility("hidden")))`。验证方法:`readelf -sW | c++filt | grep <符号>` 必须是 `HIDDEN`。 +- **R2(注册检查)**:新建任何 .cpp 必须同步注册进对应 CMakeLists,并在当批验证其符号确实从 `U` 清单消失。app 侧定义的 engine 同名函数若不加 hidden,会通过 ELF 介入把 engine .so 内部调用劫持到 app 版,可能形成跨模块无限递归。 +- **R3(undo 语义)**:`oakengine_undo_push(command, name)` **只有两个参数**(全局栈,不传栈句柄)。`Core::instance()->undo_stack()` 返回 `void*`,仅作事件订阅 handle 用。删除任何 `push` 调用时必须同步删除/替换其命令的执行路径——**命令不压栈 = 静默不执行 + 内存泄漏**。 +- **R4(linesize 约定)**:facade POD 中的 `linesize` 一律是**字节**。`olive::Frame` 有两个值:`linesize_bytes()`(字节)和 `linesize_pixels()`(像素 = 字节/bpp)。跨边界只传字节;engine 内部(纹理上传等)按各 API 既有约定(Vulkan/OpenGL 纹理上传收**像素**)。 +- **R5(VideoParams 构造)**:默认构造的 `VideoParams` 是 width=0/height=0/**depth=0**/channels=0/format=invalid。凡要喂给帧分配/纹理上传的,**必须用带参构造器**(显示 RGBA 帧用四参 `VideoParams(w, h, format, VideoParams::k_internal_channel_count)`,depth=1)。depth=0 不会报错,只会让上传字节数为 0(纯黑)。 +- **R6(接手验证)**:任何中断/交接后,第一件事是全量构建 + 全量 ctest + 对照 §3 清单 grep 复核,不要采信上一手的完成声明(包括本文 §2.3——以你实测为准)。 + +--- + +## 7. 禁止事项(硬约束) + +1. **禁止任何 git 写操作**:commit / add / push / restore / checkout / stash / clean / rebase。 +2. **禁止暴露 C++ ABI**:不得新建导出 C++ 类/模板/Qt 类型的头;不得往 liboakengine 导出表加 `_Z` 符号(新代码产生 C++ 弱符号时给实现类加 `visibility("hidden")`)。 +3. **禁止把 Qt 类型放进 core/**(liboakcore 是 Qt-free)。 +4. **禁止改 worker 的 NDJSON IPC 协议**;禁止重做 §4 已完成的任何批次。 +5. **禁止修改本文已钉死的签名**:各 facade 头现有函数、events.h 已分配的事件 ID(1–143)、§3 各批钉死的契约签名。 +6. **禁止为追求 undo 记录合并、staticMetaObject 消除等发明新机制**——按 §6.2/§6.4 妥协条款执行。 +7. **禁止降低测试标准**:新 C 函数无测试不得算完成;全量 ctest 不绿不得进入下一批。 +8. **禁止重新 cmake 配置构建目录**;禁止安装/卸载系统依赖;禁止改 CI/打包文件。 +9. **禁止在未验证构建状态前继续批次**(§6.6-R6)。 + +--- + +## 8. 决策兜底原则 + +1. roadmap 已有裁决的,从 roadmap。 +2. 能搬进 app 的纯 UI/工具代码 → 搬 app(优于 facade 化)。 +3. 纯数据类 → POD 化或头内联,优先于新增 facade 族。 +4. 必须跨边界的 → 最小 facade 族(只包 app 实际用到的成员)。 +5. GPU/帧路径、Qt 运行时耦合 → 薄封装 + `void*` 透传 + 文档注明例外。 +6. 以上都拿不准的:留下不动,写进 roadmap 遗留清单,继续下一点。**不允许为单点发明新架构。** + +--- + +## 9. 环境备忘 + +- 构建目录 `cmake-build-debug`(Ninja + Qt6,Debug);另有 cmake-build-asan / cmake-build-coverage,**不要用**。 +- 单文件增量验证:`rm -f cmake-build-debug/app/CMakeFiles/libolive-editor.dir/<相对路径>.o && cmake --build cmake-build-debug --target olive-editor -j$(nproc)`。 +- 测试素材:`tests/demo.mp4`、`tests/img.png`、`tests/project_with_footage.ove`。 +- 本机有 GPU,worker/viewer 的 Vulkan 用例真实执行(OpenGL 用例 offscreen 不可绘,会 SKIP,属正常);CI 无 GPU 会 GTEST_SKIP,两者都算通过。 +- 全量 ctest 44 个约 90-140s(olive-gtest 占 ~85s),每批必须跑完不能裁剪。 +- 调试技巧(本批实测有效):teardown 堆崩溃用 `GLIBC_TUNABLES=glibc.malloc.tcache_count=0 gdb -batch -ex run -ex bt` 可拿到真实崩溃栈;帧/纹理内容验证用 `Texture::download` 回读后求和。 diff --git a/docs/zh/facade-migration-roadmap.md b/docs/zh/facade-migration-roadmap.md index 35143e37f..18419cf2d 100644 --- a/docs/zh/facade-migration-roadmap.md +++ b/docs/zh/facade-migration-roadmap.md @@ -93,11 +93,21 @@ facade 现状覆盖:项目/序列读写、素材探测与导入、时间线查 按符号聚类的消减顺序(每步保持全绿+耦合计数下降): 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)**:应用核心外观族(oakengine_app_*):项目生命周期(create/open/save/recent/autorecovery)、剪贴板、status bar、color picker、handler 注册;信号→回调。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 面板、曲线编辑器对节点类的直接引用,按控件逐个切。 -4. **参数/色彩/导出(~50)**:VideoParams 19 / ColorManager 15 / EncodingParams 9 / ExportFormat 7——导出编解码控件、色彩管理菜单、scopes。 +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_multicam`、`oakengine_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 化或豁免。 +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_passthrough`、**marker 句柄族**(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_handle` 或 `oakengine_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 注册必然引用,需把 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_output` 收 `OakAudioParams*` + 原始字节 + 错误 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.h` 的 `generate_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 内联消解,不进动态符号表)。 @@ -113,6 +123,80 @@ facade 现状覆盖:项目/序列读写、素材探测与导入、时间线查 - **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_cast` 从 `QObject*` 校验)。 +- `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*`(原 `sender()` 语义由事件的 `source` 字段承担)。 + +### 后续批次迁移连接点的标准操作步骤 + +1. 确认目标信号已在事件 ID 表内;不在则:在 `events.h` 加宏(新 id)、`events.cpp` 的 `connect_event` 加 case(dynamic_cast 校验 + DirectConnection + POD 载荷)、`EngineEventBridge` 加对应信号和 `dispatch` case、在 `oakengine_events_test` 补一条实测。 +2. app 侧:在原来 `connect(engineObj, &EngineClass::sig, ...)` 处改为 `bridge->subscribe(reinterpret_cast(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),其余按面板需要逐个加。 @@ -136,3 +220,96 @@ comm -12 <(nm -D --defined-only engine/liboakengine.so | awk '{print $3}' | sort - [ ] oak-editor/oak-render-worker `ldd` 正常,全部启动 - [ ] 1986+ gtest 全绿,CLI ctest 全绿,5+ C ABI 测试全绿 - [ ] 三平台打包含 liboakengine(liboakengine.so/dylib/oakengine.dll),Linux 位于标准 libdir + +## 附 C:R5 批次记录 + +### F3(Task/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`/`FacadeProxyTask`(engine 自有等价物); + `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`。 + +### F4(Node 方法调用)— 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 error;ctest 43/44(flaky 不计)。 +- 反作弊:app 无 dlfcn;engine 改动仅 `engine/include/oakengine/` + `engine/src/capi/`。 + +### G1:Node 信号清零(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:渲染族信号 + RenderManager(66→58,-8) + +- PlaybackCache invalidated/validated 迁事件订阅(timeruler.cpp) +- Sequence::subtitles_changed 迁事件订阅(viewerdisplay.cpp) +- RenderManager::backend_to_string + instance() 换 C ABI(manageddisplay.cpp) + +### G3:UndoCommand C ABI(58 不变) + +- 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 打地基。 + +- **P1(F 类 facade 补齐,17)**:NodeValue 静态方法、VideoParams 构造器、 + 音频对齐算法、TimelineMarker/ShapeNodeBase/FrameHashCache/RenderManager/ + MultiCamNode/SubtitleBlock 零散单点,全部新增 C facade 替换。 +- **P2(B 类 inline 清零,8)**:app 中 113 处 `new XxxCommand(`(16 个命令类) + 替换为 facade 构造;`Node::link`/`set_value_at_time` 换 C ABI。 +- **P3(A 类 MOC staticMetaObject,9+1)**:app 信号/槽参数类型由 engine C++ 类 + 改 C ABI 句柄(OakEngineNode* 等);plugin::PluginProgressReporter 去 Q_OBJECT + 改 C 回调(推翻原"终态保留"裁决)。 +- **P4(E 类色彩管理,6)**:ManagedColor 整体迁出 engine 至 app + (colorprocessorhandle.h,纯 UI 值类型);ColorProcessor create/convert_color + 换 C ABI。nm 24→18。 +- **P5(C 类音频回调,5)**:AudioProcessor 改 C vtable 接口 + (`oakengine_audio_processor_*`,推翻原"终态保留"裁决)。nm 18→13。 +- **P6(D 类渲染/GPU,13)**:新增 `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` = **0**(oak-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 豁免清单已清空为"无豁免"。 diff --git a/docs/zh/plans/README.md b/docs/zh/plans/README.md new file mode 100644 index 000000000..2eba46592 --- /dev/null +++ b/docs/zh/plans/README.md @@ -0,0 +1,27 @@ +# 长期计划(plans) + +本目录收纳 Oak 的**中长期规划文档与并行执行计划**。当前正在进行的 +**C ABI 迁移战役**的文档在上一层(`docs/zh/`),见下方"当前进行中"。 + +## 目录 + +| 文档 | 内容 | 启动前提 | +|---|---|---| +| [`riir.md`](riir.md) | **RIIR 绞杀者模式执行计划**:C ABI 迁移完成后,把 liboakengine 安全拆成若干小模块,再逐个用 Rust 重写;含 API 冻结保证、模块图、六步流程与验证门禁 | C ABI 迁移战役验收完成 | +| [`ai-agent-design.md`](ai-agent-design.md) | **AI Agent 设计**:多模态 LLM 经 MCP 调用策展工具面自动剪辑,渲染帧回喂形成"编辑→看图→再编辑"视觉闭环;含工具面、回放回路、安全与测试 | RIIR 拆分完成(面对一堆小库) | +| [`gtest-migration-guide.md`](gtest-migration-guide.md) | **测试统一到 Google Test**:把 OAK_ADD_TEST 宏框架、纯 C assert、已有 gtest 三套收敛为单一 Google Test,ctest 仅作运行器 | R5 验收完成后启动(可与 UI 改版并行) | +| [`ui-redesign-plan.md`](ui-redesign-plan.md) | **主界面 UI 改版**:依据 `design/` 三张设计图落地 10 个工作包(工具条、双监看、效果栈检查器、节点编辑器移位、电平条、状态栏等),全部文字精确定义 | R5 验收完成后启动(可与 GTest 迁移并行) | + +## 当前进行中(不在本目录) + +C ABI 迁移战役的执行文档在 `docs/zh/`: + +- `c-abi-migration-handoff.md`(v3 交接)、`c-abi-migration-handoff-v4.md`(v4 重做计划) +- `facade-migration-roadmap.md`(批次记录) +- `r5-app-migration-guide.md`、`r5-phase2-detailed-guide.md`(R5 app 侧迁移指引) + +## 其他参考 + +- 构建:`docs/zh/build.md`、`docs/zh/build_macos-zh.md` +- 工程文件:`docs/zh/project-file-reference.md` +- 代码风格与 Google Test 要求:`CONTRIBUTING.md`(仓库根) diff --git a/docs/zh/plans/ai-agent-design.md b/docs/zh/plans/ai-agent-design.md new file mode 100644 index 000000000..eaa4cafdb --- /dev/null +++ b/docs/zh/plans/ai-agent-design.md @@ -0,0 +1,141 @@ +# AI Agent 设计文档(RIIR 拆分后长期规划) + +> 本文是 Oak 引入 AI 能力的长期设计,**执行前提是 RIIR 绞杀者拆分完成** +> (见 [`riir.md`](riir.md))。彼时 `liboakengine.so` 已不存在,取而代之的是 +> 一组以纯 C ABI 为缝的小动态库。本文面向没有当前对话记忆的执行者,自包含。 +> +> **一句话**:把多模态 LLM 当成引擎 C ABI 的**第三个一等消费者** +> (继 oak-cli、oak-render-worker 之后),用 MCP 暴露策展过的工具面, +> 用渲染管线把帧喂回给多模态模型,形成"编辑 → 看图 → 再编辑"的视觉闭环。 + +--- + +## 1. 定位与前提 + +### 1.1 前提(未满足不动工) + +- RIIR 拆分战役完成:引擎已拆为 §2.1 的小库;各库导出仅 C 符号; + `oakengine_*` facade 壳稳定且全量测试绿。 +- Google Test 已是唯一测试框架;`gtest_discover_tests` 已接入。 +- 本文不改动 RIIR 既定的模块划分,只在模块化树上**新增叶子**。 + +### 1.2 设计铁律(继承自迁移/拆分战役) + +1. AI Agent **只经 C ABI** 访问引擎,一行 engine C++ 都不碰;不污染符号边界。 +2. Agent 的一切编辑动作**必须可撤销**(undoable 原语),UI 默认"确认后执行"。 +3. 不为 AI 发明新的引擎内部机制;工具面是现有 facade/小库的组合。 +4. 引擎各模块**不得新增 Qt 依赖、不得新增 QObject 信号/moc 类**。 + +## 2. 总体架构 + +### 2.1 在模块化树上的位置 + +``` +app / oak-cli / oak-render-worker / oak-agent / oak-mcp-server + │ + liboakengine-facade(壳:capi + 事件 + init) + │ + ┌────────┬────────┼─────────┬──────────┐ + oaktask oakrender oakplugin oakaudio oakserialize + │ │ │ │ │ + └────────┴────┬───┴──────────┴──────────┘ + │ + oakmodel(节点图 + 项目模型 + 时间线模型) + │ + ┌────────┼─────────┐ + oakcodec liboakcore oakbackend(GPU 插件) + │ + ffmpeg_bridge +``` + +**新增三个叶子组件**(与 oak-cli、oak-render-worker 平级,都是纯消费者): + +- **`oak-mcp-server`**:把策展过的工具面经 MCP 暴露给任何 LLM 客户端。 +- **`oak-agent`**:无头 Agent 运行时(对话编排 + 视觉闭环),供脚本/CI/本地使用。 +- **editor AI 面板**:app 内的聊天/操作日志/确认界面,与 `oak-agent` 复用同一工具面。 + +AI 功能**不进入** oakmodel、oakrender 等引擎模块,引擎核心对 AI 无感知。 + +### 2.2 视觉闭环(本设计的核心) + +``` +多模态 LLM ──► oak-agent ──► facade/小库执行编辑 ──► 渲染取帧 ──► PNG ──► 回喂 LLM + ▲ │ + └──────────────── 看图判断(效果/切点/内容定位) ◄───────────────┘ +``` + +- **验证式**:每次编辑后取一帧,LLM 判断"效果对不对"。 +- **内容感知式**:沿时间线批量取缩略图拼 contact sheet,LLM 扫图定位 + ("人何时进画面""哪里该切"),Agent 据此下刀——自动粗剪/打点的雏形。 +- **连续回放**:经 playback 族起范围播放,按间隔采样帧。 + +## 3. 工具面(策展,非全量 facade) + +**不暴露全部 ~200+ facade 函数**,而是策展约 25 个高层工具,每个是 facade/小库 +的组合。`oak-mcp-server` 内部就是一个薄模块,链接 facade 壳与相关小库。 + +| 工具 | 落到哪个库 | 说明 | +|---|---|---| +| `create_project` / `open_project` / `save_project` | facade 壳 + oakmodel | 工程生命周期 | +| `probe_media` / `import_footage` / `get_media_info` | oakcodec + facade 壳 | 媒体探测与导入 | +| `add_track` / `add_clip` / `trim_clip` / `ripple` / `add_transition` / `add_marker` | oakmodel(经 facade 时间线族) | 时间线编辑 | +| `add_effect(effect_id)` / `set_param` / `set_keyframe` | oakmodel(经 facade node 族) | 节点与关键帧 | +| `apply_lut` / `set_color_transform` | oakmodel + facade color 族 | 调色 | +| `get_frame(time)` / `get_thumbnails(range,n)` / `get_audio_levels` | oakrender + oakbackend | **视觉闭环的取帧口** | +| `export_render(params)` | oaktask + oakcodec | 导出 | + +**取帧→PNG 通路**(视觉闭环关键路径): +`get_frame` 经 oakrender 的预览请求得到 RGBA 帧(POD:`宽/高/字节流`), +再经 oakcodec 的 OIIO 编码器出 PNG,base64 后作为图片消息发给 LLM。 +缩略图用同一路径降采样,多张拼 contact sheet。 + +## 4. 协议:MCP(Model Context Protocol) + +工具协议**定为 MCP**,理由: + +- render-worker 已在用 **NDJSON over stdin/stdout 的 IPC**——MCP 本质是该模式 + 的标准化,实现路径一致。 +- 暴露成 `oak-mcp-server` 后,**外部 LLM 客户端(Claude Desktop、各类 agent + 框架)可直接连接复用**,无需自研对话编排。 +- `oak-agent` 与 editor AI 面板都连同一个 MCP server,**一份工具面,多处消费**。 + +## 5. 模型层 + +抽象 `LLMProvider` 接口(输入:消息 + 图片;输出:文本 + tool_calls),两个后端: + +- **云端**:Claude / GPT 多模态(效果优先)。 +- **本地**:llama.cpp 跑 Qwen-VL / LLaVA 类多模态模型(隐私、离线优先)。 + +API key 只走环境变量,**绝不写入 config / 工程文件**。无 key 时优雅降级为 +"仅本地工具"(仍可用 MCP,但不做对话编排)。 + +## 6. 安全 + +- **可撤销**:所有编辑走 undoable 原语;AI 面板提供"撤销整段会话"。 +- **确认模式**:默认每次 Agent 动作需用户确认才 apply;可切换自动模式。 +- **沙箱会话**:Agent 默认在临时工程中操作,用户接受后才落盘到真实工程。 +- **资源**:取帧/扫描限帧率与分辨率上限,防止批量取帧拖垮渲染进程。 + +## 7. 可测试(与项目风格一致) + +1. **Mock LLM server**:录制/回放 tool_call 序列与固定回复,让 Agent loop 在 + CI 无 key 无网络跑通(Google Test)。 +2. **黄金帧校验**:复用 render-worker 端到端 harness(真实渲染 ≥2 帧 + + 像素非全黑 + 一致性断言),验证"Agent 的编辑确实改变了画面"。 +3. **会话回放**:tool_call + 帧哈希落盘日志,可回放复现、可作测试夹具。 + +## 8. 里程碑(RIIR 完成后启动) + +1. **M1 工具面**:`oak-mcp-server`(facade → MCP,~25 工具)+ 取帧→PNG 通路。 +2. **M2 无头闭环**:Mock LLM + `oak-agent`,跑通"LLM→工具→取帧→回喂", + CI 可测(无网络)。 +3. **M3 AI 面板**:editor 内聊天 + 操作日志 + 确认模式 + 撤销会话。 +4. **M4 本地模型与内容感知**:llama.cpp provider、时间线扫描打点、自动粗剪。 + +## 9. 风险与边界(明确不做) + +- **不**把 LLM/推理放进 oakmodel 或任何引擎模块(引擎对 AI 无感知)。 +- **不**为 AI 绕过 C ABI 直接调 engine C++(边界不污染)。 +- **不**把 API key 落盘到工程/config。 +- **不**让取帧回路阻塞 GUI 线程(取帧走渲染/后台路径,UI marshal 回主线程)。 +- 第三方大模型客户端的接入细节(OAuth、计费、配额)**超出本文范围**,按需另立文档。 diff --git a/docs/zh/plans/gtest-migration-guide.md b/docs/zh/plans/gtest-migration-guide.md new file mode 100644 index 000000000..8c834d1cb --- /dev/null +++ b/docs/zh/plans/gtest-migration-guide.md @@ -0,0 +1,154 @@ +# 测试统一到 Google Test — 迁移指引 + +> 本文指导把仓库里并存的三套测试框架统一收敛到 **Google Test**。 +> 面向执行者(DeepSeek Flash 或任何接手代理),自包含,可直接照做。 +> 工作分支:`c-abi-migration`。**启动前提:R5(C ABI app 侧迁移)验收完 +> 成之后**(R5 期间测试是唯一的回归防线,不在迁移途中换测试框架); +> 启动后可与 UI 改版计划并行(文件域不相交)。每迁完一个测试二进制 +> 立即提交,每步全量 ctest 绿才进下一步。 +> +> **ctest 的定位**:统一后 ctest 仍然是唯一的测试**运行入口** +> (`ctest --output-on-failure`),Google Test 是唯一的测试**编写框架**。 +> 两者不冲突——用 `gtest_discover_tests()` 让 ctest 按用例粒度发现 gtest 用例。 + +--- + +## 1. 现状:三套框架并存 + +| 框架 | 位置 | 编写方式 | 构建/注册 | +|---|---|---|---| +| **Google Test(目标形态)** | `tests/gtest/*.cpp` | `TEST()/TEST_F()/TEST_P()`,单一 `olive-gtest` 二进制 | `tests/gtest/CMakeLists.txt`,共享 `main.cpp`(QApplication + offscreen + OCIO) | +| 自研 OAK 宏框架 | `tests/timeline/timeline-tests.cpp`、`tests/compositing/compositing-tests.cpp` | `OAK_ADD_TEST(name)` + `OAK_ASSERT(x)` | `tests/CMakeLists.txt` 的 `olive_add_test()` 宏,正则扫宏生成 `main()` | +| 纯 C assert | `engine/tests/oakengine_*_test.cpp`、`core/tests/oakcore_*_test.cpp` | 手写 `main()` + `assert()` | `engine/CMakeLists.txt` 的 `make_oakengine_test()`,每个文件一个独立 ctest 二进制 | + +## 2. 为什么统一到 Google Test + +1. **断言可读性**:`assert(x)` 失败只说行号;`EXPECT_EQ(a, b)` 打印左右值, + 定位快一个数量级。这正是近几轮 facade 调试里最痛的一点。 +2. **fixture 替代手工样板**:纯 C 测试里每个文件都手写 `oakengine_init` / + `setenv("XDG_*")` / 临时目录 / `oakengine_project_free`,gfixture 的 + `SetUp()/TearDown()`/`SetUpTestSuite()` 一次性收口。 +3. **`GTEST_SKIP()`**:GPU/缺资源用例优雅跳过(offscreen OpenGL 不可绘现在 + 靠崩/超时区分,不好维护)。 +4. **过滤与重复**:`--gtest_filter`、重复运行(压 flaky)、死亡测试。 +5. **`assert()` 在 NDEBUG 下被吞**:纯 C 测试一旦开 Release 编译就形同虚设, + 这是统一的硬理由之一。 + +## 3. 目标结构 + +``` +tests/gtest/ # app 集成测试(已是 gtest,保持不变,按需并入新用例) +engine/tests/ # liboakengine facade 测试,改写为 gtest + CMakeLists.txt # 一个 oakengine_gtest 目标 + gtest_discover_tests +core/tests/ # liboakcore 测试,改写为 gtest + CMakeLists.txt # 一个 oakcore_gtest 目标 + gtest_discover_tests +tests/timeline/ # 删除 olive_add_test 产物,timeline-tests.cpp 改写为 gtest +tests/compositing/ # 同上 +``` + +- `tests/CMakeLists.txt` 的 `olive_add_test()` 宏与 `tests/testutil.h` 的 + `OAK_ADD_TEST`/`OAK_ASSERT`/`OAK_TEST_END` 宏全部删除。 +- `engine/CMakeLists.txt` 的 `make_oakengine_test()` 宏删除。 +- 每个新 gtest 二进制经 `gtest_discover_tests()` 进 ctest;**ctest 总 + 用例数不得少于迁移前**(迁移前列一张基线清单核对)。 + +## 4. 转换配方 + +### 4.1 OAK_ADD_TEST 宏框架(tests/timeline、tests/compositing) + +| 旧 | 新 | +|---|---| +| `OAK_ADD_TEST(name)` | `TEST(SuiteName, name)` | +| `OAK_ASSERT(x)` | `ASSERT_TRUE(x)` | +| `OAK_ASSERT_EQUAL(a, b)` | `ASSERT_EQ(a, b)`(自定义宏会打印左右值,直接换掉) | +| `TIMELINE_TEST_START`(ColorManager::set_up_default_config + Project + Sequence) | `class TimelineTest : public ::testing::Test { void SetUp() override {...} }` | +| `OAK_TEST_END / return OLIVE_TEST_SUCCESS` | 删除(gtest 自动判过) | + +例: + +```cpp +// 旧 +OAK_ADD_TEST(add_track) { + TIMELINE_TEST_START; + OAK_ASSERT(sequence.track_list(Track::k_video)->get_track_count() == 1); +} + +// 新 +TEST_F(TimelineTest, AddTrack) { + ASSERT_EQ(sequence.track_list(Track::k_video)->get_track_count(), 1); +} +``` + +### 4.2 纯 C assert(engine/tests、core/tests) + +| 旧 | 新 | +|---|---| +| 手写 `int main()` | 删除,链接共享 gtest main | +| `assert(x)` | `ASSERT_TRUE(x)` / `EXPECT_TRUE(x)` | +| `assert(fabs(a-b) < eps)` | `EXPECT_NEAR(a, b, eps)` | +| `assert(strcmp(a, b) == 0)` | `EXPECT_STREQ(a, b)` | +| `make_tmpdir()` + `setenv("XDG_*")` | `SetUpTestSuite()` 里建一次临时目录 | +| 每文件自带 `oakengine_init/shutdown` | 共享 fixture 做(见 §5.2) | + +**纯 C ABI 测试照写 C 调用**:gtest 文件是 C++,直接 `#include "oakengine/xxx.h"` +调 `oakengine_*` 函数即可,不需要把被测 API 改成 C++。断言里出现 +`OakEngineNode*` 等不透明句柄比较用 `EXPECT_EQ((void*)a, (void*)b)`。 + +**过渡期技巧(可选,不推荐长期使用)**:文件量太大时可先加一个 +`#define assert(x) ASSERT_TRUE(x)` 的兼容头,把 `main()` 删掉挂进 gtest, +再逐文件把 `assert` 换成语义化 `EXPECT_*`。但**最终态不许留 `assert()`**。 + +### 4.3 已是 Google Test 的(tests/gtest) + +不动。新增的 engine/core 用例如需 app 侧对象,可直接加进 `olive-gtest` 目标。 + +## 5. 落地步骤(按顺序,每步闭环:构建 + 全量 ctest 绿 + 提交) + +### 5.1 基线 +先跑 `ctest -N` 记录迁移前用例总数,存为 `docs/zh/gtest-migration-baseline.md` +(迁移后对比,总数只增不减)。 + +### 5.2 共享 fixture/main +- `core/tests/main.cpp`:`RUN_ALL_TESTS` + `SetUpTestSuite` 建 XDG 临时目录。 +- `engine/tests/main.cpp`:同上,外加 `oakengine_init(OAKENGINE_INIT_HEADLESS)`, + `TearDownTestSuite` 调 `oakengine_shutdown()`;`OAK_TEST_SOURCE_DIR` 经 + `target_compile_definitions` 传入(照 `make_oakengine_test` 现有做法)。 +- XDG 沙箱**每个二进制一份**,不要每个测试一份(与现状一致,避免并发冲突)。 + +### 5.3 core/tests(最小、无 Qt,先练手) +逐文件改写 `oakcore_*_test.cpp` 为 gtest,删手写 main;建 `oakcore_gtest` 目标 +(链 `oakcore` + `GTest::gtest` + `GTest::gtest_main`),`gtest_discover_tests`。 +全量 ctest 绿后提交。 + +### 5.4 tests/timeline、tests/compositing(OAK 宏框架) +按 §4.1 改写;建独立 gtest 目标或并入合适目标;删除 `olive_add_test` 调用、 +`tests/testutil.h` 宏与 `tests/CMakeLists.txt` 中的宏定义。全量 ctest 绿后提交。 + +### 5.5 engine/tests(facade 测试,量最大) +按 §4.2 改写 `oakengine_*_test.cpp`;建 `oakengine_gtest` 目标(链 `oakengine` ++ Qt + gtest),删 `make_oakengine_test`。全量 ctest 绿后提交。 + +### 5.6 收尾 +- `ctest -N` 对比基线(只增不减);全量 `--output-on-failure` 绿。 +- 全仓库 grep 确认无 `OAK_ADD_TEST`/`OAK_ASSERT`/`make_oakengine_test`/ + `olive_add_test` 残留。 +- 更新 `docs/zh/` 相关文档与本指引标注"已完成"。 + +## 6. 注意事项(别踩坑) + +1. **GPU/渲染用例**:沿用 `GTEST_SKIP()` 判定(参 + `tests/gtest/render_worker_footage_test.cpp` 的 backend 检查与 + viewer_display_repro_test 的 offscreen 跳过模式),不许靠超时/崩溃区分。 +2. **offscreen/OCIO**:需要 QApplication 的用例共享 `tests/gtest/main.cpp` 的 + 环境初始化(offscreen QPA + OCIO 配置);engine/core 的无头用例走 + `oakengine_init(HEADLESS)`,不要重复造 QApplication。 +3. **测试数据路径**:`OAK_TEST_SOURCE_DIR` 必须经 CMake 定义传入 + (`tests/demo.mp4` 等),不要硬编码相对路径。 +4. **线程/事件**:facade 事件类测试(`oakengine_events_test` 等)依赖 + DirectConnection 同步语义,迁移时保持原用例的线程假设,不要引入 + `QCoreApplication::processEvents` 之外的等待方式。 +5. **一次性迁移 vs 渐进**:按 §5 的顺序渐进,**禁止**先删框架再慢慢补测试 + (会造成不可测试的空窗)。每步都必须全量绿。 +6. **ctest 仍是入口**:CI/本地都继续用 `ctest --output-on-failure -j$(nproc)`; + `gtest_discover_tests` 注册后,单个用例可用 `ctest -R ` + 或 `./ --gtest_filter=...` 跑。 diff --git a/docs/zh/plans/riir.md b/docs/zh/plans/riir.md new file mode 100644 index 000000000..102db3259 --- /dev/null +++ b/docs/zh/plans/riir.md @@ -0,0 +1,319 @@ +# RIIR 绞杀者模式执行计划:liboakengine 模块化拆分与渐进式 Rust 重写 + +> 本文档描述在 C ABI 迁移战役(见 `c-abi-migration-handoff.md`)完成之后, +> 如何用绞杀者模式(Strangler Fig)把 liboakengine.so 安全地拆成若干小模块, +> 再逐个重写为 Rust。 +> **核心约束:每一步都可验证、可回退;任何一步失败都不影响已验证的部分。** +> +> 本文档面向未来的执行者(可能没有本文写作时的对话上下文),因此关键决策、 +> 依据和验证方法都写成自包含的形式。与交接文档的关系:交接文档管"ABI 迁移战役" +> (消灭 oak-editor 对 olive:: 的引用、liboakengine 只导出 oakengine_*), +> 本文档管那之后的"拆分与重写战役"。**拆分的前置条件是 ABI 迁移完成(§1.1)。** + +--- + +**API 冻结保证(最高优先级约束,先于一切拆分)** + +**拆分模块,但公共 API 一个不动。** 这是整个战役的硬约束,凌驾于任何 +"拆得更细"的冲动之上。三条钉死: + +1. **公共 `oakengine_*` 全程冻结。** 在整个拆分与 Rust 重写期间, + `engine/include/oakengine/*.h` 里的每个公开函数:不改签名、不删函数、 + 不改语义。唯一允许的变更是**新增**函数,且必须标注 experimental。 + 新增不等于变更——既有签名与语义一个字都不许动。 +2. **`liboakengine-facade` 自身不拆分。** facade 是唯一、薄、稳定的路由层, + 公共 `oakengine_*` 全部保留在这里。拆分发生在它**之下**:facade 的内部 + 实现从"直接调 C++"改为"转发给对应小库的 C ABI",但**对外暴露的符号表 + 与调用约定完全不变**——app / oak-cli / render-worker / 未来 AI Agent + 只链接 facade,连重新链接都不用。 +3. **模块间内部 C ABI 与公共 API 分层、分别版本化。** 拆分后模块之间 + (如 oakrender 调 oakmodel)不能再 C++ 直连,必须走**新增的内部 C ABI**。 + 这层接口:(a) 只对模块间可见,app 永远看不到;(b) 与公共 API 分开管理、 + 允许演进;(c) 命名与头文件路径必须明显区别于公共 facade(例如放 + `engine/include/oakinternal/`,前缀 `oakinternal_`),杜绝"内部接口慢慢 + 变成事实公共 API"的漂移。公共 `oakengine_*` 另加**版本字段** + (`oakengine_api_version()`),让任何公共面的意外漂移可被检测。 + +> 一句话:**缝(公共 facade)冻死,缝后面的实现随便拆随便换。** +> 任何执行步骤如果会改动公共 `oakengine_*` 的既有签名或语义,就是走错了, +> 停下来回到本节。 + +--- + +## 0. 为什么这条路是可行的(三个已验证的事实) + +1. **绞杀缝已经存在。** 迁移战役的最终产物就是一条稳定、纯 C、带测试覆盖的 + ABI 缝(`engine/include/oakengine/*.h`,~30 个头、20+ 族)。绞杀者模式最危险 + 的一步——"在没有缝的系统里造缝"——已经由当前战役完成。 +2. **插件模式在本仓库已跑通。** `ffmpeg_bridge`(独立 .so,C ABI)、 + `oakgl`/`oakvulkan`(渲染后端插件,经 `engine/render/backend/renderbackend_c.h` + 的 C ABI 由 `DynamicRenderer` 动态加载)证明"小 .so + C ABI + 运行时替换" + 在本代码库不是理论,是现状。本计划只是把同一模式推广到全引擎。 +3. **验证资产现成。** 44+ ctest、~2000 条 gtest、oak-cli(info/probe/render/ + transcode,含 PPM 帧输出)、worker 端到端 harness(NDJSON 协议真实渲染)、 + 测试素材(demo.mp4/img.png/project_with_footage.ove)。每一步验证不需要 + 新建测试体系,只需要把它们固化为"门禁脚本"。 + +--- + +## 1. 目标、前置条件与非目标 + +### 1.1 前置条件(未满足前不动工) + +- ABI 迁移战役完成:oak-editor / oak-render-worker `U _ZN5olive` = 0; + `nm -D --defined-only liboakengine.so | grep -c " T _Z"` = 0(visibility 收口); + 全量 ctest 绿。(即交接文档 §1 的四条验收。) +- 本文 §4.2 的基础设施(Rust 工具链接入 + 门禁脚本)就位。 + +> **R6 收尾状态(2026-07-26 实测)**: +> - ✅ oak-editor / oak-render-worker `U _ZN5olive` = **0**(R5→R6 迁移战役完成, +> nm 58→0;交接文档 §6.4 豁免清单已清空为"无豁免"); +> - ✅ 全量构建 0 error、全量 ctest 绿(45/45);app↔engine 边界对 app 的 +> 引用而言已是纯 C ABI——**"边界已纯"**。 +> - ⏳ **遗留项(不属 R6 范围,S0 需复核)**:engine 侧 visibility 收口未做—— +> `nm -D --defined-only liboakengine.so | grep -c " T _Z"` 实测 = **3486** +> (导出 C++ 符号),尚未降到 0。该子条件是独立的收口工作(§2 Step 2 的 +> visibility=hidden 规则),app 侧已无任何引用,收口不影响 app。 +> - 已知遗留(已论证,不泄漏符号):app 仍 include 约 40 个 engine C++ 头 +> (node/render/timeline/undo/pluginSupport,用于类型与 inline 访问器), +> nm=0 证明不产生符号引用;彻底清理超出 R6 的 58 符号目标,留待后续批次。 + +### 1.2 终态 + +- `liboakengine.so` 不复存在,取而代之的是一组小动态库(§3 模块图), + 每个只有两种实现状态:C++(待重写)或 Rust(已重写)。 +- app/worker/cli 只链接 **facade 壳库**(`liboakengine-facade`),对下层模块 + 的实现语言无感知。 +- 任何模块的 Rust 替换都经过 §2 的六步流程,全程有 C++ 版本可回退, + 直到 G6 退役门禁通过。 + +### 1.3 非目标(明确不做) + +- 不重写 Qt、FFmpeg、OpenColorIO、PortAudio 等第三方库本身。 +- 不重写 app(UI 层保持 C++/Qt;它消费的本来就是 C ABI)。 +- 不改变 `oakengine_*` 公开 facade 的任何既有签名(拆分/重写只许改实现, + 不许动契约;新增内部 ABI 允许,但必须符合同一套头文件规则)。 +- 不做 big-bang:任何时刻整个系统都必须可构建、可测试、可发布。 + +--- + +## 2. 绞杀六步(每个模块的统一流程) + +对每一个模块 X,严格按以下顺序执行;每步有对应门禁(§5),不过门禁不进下一步。 + +### Step 1 — 冻结 ABI +- 评审模块对外 C ABI 头(公开 facade 已有部分直接复用;模块间内部调用需要的 + 新增内部头,按 `c-abi-migration-handoff.md` §6.1 的同一套规则写:纯 C 类型、 + buf/size 约定、owned/borrowed 注释、错误码)。 +- 用门禁脚本生成 ABI 快照(§5-G1)并入库。**此后该头的任何改动都是显式评审行为。** + +### Step 2 — 物理拆分(C++ 实现原样搬出) +- 新建 `liboakengine-.so`:把该模块源码从 liboakengine 移入独立 CMake 目标; + 原引擎内其他部分对它的 C++ 调用**全部改走它的 C ABI**。 +- 新库同样 visibility=hidden + 只导出 C 符号。 +- 过 G2:构建绿、全量 ctest 绿、符号审计绿、ABI diff = 0。 +- **此步不改任何行为**——只搬代码和改调用方式。发现行为必须改才能拆的, + 停下来记录,先回去补 facade(回 Step 1)。 + +### Step 3 — Rust 影子实现 +- `rust//` 建 cdylib crate,实现与 Step 1 完全相同的 C ABI。 +- cbindgen 生成的头与 C++ 头做规范化 diff(§5-G3),必须一致。 +- FFI 边界硬规则:`catch_unwind` 全包裹(panic 不得跨 FFI)、错误码语义逐条 + 对齐、owned/borrowed 生命周期按注释实现(`Box::into_raw` / 借用引用)、 + 回调线程语义按契约复现(见 §6.2)。 + +### Step 4 — A/B 双跑 +- CMake 选项 `OAK_MODULE__IMPL=cpp|rust` 控制链接哪个实现。 +- 两种配置各自全量构建 + 全量 ctest + 金标准对比(§5-G4)。 +- **帧级一致**:渲染输出字节一致或 PSNR ≥ 50dB;**序列化 round-trip 字节一致**; + 其余以测试断言为准。 + +### Step 5 — 切换默认实现 +- 默认实现切到 rust;CI 三平台构建 + 全量测试。 +- C++ 实现保留一个发布周期作为回退选项(option 切回即可)。 + +### Step 6 — 退役 +- 删除该模块的 C++ 实现与 cpp 构建分支;ABI 快照锁定为最终态;全量回归(G5 同项)。 +- 在 roadmap 记录该模块重写完成。 + +--- + +## 3. 模块图与拆分顺序 + +### 3.1 依赖方向(单向,禁止循环;上层只经下层 C ABI 调用) + +``` + app / oak-cli / oak-render-worker + │ + liboakengine-facade(壳:capi + 事件 + init) + │ + ┌────────┬────────┼─────────┬──────────┐ + oaktask oakrender oakplugin oakaudio oakserialize + │ │ │ │ │ + └────────┴────┬───┴──────────┴──────────┘ + │ + oakmodel(节点图 + 项目模型 + 时间线模型) + │ + ┌────────┼─────────┐ + oakcodec liboakcore oakbackend(GPU 插件:oakgl/oakvulkan/未来的 Rust 后端) + │ + ffmpeg_bridge(已是 C ABI .so) +``` + +**关键架构事实(拆分顺序的依据)**: +- `Node` 及其子类簇(Project/Folder/Footage/Sequence/Block/Track/Clip/Gap/ + Transition/Subtitle/各效果节点)是 C++ 继承绑死的**不可拆分类型簇**—— + 跨模块做 C++ 继承不可能不导出 C++ 符号。因此它们必须整体作为一个模块 + (oakmodel)处理,重写时也作为一个重写单元。这是本计划最大的一个拆分 + 粒度结论,不要再试图把 Block/Track 从 Node 里拆出去。 +- `oakcore`(liboakcore.so)已是纯 C ABI 独立库,是天然的第一块 Rust 试验田 + (见 M0)。 +- GPU 后端已是插件,重写线与主线解耦(见 §7)。 + +### 3.2 执行顺序(依赖最少、Qt 最少、验证最容易的在前) + +| 批次 | 模块 | 内容 | 前置依赖 | 主要风险 | +|---|---|---|---|---| +| M0 | **oakcore** | liboakcore 整体(rational/timecode/bezier/samplebuffer/audioparams,Qt-free) | 无 | 极低;工具链试金石 | +| M1 | **oakaudio** | AudioProcessor、AudioSynchronizer、AudioLevelMeter、波形计算 | oakcore | 低;顺带消掉 AudioProcessor 豁免项 | +| M2 | **oakcodec** | decoder/encoder/conform/proxy | ffmpeg_bridge | 中;FFmpeg 行为复刻 | +| M3 | **oakserialize** | node/project/serializer/*(XML 项目文件) | oakmodel(经 facade node/project 族) | 中;round-trip 必须字节一致 | +| M4 | **oakundo** | UndoCommand/UndoStack/MultiUndoCommand | oakmodel(经 facade) | 中;全局调用点多 | +| M5 | **oakrender** | RenderManager/ticket/watcher/cache/PreviewAutoCacher/ColorProcessor | oakmodel、oakcodec | 高;线程与 OCIO | +| M6 | **oakmodel** | Node/NodeInput/keyframe/traverser/factory/Project/Folder/Footage/Sequence/Block/Track/效果节点 | oakcore、oakcodec | 最高;最大类型簇 | +| M7 | **oaktask** | Task/TaskManager 及各任务类型 | oakmodel、oakrender | 中;QtConcurrent | +| M8 | **facade 壳 + 收尾** | capi 各实现、事件注册表、coreengine、config、plugin(OFX) | 全部 | 中;事件机制 Rust 化 | + +每个批次内部都走 §2 的六步。**严格串行**:上一批次 G5 完成才开下一批次 +(M0 例外,可与 ABI 迁移战役收尾并行准备)。 + +**为什么 oakmodel 排第六而不是第一**:它是依赖中心,先拆它会导致所有模块 +都要先等它的内部 ABI 定型;先拆叶子模块可以用公开 facade(node.h/project.h/ +timeline.h,本就是为外部消费设计的)充当模块间缝,缝的质量先被实战检验, +最后拆 oakmodel 时它的对外接口已经是稳定态。 + +--- + +## 4. 阶段计划 + +### 4.1 阶段 S0:前置确认(0 成本,只是检查) +- 对照 §1.1 逐条核对 ABI 迁移战役验收结果。未完成则停止,回到交接文档。 + +### 4.2 阶段 S1:重写基础设施(第一批真正的活) +1. **Rust 工具链接入**:仓库根建 `rust/` workspace;CMake 集成用 Corrosion + (cmake+cargo 标准方案);`cargo`/`cbindgen` 版本锁定并写进构建文档。 + 禁止要求全局安装 cargo 之外的 Rust 组件(CI 可复现)。 +2. **门禁脚本固化**(全部进 CI): + - `scripts/abi-dump.sh`:对每个相关 .so 导出 `oakengine_*`/`oakcore_*` 符号 + + 头文件规范化哈希,产出快照文件;`scripts/abi-check.sh` 与入库快照 diff。 + - `scripts/golden-render.sh`:oak-cli render demo.mp4 指定帧 → PPM, + 与金标准比对(字节一致或 PSNR ≥ 50dB);`oak-cli transcode` PPM 同理; + project_with_footage.ove 序列化 round-trip 字节一致;worker E2E harness。 + - `scripts/symbol-audit.sh`:现有 nm 三件套的脚本化(app 侧 `U _ZN5olive`、 + 各 .so `T _Z`、facade 测试覆盖审计)。 +3. **M0:liboakcore Rust 重写**(试点)。完整走六步,目的是把工具链、A/B 流程、 + 门禁全部打通并暴露问题。它是全仓库最小最干净的模块,失败成本最低。 + M0 没全绿之前,不允许排产任何后续模块。 + +### 4.3 阶段 S2–S8:M1–M8 +按 §3.2 表格逐模块执行。每模块的"模块档案"(边界清单、Qt 依赖清单、 +信号清单、线程语义、验证重点)在 Step 1 时补写到本文 §6 对应小节。 + +--- + +## 5. 验证门禁(每步必须过,脚本化、进 CI) + +| 门禁 | 触发步 | 内容 | 通过标准 | +|---|---|---|---| +| G0 | 每批开始 | 全量构建 + 全量 ctest + golden-render 基线快照 | 全绿,快照入库 | +| G1 | Step 1 | abi-dump 快照 | 与上一基线 diff 仅含本批新增 | +| G2 | Step 2 | 构建 + 全量 ctest + 新库符号审计 + ABI diff | 全绿;新库导出仅 C;diff=0 | +| G3 | Step 3 | cbindgen 头 vs C++ 头规范化 diff;crate 单测 | diff=0;单测全过 | +| G4 | Step 4 | 双实现配置各自全量测试 + golden 对比 | 两轮全绿;帧/序列化一致 | +| G5 | Step 5 | 三平台构建 + 全量测试 + 性能抽测 | 全绿;渲染帧耗时回退 ≤10% | +| G6 | Step 6 | 删除 C++ 实现后全量回归 + ABI 快照锁定 | 全绿;快照入库 | + +- **性能抽测**:golden-render 脚本记录渲染耗时,Rust 版慢于 C++ 版 10% 以上 + 必须查明原因(允许记录后放行,但不允许无声劣化)。 +- **回退规则**:任何门禁失败 → 停止该模块,切回 cpp 实现(Step 4 之后才有 + 可切对象;之前是天然回退态),记录原因,系统保持全绿。 + +--- + +## 6. 跨模块设计约束(全部钉死) + +### 6.1 信号/通知的 Rust 化 +- engine 的 QObject 信号是当前变更通知机制;capi/events.cpp 用 `dynamic_cast` + 校验订阅 handle 的族类型。**Rust 对象没有 dynamic_cast**,因此在 M5(oakmodel + 前置)之前必须引入**句柄类型标签约定**:所有 facade 句柄指向的对象首字段为 + `uint32_t type_tag`(枚举值入 ABI 头),events.cpp 的族校验改为读标签。 + 这是对 events.cpp 的授权内改动,需配事件实测用例。 +- Rust 模块的变更通知:经同一张 oakengine_event 注册表发射(事件机制是 + app 侧唯一通道,Rust 模块只是换了发射端的实现语言)。 + +### 6.2 线程语义(ABI 契约,Rust 必须逐条复现) +- facade 回调/事件 = 发射线程同步调用(Qt::DirectConnection 等价),引擎对象 + 属 GUI 线程;回调内不得反调改同一对象的编辑原语。 +- 渲染在后台线程(当前 QtConcurrent);Rust 侧线程模型自选(std::thread/ + rayon/自建池),但**回调触发线程与顺序语义必须与原实现一致**;worker + NDJSON 协议的线程行为不得改变。 +- 每模块 Step 1 时必须把该模块涉及的线程归属写进模块档案。 + +### 6.3 内存与生命周期 +- owned/borrowed 规则按各 facade 头注释执行;Rust 侧 owned 句柄 `Box::into_raw`, + free 函数 `Box::from_raw` 回收;borrowed 句柄不接管析构。 +- 禁止在 FFI 边界传递任何 Rust 特有类型(String/Vec/ trait object);边界上只有 + C 类型,与现有头文件规则相同。 + +### 6.4 错误与 panic +- panic 不得跨 FFI(`catch_unwind` 全包裹,映射为 `OAKENGINE_E_FAILED` + + last_error 字符串)。错误码语义与 C++ 实现逐条一致(A/B 对比时断言)。 + +### 6.5 全局单例 +- Config/NodeFactory/RenderManager/AudioManager 等单例,Rust 侧用 `OnceCell`/ + 显式注册表实现;初始化/销毁时机挂在 `oakengine_init`/`oakengine_shutdown` + 既有钩子上,不引入新的隐式初始化。 + +### 6.6 第三方库 +- FFmpeg:继续经 `ffmpeg_bridge`(已是 C ABI),Rust 模块链接桥库而非直接绑 FFmpeg。 +- OCIO:ColorManager 是 C++ API 重度用户,M5 时评估:薄 C 封装进 facade vs + 保留 C++ 微库长期共存(允许作为长期 C++ 孤岛,写入 roadmap)。 +- Qt:只允许 facade 壳与 app 侧使用;M1 起的各引擎模块实现内**不得新增 Qt 依赖** + (QtCore 容器可暂用,但不得新增 QObject 信号、moc 类)。 + +--- + +## 7. GPU 后端平行线 + +- oakgl/oakvulkan 已是 `renderbackend_c.h` C ABI 插件,与主线解耦。 +- Rust 后端(建议 wgpu 起步)作为**新插件**并行开发,通过同一 ABI 被 + DynamicRenderer 加载;验收用现有 Backends gtest(Vulkan 用例即现成的 + A/B 对比器——同一测试分别加载两个后端跑)。 +- 不替代主线任何模块门禁;独立排期,不阻塞 M1–M8。 + +--- + +## 8. 风险登记册(开工前评审,施工中持续更新) + +| 风险 | 等级 | 对策 | +|---|---|---| +| oakmodel 类型簇过大,六步周期过长 | 高 | Step 2 允许分子批拆分(先项目模型后效果节点),但 ABI 一次冻结 | +| 线程语义偏差导致偶发黑屏/崩溃 | 高 | G4 双跑必须包含 worker E2E 与 Backends viewer 用例;引入压力重复(每用例 ×10) | +| OCIO 无法 Rust 化 | 中 | 允许 C++ 孤岛(§6.6),不影响其他模块 | +| QtConcurrent 行为差异 | 中 | M5/M7 档案逐条记录并发模式;A/B 含并发压力 | +| cbindgen 头漂移 | 中 | G3 规范化 diff 进 CI,漂移即红 | +| 构建复杂度(cargo+cmake)拖慢迭代 | 中 | Corrosion 单一入口;文档固化;禁止手工 rustc | +| 行为不可拆(Step 2 发现必须改行为才能拆) | 中 | 回 Step 1 补 facade; roadmap 记录;禁止带行为变更进 Step 2 | +| 性能劣化 | 低 | G5 抽测阈值;剖析后放行或回退 | + +--- + +## 9. 里程碑摘要(可直接抄进项目计划) + +1. **S1 完成**:Rust 工具链 + 门禁脚本进 CI;M0(liboakcore)G6 退役。 +2. **M1–M2 完成**:音频 DSP 与编解码 Rust 化;AudioProcessor 豁免项消除。 +3. **M3–M4 完成**:序列化与 undo Rust 化;项目文件 round-trip 金标准常青。 +4. **M5 完成**:渲染管线 Rust 化(OCIO 孤岛与否已裁决并记录)。 +5. **M6 完成**:oakmodel Rust 化——**最大里程碑**,此后 liboakengine 主体为 Rust。 +6. **M7–M8 完成**:任务系统与 facade 壳 Rust 化;liboakengine.so(C++ 版)正式退役。 +7. **GPU 平行线**:Rust 后端插件经 Backends 双后端测试验收。 diff --git a/docs/zh/plans/ui-redesign-plan.md b/docs/zh/plans/ui-redesign-plan.md new file mode 100644 index 000000000..62b228817 --- /dev/null +++ b/docs/zh/plans/ui-redesign-plan.md @@ -0,0 +1,213 @@ +# Oak 主界面 UI 改版计划 + +> 本文是主界面重新设计的执行手册,面向 DeepSeek Flash(**不识字图,本文全部 +> 用文字精确定义目标形态**)。详细程度对齐 `../r5-app-migration-guide.md`。 +> 工作分支:`c-abi-migration`。**启动前提:R5(C ABI app 侧迁移)验收完成之 +> 后**;启动后可与 Google Test 统一迁移并行——协调规则见 §2。 +> 依据:`design/Oak-UI设计图-主界面-标注版.png`、`...-效果栈版.png`、 +> `...-节点编辑器版.png`(共 10 项关键改动,本文逐项落地)。 + +--- + +## 1. 目标布局(文字定义,照此实现) + +``` +┌ 菜单栏:文件(F) 编辑(E) 视图(V) 回放(P) 序列(S) 窗口(W) 工具(T) 帮助(H) +├ 工具条(31px):14 个工具图标 + 吸附开关 + 缩放滑块 + 轨道高度滑块 +├──────────────────────────────────────────────────────────────────── +│ 素材查看器(源) │ 序列查看器(节目) │ 检查器│历史记录 +│ ·适合/安全框 │ ·适合/安全框 │ ┌──────────────────┐ +│ ·独立走带控制 │ ·节点编辑器(同组切换)│ 媒体 · xxx.mp4 │ +│ ·1920×1080·25FPS │ ·分辨率·帧率信息 │ ▼ │ +│ │ │ ≡ 变换 ✓ ×(卡片) │ +│ │ 26px电平条(右缘) │ ≡ OCIO LUT ✓ ×(卡片)│ +│ │ │ [+ 添加效果] │ +│ │ │ └──────────────────┘ +├──────────────────────────────────────────────────────────────────── +│ 时间线(全宽贯通) │ +│ 轨道头180px │ 轨道区 │ +│ V2 视频轨道1 [锁定][显示] ████████ │ +│ V1 视频轨道0 [锁定][显示] ████████████ │ +│ A1 音频轨道0 [锁定][静音][独奏] ▁▂▃▅▂▁ │ +│ A2 音频轨道1 [锁定][静音][独奏] ▁▂▁▃▂▁ │ +├──────────────────────────────────────────────────────────────────── +│ 状态栏:就绪 | 缓存:已启用 | 代理:关 | 自动保存:3分钟前 || 时间码/时长 | 25FPS | 1920×1080 +└──────────────────────────────────────────────────────────────────── +``` + +节点编辑器视图(与序列查看器同组切换、占中央最大面板): +``` +┌ 节点编辑器(中央最大面板) +│ [+] [-] [适配] ┌ 检查器(同效果栈) ┐ +│ ┌ 第一稿.mp4[视频] · 00:00:00:00–00:04:18:18 ┐ │ │ +│ │ [媒体]→[变换]→[OCIO LUT]→[输出] │ │ │ +│ └───────────────────────────────────────┘ │ │ +│ ┌ 第一稿.mp4[音频] · 00:00:00:00–00:04:18:18 ┐ │ │ +│ │ [媒体]→[音量]→[输出] │ │ │ +│ └───────────────────────────────────────┘ │ │ +│ ┌ 小地图 ┐ │ │ +└──────────────────────────────────────────────────────────────────── +``` + +**核心原则:效果栈(检查器)与节点图是同一份节点数据的两种视图**——检查器按 +「媒体 → 变换 → OCIO LUT → 输出」自上而下线性排卡片;节点编辑器把同一份数据 +画成图。默认用户像传统软件一样在检查器里线性工作,需要分支合成时切到节点 +编辑器。 + +## 2. 执行前提与并行协调 + +### 2.1 执行前提:R5 验收完成后启动 + +**本计划在 R5(C ABI app 侧迁移)验收完成之前不启动。** 这不是保守,是 +依赖关系: + +- WP4(检查器·效果栈)落在 `app/widget/nodeparamview/`、WP2(时间线轨道 + 头)落在 `app/widget/timelinewidget/`、WP5(节点编辑器移位)落在 + `app/widget/nodeview/`——这些都是 R5 符号消除的主战场。R5 先把这些文件 + 的 engine 调用点换到 facade(行为不变),本计划再在其上做 UI 重构, + 面对的才是干净的 facade 边界;提前动手只会和 R5 互相踩踏。 +- R5 完成后不存在文件重叠问题,**全部 WP 无需错峰**,按 §4 顺序执行即可。 + +启动时仍需遵守的红线(R5 的成果,永久有效): + +- **禁止**:在本计划里改 `oakengine_*` 签名、新建 engine 命令类、或把 + engine 源码再编进 app。 +- 全部改动限 **app 侧 UI 代码**(`app/`),不加 engine 符号。 + +### 2.2 与 Google Test 统一迁移(`gtest-migration-guide.md`)的并行协调 + +**结论:可以完全并行,无错峰要求。** 两份计划的文件域不相交: + +| 计划 | 动的文件 | +|---|---| +| UI 改版(本文) | `app/`(widget、panel、window、dialog、ts 翻译) | +| GTest 统一迁移 | `tests/`、`engine/tests/`、`core/tests/` 及三处测试 CMake | + +唯一的接触点与规则: + +- **`tests/gtest/`**:GTest 迁移明确"不动已有 gtest";本计划 §5 要求新增 + UI 逻辑用例,新用例**直接加进 `tests/gtest/` 现有 `olive-gtest` 目标**, + 不新建测试二进制。两边若同时改 `tests/gtest/CMakeLists.txt`,后提交者 + 普通三路合并即可(都是追加行,不会语义冲突)。 +- **ctest 基线**:GTest 迁移的硬门槛是"用例总数只增不减";本计划只**新增** + 用例、不删不改旧用例,天然满足。两边都以全量 + `ctest --output-on-failure` 绿为提交前提,谁先跑谁后跑无所谓。 +- **共享入口约定**:ctest 仍是唯一运行入口(gtest 仅编写框架),本计划 + 新增用例同样遵守,不引入别的测试框架或独立 runner。 +- **已知 flaky**(`oak_cli_transcode`、`oakengine_export_test`、 + `olive-gtest` 单次失败需单独重跑)是两个计划共同的背景噪音,判定规则 + 相同:单独重跑一次,连续两次失败才算回归。 + +## 3. 工作包(WP1–WP10,对应设计图 10 项) + +### WP1 取消独立「工具」面板 → 31px 工具条 +- **现状**:`app/panel/tool/tool.{h,cpp}` 是独立停靠面板,~8% 屏幕仅放 14 个图标。 +- **目标**:删除该面板;在时间线(`app/widget/timelinewidget/`)上方加一条 31px + 工具条,承载 14 个工具图标 + 吸附开关 + 缩放滑块 + 轨道高度滑块。 +- **文件**:删/改 `app/panel/tool/`;`app/widget/timelinewidget/timelinewidget.{h,cpp}` + 顶部加工具条;`app/panel/CMakeLists.txt`、`app/panel/panelmanager.cpp` 注册点。 +- **验证**:工具条全部按钮功能与原面板一致;布局保存/恢复无该面板。 + +### WP2 时间线全宽贯通 + 轨道头 180px +- **现状**:时间线未全宽;轨道头窄,无统一的 显示/静音/独奏/锁定 与 V/A 编号命名。 +- **目标**:时间线全宽;轨道头加宽至 180px 贴住轨道;视频轨「显示」、音频轨 + 「静音/独奏」、统一「锁定」;轨道以 `V2/V1/A1/A2` 编号 + 用途名(与主流 NLE 一致)。 +- **文件**:`app/widget/timelinewidget/`(timelinewidget、trackview、trackviewitem、 + timelineview)。**前提:R5 已完成 Track/ClipBlock facade 化,本 WP 在其上重构。** +- **验证**:轨道头控件改变 track 的 lock/mute/solo/show 状态;命名正确;全宽布局。 + +### WP3 双监看并列(源 + 节目) +- **现状**:`app/panel/footageviewer/`(源)与 `app/panel/sequenceviewer/`(节目)分开, + 审素材对位需切换标签。 +- **目标**:左右并排常显;素材查看器带独立走带控制(transport)。 +- **文件**:`app/window/mainwindow/mainwindow.cpp` 布局;`app/panel/footageviewer/`、 + `app/panel/sequenceviewer/`(KDDockWidgets 分组/dock 关系)。 +- **验证**:两查看器同屏并列;源查看器独立走带;布局可保存/恢复。 + +### WP4 参数编辑器 → 「检查器·效果栈」 +- **现状**:`app/widget/nodeparamview/` 是参数编辑器(item 列表 + 标题栏 + 关键帧控件)。 +- **目标**:改为「检查器」面板(与「历史记录」同组标签)。自上而下线性排: + 「媒体 · xxx.mp4」源卡 + 效果卡(变换、OCIO LUT、音量…)。**卡片规范**: + 标题行 = ≡(拖拽排序) ▼(折叠) 名称 ✓(启停) ×(移除);底部「+ 添加效果」即搜即加。 + 每属性行右侧保留关键帧按钮(现 `nodeparamviewkeyframecontrol`)。 +- **文件**:`app/widget/nodeparamview/`(nodeparamview、nodeparamviewitem、 + nodeparamviewitemtitlebar、nodeparamviewdockarea、nodeparamviewwidgetbridge); + 新增「检查器」容器/卡片模型。**前提:R5 已完成 Node 大族 facade 化,本 WP 在其上重构。** +- **验证**:卡片折叠/拖拽排序/启停/移除全部生效且写入节点图(undoable); + 与节点编辑器视图数据一致;「+ 添加效果」可搜可加。 + +### WP5 节点编辑器移至中央最大面板 + 缩放/小地图 +- **现状**:`app/widget/nodeview/` 节点编辑器非中央主区,大图易迷路。 +- **目标**:移到中央最大面板(与序列查看器同组切换);新增缩放控件(+/−/适配) + 与右下角小地图。 +- **文件**:`app/widget/nodeview/`(nodeview、nodeviewcontext、nodeviewminimap)、 + `app/panel/node/`、`app/window/mainwindow/mainwindow.cpp`。**前提:R5 已完成 Node 大族 facade 化,本 WP 在其上重构。** +- **验证**:节点编辑器占中央;缩放/适配/小地图可用;大图导航不迷路。 + +### WP6 音频监视器 → 26px 电平条 +- **现状**:`app/widget/audiomonitor/`、`app/panel/audiomonitor/` 占一个完整面板。 +- **目标**:改为 26px 超薄电平条,贴附在节目查看器右侧常显,释放一个面板。 +- **文件**:`app/widget/audiomonitor/`、`app/panel/sequenceviewer/`(宿主)、 + `app/panel/audiomonitor/`(去面板化)。 +- **验证**:电平条 26px 常显、随播放电平跳动;原面板释放;布局可恢复。 + +### WP7 新增全局状态栏 +- **现状**:`app/window/mainwindow/mainstatusbar.{h,cpp}` 仅显示 TaskManager 摘要。 +- **目标**:全局状态栏显示:就绪状态、缓存、代理、自动保存时间(左); + 当前时间码/时长、帧率、分辨率(右)。 +- **文件**:`app/window/mainwindow/mainstatusbar.{h,cpp}`、`app/window/mainwindow/ + mainwindow.cpp`。 +- **验证**:各项信息实时刷新;与序列状态/缓存/代理/自动保存一致。 + +### WP8 查看器细节 +- **现状**:填充条为蓝色;缩放/安全框入口不全;信息芯片不全;有与时间线重复的标尺。 +- **目标**:填充条改黑;查看器右上角提供缩放与安全框按钮;信息芯片标注 + 分辨率与帧率;移除与时间线重复的标尺。 +- **文件**:`app/panel/footageviewer/`、`app/panel/sequenceviewer/`、 + `app/widget/viewer/`(viewer、viewerdisplay)。 +- **验证**:外观与信息符合 §1 描述;无重复标尺。 + +### WP9 菜单访问键补全 +- **现状**:`app/window/mainwindow/mainmenu.{h,cpp}` 的「窗口」无访问键 (W)。 +- **目标**:「窗口」加 `(W)`,与系统及其他菜单项一致。 +- **文件**:`app/window/mainwindow/mainmenu.{h,cpp}`。 +- **验证**:菜单访问键完整一致。 + +### WP10 文案与格式统一 +- **现状**:History 未汉化;项目面板日期为英文格式;首选项有英文残留。 +- **目标**:History → 历史记录;项目面板日期改 `YYYY-MM-DD HH:mm`(如 + `2026-06-03 20:25`);首选项英文残留(Behavior、Enable hover focus、 + `1 minute(s)` 等)全部汉化。 +- **文件**:`app/panel/history/`、项目面板(`app/widget/projectexplorer/`)、 + 首选项(`app/dialog/preferences/`)、`app/ts/*.ts` 翻译。 +- **验证**:文案全部汉化、日期格式统一。 + +## 4. 执行顺序 + +R5 已验收完成(§2.1),无错峰约束。建议先做无依赖的布局项热身,再做 +重构量大的三个 WP: + +``` +第一波(布局类,互相独立): WP1 → WP3 → WP6 → WP7 → WP8 → WP9 → WP10 +第二波(重构类,建议在 R5 facade 化后的干净边界上做): + WP4(检查器)→ WP5(节点编辑器)→ WP2(时间线轨道头) +``` + +**每 WP 闭环**:现状 grep → 实现 → 全量构建 0 error → 全量 ctest 44/44 绿 +(UI 改动不得引入回归)→ 立即提交 → roadmap 补记。 + +## 5. 测试要求(沿用项目规则) + +- 所有测试用 **Google Test**(`CONTRIBUTING.md` 已立规)。 +- 检查器卡片模型、效果栈↔节点图数据一致性、轨道头控件状态、状态栏信息、 + 工具条功能:补 Google Test 用例(`tests/gtest/`,`gtest_discover_tests`)。 +- UI 行为改动以现有 `olive-gtest` 不回归为底线;新增可测逻辑(卡片模型、 + 视图模型)必须有单测。 +- 需要显示的用例沿用 offscreen QPA;渲染相关用例沿用 `GTEST_SKIP` 模式。 + +## 6. 不做 + +- 不改 `oakengine_*` 公共 API(见 `riir.md` 的 API 冻结保证)。 +- 不动 engine 内部实现、不动 R5 的 facade 工作。 +- 不重写底层渲染/播放路径;本计划只改 UI 布局、容器与交互。 +- 不做 AI 相关 UI(属 `ai-agent-design.md` 范围,另行)。 diff --git a/docs/zh/r5-app-migration-guide.md b/docs/zh/r5-app-migration-guide.md new file mode 100644 index 000000000..80f276003 --- /dev/null +++ b/docs/zh/r5-app-migration-guide.md @@ -0,0 +1,179 @@ +# R5 app 侧调用点迁移 — 实施指引 + +> 本文是 R5 阶段(消灭 oak-editor 对 `olive::` C++ 符号的引用)的执行手册。 +> 面向没有此前对话记忆的执行者,自包含。 +> 与 `c-abi-migration-handoff.md`(v3)、`c-abi-migration-handoff-v4.md` 的关系: +> 那两份管 facade(C API)建设;本文管 app 侧把对 engine C++ 类的直接调用 +> 换成 facade 调用。**facade 已就位且全绿,R5 不需要再新建 C API 族。** +> +> **工作分支:`c-abi-migration`。每完成一个文件/小步立即提交。** + +--- + +## 0. 当前已验证状态(接手先复核,不要采信转述) + +```bash +cmake --build cmake-build-debug -j$(nproc) # 必须 0 error +cd cmake-build-debug && ctest --output-on-failure -j$(nproc) # 必须 44/44 绿 +nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive" # 当前 395 +``` + +- 测试:**44/44 全绿**(R1–R4 facade 扩容已修复并通过)。 +- 符号:**395**(`nm` 实测,按类统计见 §3)。 +- facade 族已全部存在且带测试:node / timeline / project / footage / preview / + audio / color / config / disk / encoding / events / gizmo / lut / plugin / + proxy / serializer / sync / task / traverse / undo / videoparams / viewer / + worker / playback / renderer / app。 +- 事件 ID 已分配到 143,新事件从 **144** 起。 + +## 1. 关于"必须全文件集群重写、无法增量"——论断不成立 + +DS 报告称 R5 的符号"因 Q_OBJECT + 虚函数链 + 信号/槽耦合,必须全文件集群 +同时重写,无法增量"。**这是错误的**,证据: + +1. **前一次战役就是增量完成的**:同一套符号从 557 → 162,按 B1–B11a 分批、 + 每批独立闭环。被回滚抹掉的是 app 调用点,不是方法。 +2. **本阶段已经证明可增量**:符号已从回滚后的高点降到 395,全部是逐文件 + 替换得来的,没有一次"集群重写"。 +3. **facade 已就位**:R5 需要的 C API 族全部存在。剩下的工作是把 app 里的 + `olive::X::method()` 调用换成 `oakengine_x_method()`,**不重写任何 engine + 类**,自然不存在"虚函数链重写"。 +4. **"信号/槽耦合"已被事件机制解决**:`oakengine_event_subscribe` + + `app/engineeventbridge.{h,cpp}` 就是替代 `connect(engineObj, &Engine::sig, ...)` + 的标准通道(SOP 见 roadmap 附 D)。不需要为消信号而重写类。 + +结论:**R5 是机械的、可逐文件验证的调用点替换,按 §4 的顺序增量推进。** +"集群重写"是停工的借口,不是技术结论。 + +## 2. 增量方法(每个文件的标准动作) + +对每一个 app 文件,按这个顺序做,做完立即提交: + +1. **grep 该文件的 engine 直接引用**:`olive::X::`、`connect(` 到 engine 对象、 + `new SomeCommand(`(engine 命令类)、engine 头 include。 +2. **逐点替换**为 facade: + - 方法调用 → 对应 `oakengine_*` 函数(§4 表)。 + - `connect(engineObj, &Engine::sig, this, ...)` → + `bridge->subscribe(handle, OAKENGINE_EVENT_*)` + 连 bridge 的 Qt 信号 + (没有 bridge 成员就 `new EngineEventBridge(this)`)。 + - `new EngineCommand(...)` 进 undo 栈 → facade 的 undoable 原语 + (多数已有;确实缺的按 §5.2 补 facade,不要新建 app 侧命令类)。 + - undo 压栈一律 `oakengine_undo_push(command, name)`(**2 个参数**,全局栈, + 不传栈句柄;`Core::undo_stack()` 返回的 `void*` 只作事件订阅 handle)。 +3. **删不再需要的 engine 头 include**;加对应 `oakengine/*.h`。 +4. **构建 + 该文件相关测试 + `nm` 双度量**,符号应净减。 +5. **立即提交**,提交信息写清文件与消掉的符号数。 + +**禁做**:为消符号新建 app 侧 engine 命令子类、给 facade 加"stub 实现" +(见 §6 红线)、为省事把 engine 源码再编进 app。 + +## 3. 剩余符号分布(395,nm 实测,按类) + +执行顺序按"依赖最少、符号最密集、facade 最现成"排。每行:`数量 类 — 主战场文件 — 用哪个 facade 族`。 + +### 第一批:工具/服务类(快、独立,先清 ~60) + +| 数量 | 类 | 主战场 | facade 族 | +|---|---|---|---| +| 9 | QtUtils | 各 widget | 纯搬 app(B10 模式,`app/common/`,hidden visibility,见 §6-R1) | +| 9 | EncodingParams | dialog/export/*、speedduration | `oakengine/encoding.h`(已含 OakEngineEncodingParams 全字段) | +| 7 | ExportFormat | dialog/export/*、sequence 对话框 | `oakengine/encoding.h`(format/codec 元数据) | +| 11 | AudioManager | viewer、core、preferencesaudiotab | `oakengine/audio.h` + 事件 140 | +| 7 | PreviewAutoCacher / 7 RenderTicketWatcher / 5 RenderTicket / 3 RenderManager | viewer.cpp、timeruler | `oakengine/preview.h`(cacher + OakEnginePreviewRequest) | +| 7 | plugin | pluginSupport | `oakengine/plugin.h` + 事件 | +| 5 | ProxyManager | proxydialog、projectexplorer、timelinewidget | `oakengine/proxy.h` | +| 5 | ProjectSerializer | keyframeview、seekablewidget、timelinewidget、nodeview、nodeparamview、main | `oakengine/serializer.h`(OakEngineClipboard) | +| 5 | FileFunctions / 4 ColorCoding / 4 qHash / 2 Html / 1 xml_read / 1 debug_handler / 2 operator<<>> | 各 widget | 纯搬 app(B10 模式) | +| 3 | LUTLibrary | colordialog、nodeparamviewwidgetbridge、preferencesluttab | `oakengine/lut.h` | +| 2 | Config | preferences、mainmenu、core | `oakengine/config.h` + `app/common/configwrapper.h` | + +### 第二批:项目/素材/序列数据类(~90) + +| 数量 | 类 | 主战场 | facade 族 | +|---|---|---|---| +| 10 | Project / 7 Folder / 10 Footage / 5 Sequence / 5 TrackList | projectexplorer、projectproperties、footageproperties、projectviewmodel | `oakengine/project.h` + `footage.h` + `timeline.h`(folder 族、`oakengine_folder_move_child`) | +| 17 | EngineCore | core.cpp、mainwindow、各 panel | `oakengine/app.h`(`Core` 已组合转发) | +| 9 | ViewerOutput | viewer、footageviewer、各 panel | `oakengine/viewer.h`(workarea、playhead、params) | +| 15 | ColorManager / 5 ColorProcessor / 1 ManagedColor | manageddisplay、colordialog、colorbutton、scopebase、colorvalueswidget、projectproperties | `oakengine/color.h` + `app/widget/manageddisplay/colorprocessorhandle.h` | +| 9 | NodeGroup / 6 MultiCamNode | nodeview、multicam 面板 | `oakengine/node.h`(group passthrough、multicam 族) | + +### 第三批:时间线/标记/命令类(~70) + +| 数量 | 类 | 主战场 | facade 族 | +|---|---|---|---| +| 13 | Track / 12 ClipBlock / 8 TimelineWorkArea / 6 TimelineMarker | timelinewidget、timeruler、seekablewidget、trackview、timelineview | `oakengine/timeline.h`(track/clip/marker/workarea 全族) | +| 6 | Task / 6 NodeTraverser | taskview、export、viewer、nodeparamview | `oakengine/task.h`、`traverse.h` | +| 5+ | UndoStack / 各 Marker/Node 命令类(MarkerAdd/ChangeColor/ChangeName/ChangeTime/Remove、NodeAdd/EdgeAdd/EdgeRemove/Rename/OverrideColor/ParamSetStandardValue、FolderAddChild、TimelineAddTrack、TrackListRippleToolCommand) | historywidget、nodeview、timelinewidget、nodeparamview、projectviewmodel | `oakengine/undo.h` + 各 undoable 原语;**不要**新建 app 侧命令类(`TrackListRippleToolCommand` 是遗留评估点,最后单独定) | + +### 第四批:Node 大族(~55) + +| 数量 | 类 | 主战场 | facade 族 | +|---|---|---|---| +| 40 | Node / 7 NodeKeyframe / 3 NodeFactory | nodeview、nodeparamview、curvewidget、keyframeview、nodetableview、nodevaluetree、multicam、nodecombobox | `oakengine/node.h`(~60 函数:输入元数据、值读写、关键帧、dragger、undoable 批量、context、group、multicam) | +| 7 | TextGizmo / 3 DraggableGizmo | viewerdisplay | `oakengine/gizmo.h`(POD) | +| 各 1–2 | CrossDissolveTransition / SubtitleBlock / TransitionBlock / VolumeNode / TransformDistortNode / SolidGenerator / TextGeneratorV3 / ShapeNode | timeline 工具、nodeview | `oakengine_node_create_undoable` + input id getter(B4c 模式) | + +### 第五批:GPU/帧路径(~15,R6 收口) + +| 数量 | 类 | 主战场 | facade 族 | +|---|---|---|---| +| 5 Frame / 3 Renderer / 2 OpenGLRenderer / 1 DynamicRenderer / 2 Texture / 1 RenderManager | viewerdisplay、manageddisplay | `oakengine/renderer.h`(texture/frame 句柄,R6 已完成 B7 桥移除) | + +## 4. 每批闭环(不可省) + +``` +nm 基线 → 逐文件替换(§2)→ 构建 0 error → 全量 ctest 44/44 绿 +→ nm 双度量(族符号 + 总数净减)→ 立即提交 → roadmap 附 C 补记 +``` + +**全量 ctest 不绿不得进入下一批。** 已知 flaky(`oak_cli_transcode`、 +`oakengine_export_test`、`olive-gtest` 偶发 SEGFAULT)单独重跑两次仍败才算真失败。 + +## 5. 缺的 facade 怎么办 + +绝大多数调用点已被现有族覆盖。确实缺的时候: + +1. **先查**:该功能是否已被某族覆盖(grep `oakengine_*` 头)。多数"缺"是没找到现成函数。 +2. **能搬 app 的纯工具**(Qt 类型、纯函数、纯数据)→ 搬 `app/common/`, + 对该源文件加 `-fvisibility=hidden`(§6-R1),不新增 C API。 +3. **必须跨边界的** → 最小 facade 族(只包 app 实际用到的成员), + 头文件规则同现有族(纯 C 类型、buf/size、owned/borrowed 注释、错误码)。 + **新 C 函数必须配单元测试**(注册 `make_oakengine_test`)。 +4. **undoable 编辑** → 照 `engine/src/capi/node.cpp` 的 `push_or_run` 模式; + 用户语义上的单次操作必须单条 undo(`oakengine_folder_move_child` 是样板)。 + +## 6. 红线(本阶段修过的真实 bug,不得再犯) + +- **R1(ODR/符号介入)**:app 侧严禁用与 engine 相同限定名定义非 inline 符号。 + 确需同名本地副本,必须对该源文件 `-fvisibility=hidden`(`app/CMakeLists.txt` + 有样板;`#pragma GCC visibility` 对已被 engine 头以 default 声明过的符号无效)。 +- **R2(禁 no-op stub)**:facade 函数不许返回假成功(`oakengine_export_render_ + with_params` 曾是 stub;`oakengine_clip_set_media_in` 曾是直接写不可撤销)。 + 不可撤销的改图操作就是 bug——`set_media_in` 不入栈导致 `project_undo` 误删 clip。 +- **R3(undo 语义)**:`oakengine_undo_push(command, name)` 只 2 参。删除任何 + `push` 必须同步接上命令执行路径(命令不压栈 = 静默不执行 + 泄漏)。 +- **R4(单位与索引)**:facade 的 `time_ts` 是**秒**(toggle/has/closest/dragger/ + get_input_at_time 等);keyframe 的 `track`/`track_for_time` 是 **1-based**, + `set_*_many` 的 tracks 是 **0-based**;序列/clip 的 ts 用 `timestamp_to_time` + 换算,不许硬编码 `/30`。 +- **R5(POD 构造)**:`VideoParams` 用带参构造(四参 w/h/format/channels,depth=1; + 默认构造 depth=0 会让 Vulkan 上传 0 字节纯黑);`Rational` 分子是 **32 位 int**, + 哨兵值用 `INT_MAX`(`RATIONAL_MAX`),不许 `INT64_MAX`(溢出成负数)。 +- **R6(engine 语义边界)**:`Track::is_range_free` 排除 GapBlock;probe 句柄 + (`oakengine_footage_probe`)不带项目节点,import-only 族必须返回 E_INVALID; + `oakengine_sequence_add_sequence_clip` 必须查间接循环嵌套(上游依赖图含目标 + 序列即拒绝),否则真成环导致 `invalidate_cache` 数万帧递归栈溢出。 +- **R7(buf/size 约定)**:`string_to_buf` 传 NULL 也返回长度;返回 `>0` 的 + 长度查询不得对 NULL buf 特判返回 0(`group_add_input_passthrough` 曾犯)。 +- **R8(接手验证)**:任何交接后先全量构建 + 全量 ctest + nm 复核,再动手; + 不采信上一手的完成声明(包括本文 §0,以你实测为准)。 + +## 7. 里程碑 + +1. 第一批(工具/服务)清零 → 总数应跌破 ~330。 +2. 第二批(项目/素材/序列)清零 → ~240。 +3. 第三批(时间线/命令)清零 → ~170。 +4. 第四批(Node 大族)清零 → ~115。 +5. 第五批(GPU)+ B11c/B11d 收口 → 只剩豁免清单(AudioProcessor 4 + + Block/Track::staticMetaObject = 6)→ `nm -D --defined-only liboakengine.so + | grep -c " T _Z"` = 0 → 全量终验。 diff --git a/docs/zh/r5-final-sprint.md b/docs/zh/r5-final-sprint.md new file mode 100644 index 000000000..e4428c62d --- /dev/null +++ b/docs/zh/r5-final-sprint.md @@ -0,0 +1,135 @@ +# R5 最终冲刺计划:88 → 豁免清单(≤6) + +> 面向执行者(GLM-5.2 继续,或任何接手代理)。自包含。 +> 基线:`d188ef116` 之后。前置:`c-abi-migration-handoff-v6.md`(状态与 +> 规则,全部仍然有效)、`r5-phase3-final-guide.md`(红线和验收)。 +> 本文只定义剩余 88 个符号的收尾批次 G1-G4。 +> 每批闭环不变:全量构建 0 error → 全量 ctest 绿 → nm 实测 → 立即提交。 + +--- + +## 0. 现状(GLM-5.2 R5 冲刺实测) + +``` +nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive" # 58 +``` + +从 88 消除至 58(-30)。G1(Node 信号清零,-22)、G2(渲染族信号 + RenderManager, +-8)已完成。G3(UndoCommand C ABI)已替换直接调用但符号从 inline 拉入。 +G4(豁免落实)进行中:58 符号逐条写入 §6.4 豁免清单(6 类理由)。 + +| 簇 | 数 | 批次 | 状态 | +|---|---|---|---| +| MOC staticMetaObject | 9 | G4 豁免 | app 信号/槽参数类型引用 | +| Inline 函数拉入 | 8 | G4 豁免 | engine 头 inline 方法引用 | +| AudioProcessor | 5 | G4 豁免 | 实时回调边界(v3 预批) | +| 渲染/GPU 边界 | 13 | G4 豁免 | OpenGL 对象直接创建 | +| 色彩管理 | 6 | G4 豁免 | 无 C ABI 等价物 | +| 无 C ABI | 17 | G4 豁免 | 需新增 facade 函数 | + +| 簇 | 数 | 批次 | +|---|---|---| +| Node(信号连接为主) | 26 | G1 | +| 渲染族(Renderer/PlaybackCache/Frame/DynamicRenderer/DraggableGizmo/Texture/OpenGLRenderer/ColorProcessor/AudioWaveformSync/AudioSynchronizer) | 25 | G2 | +| 长尾(NodeValue 4、ManagedColor 4、VideoParams 3、UndoCommand 3、TimelineMarker 2、Sequence 2、RenderManager 2、ViewerOutput 1、UndoStack 1、SubtitleBlock 1、ShapeNodeBase 1、Project 1、MultiCamNode 1、FrameHashCache 1、AudioWaveformCache 1) | 28 | G3 | +| 豁免候选(AudioProcessor 5、plugin 4) | 9 | G4 | + +## 1. 批次 G1:Node 信号清零(26 符号,主攻) + +**作战地图**:53 处 `connect(x, &Node::signal, ...)`,11 个文件 +(按此顺序做,从依赖少的开始): + +1. `app/widget/nodeview/nodeviewitem.cpp`、`nodeviewcontext.cpp`、 + `nodeview.cpp` +2. `app/widget/nodeparamview/nodeparamviewitem.cpp`、 + `nodeparamviewarraywidget.cpp`、`nodeparamviewconnectedlabel.cpp`、 + `nodeparamviewkeyframecontrol.cpp`、`nodeparamviewwidgetbridge.cpp`、 + `nodeparamview.cpp` +3. `app/widget/keyframeview/keyframeviewinputconnection.cpp` +4. `app/widget/projectexplorer/projectviewmodel.cpp`(`label_changed` 的 + 直连,换 `OAKENGINE_EVENT_NODE_LABEL_CHANGED`) + +**做法**(每处相同): +- 事件 ID 已分配(70-95),`EngineEventBridge` 的 node_* 信号已存在, + 只差在用类里加 `EngineEventBridge *bridge_` 成员、subscribe、 + connect bridge 信号。 +- **sender() 陷阱**:bridge 迁移后槽函数里 `sender()` 是 bridge 不是 + Node。信号参数里带 `OakEngineNode *source`,用它。(ProjectViewModel + 段错误就是这个,已修过一次,别再来。) +- **订阅泄漏**:bridge_ 由父对象持有则随父析构自动解绑;裸 + `oakengine_event_subscribe`(userdata=this)必须在析构 unsubscribe。 +- **重复连接守卫**:在会多次执行的函数里 `connect(bridge_, ...)` 要 + 么加一次性 flag(参照 `seekablewidget.cpp::set_markers` 的 + `marker_connects_done_`),要么在成员初始化时只连一次。 +- 消不掉的 `staticMetaObject`(qobject_cast/模板 connect 残留)按 + v3 §6.4 写理由进豁免清单,预期 1-2 项。 + +**验证**:G1 完成后 Node 应只剩豁免项(staticMetaObject ± link/unlink +的 typeinfo)。`olive-gtest` 里 nodeview/nodeparamview/projectexplorer +相关用例必须全绿。 + +## 2. 批次 G2:渲染族(25 符号) + +逐个 grep 定位,先查现成 facade:`oakengine/playback.h`、`preview.h`、 +`renderer.h`、`gizmo.h`、`color.h`。引用点集中在 +`app/widget/viewer/viewerdisplay.cpp`、`app/widget/manageddisplay/`、 +`app/widget/audiomonitor/`、`app/widget/scope/`。 + +- `DraggableGizmo`(3):gizmo facade 已有(B9e),把残留直连换完。 +- `ManagedColor`(4):在 `colorprocessorhandle` 一带,POD 经 + `oakengine/color.h` 传递。 +- `Frame`(3)、`Texture`(2):`FrameHashCache`/`AudioWaveformCache` 的 + 句柄化参照 `cliphandle.h` 模式(app 侧 inline 适配头, facade 取数据)。 +- 剩下 Renderer/PlaybackCache/DynamicRenderer/OpenGLRenderer/ + ColorProcessor/AudioWaveformSync/AudioSynchronizer:若是 + staticMetaObject/信号,同 G1 处理;若是虚函数链调用,包 facade 函数。 + +**GPU 边界提醒**:这批不许碰渲染管线的实际行为,只换调用方式。 +渲染用例(`ViewerDisplayReproTest` 可跑通的那 3 个)不能变得更差。 + +## 3. 批次 G3:长尾(28 符号) + +逐类处理,多数一处两处: +- `NodeValue`(4):多是 `NodeValue::Type` 的 typeinfo/静态引用——用 + `oak_node_value_type` 的 C 枚举替换(**注意两套枚举序数不同**,映射 + 函数参照 `nodevaluetree.cpp` 的 `node_value_type_to_c`,不要强转)。 +- `UndoCommand`(3):残留的 `UndoCommand*` 类型引用,换 `void*` + facade。 +- `VideoParams`(3)、`Sequence`(2)、`Project`(1)、`ViewerOutput`(1): + vieweroutpututils 模式收口。 +- `TimelineMarker`(2)、`RenderManager`(2)、`UndoStack`(1)、 + `SubtitleBlock`(1)、`ShapeNodeBase`(1)、`MultiCamNode`(1)、 + `FrameHashCache`(1)、`AudioWaveformCache`(1):grep 定位单点, + 大概率是 static_cast/构造/qobject_cast,直接换 facade。 + +## 4. 批次 G4:豁免落实 + 终验 + +1. 把 AudioProcessor(5)、plugin::PluginProgressReporter(4)、 + staticMetaObject 残留(若有)逐条写进 + `c-abi-migration-handoff.md` §6.4 豁免清单(每条一句理由)。 +2. 终验(全过才算 R5 完成): + - `nm -D` oak-editor ≤ 6 且全在豁免清单;oak-render-worker = 0。 + - 全量构建 0 error;全量 ctest 绿(flaky 规则照旧)。 + - 反作弊:app 无 dlsym/dlfcn;`git diff 476714ada~1..HEAD -- engine/` + 无 inline 化、无 stub;grep 全仓库无 `// simplified`、 + `// NOTE: simplified` 类"语义简化"注释。 +3. 更新 `facade-migration-roadmap.md`(G 批次记录)、 + `r5-phase3-final-guide.md` 状态节、handoff §6.4。 +4. **R5 完成哨**:向用户报告,由用户宣布 R5 结束——随后 + `plans/gtest-migration-guide.md` 与 `plans/ui-redesign-plan.md` + 解锁(两者互为并行,见各自文档)。 + +## 5. 给执行者的自查清单(含 GLM 本轮新增教训) + +- **语义不可"简化"**:GLM 在 `set_value_hint` 里传 `(0, 0, nullptr)` + 并注释"simplified"——这就是 stub,不管名字叫什么。facade 参数映射 + 必须完整(type/index/tag 一个不能丢),映射不了就扩 facade,不许 + 丢字段。 +- 两套值类型枚举(engine `NodeValue::Type` vs C `oak_node_value_type`) + **序数不同**,必须显式映射函数,禁止 `int(t)` 强转。 +- undo 聚合用 `oakengine_undo_group_*`;单个 facade 调用即一条 undo + 的场景才允许单推。 +- 事件订阅:谁 subscribe 谁 unsubscribe(析构或换绑时);connect + bridge 信号防重复。 +- 时间单位:facade 时间戳是 `int64_t` 帧戳(timebase 转换用 + `Timecode::time_to_timestamp`),秒是 Rational num/den 对,别混。 +- 提交信息标题写 nm 实测数。 diff --git a/docs/zh/r5-phase2-detailed-guide.md b/docs/zh/r5-phase2-detailed-guide.md new file mode 100644 index 000000000..2c551bacc --- /dev/null +++ b/docs/zh/r5-phase2-detailed-guide.md @@ -0,0 +1,164 @@ +# R5 第二阶段详细指引(剩余 339 符号) + +> 本文是 `r5-app-migration-guide.md` 的续篇,针对当前剩余的 339 个符号给出 +> **逐组、逐符号**的替换映射。面向没有此前对话记忆的执行者。 +> 工作分支:`c-abi-migration`。每文件/小步立即提交。 + +--- + +## 0. 验收结论(2026-07-24 实测) + +- 符号:395 → **339**(DS 的 R5 1B–2 批次,质量良好)。 +- 构建:绿。测试:**44/44 绿**(`oak_cli_transcode` 一次 SEGFAULT 单独重跑即过, + 属已知 flaky;期间修复 `ExportFormatComboBox` 初值应为 format 总数而非 -1 的 + 1 个回归,已提交)。 +- 当前状态符合 R5 指引预期,可以按本文继续。 + +## 1. "剩余符号必须集群重写"——第三次证伪 + +DS 再次声称剩余符号"都需要文件集群级的全量重写(虚函数链、Q_OBJECT 信号槽、 +模板头文件引用)"。**逐符号核对后,每一组都有现成、已测的 facade 函数可直接 +替换**(§2 全表)。所谓"Q_OBJECT 信号槽",由事件机制( +`oakengine_event_subscribe` + `EngineEventBridge`)解决;"虚函数链"从不涉及—— +迁移替换的是 **app 侧调用点**,engine 类原样留在 liboakengine 内部;"模板头文件 +引用"(`NodeTraverser`、`NodeValueTable`)已由 `oakengine/traverse.h` 覆盖。 +**没有一组需要重写。** + +判据:如果某符号在 §2 表里能查到 facade 对应物,它就是普通替换,不是重写。 + +## 2. 逐组符号 → facade 映射 + +### 2.1 ColorManager(15)——colordialog、colorbutton、colorwheel 系列、projectproperties、colordialog + +| 符号 | facade(`oakengine/color.h`) | +|---|---| +| `list_available_colorspaces` | `oakengine_color_manager_colorspace_count/_at` | +| `list_available_displays` | `oakengine_color_manager_display_count/_at` | +| `list_available_views` | `oakengine_color_manager_view_count/_at` | +| `list_available_looks` | `oakengine_color_manager_look_count/_at` | +| `get_compliant_color_space(ColorTransform,bool)` | `oakengine_color_manager_compliant_transform` | +| `get_compliant_color_space(QString)` | `oakengine_color_manager_compliant_color_space` | +| `get_default_display` | `oakengine_color_manager_default_display` | +| `get_default_view` | `oakengine_color_manager_default_view` | +| `set_config_filename` / `get_config_filename` | `oakengine_color_manager_set/get_config_filename` | +| `set_default_input_color_space` | `oakengine_color_manager_set_default_input_color_space` | +| `get_default_config` | `oakengine_color_config_load_default` + `oakengine_color_config_free` | +| `create_config_from_file` | `oakengine_color_config_load_file` + `oakengine_color_config_free` | +| `config_changed` / `reference_space_changed`(信号) | 事件 `OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED`/`_REFERENCE_SPACE_CHANGED`(60/61)+ EngineEventBridge | +| `ColorProcessor::create` | `oakengine_color_processor_create`/`_free`/`_convert_color` | +| `staticMetaObject` | 随信号迁走事件机制后消失 | + +`oakengine_color_manager_from_project` 取项目色彩管理器句柄; +`reference_color_space`/`default_luma_coefs` 已有。全部 buf/size 约定。 + +### 2.2 EngineCore(17,含 Q_OBJECT)——core.cpp、mainwindow、各 panel + +| 符号 | facade(`oakengine/app.h`) | +|---|---| +| `staticMetaObject` / `qt_metacall` / `qt_metacast` / `EngineCore(CoreParams)` / `CoreParams()` | app 不再直接引用 `olive::EngineCore` 类型(`Core` 已组合转发),MOC 符号随类型引用消失而消失 | +| `set_active_project` | `oakengine_app_set_active_project` | +| `add_open_project` | `oakengine_app_add_open_project` | +| `remove_recently_opened_project` | `oakengine_app_remove_recent_project` / `oakengine_app_clear_recent_projects` | +| `get_auto_recovery_index_filename` | `oakengine_app_auto_recovery_index_filename` | +| `set_language` | `oakengine_app_set_language` | +| `set_autorecovery_interval` | `oakengine_app_set_autorecovery_interval` | +| `on_project_saved` | `oakengine_app_on_project_saved` | +| `set_close_project_handler` / `set_confirm_image_sequence_handler` / `set_load_layout_handler` / `set_relink_handler` / `set_save_project_handler` | `OakEngineAppCallbacks` + `oakengine_app_set_callbacks`(回调结构一次性注册) | + +**MOC 消除范式(Q_OBJECT 通用)**:app 侧不再出现 `EngineCore*`/`ColorManager*`/ +`RenderTicketWatcher*` 等 QObject 类型的成员、信号参数或 `qobject_cast`,全部改持 +facade 句柄 + `EngineEventBridge` 订阅。类型引用消失 → moc 生成的 +`staticMetaObject/qt_metacall/qt_metacast` 符号自动消失。**这就是"Q_OBJECT 信号槽" +的全部解法,不需要动任何类。** + +### 2.3 NodeTraverser(6)——nodeparamview、curvewidget、viewerdisplay + +| 符号 | facade(`oakengine/traverse.h`) | +|---|---| +| `generate_database` | `oakengine_traverse_generate_database`(返回 `OakEngineTraverseDb*`,配 `oakengine_traverse_db_free` + `db_input_count/id` + `row_count` + `row_*` 访问器) | +| `generate_table` | `oakengine_traverse_generate_table` | +| `generate_row` | `oakengine_traverse_generate_row` | +| `generate_row_value_element_index` | `oakengine_traverse_table_element_index_for_hint` | +| `transform` | `oakengine_traverse_transform` | +| `NodeTraverser()`(构造) | 不再直接构造,全部走上述函数 | + +`NodeValueTable`/`NodeValueRow` 的访问经 `oakengine_traverse_db_*` / +`oakengine_traverse_row_*` 访问器完成,模板类型不出边界。 + +### 2.4 QtUtils(9)——纯函数,搬 app(不是 facade) + +`create_horizontal_line`、`create_vertical_line`、`flip_control_and_shift_modifiers`、 +`get_formatted_date_time`、`q_font_metrics_width`、`set_combo_box_data(int)`、 +`set_combo_box_data(QString)`、`to_q_color`、`word_wrap_string` + +全部含 Qt 类型,**按 B10 模式搬到 `app/common/`**(新建 `app/common/qtutilsapp.h`, +inline 函数,namespace `olive` 不变以减少调用点改动;对该源文件 +`-fvisibility=hidden`,防 ODR 介入,见 r5 指引 §6-R1)。engine 内部用副本继续用 +engine 的 qtutils。 + +### 2.5 RenderTicketWatcher(7)+ RenderTicket(5)——viewer.cpp、timelinewidget + +| 符号 | facade(`oakengine/preview.h`) | +|---|---| +| `RenderTicketWatcher::set_ticket` | `oakengine_preview_request_single_frame` / `oakengine_preview_request_audio_range`(内部即 ticket+watcher 封装) | +| `RenderTicketWatcher::finished`(信号) | `oakengine_preview_request_set_finished_callback`(facade 自有 C 回调,不占事件号) | +| `RenderTicketWatcher::get` / `has_result` | `oakengine_preview_request_get_frame` / `oakengine_preview_request_has_result` | +| `RenderTicketWatcher::cancel` | `oakengine_preview_request_free` | +| `RenderTicketWatcher::RenderTicketWatcher` | 不再直接构造 | +| `RenderTicket` ctor/`start`/`finish`/`get` | 由 request 族内部完成,app 不再接触 | + +**红线**:`oak_playback_frame.linesize` 是**字节**;重建 display `Frame` 用四参 +`VideoParams(w,h,format,k_internal_channel_count)`(depth=1),防 depth=0 黑屏 +(r5 指引 §6-R5)。 + +### 2.6 Track(13)+ ClipBlock(12)——timelinewidget、trackview、timelineview、seekablewidget + +全部走 `oakengine/timeline.h`:track 查询(count/at/type/length/is_range_free)、 +clip 输入 id getter、`clip_get_range`/`clip_set_media_in`(undoable)/trim/ +ripple/transition/add_footage_clip/add_sequence_clip。`ClipBlock::ClipBlock()` 等 +ctor → `oakengine_node_create_undoable` / `oakengine_sequence_add_footage_clip`。 + +### 2.7 Node(40)+ NodeGroup(9)+ NodeKeyframe(7)——nodeview、nodeparamview、curvewidget、keyframeview、nodetableview、nodevaluetree、multicam、nodecombobox + +最大一组,全部走 `oakengine/node.h`(~60 函数): +- 输入元数据/值:`node_get_input_*`、`node_set_input`(undoable)、`get_input_at_time` +- 关键帧:`node_keyframe_*`(导航/toggle/set_type_many/dragger/clear) +- 图操作:`node_factory_*`、`node_add/connect/disconnect/copy_inputs/link` +- group:`group_add_input_passthrough`/`group_input_passthrough_*`/`group_resolve_input` +- context:`node_context_*`、`node_set_context_position` +- 命令类(`NodeAddCommand`/`NodeEdgeAddCommand`/`NodeRenameCommand` 等)→ 对应 + undoable 原语,**不要新建 app 侧命令类**。 + +### 2.8 ViewerOutput(9)+ VideoParams(5)——viewer、footageviewer、viewerdisplay、panels + +`oakengine/viewer.h`(playhead、length、video/audio params、workarea、 +`oakengine_viewer_from_node`)+ `oakengine/videoparams.h`(`oak_video_params` POD + +`oakengine_video_params_make/_equal/_is_valid/_bytes_per_pixel`)。 + +## 3. 执行顺序与闭环 + +按 DS 已验证有效的顺序继续(与 r5 指引一致): + +1. **2.4 QtUtils**(9,纯搬动,最快)+ **2.1 ColorManager**(15) +2. **2.3 NodeTraverser**(6)+ **2.5 RenderTicket/Watcher**(12) +3. **2.2 EngineCore MOC**(17,类型引用消除) +4. **2.6 Track/ClipBlock**(25)+ **2.8 ViewerOutput/VideoParams**(14) +5. **2.7 Node 大族**(56,最后攻坚) + +每批闭环:`nm` 基线 → 逐文件替换(r5 指引 §2 方法)→ 构建 0 error → +**全量 ctest 44/44 绿** → `nm` 双度量净减 → 立即提交 → roadmap 附 C 补记。 +**全量 ctest 不绿不得进入下一批。** + +## 4. 每完成一组的预期 + +| 完成组 | 累计消除 | 剩余约 | +|---|---|---| +| 2.4+2.1 | 24 | 315 | +| +2.3+2.5 | 18 | 297 | +| +2.2 | 17 | 280 | +| +2.6+2.8 | 39 | 241 | +| +2.7 | 56 | ~185(含豁免 6) | + +最终只剩豁免清单(AudioProcessor 4 + `Block/Track::staticMetaObject` 2 = 6)。 +消不掉且确属架构原因的,按 v3 §6.4 格式进豁免清单并写理由(`TrackListRippleToolCommand` +是目前唯一预定的遗留评估点)。 diff --git a/docs/zh/r5-phase3-final-guide.md b/docs/zh/r5-phase3-final-guide.md new file mode 100644 index 000000000..7d6d621a3 --- /dev/null +++ b/docs/zh/r5-phase3-final-guide.md @@ -0,0 +1,203 @@ +# R5 终局计划:181 → 豁免清单(≤6) + +> 面向执行者(DeepSeek Flash),自包含。工作分支:`c-abi-migration`。 +> 前置文档:`../facade-migration-roadmap.md`(批次记录)、 +> `../r5-app-migration-guide.md`(R5 总指引)、`../c-abi-migration-handoff.md` +> (v3,§6.4 豁免清单格式)。本文是 R5 的**最后一个阶段**: +> 处置当前 WIP → 修完已记录缺陷 → 把剩余 181 个 `olive::` 符号收到豁免清单。 +> 每批闭环:全量构建 0 error + 全量 ctest 绿 + nm 复核 + 立即提交。 +> +> **状态**:第 0 批(§3)与第 1 批(§4)已由 Kimi 完成。期间新增两处 +> 计划外修复:ProjectViewModel 的 `sender()` 崩溃(bridge 迁移后槽函数 +> 仍用 `sender()` 取 Folder,拿到的是 bridge 指针,段错误)与 +> `oakengine_folder_move_child` 语义修正(原来只加不删,"移动"后节点 +> 同时存在于两个文件夹;已改为 删旧+加新,并新增批量版 +> `oakengine_folder_move_children` 供拖放一次移动多项)。 +> 剩余:批 F1-F6(§5)。 + +--- + +## 1. 现状(2026-07-24 实测,GLM-5.2 交接) + +``` +nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive" # 88 +``` + +GLM-5.2 从 131 消除至 88(-43)。已完成:F3(Task/TimelineWorkArea/ +ViewerOutput/Project 全部)、F4 方法调用(14 符号 + 7 新 facade 函数)、 +F6 部分长尾(7 构造器 + 5 静态字符串)。 + +剩余 88 符号分布: + +| 簇 | 数 | 说明 | +|---|---|---| +| Node 信号+staticMetaObject | 26 | 22 信号 + staticMetaObject + link + set_standard_value + set_value_at_time | +| AudioProcessor | 5 | 豁免候选(实时音频回调边界) | +| plugin::PluginProgressReporter | 4 | 豁免候选(MOC 四件套) | +| 渲染族 | ~25 | Renderer/PlaybackCache/Frame/DynamicRenderer/DraggableGizmo/OpenGLRenderer/Texture/ColorProcessor/AudioWaveformSync/AudioSynchronizer 等 | +| 长尾 | ~28 | NodeValue/ManagedColor/VideoParams/UndoCommand 等 1-4 符号类 | + +**下一任主攻**:Node 信号连接迁移(23 符号,事件 ID 70-95 已全部分配, +EngineEventBridge 信号已存在,需为 9 个类添加 bridge 成员)。 + +## 2. 红线(新增两条,违反即返工) + +R5 既有红线不变:不改 `oakengine_*` 已发布签名、不删测试、不用 +`git checkout --`/`restore`/`clean`/`reset --hard`/`stash`、每批立即提交。 +新增: + +1. **禁止 inline 化 engine 实现刷符号**。把 engine 的 `.cpp` 实现搬进头文件 + (如本次 WIP 对 `ManagedColor`、`TimelineWorkArea` 做的那样)不会让 + 依赖消失,只是让 app 内联编译 engine 代码——C ABI 边界被架空,nm 数字 + 是假的。判定方法:engine 目录下的任何 `.h` 出现新的非平凡函数体即违规。 +2. **禁止 no-op stub**。`load()` 返回 true、`save()` 空体、`// apply ...` + 空循环这类"假成功"是最严重违规(本次 WIP 的 + `TimelineWorkArea::load/save` 即为例:项目文件的工作区会全部丢失)。 + 任何行为变更必须在提交信息里写明并接受 review。 +3. **禁止 dlsym/GetProcAddress 等运行时解析 engine C++ 符号**(DS 在 + `app/common/nodefactorywrapper.cpp` 用过,nm 统计不到 ≠ 依赖不存在)。 + facade 只能在 `engine/src/capi/` 实现、`engine/include/oakengine/` + 声明。详见 `c-abi-migration-handoff-v5.md` §2。 + +## 3. 第 0 批:当前 WIP 处置(最先做,单独一个提交) + +工作区现有 DS 停手时留下的未提交改动,分两类: + +### 3.1 回退(engine 被改坏的部分,4 个文件 + 2 个 CMake) + +用 `git show HEAD: > ` 恢复(**禁止** `git checkout --`): + +- `engine/render/managedcolor.h`、`engine/render/managedcolor.cpp`、 + `engine/render/CMakeLists.txt`(inline 化,红线 1) +- `engine/timeline/timelineworkarea.h`、`engine/timeline/timelineworkarea.cpp`、 + `engine/timeline/CMakeLists.txt`(inline 化 + load/save stub,红线 1+2) + +`TimelineWorkArea` 的 3 个符号(enabled/range 信号与 staticMetaObject) +按 §5 批 F4 的正路处理。 + +### 3.2 保留并修复(app 侧 ProjectSerializer → clipboard facade 迁移,方向正确) + +涉及:`timelinewidget.{h,cpp}`、`nodeparamview.{h,cpp}`、`nodeview.cpp`、 +`keyframeview.cpp`、`resizabletimelinescrollbar.{h,cpp}`。修三处: + +1. `nodeview.cpp::copy_selected`:`char buf[65536]` 固定缓冲会截断大节点图 + 的 XML。改两段式:先 `oakengine_clipboard_save_to_xml(cb, nullptr, 0)` + 取所需长度,再 `QByteArray(len+1, '\0')` 分配写入。全仓库同类 + buf/size 调用(`oakengine_project_filename` 等 512/256 定长)一并不再 + 扩大,仅本处改两段式(XML 体积无上限,文件名有)。 +2. `resizabletimelinescrollbar.cpp::connect_work_area`:已改为 + `oakengine_event_subscribe`(方向对,解决了直连 engine 信号),但裸 + 订阅的 userdata 是 `this`,**必须在析构里 + `oakengine_event_unsubscribe` 两个 id**,否则 widget 先死、workarea + 后发事件即悬垂回调。 +3. `nodeparamview.cpp::paste` 里的空循环 `// apply position from map`: + 原代码同样是建了 `PositionMap` 未使用(上游 Olive 遗留死代码),不算 + 回归,但既然碰了就删掉这个死块,别留占位注释。 + +闭环后提交(提交信息注明:clipboard 迁移 + 三处修复 + engine 回退)。 + +## 4. 第 1 批:已记录缺陷修复(review 累积清单,决策已写死) + +按序修,一个提交;每项都给出现象与定死的修法: + +1. **NodeParamView::DeleteSelected 重连静默失败**(严重)。 + 现码先 `oakengine_node_connect` 后 `oakengine_nodes_delete_many`, + 而契约规定输入已占用时 connect 返回 `E_STATE`——重连必然失败。 + 修法:facade 新增 + `int oakengine_nodes_delete_many_ex(nodes, contexts, node_count, edge_outputs, edge_input_nodes, edge_input_ids, edge_input_elements, edge_count, reconnect_outputs, reconnect_input_nodes, reconnect_input_ids, reconnect_input_elements, reconnect_count)` + ——engine 内部在**同一条** `NodeViewDeleteCommand` 里先删后连 + (redo 序:delete → reconnect),undo 序反向。NodeParamView 只收集 + 重连边传入,不自己 connect。`oakengine_nodes_delete_many` 保留, + 等价于 `_ex` 传 reconnect_count=0。 +2. **ProjectViewModel 切项目丢事件**(严重)。`set_project` 重建 + `bridge_` 后没重连 4 个 folder 信号。修法:把构造里的 4 个 + `connect(bridge_, ...)` 抽成私有 `connect_bridge_signals()`, + 构造函数与 `set_project` 重建后都调。 +3. **NodeParamViewWidgetBridge dragger 泄漏**。类无析构, + `oakengine_dragger_create` 的句柄永不 free。修法:加析构调 + `oakengine_dragger_free(dragger_)`。 +4. **TimeBasedWidget 旧订阅泄漏**。`connect_viewer_node` 只 + `disconnect(bridge_, nullptr, this, nullptr)`,engine 侧订阅不释放。 + 修法:`EngineEventBridge` 加 `unsubscribe_all()`(对 + `subscriptions_` 逐个 unsubscribe 并清空),在 disconnect 旁调用。 +5. **边-only 删除 undo 拆分**。`NodeView::delete_selected` 纯边分支逐边 + `oakengine_node_disconnect_ex`,N 条边 N 条 undo。修法:放宽 + `oakengine_nodes_delete_many` 契约允许 `node_count==0 && edge_count>0` + (纯边删除,报错条件改为两者同时为 0),边-only 分支改走 delete_many。 +6. **seekablewidget marker 订阅泄漏**。`set_markers` 建 3 个订阅 + (ADDED/REMOVED/MODIFIED)只存 1 个 id。修法: + `QVector marker_subs_` 存全 3 个,重设/析构全解(对齐 + `resizabletimelinescrollbar.cpp::connect_markers` 的正确模式)。 +7. **core_params 0x1 陷阱 + core.h 死代码**。`app/core.cpp:1509` + `Core::core_params()` 解引用 `0x1`;`app/core.h:337` 残留 + `EngineCore *engine_core_` + `#include "coreengine.h"` + 5 个空操作 + handler setter。修法:全删(core_params 无调用方,直接删方法)。 +8. **小项打包**: + - `projectviewmodel.cpp::connect_item` 死参数 `subscribe`(无 false + 调用点)——删参数。 + - `vieweroutpututils.h:50` 死声明 + `viewer_output_video_params_from_oak`——删。 + - `curveview.cpp` 两处 `(type == 0) ? 1 : 0` 魔法数——facade 加 + `int oakengine_keyframe_opposing_bezier_type(int type)`,调用替换; + `(opposing_type == 0) ? 0 : 1` 恒等式一并简化。 + - `export.cpp` `image_sequence_check_box_changed` 的裸 `{ }` 块缩进 + 乱——clang-format 归位。 + +## 5. 第 2+ 批:符号收尾(按簇,难度从低到高) + +每批做法相同:grep 定位引用源 → 按既有模式迁移(facade 函数 / +`cliphandle.h` 式 app 侧 inline 适配头 / CustomUndoCommand 回调 / +EngineEventBridge 订阅)→ 闭环提交。禁止走 §2 两条红线的捷径。 + +- **批 F1 撤销命令族(~30,25 个类)**:多数已有 facade 等价物直接换 + (`NodeEdgeAddCommand`→`oakengine_node_connect`, + `NodeEdgeRemoveCommand`→`oakengine_node_disconnect_ex`,Marker 五命令 + →`oakengine_marker_*` 族)。无等价物的按 `a86cb6c99` 的 + `oakengine_undo_command_create` 回调模式迁移。`TrackListRippleToolCommand` + 按 v3 §176 行处理:先尝试现有 timeline 原语组合,不行设计 + `oakengine_tracklist_ripple_*` 小族,再不行写理由进豁免清单。 +- **批 F2 Track/ClipBlock/NodeGroup/NodeKeyframe(30)**:属性访问走 + `trackhandle.h`/`cliphandle.h` 模式扩展;信号走 EngineEventBridge。 +- **批 F3 Task/Project/ViewerOutput/VideoParams(18)**:剩余多为 + `VideoParams` 构造重载(vieweroutpututils 已收口一半)与 Task 信号 + (已有 task 事件族,照 taskviewitem 先例)。 +- **批 F4 Node 大族(39,最难,放靠后)**:逐个符号 grep 定位。预期构成: + `qobject_cast`(改 `oakengine_node_type_id` 比较 + static_cast)、 + `staticMetaObject`(改字符串式 connect 或事件订阅,消不掉按 §6.4 豁免)、 + inline 方法拉的 vtable/typeinfo(把调用点换 facade)。 + `TimelineWorkArea`(3)、`UndoCommand`(3)、`Block`(3)、`NodeGroup` 残余 + 与本批同法。 +- **批 F5 渲染族(~25)**:Renderer/PlaybackCache/DynamicRenderer/ + DraggableGizmo/OpenGLRenderer/Texture/Frame/ColorProcessor/ + AudioWaveformSync/AudioSynchronizer。多数在 viewerdisplay、 + manageddisplay、audiomonitor——playback/preview facade 族已存在 + (B9a-B9c),先查 `oakengine/playback.h`、`preview.h`、`renderer.h` + 有无现成函数。AudioProcessor(5) 目标压到 4 后整体进豁免清单 + (实时回调边界,v3 已预批)。 +- **批 F6 长尾(~35)**:1-symbol 类逐个过。`*Task`(3)、 + `RenderManager`(2)、`Sequence`(2)、`TimelineMarker`(2) 等,多为 + static_cast 或构造调用,facade 已有创建函数的直接换。 + +## 6. 验收(全部满足才算 R5 完成) + +1. `nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive"` ≤ 6, + 且每个剩余符号都在 `c-abi-migration-handoff.md` §6.4 豁免清单里、 + 各带一句理由。 +2. `nm -D` 检查 `oak-render-worker` 为 0。 +3. 全量构建 0 error;全量 `ctest --output-on-failure` 绿(已知 flaky + 规则:单独重跑一次,连续两次失败才算回归)。 +4. 反作弊审计: + - `git diff ..HEAD --stat -- engine/` 逐文件过一遍,确认无 + inline 化、无 stub(§2 两条红线的全量复核); + - grep engine 头文件无新增非平凡函数体。 +5. 更新 `facade-migration-roadmap.md` 批次记录与 + `c-abi-migration-handoff.md` 状态节;本文标注"已完成"。 + +## 7. 协作分工 + +- 第 0/1 批(WIP 处置 + 缺陷修复)由 **Kimi** 执行——这些是语义陷阱, + 需要逐行判断。 +- 批 F1-F6 由 **DeepSeek** 执行,Kimi 每批只读 review(不构建不运行), + 记录问题,批间统一修。 +- 任何"消不掉"的符号:先写清尝试过的方案,再按 §6.4 格式进豁免清单, + 由 Kimi 复核理由是否成立。 diff --git a/docs/zh/r6-cleanup-plan.md b/docs/zh/r6-cleanup-plan.md new file mode 100644 index 000000000..c397b452d --- /dev/null +++ b/docs/zh/r6-cleanup-plan.md @@ -0,0 +1,518 @@ +# 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 + 提交。 + +--- + +## 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`: + +```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_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`: + +```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 零散单点(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` 或对应域头): + +```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 消失而归零)。 + +--- + +## ✅ 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: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(n))`; +3. 接收槽同步改类型,槽内 `reinterpret_cast(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_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_color` facade;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`: + +```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 vtable,RIIR 友好)**: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 配对)。 + +--- + +## ✅ 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`)无回归。 +> +> **实现说明(与原设计提议的差异,已论证)**: +> 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/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 硬指标,且渲染路径行为零变化。 +> 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 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 区注释清楚)。 + +**遗留项(已记录,后续批次)**: + +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 已证), + 彻底清理为低优先级长项。 diff --git a/docs/zh/r7-pure-abi-plan.md b/docs/zh/r7-pure-abi-plan.md new file mode 100644 index 000000000..f7438e0dd --- /dev/null +++ b/docs/zh/r7-pure-abi-plan.md @@ -0,0 +1,212 @@ +# R7 计划:从"nm=0"到 Rust-ready 纯 C ABI + +> 面向执行者(Qwen 3.8 Max),自包含。工作分支:`c-abi-migration`。 +> **背景**:R6 已使 oak-editor / oak-render-worker 的 `U _ZN5olive` = 0 +> (`d13e4e800`)。但按 riir.md 的目标(engine 拆模块 → Rust 重写), +> 还差两层皮: +> 1. `oakengine/display.h` 的灰色契约——签名是 `void *`,语义却是 C++ +> (往调用方内存写 `std::shared_ptr`、参数实为 `olive::VideoParams*`); +> 2. liboakengine.so 仍导出 3486 个 C++ 符号(`nm -D --defined-only | +> grep " T _Z"`),Rust 模块化拆分要求导出面只剩 `oakengine_*`。 +> +> 三条红线照旧(禁 inline 化 engine 实现、禁 no-op stub、禁 dlsym)。 +> 每步闭环:全量构建 0 error → 全量 ctest 绿 → 立即提交。 +> flaky 规则:`oak_cli_transcode`/`oakengine_export_test`/`olive-gtest` +> 单独重跑一次,连续两次失败才算回归(`oak_cli_transcode` 的间歇 +> SEGFAULT 是预存问题,手动跑通过为准)。 + +--- + +## R7-A:display.h 灰色契约 POD 化 + +### A.1 现状问题(为什么 nm=0 还不够) + +`engine/include/oakengine/display.h` 的 11 个函数签名全是 `void *`, +但注释约定: +- `out_texture/out_frame` 指向调用方内存中的 `TexturePtr/FramePtr` + (`std::shared_ptr`),engine 在其上拷贝构造 shared_ptr; +- `video_params` 实为 `const olive::VideoParams*`; +- `color_job` 实为 `const olive::ColorTransformJob*`。 + +Rust 无法安全持有 shared_ptr、无法构造 C++ VideoParams。app 侧 47 处 +`TexturePtr/FramePtr` 成员/变量(18 文件,见 A.4)同样要换。 + +### A.2 新契约(设计钉死,照此实现) + +**所有权协议(核心决策,不许改)**:纹理/帧句柄是不透明指针,指向 +engine 堆上的控制块(内部持有 `std::shared_ptr`,实现细节对 ABI 不可见)。 +所有权经显式 retain/free 转移,禁止任何"写入调用方内存的 shared_ptr"。 + +```c +/* engine/include/oakengine/display.h —— 重写版 */ + +/* ---- 渲染器 ---- */ +OAKENGINE_API void *oakengine_display_renderer_create_dynamic( + const char *backend_id, void *parent_qobject); +OAKENGINE_API void *oakengine_display_renderer_create_opengl( + void *parent_qobject); +OAKENGINE_API int oakengine_display_renderer_init(void *renderer, + void *gl_context); +OAKENGINE_API void oakengine_display_renderer_destroy(void *renderer); + +/* ---- 纹理(句柄 = OakEngineDisplayTexture*,不透明) ---- */ +/* params 用 oak_video_params POD(oakengine/videoparams.h 已有), + * 不再是 const void*。 */ +OAKENGINE_API void *oakengine_display_texture_create( + void *renderer, const oak_video_params *params, + const void *pixels, int linesize); +/* retain:返回同一句柄并把内部引用计数 +1(跨线程移交用, + * 见 A.3 协议)。free:-1,归零时释放。二者对 NULL 均为 no-op。 */ +OAKENGINE_API void *oakengine_display_texture_retain(void *texture); +OAKENGINE_API void oakengine_display_texture_free(void *texture); +OAKENGINE_API int oakengine_display_texture_upload( + void *texture, const void *pixels, int linesize); +OAKENGINE_API int oakengine_display_texture_download( + void *texture, void *pixels, int linesize); +/* 只读属性查询(替代 texture->params()/width()/format() 等) */ +OAKENGINE_API int oakengine_display_texture_get_params( + const void *texture, oak_video_params *out); +OAKENGINE_API int oakengine_display_texture_id(const void *texture); + +/* ---- 帧(句柄 = OakEngineCodecFrame*,不透明,同协议) ---- */ +OAKENGINE_API void *oakengine_codec_frame_create(void); +OAKENGINE_API void *oakengine_codec_frame_retain(void *frame); +OAKENGINE_API void oakengine_codec_frame_free(void *frame); +OAKENGINE_API int oakengine_codec_frame_set_video_params( + void *frame, const oak_video_params *params); +OAKENGINE_API int oakengine_codec_frame_get_params( + const void *frame, oak_video_params *out); +OAKENGINE_API int oakengine_codec_frame_allocate(void *frame); +OAKENGINE_API void *oakengine_codec_frame_data(void *frame); +OAKENGINE_API int oakengine_codec_frame_linesize(const void *frame); + +/* ---- 色彩管理 blit ---- */ +/* oak_color_transform_job POD(新定义,字段以 engine + * ColorTransformJob 拍平:processor 句柄 + input/output 空间 id + + * 各向异性等)。 */ +OAKENGINE_API int oakengine_display_renderer_blit_color_managed( + void *renderer, const oak_color_transform_job *job, + void *dst_texture, const oak_video_params *params); + +/* 跨后端纹理下载(viewerdisplay 的 download_from_texture 路径) */ +OAKENGINE_API int oakengine_display_renderer_download_from_texture( + void *renderer, int texture_id, const oak_video_params *params, + void *dst_pixels, int linesize); +``` + +engine 实现(`engine/src/capi/display.cpp` 重写): + +```cpp +struct OakEngineDisplayTexture { olive::TexturePtr ptr; }; +struct OakEngineCodecFrame { olive::FramePtr ptr; }; +// create: new OakEngineDisplayTexture{renderer->create_texture(...)} +// retain/free: new/delete 控制块(引用计数即 shared_ptr 自身) +// POD↔C++:oak_video_params ↔ olive::VideoParams 的转换函数若 +// capi 已有(viewer.cpp 的 get 路径)就抽成内部共享 helper +// (放 engine/src/capi/videoparamsinternal.h),不许复制粘贴第三份。 +``` + +### A.3 跨线程移交协议(最容易写错的地方,钉死) + +`viewerdisplay` 的 `load_frame_`/`load_texture_` 在解码线程生产、 +显示线程消费。原语义靠 shared_ptr 引用计数保活。新协议: + +1. 生产侧 `oakengine_display_texture_retain(t)` 后写入共享槽; +2. 消费侧取走句柄,旧句柄 `free`; +3. 槽清空时持有一方负责 `free`。 + **每个 retain 必须配对恰好一个 free**。写完后 grep 审计配对数。 + +### A.4 app 侧触点清单(47 处,按文件做,每文件一提交) + +| 文件 | 处数 | 要点 | +|---|---|---| +| `app/widget/viewer/viewerdisplay.{h,cpp}` | 19+15 | 最大。`texture_`/`load_texture_`/gizmo 纹理全换句柄;析构与各 reset 路径补 free;A.3 协议主要在这里 | +| `app/widget/scope/scopebase/scopebase.{h,cpp}` | 6+9 | 同模式 | +| `app/widget/manageddisplay/manageddisplay.cpp` | 6 | create_texture/blit/download | +| `app/widget/viewer/viewer.{h,cpp}` | 1+9 | FramePtr 成员换句柄 | +| `app/widget/multicam/multicamdisplay.{h,cpp}` | 1+4 | | +| `app/widget/scope/histogram/histogram.{h,cpp}` | 2+1 | | +| `app/widget/scope/waveform/waveform.{h,cpp}` | 1+1 | | +| `app/widget/scope/vectorscope/vectorscope.{h,cpp}` | 1+1 | | +| `app/panel/viewer/viewerbase.h`、`app/panel/scope/scope.{h,cpp}` | 3 | | + +完成判据:app 全仓库 grep `TexturePtr|FramePtr` = 0; +`oakengine/display.h` 全文无 `shared_ptr`、无 `olive::` 出现在**签名** +(注释里也不许写 "olive::TexturePtr storage" 这种约定)。 + +### A.5 验证 + +- 新增 `engine/tests/oakengine_display_test.cpp`(无 GL 环境测错误 + 路径与 retain/free 配对;GL 相关断言用现有 backend 检测跳过模式)。 +- `olive-gtest` 的 `ViewerDisplayReproTest` 三个可跑通用例**必须全过** + (这是显示路径的回归网,挂一个就是真挂)。 +- 手动验证(报告里写明):打开素材 → 画面非黑;scope 面板渲染正常。 + +--- + +## R7-B:engine visibility 收口(3486 → 只导出 oakengine_*) + +### B.1 原理(已具备的条件) + +`OAKENGINE_API` 在 GCC/Clang 已是 +`__attribute__((visibility("default")))`(export.h:40)。给 +`oakengine` 目标加 `-fvisibility=hidden` 后,只有 `oakengine_*` 导出。 +`oakgl`/`oakvulkan` 两个动态后端同理(已有 `*-cabi-check` OBJECT +目标,顺带确认它们导出面也只剩 C ABI)。 + +### B.2 唯一难点:测试链接 + +隐藏符号后,直接引用 engine C++ 内部的测试会断链: +- `olive-gtest`:**1006** 个 `U _ZN5olive` +- `timeline-tests`:29 +- `oakengine_export_test`:9(`make_oakengine_test` 族里引用内部的) +- `compositing-tests`:0(纯 facade,无碍) + +**解法(钉死)**:engine 源码改出 OBJECT 库,测试链对象文件而非 +`.so`: + +```cmake +# engine/CMakeLists.txt +add_library(oakengine-obj OBJECT ${OLIVE_SOURCES}) +# (POSITION_INDEPENDENT_CODE ON) +add_library(oakengine SHARED $) +target_compile_options(oakengine-obj PRIVATE -fvisibility=hidden) +# 引用 engine C++ 内部的测试目标: +target_link_libraries( PRIVATE oakengine-obj) # 替代 oakengine +``` + +- `olive-gtest`(tests/gtest/CMakeLists.txt)改链 `oakengine-obj` + + `oakengine`(facade 符号从 .so 来,避免重复定义;若 ODR 冲突则只链 + oakengine-obj,先把 facade 函数符号在 object 里的重复问题解决—— + 二选一,以链接通过且 ctest 全绿为准,把选择写进提交信息)。 +- `timeline-tests`、引用内部的 `oakengine_*_test` 同法。 +- **禁止**:为了让测试过而把 engine 内部符号加 visibility("default") + 白名单——那是开天窗。 + +### B.3 验证 + +``` +nm -D --defined-only cmake-build-debug/engine/liboakengine.so | grep -c " T _Z" # 目标 0 +nm -D --defined-only cmake-build-debug/engine/liboakengine.so | grep -c " T " # 应等于 oakengine_* 函数数 +nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive" # 必须仍为 0 +``` +全量构建 0 error + 全量 ctest 绿(45 个)才提交。 + +--- + +## R7-C(低优先级,时间够再做):app 的 engine C++ 头清理 + +app 仍 include ~40 个 engine C++ 头(`grep -rn '#include "' app/ | +grep -E '"(node|render|timeline|undo|task|pluginSupport)/'`)。不产生 +符号引用(nm=0 已证),但 RIIR 拆模块时这些 include 会全部失效。 +逐个换 facade/句柄头(`cliphandle.h`、`nodevaluehandle.h` 模式)。 +**本批不设完成判据**,收尾时把剩余清单写进 riir.md 附录即可。 + +## 验收(R7 完成判据) + +1. `display.h` 全文无 C++ 类型签名/契约注释;app 无 TexturePtr/FramePtr。 +2. liboakengine.so ` T _Z` = 0;oak-editor ` U _ZN5olive` 保持 0。 +3. 全量构建 0 error;全量 ctest 绿。 +4. 更新 `plans/riir.md` 状态(边界已纯 → 可进 Step 1 拆分)、 + `facade-migration-roadmap.md` R7 批次记录。 +5. 向用户报告,由用户宣布进入 riir.md §4 的模块拆分阶段。