docs: update README and remove obsolete documentation/files

- Update CI badge URL to OakVideoEditorCommunity in README.
- Remove outdated TODO files, vcpkg.json, patch file and stale log.
- Clean up obsolete docs and move ofx-pluginrenderer-functions-zh.md
  into docs/zh/.
This commit is contained in:
2026-07-13 13:14:37 +08:00
parent 182bfd0aad
commit 3abb4ff75b
17 changed files with 1 additions and 3356 deletions
-63
View File
@@ -1,63 +0,0 @@
import { defineUserConfig } from "vuepress";
import { hopeTheme } from "vuepress-theme-hope";
export default defineUserConfig({
base: process.env.BASE || "/",
locales: {
"/": {
lang: "en-US",
title: "Oak Video Editor",
description:
"Open-source, non-linear video editor focused on speed and clarity.",
},
"/zh/": {
lang: "zh-CN",
title: "Oak 视频编辑器",
description: "面向创作者的开源非线性剪辑软件。",
},
},
theme: hopeTheme({
logo: "/images/oak-icon.png",
locales: {
"/": {
selectLanguageName: "English",
navbar: [
{ text: "Home", link: "/" },
{ text: "Build", link: "/build.html" },
{ text: "Project Files", link: "/project-file-reference.html" },
{ text: "Test Plan", link: "/test-plan.html" },
],
sidebar: [
{
text: "Documentation",
children: [
"/build.md",
"/project-file-reference.md",
"/test-plan.md",
],
},
],
},
"/zh/": {
selectLanguageName: "简体中文",
navbar: [
{ text: "首页", link: "/zh/" },
{ text: "构建", link: "/zh/build.html" },
{ text: "工程文件", link: "/zh/project-file-reference.html" },
{ text: "测试计划", link: "/zh/test-plan.html" },
],
sidebar: [
{
text: "文档",
children: [
"/zh/build.md",
"/zh/project-file-reference.md",
"/zh/test-plan.md",
"/zh/structure.md",
],
},
],
},
},
}),
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

-93
View File
@@ -1,93 +0,0 @@
# Oak Video Editor Testing Strategy and Plan
This document describes the automated testing strategy for Oak Video Editor, including unit tests, integration tests, and CI execution.
## Goals
- Maximize automation and reduce manual testing.
- Cover all modules with at least one automated test.
- Keep integration tests headless (no GUI interaction).
- Make failures reproducible on Windows/macOS/Linux CI.
## Test Layers
### 1) Unit Tests (GoogleTest)
- Focus: small units, deterministic behavior, no GUI.
- Location: `tests/gtest/`.
- Execution: `ctest` target `olive-gtest`.
### 1.5) Module Smoke Tests (GoogleTest)
- Focus: compile-time and link-time coverage for GUI-heavy modules without instantiating widgets.
- Location: `tests/gtest/module_smoke_test.cpp`.
- Execution: `ctest` target `olive-gtest`.
### 2) Integration Tests (GoogleTest)
- Focus: cross-module flows without GUI (e.g., serialize → deserialize → resolve).
- Location: `tests/gtest/` (prefixed with `ProjectSerializer`, `TaskManager`, etc.).
### 3) Legacy Tests (Olive macro tests)
- Existing tests in `tests/general`, `tests/timeline`, `tests/compositing` remain.
## Module Coverage Map
Each top-level module has at least one test that exercises its core API or serialization path.
- `app/common`: `common_current_test.cpp`, `common_xmlutils_test.cpp`
- `app/config`: `config_test.cpp`
- `app/node`: `node_value_test.cpp`, `node_keyframe_test.cpp`, `node_serialization_test.cpp`
- `app/node/project/serializer`: `project_serializer_test.cpp`
- `app/render`: `render_videoparams_test.cpp`, `render_audioparams_test.cpp`
- `app/timeline`: `timeline_marker_test.cpp`
- `app/undo`: `undo_stack_test.cpp`
- `app/task`: `task_taskmanager_test.cpp`
- `app/codec`: `codec_frame_test.cpp`
- `app/pluginSupport`: `plugin_support_test.cpp`
- `app/audio`, `app/cli`, `app/dialog`, `app/panel`, `app/tool`, `app/ui`, `app/widget`, `app/window`: `module_smoke_test.cpp`
If a module has a GUI dependency (e.g., widgets), tests focus on non-visual data/model components.
## Integration Test Details
### Project Serializer Roundtrip
- Creates a minimal project with a built-in node.
- Saves to XML via `ProjectSerializer::Save`.
- Loads with `ProjectSerializer::Load`.
- Verifies that nodes are restored.
### Task Manager Execution
- Adds a dummy task to `TaskManager`.
- Waits for completion using an event loop.
- Verifies the task ran.
## Unit Coverage Highlights (Expanded)
- `app/undo`: `undo_stack_test.cpp` now covers empty stack state, model data, redo list coloring, jump behavior, and ignored empty multi-commands.
- `app/timeline`: `timeline_marker_test.cpp` now covers list ordering, closest-marker lookup, list save/load with unknown elements, and marker add/remove/change commands.
- `app/pluginSupport`: `plugin_support_image_test.cpp` now checks OFX property wiring (bounds/ROD, pixel depth, components, premult) and allocation clearing behavior.
- `app/render`: `render_videoparams_branch_test.cpp` now covers auto divider selection, pixel aspect validation, square-pixel width, and Save/Load roundtrip.
## Headless Execution
- Tests avoid QWidget usage.
- CI sets `QT_QPA_PLATFORM=offscreen` to prevent GUI initialization issues.
## Continuous Integration
CI runs on Windows, macOS, and Linux:
1. Install system dependencies (Qt, FFmpeg, OpenImageIO, OpenColorIO, OpenEXR, PortAudio, Expat).
2. Configure with `-DBUILD_TESTS=ON`.
3. Build with CMake + Ninja.
4. Run `ctest` with output on failure.
### Dependency Installation Notes
- Linux: use distro packages (`apt` on Ubuntu) for Qt6, FFmpeg, OpenImageIO, OpenColorIO, OpenEXR, PortAudio, Expat, OpenGL headers.
- macOS: use Homebrew for Qt6 and media/color/image libraries.
- Windows: use system installers where available (Qt via `install-qt-action`), and vcpkg for the remaining C/C++ libraries.
## Adding New Tests
- Place new unit tests in `tests/gtest`.
- Use GoogleTest conventions.
- Prefer deterministic fixtures and local-only resources.
- When adding a new module, add at least one unit test and one integration scenario if applicable.
-114
View File
@@ -1,114 +0,0 @@
# v0.4 调色与 LUT 实施计划
本文档对应 `docs/zh/README.md` 路线图中 v0.4「调色与 LUT」里程碑。
## 目标
- 支持 `.cube``.3dl` LUT 文件作为可用调色入口。
- 完成示波器面板的波形、矢量、直方图三类视图。
- 提供三向色轮面板,面向阴影、中间调、高光做基础调色控制。
- 尽量复用现有 OpenColorIO、节点系统、Viewer/Scope 面板和 GPU 渲染管线,不引入独立的调色框架。
## 当前状态
- 已有 OpenColorIO 基础能力:颜色管理、显示变换、OCIO 调色节点和渲染侧配置。
- Scope 面板已提供波形、矢量、直方图三类视图。
- 已有色轮基础控件;当前三向调色先通过节点参数面板暴露 Shadows、Midtones、Highlights 三组颜色与强度参数。
- LUT 节点已接入节点工厂,并已增加 `.cube`/`.3dl` 相关测试。
## 阶段 1LUT 节点入口
状态:已完成首版。
交付内容:
- 新增 OCIO LUT 节点,使用 OpenColorIO `FileTransform` 读取外部 LUT 文件。
- 明确支持 `.cube``.3dl` 扩展名,并拒绝未知格式。
- 在节点工厂中注册 LUT 节点,保证工程加载和节点创建路径一致。
- 增加 gtest 覆盖 LUT 扩展名支持和简单 LUT 转换结果。
验收标准:
- `olive-gtest` 中 LUT 相关测试通过。
- `olive-editor``olive-render-worker` 可正常构建。
- LUT 文件缺失、格式不支持、OCIO 处理器创建失败时不会导致崩溃。
## 阶段 2:示波器补齐
状态:已完成首版。
交付内容:
- 保留现有波形和直方图视图。
- 新增矢量示波器视图,并接入 Scope 面板下拉选择。
- 矢量示波器应使用当前 Viewer 帧,并经过现有显示/颜色管理路径。
- 为新增 shader 或资源入口增加资源存在性测试。
验收标准:
- Scope 面板可在 Waveform、Vectorscope、Histogram 间切换。
- 无当前帧时视图保持空白或安全占位,不崩溃。
- shader 资源测试和编辑器构建通过。
## 阶段 3:三向色轮面板
状态:已完成节点参数面板首版;独立三向色轮 dock 面板作为后续体验增强。
交付内容:
- 基于现有参数面板提供 Shadows、Midtones、Highlights 三组控制。
- 为每组控制提供色彩偏移和强度/亮度相关参数。
- 将三向色轮参数映射到现有 OCIO 调色节点,或新增可序列化节点承载参数。
- 保证参数能随工程保存、加载、撤销和重做。
验收标准:
- 用户可以在 UI 中操作三向调色参数并看到 Viewer 结果变化。
- 参数在工程文件中可序列化并可恢复。
- 节点参数变更不破坏现有 OCIO 调色节点兼容性。
## 阶段 4:集成与体验
状态:当前范围已完成;独立三向色轮 dock 面板和更细的交互体验作为后续增强。
交付内容:
- 为 LUT 节点补齐清晰的文件选择过滤器和用户可见名称。已完成。
- 在调色相关 UI 中保持命名一致:LUT、Waveform、Vectorscope、Histogram、Shadows、Midtones、Highlights。已完成首版。
- 更新中文文档,说明 LUT、示波器和三向调色的当前入口。已在本文档记录。
验收标准:
- 用户能从现有节点/UI 路径发现 LUT 和调色功能。
- 文档与实际 UI 命名一致。
- LUT 文件选择器限制为 `.cube``.3dl`,并保留 All Files 兜底。
- 不引入和现有翻译系统冲突的硬编码字符串。
## 阶段 5:验证
状态:自动化构建和核心测试已通过;手动 Viewer/Scope 观感检查将在真实项目中继续验证。
构建命令:
```sh
ninja -C cmake-build-debug olive-gtest olive-editor olive-render-worker -j2
```
测试命令:
```sh
QT_QPA_PLATFORM=offscreen cmake-build-debug/tests/gtest/olive-gtest --gtest_filter='ColorLut.*:ColorV04.*:Shaders.*:NodeSerialization.*:NodeValue.*' --gtest_brief=1
```
手动检查:
- 打开工程并加载一段素材。
- 在 Scope 面板分别切换 Waveform、Vectorscope、Histogram。
- 添加 LUT 节点并选择 `.cube``.3dl` 文件。
- 调整三向色轮参数,确认 Viewer 输出和工程保存/加载行为。
## 风险与待定点
- 三向色轮应优先映射到 OCIO 现有调色能力;如果现有节点表达能力不足,再新增独立节点。
- 矢量示波器 shader 需要兼容当前 OpenGL 版本和已有渲染抽象,避免只在单一驱动上可用。
- LUT 文件路径序列化需要尊重现有工程文件路径策略,避免绝对路径导致工程不可迁移。
-153
View File
@@ -1,153 +0,0 @@
# 代理媒体 v0.4 实施计划
## 背景
v0.4 已合并“调色、音频与性能”范围,其中代理媒体工作流负责解决 4K/8K 素材在时间线预览、剪辑和调色时的可用性问题。当前代码里已有音频 conform:`ConformManager` 会把音频流转为 PCM cache,但它不适合作为视频代理的直接扩展,因为视频代理需要保留容器、视频编码参数、文件生命周期和解码路由。
## 当前状态
实施进度:
- 阶段 1 已完成:已写计划,新增代理状态、稳定文件名函数、`Footage` 代理字段和 XML roundtrip 测试。
- 阶段 2 已完成:已新增 `ProxyTask``ProxyManager`,使用 `.working` 临时文件、成功 rename、失败清理,并覆盖状态测试。
- 阶段 3 已完成:`FootageJob` 携带代理解码信息,预览路径使用 ready 代理,导出/online 路径默认原片,代理缺失自动回退。
- 阶段 4 已完成:时间线右键已有 `Generate Proxy``Use Proxy``Reveal Proxy``Delete Proxy`
- 阶段 5 自动验证已完成;仍需实际 4K/8K 素材做手工播放、重开项目和导出确认。
- `app/codec/conformmanager.{h,cpp}` 只处理音频 PCM conform,输出按声道拆分的 `.pcm` 文件。
- `app/task/conform/conform.{h,cpp}` 只调用 `Decoder::ConformAudio()`
- `Decoder::CodecStream` 当前只包含原始 `filename + stream index + block`,解码时会检查该文件存在。
- `RenderProcessor::ProcessVideoFootage()``ProcessAudioFootage()` 通过 `FootageJob` 的 filename/decoder/stream index 打开素材。
- `Footage` 当前保存原始文件名、探测参数、source start time 等项目级元数据,但还没有代理文件状态。
- timeline 已有 cache/thumbnail/waveform 机制,但这是渲染缓存,不是替代源媒体的代理媒体。
## 目标
第一阶段交付一个最小但完整的代理工作流:
- 右键选中项目素材或时间线 clip 可生成代理。
- 代理文件写入项目 cache/proxy 目录,使用稳定 hash 命名。
- `Footage` 记录代理状态,项目保存/加载后仍能识别代理。
- 播放/预览时可选择使用代理,导出默认使用原始素材。
- 代理缺失、生成中、失败时能安全回退原始素材。
- 生成任务进入现有 `TaskManager`,支持取消和失败清理。
## 非目标
- 不在第一版实现复杂代理 preset UI。
- 不在第一版实现云端/跨机器代理 relink。
- 不在第一版替代现有 sequence render cache。
- 不改变音频 conform 的 PCM 路径。
- 不让导出默认走代理,避免质量风险。
## 设计
### 1. 新增 ProxyManager
新增 `app/codec/proxymanager.{h,cpp}`,职责类似但独立于 `ConformManager`
- 根据源文件、stream index、代理参数生成目标文件名。
- 判断代理状态:missing、generating、ready、failed。
- 避免同一素材重复生成任务。
- 使用 `.working` 临时文件,成功后原子 rename。
- 发出 `ProxyReady` 信号通知 UI/缓存失效。
### 2. 新增 ProxyTask
新增 `app/task/proxy/proxy.{h,cpp}`
- 输入原始文件、decoder id、视频 stream、代理参数、输出路径。
- 第一版优先使用 FFmpeg CLI 或内部 FFmpeg 编码路径生成 H.264/MP4 代理。
- 目标默认参数:较短边不超过 720p,保持宽高比,8-bit 4:2:0CRF 23 左右。
- 失败时写清晰 error,删除 `.working`
如果当前构建环境不适合直接调用外部 `ffmpeg`,则优先使用项目内部编码接口;否则在任务内检测 `ffmpeg` 可执行文件并给出失败信息。
### 3. 扩展 Footage 代理元数据
`Footage` 中增加:
- `proxy_enabled`
- `proxy_path`
- `proxy_state`
- `proxy_video_stream_index`
- `proxy_generation_preset/version`
项目 XML 写入 `<proxy enabled="..." state="..." stream="..." preset="...">path</proxy>`
`Clear()` 不能无条件清掉已保存代理路径,只有换源文件或重新探测时才重置不兼容代理。
### 4. 解码路由
提供统一方法选择实际解码源:
- 在线预览/时间线播放:如果项目/素材启用代理且代理 ready,则使用代理文件。
- 离线渲染/导出:默认使用原文件。
- 用户以后可加“导出使用代理”选项,但默认关闭。
优先在 `FootageJob` 构造或 `RenderProcessor::ProcessVideoFootage()` 前完成选择,避免把代理逻辑散落到 decoder 内部。
### 5. UI 入口
第一版入口:
- 时间线 clip 右键:`Generate Proxy``Use Proxy``Reveal Proxy``Delete Proxy`
- 项目素材右键若已有菜单结构可复用,也增加同样入口;如果项目素材菜单结构分散,先实现时间线入口。
- 菜单启用规则:仅视频素材可生成代理;代理生成中禁用重复生成;代理缺失时 `Use Proxy` 可显示但禁用。
### 6. 缓存和失效
代理 ready 后:
- 触发相关 footage/clip 的 video frame cache、thumbnail cache invalidation。
- 不触碰 audio conform cache。
- 不删除已有原始媒体 render cache,避免用户切换代理/原片时状态不可恢复。
## 实施阶段
### 阶段 1:计划和基础数据结构
- 写本计划。
- 添加 proxy 状态枚举和 filename 生成函数。
- 添加 `Footage` 代理字段和 XML 保存/加载测试。
### 阶段 2:代理生成任务
- 添加 `ProxyTask`
- 添加 `ProxyManager`
- 生成 `.working` 文件,成功 rename。
- 增加单元测试覆盖文件名稳定性和状态转换。
### 阶段 3:解码路由
- 扩展 `FootageJob` 或其创建点,携带“实际解码文件”。
- 在线模式优先代理,离线/导出默认原片。
- 代理缺失自动回退原片。
### 阶段 4:时间线 UI
- 时间线右键增加生成/启用/删除代理动作。
- 动作进入 undo 或直接项目状态变更;代理生成任务本身不进 undo。
- 代理 ready 后刷新 timeline/viewer。
### 阶段 5:验证
- `ninja -C cmake-build-debug olive-gtest olive-editor -j22`
- 代理字段 XML roundtrip 测试。
- ProxyManager 状态测试。
- 手工测试:导入 4K 素材、生成代理、启用代理、播放、关闭重开项目、删除代理、导出确认默认原片。
## 风险
- 外部 `ffmpeg` 依赖不可用会让代理生成失败;需要清晰错误并不影响原片播放。
- 代理视频 stream index 可能不同于原片,需要解码路由在代理文件里使用正确 stream。
- 代理分辨率改变会影响 thumbnails/cache,需要切换后明确 invalidation。
- 项目保存相对/绝对代理路径策略要谨慎;第一版使用 cache 目录内路径并可重建。
## 完成标准
- 用户能从时间线对视频 clip 生成代理。
- 代理生成结束后,启用代理的在线预览路径实际读取代理文件。
- 项目重开后代理状态保留。
- 代理缺失或生成失败时不影响原始素材播放。
- 相关构建和 gtest 通过。
-161
View File
@@ -1,161 +0,0 @@
# 动态渲染后端拆分计划
## 目标
将当前强绑定 OpenGL 的渲染实现拆成可动态加载的后端库,使主程序只依赖一个轻量适配器:
- OpenGL 后端封装到私有动态库。
- Vulkan 后端封装到独立动态库。
- 后端库内部继续使用 C++ 实现。
- 后端库对外只导出 C ABI。
- C ABI 使用不透明 handle 表示 C++ 对象。
- 每个 C 函数对应一个后端类成员函数。
- 构造函数导出为特殊 create 函数,析构函数导出为特殊 destroy 函数。
- 主程序适配器构造时按配置显式加载后端库并调用 create/init,析构时调用 destroy 并卸载库。
## 命名约束
用户期望后端名为 `libgl.so``libvulkan.so`。Linux 系统上 `libGL.so`/`libgl.so` 容易和系统 OpenGL loader 混淆,因此工程实现应优先使用私有库名或私有目录,例如:
- `liboakgl.so`
- `liboakvulkan.so`
-`render_backends/libgl.so``render_backends/libvulkan.so`
适配器只从 Oak 私有后端目录查找,避免加载到系统图形库。
## 阶段 1OpenGL 动态后端骨架
- 新增稳定 C ABI 头:`app/render/backend/renderbackend_c.h`
- 新增 `DynamicRenderer` 适配器,继承现有 `Renderer`,内部用 `QLibrary` 加载后端。
- 将现有 `OpenGLRenderer` 包装成 OpenGL 后端导出函数。
- `RenderManager``GraphicsBackend` 选择加载 OpenGL 或 Vulkan 后端。
- 在 Vulkan 后端未实现前,请求 Vulkan 时加载占位后端或回退 OpenGL,并记录明确 warning。
## 阶段 2:两层适配器 ABI
本计划不是把 OpenGL 代码用 C 重写。动态库一侧继续保留现有 C++ `OpenGLRenderer`/未来 `VulkanRenderer` 实现,只在导出边界增加一层 C wrapper;主程序和渲染进程一侧再用 `DynamicRenderer` 把 C 函数封回 C++ `Renderer` 接口。
第一阶段 C ABI 可以用 `void *` 承载现有 C++ 对象指针,例如 `QVariant``VideoParams``ShaderCode``Texture``AcceleratedJob`。C 函数内部只做类型转换并调用对应 C++ 成员函数。这样两侧代码都不用大改,但有一个前提:后端库和主程序必须用同一套头文件、编译器 ABI 和 Qt/FFmpeg/OpenFX 依赖构建。
长期要把 ABI 稳定下来时,再逐步引入更明确的 C 结构,避免跨库暴露 Qt/C++ 类型:
- texture handle`OakBackendTextureHandle`
- shader handle`OakBackendShaderHandle`
- video params C struct:宽、高、depth、pixel format、channel count、linesize
- shader code C structvertex/fragment 字符串
- blit job C struct:输入 texture handle、uniform 数组、输出 texture handle
- readback/upload 使用裸指针和 stride
这一步是 ABI 稳定化,不是把后端内部实现改成 C。
## 阶段 2.5:最小化 OpenGL/Vulkan 后端链接边界(已完成)
此前 `oakgl`/`oakvulkan` 通过 `$<TARGET_OBJECTS:libolive-editor>` 把整个 editor 对象库链进动态库,导致后端库包含项目、节点、任务、cache、UI 等大量 editor 代码和全局状态。
本次已完成链接边界收敛:
- 新增静态库 `libolive-rendercore`,仅包含渲染核心代码:
- 渲染器基类与数据类型:`Renderer``Texture``VideoParams``ShaderCode``AcceleratedJob``ShaderJob`
- 动态适配器:`DynamicRenderer``renderbackend_c.h`
- 必要的 value/config/工具:`node/value``node/param``node/valuedatabase``config/config``common/filefunctions``common/qtutils``common/avframeptr`
- `oakgl`/`oakvulkan` 现在只链接 `libolive-rendercore`,不再链接完整 `libolive-editor`
- `liboakgl.so` / `liboakvulkan.so` 不再链接完整 editor 对象库;实际体积取决于构建类型、符号表和系统依赖链接方式,当前 debug 构建仍会显著大于 release/strip 后体积。
- 为隔离依赖做的头文件清理:
- `renderer.h` 移除 `node/node.h``render/colorprocessor.h``render/job/colortransformjob.h``job/pluginjob.h`,改为前向声明。
- `videoparams.h` 移除 `ofxImageEffect.h`OFX 字符串 setter 实现下移到 `videoparams.cpp`
- `texture.h` 用新增的 `common/avframeptr.h` 替代 `common/ffmpegutils.h`,避免后端拉入大量 FFmpeg 工具代码。
- `renderer.cpp` 的颜色管理(`GetColorContext` / `BlitColorManaged`)和隔行(`InterlaceTexture`)实现分别拆到 `render/colormanagement.cpp``render/interlacetexture.cpp`,这两个文件仍由 editor/worker 链接,但不进入后端库。
- 修复了拆分过程中暴露的 `StyleManager::kDefaultStyle` 跨库符号问题:改为 header 内 `inline static` 定义,使 `config.cpp` 在后端库中自包含。
剩余优化空间:
- 长远可将 `libolive-editor` 也改为依赖 `libolive-rendercore`,彻底消除渲染核心代码在主程序与后端库之间的重复编译/重复链接。当前阶段先保证后端边界干净、主程序保持兼容。
## 阶段 3Vulkan 后端(offscreen 核心已实现,运行时依赖可用 Vulkan ICD)
- 新增 Vulkan 后端库 `liboakvulkan.so`(当系统安装了 Vulkan 头文件/库时构建;无 Vulkan 环境时 CMake 自动跳过)。
- 新增 `VulkanRenderer` 类,继承 `Renderer`,使用原生 Vulkan API 实现 offscreen 渲染管线;代码已合入,并在本机 NVIDIA Vulkan 驱动上通过了基础端到端渲染测试。
- CMake 集成:根目录查找 `Vulkan``shaderc`(可选);`oakvulkan` 目标链接 `Vulkan::Vulkan``shaderc_shared`;若 `Vulkan` 未找到则不构建该库,避免无 Vulkan 头文件时编译失败。
- 实现 Vulkan instance/device/queue/command pool 管理(代码层完成)。
- 实现 offscreen image/texture 管理(`CreateNativeTexture` / `DestroyNativeTexture`),支持 2D/3D、多种 pixel formatU8/U16/F16/F32 × 1/2/3/4 channel);3-channel 格式会探测 `COLOR_ATTACHMENT` 支持并自动回退到 4-channel 等价格式。
- 实现 staging buffer 上传/下载(`UploadToTexture` / `DownloadFromTexture`)。
- 实现 `ClearDestination``vkCmdClearColorImage`)。
- 实现 `Flush``vkDeviceWaitIdle`)。
- 实现 GLSL → SPIR-V 运行时编译(通过 `shaderc`),支持顶点/片段共享 UBO、显式 sampler binding、顶点 uniform(如 `ove_mvpmat`)和常用 varyings。
- 实现基础 graphics pipeline 用于 `Blit`(全屏 quad、顶点缓冲、按格式缓存的 render pass、combined image sampler descriptor set、persistent linear/nearest sampler、per-texture framebuffer cache)。
- 提供 `GetPixelFromTexture`(基于 `DownloadFromTexture` 的简化实现)。
- `oak_renderer_is_available` 现在会在首次检查时尝试 `Init()`,成功后报告 Vulkan 可用。
- 测试更新:
- `LoadsExperimentalVulkanBackendWhenAvailable`:验证 Vulkan 后端可加载、初始化、报告能力位。
- `FallsBackWhenExperimentalVulkanUnavailable`:在 Vulkan 不可用的系统上验证回退 OpenGL;在 Vulkan 可用的系统上自动 SKIP。
- `VulkanUploadBlitDownload`:创建 Vulkan backend,上传 U8 RGBA 纹理,经默认 pass-through shader Blit 到目标纹理,再下载并验证像素一致;该测试在当前开发环境的真实 Vulkan 驱动上通过。
- **已修复的明显问题(代码层)**:
- 初始化幂等性:`Init()` / `PostInit()` 可安全重复调用。
- `Blit` 中的 descriptor/sampler 生命周期:sampler 与 descriptor set 在 `EndOneTimeCommands` 后统一释放。
- sampler binding:从数组绑定改为显式 `layout(set=0, binding=N)`,避免跨驱动 array-of-samplers 行为不一致。
- image layout 跟踪:输入纹理在绘制前被过渡到 `SHADER_READ_ONLY_OPTIMAL`
- viewport/scissor:改为 dynamic state,避免 pipeline 缓存 key 遗漏视口尺寸。
- render pass clear`clear_destination` 为 true 时 `loadOp` 设为 `CLEAR`
- 格式支持探测:通过 `vkGetPhysicalDeviceFormatProperties` 检查 `COLOR_ATTACHMENT` 能力,3-channel 不支持时回退到 4-channel(上传/下载的 CPU 侧通道对齐仍待完善)。
- framebuffer / sampler 缓存:每张纹理延迟创建并复用 framebuffer;按插值模式复用 linear/nearest sampler。
- 单通道纹理 swizzleimage view 组件映射为 R→RGB、A=1,匹配 OpenGL 灰度行为。
- 纹理启用标志:为声明 `NAME_enabled` 的 shader 自动设置 0/1。
- **已修复 / 已实现**
- 链接边界已最小化,`liboakvulkan.so` 现在只依赖 `libolive-rendercore`
- 单通道/3-channel 格式的上传/下载 CPU 侧对齐:当 GPU 回退格式(如 3→4 channel)与请求格式不一致时,staging buffer 按实际 `VkFormat` 大小分配,并在 CPU 侧进行通道数转换(alpha 填最大值)。
- `Blit` 已实现 iterative/pin-pong 多 pass:根据 `ShaderJob::GetIterationCount` / `GetIterativeInput` 创建临时 ping-pong 纹理,每 pass 更新 `ove_iteration` 并替换迭代输入;最后一 pass 写入目标纹理。
- null-destination Blit 实现为渲染到临时 offscreen texture,保证调用不崩溃。
- 新增自动化测试:
- `VulkanNullDestinationBlitDoesNotCrash`
- `VulkanIterativeBlitPingPong`2 pass 折半,验证 ping-pong 结果)
- `VulkanUploadDownloadThreeChannel`(验证 3-channel RGB 上传/下载与回退格式转换)
- **当前验证状态**
- 自动化测试已覆盖 Vulkan 后端加载、texture upload/download、Blit、null-destination fallback、iterative ping-pong、3-channel upload/download fallback;这些测试会在运行环境存在可用 Vulkan ICD 时执行。
- 当前开发环境可找到 Vulkan loader/headers,但运行时 loader 只发现不可用的 NVIDIA ICD`vkCreateInstance``Found no drivers`;因此 Vulkan 用例会按设计 SKIP,不能作为 Vulkan 渲染通过的证据。
- Viewer / proxy / 导出的完整交互流程仍需具备显示环境和可用 Vulkan runtime 的项目做最终验证;当前已在代码路径层面确认 backend-neutral viewer readback、proxy/export 渲染入口均使用 `Renderer` 抽象,无硬编码 OpenGL 依赖。
## 阶段 4Viewer 双后端(backend-neutral 路径已落地,Vulkan viewer 为原型)
- 当前 viewer display 基于 OpenGL widget 和 GL texture id。
- 默认构建下 Viewer 的 managed display 现在使用 `DynamicRenderer` 创建 renderer,并把现有 `QOpenGLContext` 传入动态后端;若动态后端加载失败则回退到 `OpenGLRenderer`
- `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 默认改为 `ON`,保留 `OFF` 作为应急开关。
- 新增 backend-neutral viewer path 框架:
- `ManagedDisplayWidget` 支持非 OpenGL inner widget(普通 `QWidget`),通过 `Renderer::IsOpenGL()` 判断。
- `RenderManager` 不再在 `requested_backend_ == kVulkan` 时强制 fallback。
- `ViewerDisplayWidget` 已移除 `glIsTexture()` 的直接 OpenGL 依赖,改为通用的跨 renderer texture 拷贝。
- `ScopeBase` 在 backend-neutral 时安全跳过(TODO:完整 scope display 路径)。
- OpenGL 使用现有 `QOpenGLWidget/QOpenGLWindow`
- Vulkan / backend-neutral viewer readback display 路径(offscreen texture → download → QImage → QPainter)已搭建:
- 新增 `ManagedDisplayWidgetBackendNeutral`,在普通 `QWidget``paintEvent` 中转发到 `ManagedDisplayWidget::OnPaint`
- `ViewerDisplayWidget::OnPaint` 在 backend-neutral 模式下改用 `QPainter` 填充背景,将颜色管理后的画面渲染到 U8 RGBA offscreen texture,再 `Download` 到 CPU buffer,最后用 `QImage::Format_RGBA8888_Premultiplied` + `setDevicePixelRatio` 绘制到 inner widget。
- OpenGL 路径保持原有 `BlitColorManaged` 直接到 widget 不变。
- Viewer 只消费后端 texture handle 或 readback frame,不直接假设 GL texture id。
- **状态说明**backend-neutral 代码已合并;VulkanRenderer 现在可完成单 pass BlitViewer 的 backend-neutral readback 路径在代码层面可工作,但尚未在完整 UI 播放/导出流程中验证。
## 阶段 5:OpenFX 处理边界(边界框架已完成,Vulkan 路径待验证)
- OpenFX 插件 OpenGL 渲染路径保留 OpenGL 依赖,不强行改写。
- `PluginRenderer` 不再继承 `OpenGLRenderer`,改为持有通用的 `Renderer *`
- OpenGL 渲染路径仅在 `renderer_->IsOpenGL()` 为 true 时启用,并正确调用 `OlivePluginInstance::setOpenGLEnabled(use_opengl)`
- 非 OpenGL 渲染器(Vulkan、DynamicRenderer 加载的任意后端)自动回退到 CPU readback/upload 路径,不再因缺少 OpenGL context 而直接跳过插件渲染。
- 将 OFX 输出纹理绑定/解绑抽象为 `Renderer::AttachOutputTexture` / `DetachOutputTexture`
- `OpenGLRenderer` 实现为 `AttachTextureAsDestination` / `DetachTextureAsDestination`
- C ABI 新增 `oak_renderer_attach_output_texture` / `oak_renderer_detach_output_texture`
- `DynamicRenderer` 通过 C ABI 转发,使动态 OpenGL 后端也能支持 OFX OpenGL 渲染。
- `VulkanRenderer` 默认 no-opVulkan 项目中的 OFX 插件回退到 CPU 路径。
- 格式转换(`ConvertFrameIfNeeded``ConvertTextureForParams`)、readback`ReadbackTextureToFrame`)、upload 等辅助函数保持后端无关,通过 `Renderer` 接口调用,无需移入后端库。
- `RenderProcessor::ProcessPluginJob` 不再要求 `render_ctx_` 实现 `OpenGLContextProvider`,任何 `Renderer` 都能驱动插件渲染。
- 更新相关 gtest`PluginRenderer` 构造函数现在需要传入 renderer 指针,测试传入 `nullptr` 验证纯 CPU 路径。
- **状态说明**:后端无关的边界框架和 OpenGL 动态路径已可编译并通过现有测试;Vulkan 下的 OFX CPU 回退路径代码已就位,并在 Vulkan 可完成基础 Blit 的当前版本上具备验证条件。
## 完成标准
- [x] 主程序默认不再直接 new `OpenGLRenderer`,而是通过 `DynamicRenderer` 动态加载 OpenGL/Vulkan 后端;加载失败时保留回退到 `OpenGLRenderer` 的安全路径。
- [x] `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 默认 `ON``liboakgl.so` 默认构建并安装;`liboakvulkan.so` 在检测到 Vulkan 开发库时构建并安装。
- [x] OpenGL 后端库可单独构建、加载、初始化、销毁。
- [x] 用户能在配置中选择 OpenGL/Vulkan。
- [x] Vulkan 不可用时自动回退到 OpenGL,不崩溃;`RenderManager::backend()` 会在 `DynamicRenderer` 内部回退后同步为实际运行后端。
- [x] 链接边界已最小化:`oakgl` / `oakvulkan` 现在只链接独立的 `libolive-rendercore`,不再拉入完整 editor 代码;库体积需按 release/strip 构建重新记录。
- [ ] Vulkan / backend-neutral viewer readback display 路径已搭建(offscreen texture → download → QImage → QPainter);仍需在可用 Vulkan runtime 和显示环境下验证完整 Viewer/proxy/导出流程。
- [x] OpenFX 插件渲染边界已处理:`PluginRenderer` 后端无关化,非 OpenGL 渲染器自动回退 CPU 路径,动态 OpenGL 后端通过 C ABI 支持 OFX OpenGL 输出绑定。
- [x] 自动化测试覆盖 device init、texture create/upload/download(含 3-channel fallback)、shader compilation、Blit with destination、null-destination fallback、iterative shaders;无可用 Vulkan ICD 时相关用例按设计 SKIP。
- [ ] 手工测试计划覆盖 viewer、proxy、scope、导出等完整路径;`ScopeBase` 当前在 backend-neutral 时仍是安全跳过,不是完整 Vulkan scope display。
-280
View File
@@ -1,280 +0,0 @@
# 渲染独立进程化 — 实现计划
> **状态**:实施中(阶段 0–5 已完成,阶段 6 可选优化未做)
> **分支**`feat/render-process-isolation`
> **范围**:把视频帧渲染拆到独立进程,主进程通过共享内存 + stdio 调度多个渲染 worker,全程无锁。
---
## 1. 背景(为什么做)
OakOlive 分叉,Qt6/C++17 视频编辑器)当前是**单进程**架构:所有渲染在主进程的后台
`QThread` 里完成(`app/render/rendermanager.cpp``video_thread_` / `audio_thread_` /
`waveform_threads_` 等),通过 `RenderManager::RenderFrame()``RenderThread` 队列 →
`RenderProcessor::Process()` 的 ticket 异步管线工作。
把渲染留在主进程有三个问题:
1. **崩溃传染** —— OFX 第三方插件(0.3 里程碑的核心目标“任意 OFX 插件加载不崩溃”)一旦崩溃,会带走整个编辑器,丢失未保存的工作。
2. **难以横向扩展** —— GPU 上下文、解码器缓存都绑在一个进程里,无法利用多核/多 GPU 并行。
3. **预渲染受限** —— 预渲染窗口(见 `TODO.md` 的 LRU 预渲染计划)受单进程资源约束。
**目标**:把**视频帧渲染**(节点图遍历 + GPU 合成 + OFX 插件 + 颜色变换,即 `RenderProcessor`
的视频路径)拆到**独立的渲染进程**。主进程作为调度器,通过**共享内存 + stdio** 与**多个**渲染
worker 通信。硬性要求**无锁**:跨进程数据交换走预分配的共享内存 slot 池 + SPSC 环形索引队列,
控制平面走 stdio 上的换行分隔消息。
### 1.1 已确认的范围决策
| 维度 | 决策 |
|---|---|
| **拆分范围** | 仅**视频帧渲染**。音频/波形/dry-run 暂留主进程。→ worker 链接 OpenGL / OCIO / OpenImageIO / OFX**不**链接 UIWidgets)。 |
| **GPU 上下文** | 每个 worker **自建 offscreen `QOpenGLContext`**,渲染后 `DownloadFromTexture` 到共享内存里的 CPU 帧;主进程只负责显示上传。 |
| **素材输入** | **主进程解码**(复用现有 `DecoderCache`),把解码后的原始帧经共享内存喂给 worker。→ worker **不**链接 FFmpeg。 |
| **图同步** | **全量序列化**整个节点图(复用 `ProjectSerializer`),架构预留增量通道。 |
| **帧回传** | **固定 slot 池 + 无锁环形队列**(按最大分辨率预分配)。 |
| **控制协议** | **纯文本 NDJSON**(每行一条 JSON),便于 `cat`/`tee` 调试、手工注入测试。大块图数据走临时文件传路径。 |
| **落地策略** | **分阶段**,每步可编译可验证,旧的进程内渲染保留为默认,用开关切换。 |
---
## 2. 现有架构锚点(复用,不重写)
| 关注点 | 文件 / 符号 |
|---|---|
| 渲染调度/线程池 | `app/render/rendermanager.{h,cpp}``RenderManager``RenderThread` |
| 视频渲染核心 | `app/render/renderprocessor.{h,cpp}``RenderProcessor::Process()``GenerateTexture/GenerateFrame` |
| 渲染抽象 | `app/render/renderer.h``app/render/opengl/openglrenderer.{h,cpp}``Init()``PostInit()``DownloadFromTexture` |
| 异步票据 | `app/render/renderticket.{h,cpp}``RenderTicket``RenderTicketWatcher``Finish(QVariant)` |
| 图复制/增量更新(IPC 协议蓝本) | `app/render/projectcopier.{h,cpp}``QueuedJob` 枚举、`ProcessUpdateQueue()` |
| 全量序列化 | `app/node/project/serializer/serializer*.{h,cpp}``ProjectSerializer::Save/Load``LoadType::kProject` |
| 自动缓存协调 | `app/render/previewautocacher.{h,cpp}` — 票据的实际消费者 |
| 帧内存(单段连续 buffer) | `app/codec/frame.{h,cpp}` + `app/render/framemanager.h``data_`/`linesize_`/`allocated_size()` |
| 帧消费/显示 | `app/widget/viewer/viewer.cpp``SetDisplayImage()``ticket->Get()` |
| 进程入口 | `app/main.cpp``QSurfaceFormat` 设置(OpenGL 3.2 core)、`AA_ShareOpenGLContexts` |
| 构建 | 根 `CMakeLists.txt``app/CMakeLists.txt``add_executable(olive-editor ...)` + `libolive-editor` OBJECT 库 |
**关键观察**
- `RenderProcessor::Process()` 已是无状态静态入口,参数全在 `ticket->property(...)` 里。这是进程边界的天然切割点。
- `Frame` 的数据是**单段连续 malloc**`FrameManager::Allocate`),`linesize` 为步长 → 可直接 memcpy 进/出共享内存 slot。
- `OpenGLRenderer::Init()`(无参版)已能自建 `QOffscreenSurface` + `QOpenGLContext``PostInit()` 使其 current —— worker 直接复用。
- 项目原先**完全没有** QSharedMemory / QLocalSocket / mmap / shm_open / 环形缓冲 → 全部 IPC 原语需新建。
- worker 做 GPU 渲染但不解码 → `RenderProcessor::ProcessVideoFootage()`(当前直接调 `DecoderCache`)在 worker 侧必须改为**从主进程推入的输入帧取数据**,这是关键重构点(阶段 4)。
---
## 3. 目标架构
```
┌─────────────────── 主进程 (olive-editor) ───────────────────┐
│ Viewer / PreviewAutoCacher │
│ │ GetSingleFrame() │
│ ▼ │
│ RenderManager (调度器) │
│ ├─ DecoderCache ← 解码原始素材帧 │
│ ├─ RenderWorkerPool ← 新增 │
│ │ ├─ WorkerProcess #0 (QProcess + stdio + SHM) │
│ │ ├─ WorkerProcess #1 │
│ │ └─ ... │
│ └─ ProjectSerializer ← 全量图快照 │
└─────────────────────────────────────────────────────────────┘
stdio (控制平面: NDJSON, 每行一条 JSON 消息)
SHM (数据平面: 输入素材帧 slot 池 + 输出帧 slot 池, 无锁环形索引)
┌──────────────── 渲染进程 (olive-render-worker) ×N ───────────┐
│ workermain: 读 stdin NDJSON 控制循环 │
│ ├─ 反序列化节点图 (ProjectSerializer::Load) │
│ ├─ offscreen QOpenGLContext + OpenGLRenderer │
│ ├─ RenderProcessor (视频路径; ProcessVideoFootage 改为 │
│ │ 从输入 SHM slot 取帧, 不再直接解码) │
│ └─ DownloadFromTexture → 写输出 SHM slot → 发 frame_ready │
└─────────────────────────────────────────────────────────────┘
```
### 3.1 无锁 IPC 设计
**控制平面(stdio**worker 的 stdin/stdout**纯文本 NDJSON**——每条消息一行
compact `QJsonObject``\n` 结尾。仅承载低频控制流量(握手、提交任务、取消、关闭)。
纯文本便于 `cat`/`tee` 抓管道调试、手工注入测试;单读单写天然无锁。诊断信息走 stderr,
绝不污染 stdout 控制通道。**大块图数据走临时文件**:`load_graph` 不在行内塞字节,主进程把
序列化图写临时文件,消息只带路径 `{"type":"load_graph","path":"/tmp/xxx.ove"}`
**数据平面(共享内存)**:每个 worker 一段共享内存,封装在 `SharedMemoryRegion`
POSIX `shm_open`+`mmap` / Windows `CreateFileMapping`+`MapViewOfFile`)。布局由
`FrameSlotPool` 管理:
- **两个 SPSC 环形队列**`SpscRingBuffer`)的原子游标(`std::atomic<uint32_t>` head/tail
`memory_order_acquire/release`):`free_ring`(空闲 slot 索引)和 `ready_ring`(已填充 slot
索引)。每个环单生产者单消费者 → 无需互斥锁。
- **定长 slot 数组**:按最大分辨率(如 8K RGBA half)预分配的等长槽,外加每槽
`FrameSlotMeta`width/height/format/linesize/timestamp 等 POD)。
- **所有权靠索引转移**:填充方 `Acquire()`(从 free 环弹出)→ 写 meta+像素 → `Publish()`
(压入 ready 环);消费方 `Consume()`(从 ready 环弹出)→ 读 → `Release()`(压回 free 环)。
环满即天然背压,无需额外锁。
一个 pool 建模单向帧流。输出方向(worker→主)放渲染结果;输入方向(主→worker)放解码素材。
---
## 4. 分阶段实现计划
> 每个阶段都能独立编译、独立验证。前期阶段不改变现有行为(进程内渲染仍是默认),
> 用开关切到多进程路径,最后再切默认。
### ✅ 阶段 0:IPC 基础设施(已完成)
新增 `app/render/ipc/` 模块:
- `spscringbuffer.h` —— header-only`std::atomic` 游标的单生产者单消费者环形索引队列,POD,可直接放共享内存。
- `sharedmemoryregion.{h,cpp}` —— 跨平台共享内存段封装(POSIX `shm_open`+`mmap` / Windows `CreateFileMapping`+`MapViewOfFile`)。直接用原生 API 而非 `QSharedMemory`(后者带隐式信号量与引用计数,不适合大帧)。
- `frameslotpool.{h,cpp}` —— 在共享内存段上布局两个环 + 定长 slot 池;提供 `Acquire/Publish/Consume/Release``FrameSlotMeta`
- `ipcmessage.{h,cpp}` —— NDJSON 控制消息编解码(`WriteMessage`/`ReadMessage` + 各类型的 `ToJson/FromJson`)。
**控制消息类型**NDJSON`type` 字段区分):`handshake``load_graph`(图临时文件路径)、
`render_frame`node-uuid、time、vparams)、`frame_ready`(输出 slot 索引、ticket-id)、
`cancel`ticket-id)、`shutdown``error`。预留 `graph_update` 增量类型(阶段 6 实现)。
**测试**`tests/gtest/render_ipc_test.cpp`Google Test):
- 环形队列:基础语义 + 回绕 + **并发 200 万值** FIFO 无丢失无重复。
- slot 池:单线程握手 + 耗尽/回填 + **并发 20 万帧**数据完整性。
- NDJSON:类型往返 + 逐字节半包 + 畸形行跳过 + 错误类型拒绝。
> 注意:`SpscRingBuffer` 内部数组访问器命名为 `slot_array()` 而非 `slots()`,以规避 Qt 的 `slots` 宏。
### ✅ 阶段 1:worker 可执行目标(已完成)
- `app/CMakeLists.txt` 新增 `add_executable(olive-render-worker ...)`,复用 `libolive-editor` OBJECT 库 + `olive-version-obj`,与 `olive-gtest` 同款链接方式。
- 新增 `app/render/worker/workermain.cpp`:用 **`QGuiApplication`**(非 `QApplication`,无 Widgets;也非纯 `QCoreApplication`,因为需要平台 GL 集成)。
- 安装与主进程一致的 `QSurfaceFormat`OpenGL 3.2 core24 位深度),设置 `AA_UseDesktopOpenGL` / `AA_ShareOpenGLContexts`
- 当前行为:`OpenGLRenderer::Init()` + `PostInit()` 建 offscreen GL 上下文 → 校验 `context()->isValid()` → 在 stdout 打一行 NDJSON 握手(含实际 GL 版本)→ 干净退出。
- **链接说明**:当前用全量 `OLIVE_LIBRARIES`(含 Widgets/FFmpeg),裁剪 UI-only 依赖留到后续阶段。
**验证结果**worker 在默认平台与 `-platform offscreen` 下均成功输出
`{"gl_major":3,"gl_minor":2,...,"type":"handshake"}`stdout 仅一行合法 JSON,退出码 0。
### 阶段 2:worker 主循环 + 单帧渲染回路(基础回路已接入)
-`workermain.cpp`:读 stdin NDJSON 控制消息循环,支持 `handshake` / `load_graph` /
`render_frame` / `cancel` / `shutdown`,启动握手仍保持 stdout 单行 NDJSON。
-`load_graph``ProjectSerializer::Load(LoadType::kProject)` 反序列化出 `Project` + 节点图;
`ProjectSerializer::LoadData` 现在暴露旧 ptr token → 新 `Node*` 映射,worker 用它解析
`render_frame.node`。旧版 serializer 已有的 node UUID 映射也保留兼容。
-`render_frame` → 构造本地 `RenderTicket`(参数从消息填 property,复刻
`RenderManager::RenderFrame` 的关键 `setProperty`)→
`RenderProcessor::Process(ticket, renderer, decoder_cache=nullptr, shader_cache)`
- ✅ 先**不**接输入素材:渲染结果为 `FramePtr` 后写入输出 `FrameSlotPool` slot
`FrameSlotMeta`,发布 slot 并回 `frame_ready`
- ✅ 临时测试驱动启动 1 个 worker,加载最小 SolidGenerator 项目,主进程从输出 slot
读回 64x64 F32 RGBA 帧并校验元数据与像素非零。待固化为自动化测试。
**验证结果**
- `cmake --build build --target olive-render-worker olive-gtest -j2` 通过。
- `QT_QPA_PLATFORM=offscreen build/tests/gtest/olive-gtest --gtest_filter='SpscRingBuffer*:*FrameSlotPool*:*IpcMessage*:*ProjectSerializer*' --gtest_brief=1`
通过,11 个测试全部通过。
- 非沙箱环境直接运行 worker 通过,输出合法启动握手并退出码 0;工具沙箱内直接运行会以
134 退出,gdb/非沙箱复测确认不是 worker 代码路径崩溃。
- 有效共享内存 attach 测试通过:测试驱动创建 POSIX shm + `FrameSlotPool`worker attach 后
shutdown,退出码 0。
- 单帧渲染闭环测试通过:临时驱动加载 SolidGenerator,发送 `render_frame`,收到
`frame_ready`;输出 slot 元数据为 `id=1001, 64x64, fmt=3, channels=4, bytes=65536`
前 4KB 像素存在非零数据。
### 阶段 3:主进程 WorkerPool + 调度器接线(单 worker MVP 已接入)
- ✅ 新增 `app/render/renderworkerpool.{h,cpp}`
- 当前 MVP 用后台 `QThread` 持有任务队列,每个任务启动 1 个 `olive-render-worker`
建立输出 SHM 段 + stdio 管道。
- `SubmitFrame(RenderTicketPtr, RenderVideoParams)`:写全量图快照临时文件 →
发送 `handshake` / `load_graph` / `render_frame` → worker 回 `frame_ready` 后从输出 slot
拷出 `FramePtr``ticket->Finish(...)`。对上层 `RenderTicketWatcher`/`Viewer` 保持透明。
- 当前仅支持普通视频 `ReturnType::kFrame`;素材输入仍按阶段 4 处理,失败或不支持时回退旧路径。
-`RenderManager` 增加 `kMultiProcess` backend 分支(与 `kOpenGL` 并存),开关开启且
WorkerPool 接受任务时 `RenderFrame()``RenderWorkerPool`
- ✅ 多进程渲染已设为唯一视频渲染路径,`RenderProcessIsolationEnabled` 配置项已移除。
- 待补:常驻 N worker、忙闲/负载派发、崩溃重启与重派、Viewer 开关实测。
**验证结果**
- `cmake --build build --target olive-render-worker olive-editor -j22` 通过。
- `QT_QPA_PLATFORM=offscreen build/tests/gtest/olive-gtest --gtest_filter='SpscRingBuffer*:*FrameSlotPool*:*IpcMessage*:*ProjectSerializer*' --gtest_brief=1`
通过,11 个测试全部通过。
- 非沙箱环境 `printf '{"type":"shutdown"}\n' | build/app/olive-render-worker` 通过,输出合法
handshake。
### 阶段 4:素材输入解耦(关键重构)
-`Decoder` 增加 CPU 帧接口 `RetrieveVideoFrame()`FFmpeg 路径输出 packed RGBA CPU frameOIIO 路径返回 still frame CPU buffer。
-`RenderWorkerPool` 派发前 dry-run 遍历当前帧素材输入,使用主进程 `DecoderCache` 预解码,成功后写入 main→worker 输入 `FrameSlotPool`
-`render_frame` 支持有序 `input_slots` 列表;worker 按顺序 consume/release`RenderProcessor::ProcessVideoFootage()` 从 slot 上传纹理并继续原有色彩管理。
- ✅ 没有输入 slot 且 worker 无 `DecoderCache` 时,素材节点安全跳过,不再空指针崩溃。
- ✅ worker 和 `RenderProcessor` 都会校验 IPC 输入 slot 范围,畸形 `input_slots` 不会越界访问共享内存。
- ✅ 真实素材 CPU 预解码已由 `CodecDecoder.RetrieveVideoFrameFromDemoMp4` 覆盖;IPC slot 顺序由
`IpcMessage.TypedRoundTrip`/`FrameSlotPool` 回归覆盖;CPU 预解码失败时 `RenderWorkerPool::SubmitFrame()`
拒绝接管,`RenderManager::RenderFrame()` 自动回退进程内路径。
### 阶段 5:多 worker、取消、健壮性
-`RenderManager::RemoveTicket()` 已转发到 `RenderWorkerPool`,多进程渲染 ticket 可被统一取消。
-`RenderWorkerPool::RemoveTicket()` 支持移除尚未开始的排队任务,并同步清理对应图快照临时文件。
- ✅ 正在执行的 worker 任务会标记 `RenderTicket` 取消,并通过保存的 worker PID 终止对应进程,避免跨线程直接操作 `QProcess*`;由 pool 执行线程收尾 `Finish()`
-`RenderWorkerPool` 现在使用共享队列 + 多执行循环,worker 数量按 `QThread::idealThreadCount() - 2`,并发消费 `PreviewAutoCacher`/Viewer 提交的帧任务。
- ✅ worker 启动、握手、`load_graph``render_frame` 或等待 `frame_ready` 失败时,未取消 ticket 会重建 SHM/input slots 并重启新 worker 重派一次。
- ✅ worker 响应超时/提前退出的日志包含 `QProcess` 状态、退出状态、退出码与进程错误,便于区分崩溃、正常退出和启动/管道错误。
- ✅ OFX/插件基础路径由 `PluginSmoke``PluginSupport``PluginOfxMisc``PluginRenderPipeline`
回归覆盖;worker 启动/握手/加载图/渲染等待失败均按异常 worker 退出路径重试一次,覆盖崩溃隔离的调度语义。
- 背压:slot 池/环满时调度器暂缓派发(环满即天然背压)。
### 阶段 6:图增量同步(可选优化)
-`ProjectCopier``QueuedJob`kNodeAdded/kEdgeAdded/kValueChanged…)编码成 `graph_update` 消息,worker 侧等价 `ProcessUpdateQueue`,省去每次全量序列化。
- 阶段 0 已预留消息类型,此处填实现。
---
## 5. 文件清单
**新增**
| 文件 | 阶段 | 状态 |
|---|---|---|
| `app/render/ipc/spscringbuffer.h` | 0 | ✅ |
| `app/render/ipc/sharedmemoryregion.{h,cpp}` | 0 | ✅ |
| `app/render/ipc/frameslotpool.{h,cpp}` | 0 | ✅ |
| `app/render/ipc/ipcmessage.{h,cpp}` | 0 | ✅ |
| `app/render/ipc/CMakeLists.txt` | 0 | ✅ |
| `tests/gtest/render_ipc_test.cpp` | 0 | ✅ |
| `app/render/worker/workermain.cpp` | 1/2 | ✅ 基础主循环 |
| `app/render/renderworkerpool.{h,cpp}` | 3 | ✅ 单 worker MVP |
**修改**
| 文件 | 阶段 | 状态 |
|---|---|---|
| `app/render/CMakeLists.txt`(加 `add_subdirectory(ipc)` | 0 | ✅ |
| `tests/gtest/CMakeLists.txt`(注册 ipc 测试) | 0 | ✅ |
| `app/CMakeLists.txt`(新增 `olive-render-worker` target | 1 | ✅ |
| `app/node/project/serializer/serializer*.{h,cpp}`(暴露加载映射供 worker 查节点) | 2 | ✅ |
| `app/render/rendermanager.{h,cpp}``kMultiProcess` 分支 + WorkerPool 接线) | 3 | ✅ 单 worker MVP |
| `app/codec/decoder.{h,cpp}` + `app/codec/{ffmpeg,oiio}`CPU frame 解码接口) | 4 | ✅ 首版 |
| `app/render/renderworkerpool.{h,cpp}`(主进程预解码并填 input slot | 4 | ✅ 首版 |
| `app/render/renderprocessor.cpp``ProcessVideoFootage` 改取输入 slot | 4 | ✅ 首版 |
| `app/config/config.cpp`(多进程开关默认值) | 3 | ✅ 默认关闭 |
---
## 6. 验证方式(端到端)
1. **IPC 单元测试**:多线程压测 SPSC 环形队列 + slot 池,确认无锁正确性(无丢失/重复/数据竞争,可配 TSan)。— 阶段 0 已覆盖。
2. **像素一致性回归**:同一项目同一帧,`kOpenGL`(进程内)vs `kMultiProcess` 逐像素对比应一致(先纯生成节点,再含真实素材)。
3. **运行实测**:开关打开后启动编辑器,播放/拖拽时间线,Viewer 正常无卡死;`ps` 能看到 `olive-render-worker` 子进程,主进程退出时子进程随之退出。
4. **崩溃隔离**:人为让 worker 段错误(或加载会崩的 OFX 插件),确认主进程存活、WorkerPool 自动重启并恢复渲染。
5. **性能**:多 worker 预渲染窗口吞吐 vs 单进程基线对比。
---
## 7. 开放问题(实现时定)
- SHM slot 尺寸/数量的默认值(按硬件分档,参考 `TODO.md` 同款问题)。
- worker 数默认值(CPU/GPU 数推导)。
- OFX 插件在多 worker 下的句柄/许可证并发是否有限制。
- worker 链接集裁剪时机:何时安全移除 Widgets/FFmpeg 依赖(依赖阶段 4 素材解耦完成)。
-122
View File
@@ -1,122 +0,0 @@
# 素材读入强制转换为 RGBAF32 并内部全链路使用 F32 处理 — 实施计划
## 1. 现状分析
### 1.1 视频读入位置
视频/图像素材在以下位置被读入并解码为 GPU Texture
| 层级 | 文件 | 职责 |
|------|------|------|
| 解码接口 | `app/codec/decoder.h` / `.cpp` | 基类 `Decoder`,定义 `RetrieveVideo(RetrieveVideoParams)` 公共接口 |
| FFmpeg 解码 | `app/codec/ffmpeg/ffmpegdecoder.cpp` | `FFmpegDecoder::RetrieveVideoInternal()` —— 核心视频解码路径 |
| OIIO 解码 | `app/codec/oiio/oiiodecoder.cpp` | `OIIODecoder::RetrieveVideoInternal()` —— 静态图片解码路径 |
| 渲染触发 | `app/render/renderprocessor.cpp` | `ProcessVideoFootage()` —— 在节点图遍历中触发解码,并做颜色管理转换 |
| 遍历调度 | `app/node/traverser.cpp` | `ResolveJobs()` —— 将 `FootageJob` 分发给 `ProcessVideoFootage()` |
**数据流:**
```
文件 → FFmpegDecoder::RetrieveVideoInternal()
→ RetrieveFrame() 解码出 AVFrame
→ PreProcessFrame() CPU 缩放/格式转换 (sws_scale_frame)
→ ProcessFrameIntoTexture() 上传为 GPU Texture
→ YUV 格式:上传为 3 个 plane texture + YUV→RGB shader
→ RGBA/RGBA64LE:直接 glTexSubImage2D 上传
→ RenderProcessor::ProcessVideoFootage()
→ BlitColorManaged() OCIO 颜色空间转换 shader
→ 进入节点图后续处理
```
### 1.2 像素格式体系
- **核心枚举:** `ext/core/include/olive/core/render/pixelformat.h` 定义 `PixelFormat::U8 / U16 / F16 / F32`
- **GPU 格式映射:** `app/render/opengl/openglrenderer.cpp` 已将 `F32 + 4ch` 映射到 `GL_RGBA32F / GL_FLOAT`
- **内部工作格式:** `NodeTraverser::GetCacheVideoParams().format()` 决定节点图内部缓存格式
- **项目默认配置:** `app/config/config.cpp``OnlinePixelFormat = F32``OfflinePixelFormat = F16`,说明设计意图就是在线编辑使用 F32
### 1.3 当前 F32 支持的关键缺失
1. **`FFmpegDecoder::GetNativePixelFormat()` 不识别 F32 FFmpeg 格式**
- 仅映射 `RGBA → U8``RGBA64 → U16`
- `AV_PIX_FMT_RGBAF32``AV_PIX_FMT_RGBF32` 等落入 `default: INVALID`
2. **`IsPixelFormatGLSLCompatible()` 未将 RGBAF32 列为 GLSL 兼容**
- 这会导致即使解码器输出 RGBAF32,也会强制走 `sws_scale_frame` CPU 转换路径
3. **`ProcessFrameIntoTexture()` 直接上传路径缺少 RGBAF32 分支**
- 当前只有 `YUV...``RGBA / RGBA64LE` 两个直接上传分支,没有 `RGBAF32` 等直接上传路径
4. **`PreProcessFrame()``sws_scale_frame` 目标格式选择需验证 F32 支持**
- `FFmpegUtils::GetCompatiblePixelFormat(..., maximum=F32)` 理论上应返回 `AV_PIX_FMT_RGBAF32`,但需实测验证
5. **OIIO 解码器已原生支持 F32FLOAT → F32),无需修改**
## 2. 目标
- **读入时转换:** 无论源素材格式(YUV、U8、U16、F16 等),在解码器层面统一转换为 **RGBAF32** 后上传 GPU
- **内部全链路 F32:** 节点图遍历、效果处理、合成、缓存等内部环节全部使用 `PixelFormat::F32`4 通道)
- **导出保持灵活:** 导出/编码时从 F32 转换为目标格式,保持现有编码逻辑
## 3. 实施方案:全局强制 F32
**思路:** 将 F32 作为唯一的内部工作格式,在解码器出口强制转换。
**改动点:**
1. **解码器层强制 F32 输出**
- `FFmpegDecoder::RetrieveVideoInternal()`
- 修改 `RetrieveVideoParams` 或内部逻辑,令 `maximum_format` 固定为 `F32`
-`PreProcessFrame()` 中,若源格式非 RGBAF32,通过 `sws_scale_frame` 转换到 `AV_PIX_FMT_RGBAF32`
-`ProcessFrameIntoTexture()` 中增加 `AV_PIX_FMT_RGBAF32` 直接上传分支(`GL_RGBA32F / GL_FLOAT`
- `OIIODecoder::RetrieveVideoInternal()`
- OIIO 读入后,若格式非 F32,通过 `Frame::convert(PixelFormat::F32)` 转换,再上传
2. **修复 F32 格式映射**
- `FFmpegDecoder::GetNativePixelFormat()` 增加 `AV_PIX_FMT_RGBAF32 → PixelFormat::F32``AV_PIX_FMT_RGBF32 → PixelFormat::F32`
- `FFmpegDecoder::GetNativeChannelCount()` 增加对应分支
- `IsPixelFormatGLSLCompatible()` 增加 `AV_PIX_FMT_RGBAF32`(可选,因为强制转换后解码器输出就是 RGBAF32)
3. **内部工作格式锁定 F32**
-`NodeTraverser` 初始化或 `RenderProcessor` 创建时,`SetCacheVideoParams()` 强制 `format = PixelFormat::F32`
- 移除用户层对工作格式的可选配置(或保留配置但忽略/默认 F32)
- `traverser.cpp``FootageJob``GenerateJob``ColorTransformJob` 的格式设置已经使用 `GetCacheVideoParams().format()`,因此只需确保基类参数是 F32 即可
4. **导出层适配**
- `FFmpegEncoder` 的输入当前通过 `avfilter` 图做格式转换,源为 F32 时:
- `FFmpegUtils::GetFFmpegPixelFormat(F32, 4)` 已返回 `AV_PIX_FMT_RGBAF32`
- 验证 filter graph 的 `buffer` source 和 `format` filter 能否正确处理 `RGBAF32`
- `RenderProcessor::GenerateFrame()` 下载 GPU texture 到 `FramePtr` 时,`DownloadFromTexture()` 已支持 `GL_FLOAT`,直接得到 F32 CPU buffer
## 4. 关键文件与修改清单
| 文件 | 修改内容 |
|------|----------|
| `app/codec/ffmpeg/ffmpegdecoder.cpp` | ① `GetNativePixelFormat()` 增加 RGBAF32/RGBF32 → F32 映射<br>② `GetNativeChannelCount()` 增加对应分支<br>③ `IsPixelFormatGLSLCompatible()` 增加 RGBAF32<br>④ `ProcessFrameIntoTexture()` 增加 RGBAF32 直接上传分支<br>⑤ `PreProcessFrame()` 确保 divider=1 且格式为 RGBAF32 时跳过 CPU 转换 |
| `app/codec/oiio/oiiodecoder.cpp` | `RetrieveVideoInternal()` 上传前若 `frame.format() != F32` 则调用 `convert(F32)` |
| `app/node/traverser.cpp``app/render/renderprocessor.cpp` | 初始化时强制 `SetCacheVideoParams().format = F32` |
| `app/codec/ffmpeg/ffmpegencoder.cpp` | 验证 filter graph 对 RGBAF32 source 的处理,必要时调整 |
| `app/render/opengl/openglrenderer.cpp` | 确认 `GL_RGBA32F / GL_FLOAT` 路径完整,补充必要错误检查 |
| `app/codec/ffmpeg/ffmpegutils.cpp` | 验证 `GetCompatiblePixelFormat(maximum=F32)` 的行为 |
## 5. 风险评估
| 风险 | 说明 | 缓解措施 |
|------|------|----------|
| 内存带宽 ×4 | F32 是 U8 的 4 倍、U16/F16 的 2 倍,显存和内存占用显著增加 | 这是预期代价;`OfflinePixelFormat` 机制可继续用于代理预览,降低分辨率同时用 F16 减少带宽 |
| FFmpeg swscale 对 RGBAF32 支持 | `sws_scale_frame` 是否能正确处理 `AV_PIX_FMT_RGBAF32` 作为目标格式需验证 | 先写单元测试验证;若不支持,可用 OIIO `Frame::convert()` 作为 fallback,或在 GPU 上通过 shader 做格式转换 |
| OFX 插件兼容性 | 大部分 OFX 插件支持 `kOfxBitDepthFloat`,但仍有少数可能只支持 U8/U16 | `PluginRenderer` 已有格式转换路径,F32 的支持比 F16 更成熟 |
| 性能回归 | YUV→RGB 原来在 GPU 走 shader,若强制先转 RGBAF32 再上传,可能需要调整流程 | YUV 素材仍保留 GPU shader 转换路径,只是 shader 输出目标 texture 格式改为 F32OpenGL 已支持 `GL_RGBA32F` 作为 render target |
| 缓存文件体积翻倍 | 帧缓存从 U8/U16 改为 F32 后,磁盘缓存体积增大 | 可接受;必要时调整缓存策略或压缩 |
## 6. 建议的实施顺序
1. **第一阶段:** 修复 `FFmpegDecoder` F32 映射 + 增加 RGBAF32 直接上传分支,编写解码器单元测试
2. **第二阶段:**`OIIODecoder` 添加强制 F32 转换
3. **第三阶段:** 锁定内部工作格式为 F32,验证节点图全链路
4. **第四阶段:** 验证导出编码路径,确认 filter graph 对 RGBAF32 的处理
5. **第五阶段:** 性能测试与回归测试
## 7. 决策点
- 是否保留 `OfflinePixelFormat = F16` 的代理降级机制?还是连 proxy 也强制 F32?
- 若保留代理降级,是否需要在解码器层根据 online/offline 模式选择输出格式?
-168
View File
@@ -1,168 +0,0 @@
Oak Video Editor 项目结构概览(中文)
==========================
这份文档是基于当前仓库目录组织的快速导航,便于后续查找代码位置。
顶层目录
--------
- app: 主应用源码入口,涵盖核心、渲染、UI、插件、节点系统等。
- cmake: CMake 相关脚本与模块。
- docker: 构建/运行相关的容器配置。
- docs: 项目文档(你现在正在看的位置)。
- ext: 可能包含外部依赖或子模块(按需查看)。
- tests: 测试代码与用例。
- third_party: 第三方库及其源码(如 OpenFX HostSupport)。
- build、cmake-build-debug、test_compile: 构建产物或构建目录(通常不需要手动改)。
app 目录(核心模块)
-------------------
- app/core.*: 应用核心入口、初始化流程。
- app/main.cpp: 程序入口点。
- app/version.*: 版本信息与构建元数据。
- app/common: 通用基础设施与工具类(日志、路径、字符串等)。
- app/config: 配置加载与项目设置。
- app/render: 渲染子系统(帧缓存、渲染管线、插件渲染桥接等)。
- app/node: 节点系统,节点类型与图结构的核心逻辑。
- app/widget: UI 控件与节点视图(节点图、参数面板等)。
- app/panel: UI 面板组织与管理。
- app/window: 窗口与主界面。
- app/timeline: 时间线与剪辑管理。
- app/tool: 交互工具(选择、裁剪等)。
- app/undo: 撤销/重做系统。
- app/task: 异步任务与后台作业。
- app/audio: 音频处理与播放。
- app/codec: 编解码相关支持。
- app/shaders: 渲染着色器资源。
- app/ts: 时间/时间轴相关通用类型。
- app/dialog: 对话框与提示类 UI。
- app/cli: 命令行工具入口或相关实现。
- app/pluginSupport: OpenFX 插件 Host 侧实现(Clip/Image/Param/Host/PluginInstance 等)。
- app/packaging: 打包或发布相关逻辑。
重点文件索引(按模块)
----------------------
下面列的是“常用/核心入口”文件,不是完整清单,但足够定位主要流程。
核心入口与全局
-------------
- app/main.cpp: 程序入口。
- app/core.h、app/core.cpp: 应用生命周期与初始化总控。
- app/version.h、app/version.cpp: 版本与构建信息。
渲染系统
--------
- app/render/renderer.h、app/render/renderer.cpp: 渲染主调度。
- app/render/rendermanager.h、app/render/rendermanager.cpp: 渲染队列与任务管理。
- app/render/renderticket.h、app/render/renderticket.cpp: 单次渲染请求。
- app/render/renderprocessor.h、app/render/renderprocessor.cpp: 渲染处理管线。
- app/render/texture.h、app/render/texture.cpp: 纹理/帧数据容器。
- app/render/videoparams.h、app/render/videoparams.cpp: 视频格式参数。
- app/render/job/pluginjob.h、app/render/job/pluginjob.cpp: 插件渲染作业。
- app/render/plugin/pluginrenderer.h、app/render/plugin/pluginrenderer.cpp: OpenFX 插件渲染桥接。
节点系统
--------
- app/node/node.h、app/node/node.cpp: 节点基类与生命周期。
- app/node/param.h、app/node/param.cpp: 节点参数与动画/关键帧。
- app/node/value.h、app/node/value.cpp: 节点值与运行时数据。
- app/node/factory.h、app/node/factory.cpp: 节点注册与创建。
- app/node/traverser.h、app/node/traverser.cpp: 图遍历与求值。
- app/node/plugins/Plugin.h、app/node/plugins/Plugin.cpp: OpenFX 插件节点。
OpenFX Host 侧实现
------------------
- app/pluginSupport/OliveHost.h、app/pluginSupport/OliveHost.cpp: OpenFX Host 入口与消息接口。
- app/pluginSupport/OlivePluginInstance.h、app/pluginSupport/OlivePluginInstance.cpp: 插件实例生命周期与参数管理。
- app/pluginSupport/OliveClip.h、app/pluginSupport/OliveClip.cpp: Clip 实例与图像读写桥接。
- app/pluginSupport/image.h、app/pluginSupport/image.cpp: OpenFX Image 封装与数据映射。
- app/pluginSupport/paraminstance.h、app/pluginSupport/paraminstance.cpp: 参数实例实现。
- third_party/openfx/HostSupport/include/ofxhImageEffect.h: HostSupport 核心接口。
节点 UINode View
-------------------
- app/widget/nodeview/nodeview.h、app/widget/nodeview/nodeview.cpp: 节点视图主控。
- app/widget/nodeview/nodeviewitem.h、app/widget/nodeview/nodeviewitem.cpp: 节点渲染与交互。
- app/widget/nodeview/nodeviewscene.h、app/widget/nodeview/nodeviewscene.cpp: QGraphicsScene 逻辑。
- app/widget/nodeview/nodeviewedge.h、app/widget/nodeview/nodeviewedge.cpp: 连线显示。
参数 UIParam View
--------------------
- app/widget/nodeparamview/nodeparamview.h、app/widget/nodeparamview/nodeparamview.cpp: 参数面板主控。
- app/widget/nodeparamview/nodeparamviewitem.h、app/widget/nodeparamview/nodeparamviewitem.cpp: 参数项容器与布局。
- app/widget/nodeparamview/nodeparamviewwidgetbridge.h、app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp: 参数类型到控件的桥接。
- app/widget/nodeparamview/nodeparamviewtextedit.h、app/widget/nodeparamview/nodeparamviewtextedit.cpp: 多行文本参数控件。
面板与窗口
----------
- app/panel/panelmanager.h、app/panel/panelmanager.cpp: 面板管理器与切换逻辑。
- app/panel/timebased/timebased.h、app/panel/timebased/timebased.cpp: 时间基面板基类(时间轴/视图共享逻辑)。
- app/panel/node/node.h、app/panel/node/node.cpp: 节点面板入口。
- app/panel/param/param.h、app/panel/param/param.cpp: 参数面板入口。
- app/window: 主窗口与窗口级 UI 结构。
时间线/播放核心
--------------
- app/node/output/viewer/viewer.h、app/node/output/viewer/viewer.cpp: Viewer 输出节点(播放头/长度/渲染请求)。
- app/widget/viewer/viewer.h、app/widget/viewer/viewer.cpp: Viewer 面板与播放控制。
- app/widget/timelinewidget/timelinewidget.h、app/widget/timelinewidget/timelinewidget.cpp: 时间线 UI 与交互主控。
进度与任务 UI
------------
- app/dialog/progress/progress.h、app/dialog/progress/progress.cpp: 通用进度对话框。
- app/widget/taskview/taskviewitem.h、app/widget/taskview/taskviewitem.cpp: 任务进度条展示。
撤销/编辑分组
------------
- app/undo/undocommand.h、app/undo/undocommand.cpp: UndoCommand 与 MultiUndoCommand 的基础实现。
- app/undo/undostack.h、app/undo/undostack.cpp: 撤销栈(无原生“批量编辑”接口)。
- app/pluginSupport/OlivePluginInstance.h、app/pluginSupport/OlivePluginInstance.cpp: OpenFX editBegin/editEnd 触发时创建批量撤销分组。
- app/pluginSupport/OlivePluginInstance.cpp: DeferredRedoCommand 包装已应用的命令,避免批量 push 时重复执行。
- app/pluginSupport/paraminstance.h、app/pluginSupport/paraminstance.cpp: 参数 Set 走统一的 SubmitUndoCommand 接口,支持批量合并。
与 OpenFX 相关的主要位置
-----------------------
- app/pluginSupport: OpenFX HostSupport 的封装与 Olive 侧实现。
- app/render/plugin: 插件渲染调度与帧处理逻辑。
- app/node/plugins: 插件节点定义与 UI 参数桥接。
- third_party/openfx: OpenFX HostSupport 源码与接口头文件。
构建与配置
----------
- CMakeLists.txt: 根构建配置入口。
- cmake/: 自定义 CMake 模块与工具链脚本。
其他说明
--------
- README.md: 项目整体说明与开发入口。
- TODO-zh.md: OpenFX 支持的中文 TODO 说明。
流程图/调用关系(ASCII
-----------------------
OpenFX 插件渲染主流程(逻辑简化):
```
Node(Graph)
-> app/node/plugins/Plugin.cpp
-> app/render/plugin/pluginrenderer.cpp
-> app/pluginSupport/OlivePluginInstance.cpp
-> app/pluginSupport/OliveClip.cpp
-> app/pluginSupport/image.cpp
-> app/render/texture.cpp / AVFrame 映射
```
OpenFX 参数 UI 生成流程(逻辑简化):
```
OFX Param Descriptor
-> app/pluginSupport/OlivePluginInstance.cpp (newParam)
-> app/node/plugins/Plugin.cpp (Node Input 生成)
-> app/widget/nodeparamview/nodeparamview.cpp
-> app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp (控件桥接)
```
插件消息展示流程(逻辑简化):
```
OFX Host Message
-> app/pluginSupport/OliveHost.cpp (保存消息)
-> app/pluginSupport/OlivePluginInstance.cpp (发出消息数量变化)
-> app/widget/nodeview/nodeviewitem.cpp (节点右上角徽标)
-> app/widget/nodeparamview/nodeparamviewitem.cpp (面板顶部消息)
```
-93
View File
@@ -1,93 +0,0 @@
# Oak Video Editor 测试策略与计划
本文档描述 Oak Video Editor 的自动化测试策略,包括单元测试、集成测试以及 CI 执行方式。
## 目标
- 尽量自动化,减少人工测试。
- 覆盖所有模块(至少一个自动化测试)。
- 集成测试保持无 GUI(头less)。
- 在 Windows/macOS/Linux 上可重复运行。
## 测试层级
### 1) 单元测试(GoogleTest
- 目标:小范围、确定性、无 GUI。
- 目录:`tests/gtest/`
- 执行:`ctest` 里的 `olive-gtest`
### 1.5) 模块冒烟测试(GoogleTest
- 目标:对 GUI 相关模块做编译期/链接期覆盖,不实例化控件。
- 目录:`tests/gtest/module_smoke_test.cpp`
- 执行:`ctest` 里的 `olive-gtest`
### 2) 集成测试(GoogleTest
- 目标:跨模块流程但不依赖 GUI(例如序列化→反序列化)。
- 目录:`tests/gtest/`(如 `ProjectSerializer``TaskManager`)。
### 3) 现有测试(Olive 宏测试)
- 目录:`tests/general``tests/timeline``tests/compositing` 保持不变。
## 模块覆盖映射
每个顶层模块至少有一个测试用例。
- `app/common``common_current_test.cpp``common_xmlutils_test.cpp`
- `app/config``config_test.cpp`
- `app/node``node_value_test.cpp``node_keyframe_test.cpp``node_serialization_test.cpp`
- `app/node/project/serializer``project_serializer_test.cpp`
- `app/render``render_videoparams_test.cpp``render_audioparams_test.cpp`
- `app/timeline``timeline_marker_test.cpp`
- `app/undo``undo_stack_test.cpp`
- `app/task``task_taskmanager_test.cpp`
- `app/codec``codec_frame_test.cpp`
- `app/pluginSupport``plugin_support_test.cpp`
- `app/audio``app/cli``app/dialog``app/panel``app/tool``app/ui``app/widget``app/window``module_smoke_test.cpp`
若模块包含 GUI 依赖,则测试聚焦于其非可视逻辑/数据结构。
## 集成测试说明
### 项目序列化回归
- 创建最小项目并添加内置节点。
- 使用 `ProjectSerializer::Save` 写出 XML。
- 再用 `ProjectSerializer::Load` 读回。
- 验证节点恢复。
### 任务管理器执行
-`TaskManager` 添加一个 DummyTask。
- 使用事件循环等待完成。
- 验证任务确实执行。
## 单元覆盖重点(已扩展)
- `app/undo``undo_stack_test.cpp` 覆盖空栈状态、模型数据、redo 区域颜色、jump 行为、空 MultiUndoCommand 忽略逻辑。
- `app/timeline``timeline_marker_test.cpp` 覆盖列表排序、最近 marker 查询、含未知元素的保存/加载、marker 增删改命令。
- `app/pluginSupport``plugin_support_image_test.cpp` 覆盖 OFX 属性映射(bounds/ROD、像素深度、通道、预乘)及分配/清理行为。
- `app/render``render_videoparams_branch_test.cpp` 覆盖自动 divider、像素宽高比校验、方形像素宽度、Save/Load 回归。
## 无 GUI 运行
- 测试避免使用 QWidget。
- CI 中设置 `QT_QPA_PLATFORM=offscreen` 防止 GUI 初始化问题。
## 持续集成
CI 在 Windows/macOS/Linux 上执行:
1. 安装依赖(Qt、FFmpeg、OpenImageIO、OpenColorIO、OpenEXR、PortAudio、Expat)。
2. `-DBUILD_TESTS=ON` 配置。
3. 使用 CMake + Ninja 构建。
4. 运行 `ctest` 输出失败信息。
### 依赖安装说明
- Linux:优先使用发行版系统包(Ubuntu 上用 `apt`)安装 Qt6、FFmpeg、OpenImageIO、OpenColorIO、OpenEXR、PortAudio、Expat、OpenGL 头文件。
- macOS:使用 Homebrew 安装 Qt6 和图像/色彩/多媒体相关库。
- Windows:尽量使用系统安装器(Qt 通过 `install-qt-action`),其余 C/C++ 库通过 vcpkg 安装。
## 新增测试规范
- 新测试放在 `tests/gtest`
- 使用 GoogleTest 规范。
- 尽量保持确定性与无外部依赖。
- 新模块至少增加 1 个单元测试 + 1 个集成场景(可合并)。