删除plot_core

This commit is contained in:
2026-08-13 00:35:00 +08:00
parent 15c3d21f2d
commit 05263cd506
67 changed files with 4154 additions and 1336 deletions
-194
View File
@@ -1,194 +0,0 @@
# Renderive Scene 与统一 Render DAG
## Scene 类型层次
Scene 不再使用维度枚举。运行时类型关系为:
```text
Scene_Base
├── Scene_2D_Base
└── Scene_3D_Base
```
`Scene2D_Context` 通过 `Triple_State_Strategy<Scene_2D_Base, ...>` 继承 `Scene_2D_Base``Scene3D_Context` 通过 `Triple_State_Strategy<Scene_3D_Base, ...>` 继承 `Scene_3D_Base`。调用方通过类型系统、概念或 `dynamic_cast` 区分 2D 与 3D Scene。
## Scene 三缓冲状态
`Scene2D_Context``Scene3D_Context` 都公开类型别名 `Scene_State_Strategy`。状态访问使用基类名限定,避免多继承时成员歧义:
```cpp
scene.Scene_State_Strategy::set<&Scene_State::value>(value);
scene.Scene_State_Strategy::publish();
```
三缓冲分别承担正在渲染、已发布待获取、前台写入三种职责。`render()` 提交任务前调用 `acquire_render_state()`,后台帧获得稳定的状态版本。渲染状态不提供公共读取接口,只能在 `Renderable` 的 paint 调用栈中通过 `Render_State_View` 读取当前帧的稳定引用。
## Scene 观察者
Scene 状态观察者记录状态缓存修改、发布和渲染获取事件。Scene 生命周期观察者记录:
- `render_submitted`
- `render_started`
- `render_completed`
- `render_failed`
观察器和时间源沿用 `Observer_State`
## 单一 Renderable
Renderable 不再拆分为 Scene 持有缓存和独立持有缓存两类。所有对象统一继承 `Renderable_Base`
```cpp
struct Spectrum : Renderable_Base {
explicit Spectrum(Scene_Base& scene)
: Renderable_Base(scene, {.cache_enabled = true}) {}
};
```
`Renderable_Configuration::cache_enabled` 只控制渲染结果是否跨帧复用。默认关闭,以保持每次 Scene 提交都重新渲染的旧语义。Scene2D 仍为每个 Renderable 维护颜色缓存,以支持渲染依赖顺序和显示合成顺序相互独立。
缓存开启后,Prepare Buffer 与 Paint Buffer 分别按自己的 revision 判定有效性。Prepare 失效会使 Paint 失效,Paint-only 变化不会反向污染 Prepare;命中的阶段在编译本帧 Render Plan 时直接裁剪,不生成空任务。缓存关闭时两个阶段每帧执行。`configuration()` 返回线程安全的配置快照,配置修改统一通过 `Scene_Base::set_renderable_configuration()` 完成。
## Renderable 内部 Render DAG
每个 Renderable 通过 `build_prepare_graph(Renderable_Graph_Builder&)``build_paint_graph(Renderable_Graph_Builder&)` 向同一个内部图写入任务。接口只暴露 Renderive 的任务句柄,不暴露 `tf::Taskflow``tf::Task` 或任何 Taskflow 头文件:
```cpp
void build_prepare_graph(Renderable_Graph_Builder& builder) override {
auto prepare = builder.emplace(
"prepare", "Prepare", Render_Node_Kind::prepare,
[this](const Scene_Render_Context&) { prepare_data(); });
auto chunk = builder.emplace(
"chunk:0", "Chunk 0 Prepare", Render_Node_Kind::prepare,
[this](const Scene_Render_Context&) { prepare_chunk(0); });
builder.precede(prepare, chunk);
}
void build_paint_graph(Renderable_Graph_Builder& builder) override {
auto paint = builder.emplace(
"paint", "Paint", Render_Node_Kind::paint,
[this](const Scene_Render_Context&) { paint_buffer(); });
builder.precede(builder.find("chunk:0"), paint);
}
```
简单 Renderable 的默认 Prepare 节点调用 `prepare()`;需要 Paint 的类型显式提供 Paint 图。内部图快照不是公共 API,Scene 在构图同步边界内获取它。`rebuild_render_graph()` 与内部快照读取由 Renderable 串行化,已经提交的帧持有自己的不可变执行绑定。
## 外部依赖与总图组合
依赖树只表达跨 Renderable 的 Prepare 数据依赖;显示树只表达 Composite 顺序。关系修改统一通过 Scene 完成,例如 `scene.set_dependency_parent(child, parent)`,不会绕过 Scene 的渲染同步边界。每次后台渲染会:
1. 构造或复用各 Renderable 的内部图描述。
2. 根据 Prepare/Paint 缓存有效性裁剪节点。
3. 将 dependency 连接为 `parent Prepare -> child Prepare`
4. 将各 Renderable 的 Paint 连接到自己的 Composite,并按显示树串联 Composite。
5. 发布带版本的唯一 Render Plan。
6.`1 Render Node = 1 Taskflow Task``1 Render Edge = 1 Taskflow dependency` 编译一个 Taskflow。
Render Plan 只保存拓扑和最小节点元数据;当前帧的执行闭包由 `Render_Task` 临时持有,因此历史 Plan 不捕获 Renderable 指针。Taskflow 只在 `Scene_Base.cpp` 中包含。`Scene_Base` 通过内部执行上下文访问进程内共享的 `tf::Executor`Executor 跨 Scene、跨帧复用。`Renderive_Kernel` 对 Taskflow 使用 `PRIVATE` 链接,外部头文件不会受到 Taskflow 宏、类型或包含链污染。
## 显示树与依赖树
显示树只决定 Composite 顺序,依赖树只约束 Prepare。两棵树保持独立,因此轴 Prepare 可以先于曲线 Prepare,而各自 Paint 仍可并行,轴最后 Composite。节点对象不作为公开拓扑读接口;调用方通过 `topology_snapshot()` 获取线程安全关系快照,父子关系只通过 `set_display_parent()``set_dependency_parent()` 修改。参与显示树或依赖树的 Renderable 必须先 attach 到所属 Scene,避免拓扑引用未被 Scene 持有的对象。
## 测试入口
`RENDERIVE_BUILD_TESTS=ON` 时遍历 `tests`,为每个测试源文件生成独立 target 并注册到 CTest`RENDERIVE_BUILD_TESTS=OFF` 时只构建 `Renderive_Kernel` 库,不编译测试源文件。
## 线程模型
Scene、Frame Strategy、Real-Time-Data、State Strategy、Render DAG 与 Frame Capture 的允许并发组合、生命周期边界和对应测试统一记录在 `threading.md`。多线程语义测试作为普通测试套件的一部分构建;支持的平台应额外用 TSan 执行同一套测试。
## Taskflow 查找
`Kernel/CMakeLists.txt` 通过 `find_package(Taskflow CONFIG REQUIRED)` 查找 Taskflow。Kernel 不提供替代执行器,配置时必须提供真实 Taskflow 包。
## Flow 并发队列
`Flow_Refresh_Strategy` 当前使用 `std::pmr::deque<Memory_Resource_Unique_Ptr<Frame>>` 保存待渲染帧,不引入 Boost.Lockfree 或其他新增队列依赖。多个 painter 可以并发创建 Frame,入队由 `state_mutex_` 串行化;多个 renderer 调用也安全,但实际消费由 `render_mutex_` 串行化,因此每个 Frame 最多被消费一次。多 producer 下的全局顺序以实际完成入队的线性化顺序为准。
Flow 的 observer 允许同线程重入 Observer_Staterenderer observer 中再次申请同一个 Flow renderer 会立即得到空 lease,不会等待当前 renderer lease 自身释放。完整线程契约和多线程压力测试见 `threading.md`
## 实时数据附件所有权
`With_Real_Time_Data` 接收 `std::shared_ptr<Data>``Attach_Real_Time_Data` 在自身生命周期内持有这些实时数据源,避免 Renderable 仍存活时外部提前销毁 Data 产生悬空指针:
```cpp
auto data = std::make_shared<Latest_Real_Time_Data<int>>();
auto renderable = std::make_shared<Attach_Real_Time_Data<My_Renderable, Latest_Real_Time_Data<int>>>(
With_Real_Time_Data(data), scene);
```
## Scene 内存域
`Scene_Base` 接受外部 `std::pmr::memory_resource` 作为上游资源。Scene 内部建立 `std::pmr::synchronized_pool_resource`,所有可能跨线程访问的持久容器都使用该线程安全内存域:
```cpp
std::pmr::unsynchronized_pool_resource application_resource;
Scene2D_Context<> scene(application_resource);
```
外部资源决定最终的上游分配机制。Scene 内部的同步池负责将来自调用线程、后台 Scene 线程和 Renderable 构图路径的分配串行化。外部资源必须比 Scene 以及由 Scene 工厂创建后仍存活的 Renderable 更长寿。
可通过以下接口访问两个层级:
```cpp
scene.upstream_memory_resource();
scene.memory_resource();
```
`upstream_memory_resource()` 返回构造时传入的资源;`memory_resource()` 返回 Scene 实际使用的线程安全同步池。
单次渲染的 Taskflow、节点索引、执行绑定和任务句柄只由当前 `Render_Task` 持有,帧完成后释放,不进入 Render Plan 历史。
## Renderable 两种构造模式
Scene 工厂模式同时控制 Renderable 对象、`shared_ptr` 控制块以及 Renderable 内部关系节点分配:
```cpp
Scene2D_Context<> scene(memory_resource);
auto renderable = scene.make_renderable<Spectrum_Renderable>(configuration);
scene.attach_renderable(renderable);
```
工厂只构造对象,不自动挂载,`attach_renderable()` 的原有语义不变。工厂分配器持有 Scene 内存域的共享所有权,因此 Renderable 析构时内存域仍然有效。
独立构造模式继续保留:
```cpp
auto renderable = std::make_shared<Spectrum_Renderable>(scene, configuration);
Spectrum_Renderable stack_renderable(scene, configuration);
```
独立模式下,Renderable 对象本身和 `shared_ptr` 控制块由调用方选择的机制分配;关系节点仍使用所属 Scene 的内存域。Renderable 可以晚于 Scene 析构以完成自身释放;Scene 销毁后实时数据绑定自动失效,继续调用 `scene()` 会抛出 `std::logic_error`,不会返回悬空 Scene 引用。
## 独立 allocator-aware 组件
以下组件可脱离 Scene 独立注入资源:
- `Multiway_Node`
- `Recording_Color_Cache`
- `Flow_Refresh_Strategy`
- `History_Real_Time_Data`
- `Property_Builder::build_with_resource()`
- `Property_Builder::build_unique_with_resource()`
`History_Real_Time_Data` 的时间戳容器始终使用传入资源。历史值容器只有在 `Container` 本身支持 PMR allocator 时才使用该资源:
```cpp
History_Real_Time_Data<int, std::pmr::vector<int>> history(memory_resource);
```
## 不受 Scene PMR 控制的分配
以下内存不属于 Renderive 自己的 allocator-aware 场景域:
- Taskflow 4.1.0 内部任务节点、执行队列和 executor 工作资源
- `std::function` 超出小对象优化后的内部存储
- `std::thread` 的系统线程对象和线程栈
- 异常运行时对象
- 用户自定义 State、Frame、Observer、Renderable 成员内部自行使用的非 PMR 容器
- 第三方渲染后端内部资源
Scene PMR 的语义是控制 Renderive 明确实现为 allocator-aware 的核心对象,不承诺接管第三方库和用户类型的全部动态分配。
+790
View File
@@ -0,0 +1,790 @@
# Renderive 线程同步模型
> 本文只描述线程同步、所有权、锁和并发约束,不提出新的架构改造方案。
>
> 当前版本按已经回退到原有 Frame Strategy 语义后的设计理解编写。
>
> 文中分为:
>
> - **已确认**:已经明确的设计语义。
> - **待确认**:需要结合最终回退代码或由项目设计者确认的细节。
>
> 本文不把 `viewport`、`configuration`、`render graph` 重新设计成额外的加锁对象,也不把 `Render_Lease` 改成短 claim/CAS。
## 1. 总体线程模型
Renderive 的线程模型分成四类执行活动:
1. Plot / Scene 控制调用。
2. Frame Strategy 的帧生产、渲染和发布。
3. Scene Render DAG 的 Taskflow 并行执行。
4. Real-Time DataRTD)生产、维护和读取。
`render_2D` 层,一个 `Plot_Core` 持有一个具体的 `Scene2D_Context`。因此日常讨论 2D Plot 时,可以把“一个 Scene 的渲染执行域”理解成“一个 Plot 的渲染执行域”。
概念关系:
```text
Plot_Core
└── Scene2D_Context
├── Renderable A
│ └── local DAG
├── Renderable B
│ └── local DAG
├── Renderable C
│ └── local DAG
└── global Render DAG
```
多个 Renderable 的局部 DAG 组合成当前 Scene / Plot 的全局 Render DAG。
## 2. Scene / Plot 控制域
### 2.1 控制域负责的内容
控制域负责修改 Plot / Scene 的结构和配置,例如:
```text
attach / detach Renderable
Renderable 配置修改
Renderable DAG 结构修改请求
viewport 修改
layer / dependency 修改
Frame Strategy 配置修改
```
### 2.2 控制域和正在执行的 Render 的关系
**已确认:**
控制操作不能破坏当前正在执行的 Render DAG 所依赖的数据和结构。
当前已有的 Scene 串行化 / render-idle 机制负责保证部分控制操作不会和不允许并发的 Scene 操作重叠。
这里的同步应该依赖 Scene 原有线程契约,而不是给每一个普通字段都额外套一把 mutex。
### 2.3 viewport
**已确认:**
`viewport` 本身不应该因为 Render DAG 读取而额外加锁。
Render DAG 内不应该通过一个带 mutex 的 `Plot_Core::viewport_size()` 热路径反复取 viewport。
需要区分:
```text
控制侧修改 viewport
```
和:
```text
当前 Frame 渲染使用 viewport
```
具体由现有 Scene / Frame 调用顺序保证一致性。
**待确认:**
- `set_viewport_size()` 是否被明确限定只能由某一控制线程调用。
- Frame 开始后是否允许其他线程修改 Plot viewport。
- 当前 Frame 实际使用的 viewport 在哪一个现有对象中固定下来。
本文不擅自为 viewport 新增 mutex、atomic 或新的 publication 结构。
## 3. Renderable configuration
**已确认:**
`Renderable configuration` 不应该在 Render DAG 热路径为了读取配置而加锁。
配置修改属于控制行为。
渲染执行读取配置应该依靠原有 Scene 控制串行化和配置修改规则保证安全,而不是:
```text
Prepare/Paint
configuration()
configuration_mutex_
```
这种每次执行节点都重新同步的方式。
**待确认:**
- 配置修改是否全部必须经过 `Scene_Base::lock_render_idle()` 或等价机制。
- 是否存在允许控制线程在 Scene render 期间直接修改 configuration 的公开 API。
- 如果存在,哪些字段允许并发修改,哪些字段必须等待 render idle。
在这些约束确认前,不应新增 per-Renderable configuration mutex 作为兜底。
## 4. Renderable graph
### 4.1 Graph 的语义
每个 Renderable 拥有自己的局部 DAG。
局部 DAG 可以:
```text
fan-out
fan-in
一对多
多对一
局部串行
局部并行
多入口
多出口
子图组合
```
它不是树,只要求整体保持无环。
例如:
```text
┌─ B ─┐
A ──────┼─ C ─┼─ F
└─ D ─┘
```
也是合法局部 DAG。
### 4.2 Graph 同步
**已确认:**
Render DAG 执行阶段读取当前 Renderable graph 不应该再通过 `render_graph_mutex_` 做节点级同步。
Graph 结构变化属于控制 / rebuild 行为。
当前 Frame 已经开始执行以后,不应该一边执行同一份 graph,一边原地修改这份 graph。
因此正确线程语义必须是:
```text
旧 graph 正在被当前 Render 使用
||
控制侧请求下一次 graph 结构变化
```
而不是:
```text
worker 正在遍历 graph
||
另一个线程原地修改同一个 graph 对象
```
**待确认:**
- `rebuild_render_graph()` 的真正执行线程。
- dirty 标记和真正 rebuild 是否分离。
- 当前实现是否已经通过不可变 graph 对象 / shared ownership 保证旧 graph 生命周期。
- graph rebuild 是否只能发生在 Scene 的串行 publication / render-idle 区域。
本文不新增 graph mutex;最终文档应根据回退代码把上述四点写死。
## 5. Frame Strategy
Frame Strategy 管理的是帧生命周期和帧 ownership,不负责 Render DAG 内部节点的数据同步。
不同 Strategy 可以拥有不同的 Frame 队列 / buffer / publish 规则,例如:
```text
Manual
Flow
Low Latency
其他 Frame Strategy
```
但 Render DAG 的 Prepare / Paint / Composite 语义不因 Strategy 改变。
## 6. Render_Lease
### 6.1 已确认语义
**已确认:原有 `Render_Lease` 生命周期互斥设计保留。**
`Render_Lease` 不是仅用于“哪个 renderer 抢到 Frame”的短 claim。
它的锁可以覆盖:
```text
acquire renderer
scene.render()
Taskflow 执行
wait_for_render()
Render 完成
Render_Lease 结束
```
如果 Strategy 的 `render_mutex_` 同时承担:
```text
当前 Frame 正在 render
该 Frame publish / swap
```
之间的生命周期互斥,那么该锁必须覆盖完整 render 生命周期。
核心不变量:
> **没有完成渲染的 Frame 不能被发布出去。**
因此:
```text
rendering Frame
```
和:
```text
publish / swap same Frame
```
必须按当前 Strategy 设计保持互斥。
### 6.2 这把锁不属于 DAG 内部数据锁
即使 `Render_Lease` 外层持有 `render_mutex_`,也不意味着 DAG worker 内部靠这把锁串行执行。
正确关系是:
```text
caller thread
└── 持有 Frame Strategy 生命周期锁
└── 等待 Taskflow
├── worker 0
├── worker 1
├── worker 2
└── worker N
```
Taskflow workers 仍然根据 DAG dependency 并行执行。
因此必须区分:
```text
Frame Strategy 生命周期锁
```
和:
```text
Render DAG 数据访问锁
```
前者允许存在;后者应该尽量不存在于本来无需同步的数据上。
### 6.3 禁止擅自改成短 claim
除非 Strategy 本身重新设计并明确提供等价的:
```text
render 未完成
禁止 publish
```
保证,否则不能把完整 Render Lease
```text
lock → render → wait → unlock
```
擅自改成:
```text
lock → claim → unlock → render
```
更不能因为想“无锁”就自行改成 CAS。
## 7. 三缓冲 / 多缓冲交换
### 7.1 Frame buffer ownership
Frame Strategy 的 buffer ownership 必须保证:
```text
producer 正在写的 buffer
renderer 正在渲染的 buffer
consumer / publisher 可见的 buffer
```
不会在不允许的阶段被同一参与者同时修改。
### 7.2 publish 条件
**已确认:**
Frame 只有在完成完整 Render DAG 后才能进入可发布状态。
也就是至少:
```text
Prepare
Paint
Composite
```
需要满足当前 Frame Strategy 所要求的完成条件。
Frame publish / swap 的同步属于 Frame Strategy 层,不应该被“清理 Render DAG 内部锁”的工作改动。
## 8. Scene Render DAG
### 8.1 全局 DAG
一个 Scene / Plot 的 Renderables 最终组合为一张全局 DAG。
Renderable 自己拥有局部子 DAG。
全局 DAG 不是严格顺序图,而是 dependency graph。
例如:
```text
A.prepare ─────────→ A.paint ───────┐
B.prepare ─→ B.work ─→ B.paint ─────┼→ composite
C.prepare ─────────→ C.paint ───────┘
```
只要 dependency 满足,节点就可以执行。
### 8.2 没有全局 Prepare barrier
**已确认:**
不存在:
```text
所有 Prepare 全部完成
所有 Paint 才允许开始
```
这种全局 barrier。
例如如果 A.paint 只依赖 A.prepare,那么:
```text
A.prepare 完成
```
以后 A.paint 可以开始,即使:
```text
B.prepare
```
还没有结束。
### 8.3 worker 之间的同步
Taskflow worker 之间主要通过 DAG dependency 建立先后关系。
原则:
```text
没有 dependency edge
允许并发
```
而不是通过一个全局 Render mutex 把所有 Renderable 串起来。
## 9. Prepare
Prepare 是 Renderable 的数据准备阶段。
它可以执行:
```text
geometry 计算
layout 计算
clip 计算
需要的状态读取
对 Paint 所需数据的准备
局部任务拆分
```
Prepare 内部也可以是一张 DAG。
### 9.1 Prepare 的锁
不能简单规定“Prepare 完全不允许锁”。
是否需要锁取决于它访问的数据本身。
例如:
```text
RTD
```
是动态共享实时数据,本身有同步要求,因此 Prepare 访问 RTD 时允许按 RTD 自身线程模型获取锁。
但是:
```text
viewport
configuration
render graph
```
不能因为实现方便就在 Prepare 每次读取时新加一层无意义 mutex。
## 10. Paint
Paint 根据当前 Renderable 已准备好的数据生成像素 / layer / paint output。
### 10.1 Paint 并发
不同 Renderable 的 Paint 是否能并发,由:
```text
DAG dependency
paint output ownership
composite dependency
```
决定。
不能依赖一个全局 mutex 控制 Paint 顺序。
### 10.2 Paint 数据访问
Paint 不应该为了获取普通固定渲染输入反复进入:
```text
Plot mutex
configuration mutex
render graph mutex
```
### 10.3 跨 Renderable 数据
**已确认的设计方向:**
跨 Renderable 的依赖应该显式体现在 Render DAG / Prepare dependency 中,而不是 Paint 临时从其他 Renderable 拉动态数据。
最终允许 Paint 访问哪些 Scene / Renderable API,需要结合回退代码再列出白名单。
## 11. Composite
Composite 负责按照明确的 layer / z-order / dependency 组合各 Paint 输出。
Paint 的实际线程完成顺序不能代替 layer order。
例如:
```text
A.paint ─┐
B.paint ─┼→ composite
C.paint ─┘
```
即使 C.paint 最先结束,也不能因此改变最终覆盖顺序。
Composite 顺序是渲染语义,不是 mutex 获得顺序。
## 12. Real-Time DataRTD
RTD = Real-Time Data。
RTD 是持续被生产线程更新、同时可能被渲染线程读取的动态共享数据。
### 12.1 RTD 与普通配置不同
RTD 不能套用:
```text
viewport/config/graph 不加锁
```
这一规则。
RTD 本身就是并发共享动态数据,因此它需要自己的同步设计。
**已确认:RTD 原有合理锁保留。**
### 12.2 RTD 可能包含的并发活动
例如:
```text
producer append data
producer update state
discard old data
observer notification
Prepare read RTD
```
这些活动之间的同步必须由 RTD 自己保证。
不能为了追求“Render DAG 无锁”而直接删掉 RTD mutex。
### 12.3 RTD 的具体锁序
**待确认:**
需要根据回退后的实际代码逐项写清:
```text
RTD mutation mutex
RTD data/state mutex
observer callback 前后锁释放位置
discard 与 reader 的关系
render read 是否持锁
```
尤其必须说明 callback 是否在锁外执行。
## 13. State Strategy
普通 Renderable State 和 RTD 不是一个概念。
Renderable State Strategy 负责:
```text
update/cache/render state
publish/swap
revision
```
Double State / Triple State 的具体同步规则必须按当前 Strategy 实现描述。
### 13.1 已确认原则
State 的 publish / swap 本身允许使用锁。
不能把:
```text
发布动作使用 mutex
```
误判为:
```text
Render DAG worker 因读取普通数据而加锁
```
这两种锁语义完全不同。
### 13.2 待确认
需要根据回退版本最终写明:
- render state getter 是否直接读 render state。
- publish/swap 是否加锁。
- cache/update state 谁能写。
- observer 是锁内还是锁外调用。
- Triple State 的 published/render/cache 三者什么时候交换。
## 14. Observer / callback
Callback 是线程同步中最容易出现重入问题的地方。
原则:
> 不应该在持有可能被 callback 重入的内部数据锁时执行未知用户 callback。
典型安全模式:
```text
lock
修改内部状态
准备 Observation
unlock
调用 observer
```
而不是:
```text
lock
修改内部状态
调用 observer
unlock
```
### 14.1 待确认
需要结合实际代码列出:
```text
State observer
RTD observer
Frame observer
Renderable callback
```
分别在哪个线程执行,以及调用时持有哪些锁。
## 15. Scene worker 与 Taskflow worker
**待结合回退代码最终确认线程名称和数量,但概念上需要明确区分两层:**
### 15.1 Scene render 生命周期线程
负责一帧的高层流程,例如:
```text
开始一帧
组织/提交 Render Plan
启动 Taskflow
等待 Taskflow
完成 Frame
```
### 15.2 Taskflow executor workers
真正执行 DAG node
```text
Prepare node
Paint node
Composite node
```
因此某个 mutex 被 Scene caller / Scene worker 持有,不等于 Taskflow nodes 被这把 mutex 串行化。
分析锁竞争时必须说明:
```text
谁拿锁
谁等待锁
锁是否进入 Taskflow node
```
不能只看到“整帧期间存在一把锁”就判断它影响 DAG 并行度。
## 16. 当前锁分类
锁应按用途分类,而不是简单分为“好锁/坏锁”。
| 类型 | 典型用途 | 是否允许 |
|---|---|---|
| Frame Strategy 生命周期锁 | render 与 publish/swap 互斥 | 允许 |
| Frame/Buffer ownership 锁 | buffer 生命周期、队列、交换 | 允许 |
| RTD 数据锁 | 动态共享实时数据 | 允许 |
| State publish/swap 锁 | 状态发布和交换 | 允许 |
| Scene topology/control 锁 | attach/detach/控制操作 | 允许 |
| Observer 内部管理锁 | observer 注册/状态维护 | 按实际实现 |
| viewport 读取锁 | DAG 内读取 viewport | 不应该有 |
| configuration 读取锁 | DAG 内读取普通配置 | 不应该有 |
| render graph 读取锁 | DAG 内读取当前 graph | 不应该有 |
| Paint 全局锁 | 串行化所有 Paint | 不应该有 |
| Prepare 全局锁 | 串行化所有 Prepare | 不应该有 |
## 17. “内部渲染无锁”的准确含义
项目里的“清理内部渲染锁”不能理解成:
> 整个 Renderive 在 Frame render 期间任何地方都不能存在 mutex。
正确含义应该是:
> **Render DAG 不因为读取本来已经由现有线程契约保证稳定的普通渲染数据而重复获取 mutex。**
明确包括:
```text
viewport
configuration
render graph
```
不应该靠新的 mutex 保护 DAG 内读取。
但不包括:
```text
Frame Strategy 生命周期互斥
Frame publish/swap
RTD 动态共享数据同步
State publish/swap
Scene 控制操作
```
这些同步有独立语义,不能因为“内部渲染无锁”被删除。
## 18. 当前禁止擅自引入的改动
在没有重新确认线程模型之前,不进行以下改动:
```text
不把 Render_Lease 擅自改成短 claim
不把 Frame Strategy 生命周期锁改成 CAS
不重新设计三缓冲 ownership
不删除 RTD 正常数据锁
不为 viewport 新增 mutex/atomic
不为 configuration 新增 mutex 兜底
不为 render graph 新增 worker-side mutex
不引入新的“统一快照层”改变原有函数语义
```
后续任何锁优化必须先证明:
```text
这把锁保护的具体对象是什么
谁会并发访问
现有更高层同步是否已经保证安全
删除后是否改变旧函数语义
```
## 19. 需要项目设计者确认的线程契约
下面这些点需要在最终版本中由代码或设计者明确,确认后再更新本文,不应由实现者自行猜测:
1. `viewport` 修改允许在哪些线程发生。
2. Frame render 期间是否允许修改 viewport。
3. Renderable configuration 的所有修改入口及其线程约束。
4. `rebuild_render_graph()` 的调用线程和真正 rebuild 时机。
5. 当前 graph 的生命周期如何保证。
6. Scene 高层 render 生命周期实际在哪个线程执行。
7. 三种 Frame Strategy 的 `Render_Lease` 分别保护哪些状态。
8. 三种 Strategy 的 publish/swap 与 render 的锁顺序。
9. RTD 每把锁分别保护什么。
10. RTD observer 是否一定在内部数据锁释放以后调用。
11. State Strategy 的 render getter 是否无锁。
12. Double/Triple State publish/swap 的精确锁范围。
13. Scene control lock 与 Frame Strategy lock 是否允许嵌套。
14. Frame observer/callback 在哪个线程调用。
15. 是否存在任何公开 API 允许在 Render DAG 执行期间修改当前 Renderable 的 graph/config。
## 20. 文档最终需要达到的目标
最终 `threading.md` 必须让实现者只看本文就能回答:
```text
某个字段谁写?
某个字段谁读?
允许哪些线程同时访问?
需要哪把锁?
为什么需要?
锁覆盖多大范围?
callback 时是否还持锁?
Frame Strategy 为什么可以整帧持锁?
RTD 为什么需要锁?
viewport/config/graph 为什么不需要额外读锁?
两个 Renderable 为什么可以并行?
什么时候必须通过 DAG edge 而不是 mutex 建立顺序?
```
只有这些问题全部能从文档直接得到确定答案以后,才根据文档做下一轮锁清理。
@@ -0,0 +1,20 @@
#pragma once
#include <cstddef>
#include <cstdint>
using Capture_Session_Id = std::uint64_t;
struct Capture_Frame_Ticket {
Capture_Session_Id session_id{};
bool capture{};
};
struct Capture_Controller_State {
Capture_Session_Id session_id{};
std::size_t remaining_frame_count{};
[[nodiscard]] bool enabled() const noexcept {
return remaining_frame_count != 0;
}
};
@@ -0,0 +1,8 @@
#pragma once
struct Renderable_Configuration {
bool cache_enabled{false};
friend bool operator==(Renderable_Configuration,
Renderable_Configuration) = default;
};
+12 -6
View File
@@ -10,19 +10,24 @@
#include "renderive/base/observer/Observer.hpp"
#include "renderive/frame_control/Frame_Control.hpp"
#include "renderive/renderable/color/Concepts.hpp"
#include "renderive/state/Triple_State_Strategy.hpp"
#include "renderive/state/Double_State_Strategy.hpp"
#include "base/Scene_Base.hpp"
#include "base/Frame_Viewport.hpp"
struct Scene2D_Frame_Data : Abstract_Frame {};
struct Scene2D_State {
std::uint64_t revision{};
Frame_Viewport viewport;
};
template <class State>
concept Scene2D_State_Value = State_Value<State> && requires(State& state) {
requires std::same_as<std::remove_cvref_t<decltype(state.viewport)>, Frame_Viewport>;
};
template <class Frame_Control, class... Args>
concept Scene2D_Frame_Control_Constructible = std::constructible_from<Frame_Control, Args...> || std::constructible_from<Frame_Control, std::pmr::memory_resource&, Args...>;
template <class Strategy = Low_Latency_Strategy<Scene2D_Frame_Data>, Color_Cache_Type Cache = Recording_Color_Cache, class State = Scene2D_State, class State_Observer = Observer_State<>, class Scene_Observer = Observer_State<>>
requires Frame_Control_Strategy_For<Strategy, Scene2D_Frame_Data> && State_Value<State>
class Scene2D_Context final : public Triple_State_Strategy<Scene_2D_Base, State, Atomic_Spin_Mutex, State_Observer>, public Scene_Compositor {
requires Frame_Control_Strategy_For<Strategy, Scene2D_Frame_Data> && Scene2D_State_Value<State>
class Scene2D_Context : public Double_State_Strategy<Scene_2D_Base, State, Atomic_Spin_Mutex, State_Observer>, public Scene_Compositor {
public:
using Scene_State_Strategy = Triple_State_Strategy<Scene_2D_Base, State, Atomic_Spin_Mutex, State_Observer>;
using Scene_State_Strategy = Double_State_Strategy<Scene_2D_Base, State, Atomic_Spin_Mutex, State_Observer>;
using Render_Task = Scene_Base::Render_Task;
using Renderable = Scene_Base::Renderable;
using Renderable_List = Scene_Base::Renderable_List;
@@ -108,6 +113,7 @@ protected:
void prepare_render_task(Render_Task& task, const Renderable_List& renderables) override {
task.render_order = Scene_Base::dependency_order(renderables);
task.display_order = Scene_Base::display_order(renderables);
task.viewport = this->Scene_State_Strategy::render_state_value().viewport;
}
Color_Cache* prepare_renderable_cache(Renderable_Base& renderable, bool clear) override {
Cache* cache = color_caches_.at(&renderable).get();
@@ -123,7 +129,7 @@ protected:
final_color_cache_.composite(*color_caches_.at(&renderable));
}
std::uint64_t acquire_scene_state() override {
return this->Scene_State_Strategy::acquire_render_state();
return this->Scene_State_Strategy::state_revision();
}
void observe_scene(const Scene_Base::Observation& observation) noexcept override {
scene_observer_.observe(observation);
@@ -0,0 +1,50 @@
#pragma once
#include <cstdint>
#include <memory>
#include <vector>
#include "renderive/capture/Capture_Types.hpp"
#include "renderive/frame_control/base/Frame_Control_Strategy_Base.hpp"
#include "renderive/renderable/Renderable_Configuration.hpp"
#include "renderive/state/Render_State_View.hpp"
#include "Frame_Viewport.hpp"
class Color_Cache;
class Renderable_Base;
class Scene_Base;
struct Renderable_Graph;
class Renderable_Frame_State {
public:
std::uint64_t renderable_id{};
bool visible{true};
Renderable_Configuration configuration;
std::uint64_t prepare_revision{};
std::uint64_t prepared_revision{};
std::uint64_t paint_revision{};
std::uint64_t painted_revision{};
std::uint64_t painted_prepare_revision{};
bool prepare_required{};
bool paint_required{};
std::shared_ptr<const Renderable_Graph> render_graph;
std::vector<std::uint64_t> dependency_parent_ids;
private:
friend class Scene_Base;
std::shared_ptr<Renderable_Base> owner_;
std::shared_ptr<Color_Cache> paint_buffer_;
};
class Frame_Render_Snapshot {
public:
std::uint64_t frame_id{};
std::uint64_t render_sequence{};
std::uint64_t scene_state_revision{};
Frame_Viewport viewport;
Frame_Control_Strategy_Base::State frame_control_state;
Capture_Controller_State capture_state;
Capture_Frame_Ticket capture_ticket;
Render_State_View render_state;
std::vector<Renderable_Frame_State> renderables;
std::vector<std::uint64_t> display_order;
};
@@ -0,0 +1,9 @@
#pragma once
struct Frame_Viewport {
int width{};
int height{};
[[nodiscard]] bool empty() const noexcept {
return width <= 0 || height <= 0;
}
friend bool operator==(Frame_Viewport, Frame_Viewport) = default;
};
@@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
#include "Frame_Render_Snapshot.hpp"
class Color_Cache;
struct Node_Execution_Metrics;
struct Prepare_Render_Context {
const Frame_Render_Snapshot& frame;
const Renderable_Frame_State& renderable;
Node_Execution_Metrics* metrics{};
};
struct Paint_Render_Context {
const Frame_Render_Snapshot& frame;
const Renderable_Frame_State& renderable;
Color_Cache& color_cache;
Node_Execution_Metrics* metrics{};
};
struct Composite_Render_Context {
const Frame_Render_Snapshot& frame;
const Renderable_Frame_State* renderable{};
const Color_Cache* color_cache{};
Node_Execution_Metrics* metrics{};
};
+10 -3
View File
@@ -245,18 +245,22 @@ void Scene_Base::set_display_parent(Renderable_Base& renderable, Renderable_Base
}
void Scene_Base::publish_frame_state() {
auto task_lock = lock_render_idle();
if (auto* state = dynamic_cast<State_Strategy_Base*>(this))
state->publish();
Renderable_List renderables(&memory_resource());
{
std::lock_guard<std::mutex> lock(renderable_mutex_);
renderables = *cache_renderables_;
}
for (const Renderable& renderable : renderables) {
if (auto* state = dynamic_cast<State_Strategy_Base*>(renderable.get())) {
if (auto* state = dynamic_cast<State_Strategy_Base*>(renderable.get()))
state->publish();
}
renderable->publish_real_time_data();
}
}
void Scene_Base::notify_model_dirty() noexcept {
model_dirty_.store(true, std::memory_order_release);
}
void Scene_Base::add_display_parent(Renderable_Base& renderable, Renderable_Base& parent) {
auto task_lock = lock_render_idle();
validate_renderable_scene(renderable);
@@ -486,6 +490,9 @@ std::unique_lock<std::recursive_mutex> Scene_Base::lock_render_idle() {
bool Scene_Base::is_render_worker_thread() const noexcept {
return active_execution_scene_ == this || (worker_.joinable() && std::this_thread::get_id() == worker_.get_id());
}
bool Scene_Base::consume_model_dirty() noexcept {
return model_dirty_.exchange(false, std::memory_order_acq_rel);
}
void Scene_Base::shutdown() noexcept {
{
std::unique_lock<std::recursive_mutex> lock(task_mutex_);
@@ -720,7 +727,7 @@ void Scene_Base::execute_taskflow(Render_Task& task) {
Node_Execution* execution = task.frame->execution_slot(node.execution_index);
Scene_Render_Context context{this, task.frame_control_state,
task.scene_state_revision, task.render_sequence,
renderable, color_cache,
task.viewport, renderable, color_cache,
execution ? &execution->metrics : nullptr};
if (!execution) {
Render_Execution_Scope scope(*this);
@@ -18,6 +18,7 @@
#include "renderive/render_graph/Render_Plan.hpp"
#include "Abstract_Frame.hpp"
#include "Scene_Lifetime.hpp"
#include "Frame_Viewport.hpp"
#include "Scene_Render_Context.hpp"
class Color_Cache;
class Scene_Compositor {
@@ -67,6 +68,7 @@ public:
void render(Abstract_Frame& frame);
void wait_for_render();
void publish_frame_state();
void notify_model_dirty() noexcept;
void attach_renderable(Renderable renderable);
void detach_renderable(Renderable_Base& renderable);
void set_display_parent(Renderable_Base& renderable, Renderable_Base* parent);
@@ -115,6 +117,7 @@ protected:
Frame_Control_Strategy_Base::State frame_control_state;
std::uint64_t scene_state_revision{};
std::uint64_t render_sequence{};
Frame_Viewport viewport;
Renderable_List render_order;
Renderable_List display_order;
std::pmr::vector<Stage_Commit> stage_commits;
@@ -142,6 +145,7 @@ protected:
virtual std::uint64_t observer_now_ns() const noexcept;
std::unique_lock<std::recursive_mutex> lock_render_idle();
bool is_render_worker_thread() const noexcept;
bool consume_model_dirty() noexcept;
void shutdown() noexcept;
private:
friend class Renderable_Base;
@@ -165,6 +169,7 @@ private:
inline static thread_local Scene_Base* active_submitted_observer_scene_{};
std::shared_ptr<Scene_Lifetime> scene_lifetime_;
std::shared_ptr<Scene_Memory_Domain> memory_domain_;
std::atomic_bool model_dirty_{true};
std::array<Renderable_List, 2> renderable_states_;
Renderable_List* render_renderables_;
Renderable_List* cache_renderables_;
@@ -1,6 +1,7 @@
#pragma once
#include <cstdint>
#include "renderive/frame_control/base/Frame_Control_Strategy_Base.hpp"
#include "Frame_Viewport.hpp"
class Color_Cache;
class Renderable_Base;
class Scene_Base;
@@ -10,6 +11,7 @@ struct Scene_Render_Context {
Frame_Control_Strategy_Base::State frame_control_state;
std::uint64_t scene_state_revision{};
std::uint64_t render_sequence{};
Frame_Viewport viewport;
Renderable_Base* renderable{};
Color_Cache* color_cache{};
Node_Execution_Metrics* metrics{};
@@ -93,11 +93,12 @@ struct Double_State_Strategy : That, State_Strategy_Base {
std::lock_guard lock(mtx);
return publish_count;
}
private:
friend class Render_State_View;
protected:
const State& render_state_value() const noexcept {
return *render_state;
}
private:
friend class Render_State_View;
Observer observer;
State states[3];
State* render_state;
@@ -214,3 +214,26 @@ TEST(scene2d_context_test, failed_attach_leaves_renderable_fully_detached) {
EXPECT_NO_THROW(scene.attach_renderable(renderable));
EXPECT_EQ(scene.renderable_count(), 1);
}
struct Scene2D_Viewport_Snapshot_Renderable : Renderable_Base {
explicit Scene2D_Viewport_Snapshot_Renderable(Scene_Base& scene) : Renderable_Base(scene) {}
void prepare(const Scene_Render_Context& context) override {
viewport = context.viewport;
}
Frame_Viewport viewport;
};
TEST(scene2d_context_test, published_scene_viewport_is_frozen_for_render) {
Scene2D_Context<> scene;
auto renderable = std::make_shared<Scene2D_Viewport_Snapshot_Renderable>(scene);
scene.attach_renderable(renderable);
scene.Scene_State_Strategy::set<&Scene2D_State::viewport>(Frame_Viewport{320, 180});
scene.publish_frame_state();
scene.Scene_State_Strategy::set<&Scene2D_State::viewport>(Frame_Viewport{640, 360});
scene.render();
scene.wait_for_render();
EXPECT_EQ(renderable->viewport, (Frame_Viewport{320, 180}));
scene.publish_frame_state();
renderable->invalidate_prepare();
scene.render();
scene.wait_for_render();
EXPECT_EQ(renderable->viewport, (Frame_Viewport{640, 360}));
}
@@ -6,7 +6,7 @@
#include <vector>
#include "renderive/scene/Scene.hpp"
#include "../state/State_Test_Types.hpp"
struct Scene_State_Observer_Test_State {
struct Scene_State_Observer_Test_State : Scene2D_State {
int value{};
};
struct Scene_State_Observer_Test_Time_Source {
@@ -26,7 +26,7 @@ struct Scene_State_Observer_Test_Recorder {
};
using Scene_State_Observer_Test_Scene_Observer = Observer_State<Scene_State_Observer_Test_Recorder, Scene_State_Observer_Test_Time_Source>;
using Scene_State_Observer_Test_State_Observer = Observer_State<>;
TEST(scene_state_observer_test, scene_uses_triple_state_and_observes_lifecycle) {
TEST(scene_state_observer_test, scene_uses_double_state_and_observes_lifecycle) {
State_Render_State_Reader render;
Scene_State_Observer_Test_Recorder recorder;
using Scene = Scene2D_Context<Low_Latency_Strategy<Scene2D_Frame_Data>, Recording_Color_Cache, Scene_State_Observer_Test_State, Scene_State_Observer_Test_State_Observer, Scene_State_Observer_Test_Scene_Observer>;
@@ -8,5 +8,5 @@ static_assert(!Scene_3D<Scene2D_Context<>>);
TEST(scene_concept_test, accepts_2d_and_3d_contexts) {
SUCCEED();
}
static_assert(std::is_final_v<Scene2D_Context<>>);
static_assert(!std::is_final_v<Scene2D_Context<>>);
static_assert(std::is_final_v<Scene3D_Context<>>);
-107
View File
@@ -1,107 +0,0 @@
# Renderive 线程模型
## 总原则
同一个 Renderive 对象的析构不能和调用方主动发起的普通成员函数并发执行。唯一特意处理的跨生命周期路径是实时数据通知到 Scene:实时数据更新允许与 Scene 析构竞争,`Scene_Lifetime` 会阻止通知进入已经失效的 Scene。
Frame lease 是线程绑定的独占操作句柄。内置 `Painter_Lease``Render_Lease` 都显式声明 `thread_affine = true`。lease 可以在取得它的线程内 move,但不能把仍持有底层互斥锁的 lease 转移到另一线程使用或析构;Frame 内容也不能被多个线程同时操作。
用户实现的 Renderable 任务会由 Taskflow 并行执行。不同 Renderable 如果没有依赖关系可以并行,同一个 Renderable 内部没有依赖边的 Prepare 任务也可以并行。写同一个 Paint Buffer 的节点必须有真实依赖或收敛为单 Paint 节点;不同 Renderable 的独立 Paint Buffer 可以并行。用户任务共享的数据必须由用户自己提供同步;Kernel 只保证自己的 Scene、状态、缓存元数据和 Render DAG 描述不会产生数据竞争。
## Scene
`render()` 支持多个调用线程同时提交。Scene 不保存多任务提交队列,而是使用 `task_mutex_` 将提交串行化;后一个 `render()` 会等待当前帧结束后再生成下一帧,因此不会覆盖已有提交。
`wait_for_render()` 支持多个等待线程。同一次失败渲染的所有 waiter 都读取同一个 `Render_Completion`,不会由第一个 waiter 消耗异常。若失败帧尚未被任何 waiter 观察就提交了下一帧,失败会保存为 pending exception,并由后续 `wait_for_render()` 报告,不会被新的成功 completion 覆盖。
`attach_renderable()``detach_renderable()``set_display_parent()``set_dependency_parent()``set_renderable_configuration()` 与后台渲染互斥。它们通过 `lock_render_idle()` 等待 Scene 空闲后再修改 Renderable 集合或拓扑。
`topology_snapshot()``renderable_count()` 可以与 Scene 拓扑修改并发调用。拓扑快照持有 `shared_ptr<const Renderable_Base>`,所以返回以后即使对应对象从 Scene detach,快照内对象生命周期仍然有效。
`with_final_color_cache()` 在普通线程上等待后台渲染结束后读取最终缓存。若从 Scene 自己的 render worker 调用,则直接读取当前 worker 可见的缓存。回调在 Scene 的控制锁范围内执行,不应启动另一个线程并等待该线程重新进入同一个 Scene 控制 API。
Scene observer 在触发事件的线程同步执行。`render_submitted` observer 中再次调用 `render()` 会登记为 deferred render,在当前 submitted callback 返回并启动当前帧后顺序提交;在该 callback 内调用 `wait_for_render()` 为 no-op,避免同步 observer 自锁。render worker 和 Taskflow executor worker 中调用 `wait_for_render()` 同样为 no-op;这些 render execution 线程调用 `render()` 或拓扑/配置修改接口会直接抛出 `std::logic_error`,不会等待当前帧形成自锁。
## Renderable
Prepare 与 Paint 分别使用原子 input/output revision,可以和后台渲染并发。渲染开始时捕获 revision,结束后只发布该次捕获值,因此渲染过程中发生的 invalidate 不会丢失;Paint invalidation 不修改 Prepare revision。
`configuration()` 返回值快照;`set_renderable_configuration()` 通过 Scene 串行修改内部配置。
Renderable 内部图构建和 `rebuild_render_graph()``render_graph_mutex_` 串行化。内部不可变图快照只由 Scene 和当前帧持有;`build_prepare_graph()``build_paint_graph()` 回调不得重入同一个 Renderable 的构图或重建操作。Render Plan 历史只保存纯拓扑,不保存捕获 Renderable 的执行闭包。
Capture 请求通过原子 controller 在帧开始时一次性保留配额。每个 Capture Frame 按 Plan 的 `execution_index` 预分配 slot,不使用 node-id 哈希表;不同 Taskflow worker 只写自己的 slot。失败帧丢弃 Abstract Frame 中的执行数据并归还配额,只有成功完成的不可变 Frame Snapshot 才进入带锁的冷路径 repository 和分析阶段。
Renderable 可以晚于 Scene 析构以完成自身释放,但 `scene()` 只用于调用方已经保证 Scene 生命周期有效的同步访问。不要让 `scene()` 返回的引用与 Scene 析构竞争。Kernel 内部需要跨 Scene 生命周期访问的实时数据路径使用 `Scene_Lifetime::Lease`,不依赖这个裸引用。
## Flow 帧策略
当前 `Flow_Refresh_Strategy` 不依赖 Boost.Lockfree,也没有其他新增队列依赖。Frame 使用传入的 `std::pmr::memory_resource` 分配,待渲染帧保存在 `std::pmr::deque` 中。
多个 painter 线程可以同时调用 `acquire_painter()`。Frame 分配、队列容器操作和 Frame 回收都纳入 `state_mutex_` 的同一同步边界,因此即使调用方传入 `std::pmr::unsynchronized_pool_resource`,Kernel 也不会并发进入该资源。多个 renderer 调用线程也不会产生数据竞争;`render_mutex_` 保证一次只有一个 renderer lease 真正消费队列,所以每个已入队 Frame 最多消费一次。
Flow 的并发语义是线程安全队列访问,不是 lock-free 保证。单线程生产时保持 FIFO;多个生产线程时,全局顺序由实际进入 `state_mutex_` 并完成入队的线性化顺序决定。
## Manual 帧策略
多个 painter 调用由 `painter_mutex_` 串行化。`refresh()` 和 renderer 共用 `render_mutex_`,所以不会在 renderer 正在读取 render frame 时交换 pending/render 指针。Frame 指针和状态切换同时受 `state_mutex_` 保护。
Manual 只保证最新 pending frame 被 refresh;并发 producer 产生的中间帧可以按策略语义被后来的 prepared frame 替换,这不是丢帧 bug。
## Low Latency 帧策略
多个 painter 调用由 `painter_mutex_` 串行化,多个 renderer 调用由 `render_mutex_` 串行化。paint/cache/render 三个 Frame 指针只在 `state_mutex_` 下交换,因此 painter 写入和 renderer 读取不会落到同一个 Frame 上。
`set_frequency_hz()`、实时数据通知、pending frame discard、状态读取都和 Frame 指针切换使用同一个 `state_mutex_`。频率的发布状态另外通过 `Frame_Control_Strategy_Base` 的内部 mutex 保护。
## 实时数据
`Latest_Real_Time_Data` 的 update/snapshot/state 访问由数据 mutex 保护。`History_Real_Time_Data` 的 update/clear/discard/snapshot/state 同样受数据 mutex 保护。`Scene_Lifetime::Lease` 使用活动 lease 计数保护 Scene 生命周期,不再把 lifetime mutex 持有到 Frame Strategy observer callback 结束,因此 observer 中再次通过 Renderable 获取同一 Scene 不会递归锁死。
多线程 mutation 还使用独立 `mutation_mutex_` 将一次 mutation 与对应 observer 通知作为一个顺序单元。这样 revision 1 的通知一定先于 revision 2,不会出现较新的实时数据已经通知 Frame Strategy 后,又被较旧 observation 覆盖 `last_real_time_data_update_` 的情况。`mutation_mutex_` 使用 recursive mutex,使 observer 同线程重入实时数据 mutation 时不会因为通知顺序锁自身死锁。
实时数据 attachment 持有数据源 `shared_ptr`。bind/unbind 与并发 update 由 observer binding state 同步;Scene 析构和实时数据 update 可以并发,通知在获取不到有效 `Scene_Lifetime::Lease` 时直接跳过。
## 状态策略
Double/Triple State 的 set/get/publish/acquire/render snapshot 都在策略 mutex 下完成。对外读取返回值快照,不把内部 buffer 引用暴露到锁外。
Triple State 的 render acquire 与前台 publish 可以并发;每次 acquire 得到完整的某一个 published revision,不会观察到半更新 State。
## Observer
默认 `Observer_State` 使用 recursive mutex,将多个线程同时触发的 observer callback 串行化,并允许同一线程的 observer callback 再次进入同一个 Observer_State。
Observer callback 是同步调用,不是异步事件队列。回调耗时会直接增加触发线程耗时;用户 observer 自身创建的外部对象生命周期和跨对象锁顺序仍由用户负责。
## 多线程测试
`tests/renderive/threading/Threading_Contract_Test.cpp` 专门验证线程契约:
- 8 个线程同时进入 `Observer_State`,确认 observer callback 最大并发数为 1。
- Flow 使用 4 producer + 4 consumer,验证所有 Frame 恰好消费一次、无重复、无撕裂、pending 计数归零;另用 8 producer + `std::pmr::unsynchronized_pool_resource` 验证 Frame 分配和回收不会并发进入非线程安全 PMR。
- Manual 使用 4 producer,同时运行 refresher 和 renderer,验证 Frame 指针交换期间不会读到撕裂 Frame。
- Low Latency 使用 4 producer,同时运行 renderer、frequency configurator 和实时数据 updater,验证三缓冲 Frame 与状态更新并发安全。
- Latest 实时数据使用 4 updater + snapshot reader,验证 revision、update count 和 value 快照一致。
- 实时数据 observer 使用 8 updater,验证 observer 收到的 revision 严格按 mutation revision 递增。
- History 实时数据同时 update、snapshot、discard,验证容器和统计状态一致。
- Double State 同时 set/publish、读取 cache 和 render snapshot,验证 State 不出现撕裂。
- Triple State 同时 set/publish、acquire render state 和读取 cache,验证 published revision 快照一致。
- Scene 使用 4 个 render submitter,验证并发提交不会覆盖任务,最终 render sequence 与实际执行次数一致。
- `render_submitted` observer 重入 `render()`,验证重入提交会 deferred 而不是和 Scene worker 的 observer 锁形成死锁。
- Manual `refresh_succeeded` observer 重入 `acquire_renderer()`,验证用户 callback 运行时不再持有 `render_mutex_`
- RTD -> Frame Strategy observer 中再次获取 Renderable 的 Scene,验证 `Scene_Lifetime` lease 不跨用户 callback 持有 lifetime mutex。
- 第一帧失败且未 wait、随后第二帧成功,验证第一帧异常仍由后续 `wait_for_render()` 报告。
- detach dependency parent 后自动提升 child,验证被提升的缓存 child 会 invalidate 并重新渲染。
- Renderable task 中调用 `wait_for_render()`,验证当前 Scene execution thread 不等待自身;同线程调用 `render()` 或配置修改会明确拒绝。
- Built-in Frame lease 明确声明 `thread_affine = true`,测试固定同线程生命周期契约。
- Scene render 与 `rebuild_render_graph()` 并发执行,验证不可变内部图快照不会在执行期间失效。
- 并发 Capture ticket 预留,验证只保留请求数量且失败 ticket 会归还。
- Capture 失败帧,验证 Abstract Frame 不发布 completed snapshot,下一完整帧继续使用该配额。
- 实时数据 update 与 Scene 析构竞争,验证 Scene lifetime binding 不产生 UAF。
原有测试另外覆盖 render 期间 cache invalidate、任务图构建期间 rebuild、Scene topology 写入与 snapshot 并发、多个 render failure waiter、RTD unbind 与 update 并发等针对性边界。
真实 Taskflow 存在时还会运行独立内部任务并行执行测试;内置 dependency-graph executor 只验证 DAG 关系,不伪造并行能力。
Linux/GCC 或 Clang 环境建议同时跑 ThreadSanitizer。普通单元测试负责语义和确定性不变量,TSan 负责检测测试覆盖路径上的实际 data race;两者不能互相替代。