From 05263cd5066e3dc148e97e8180714b7c2c0a4e9b Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Thu, 13 Aug 2026 00:35:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4plot=5Fcore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Kernel/readme.md | 194 -- Kernel/renderive_threading_sync_draft.md | 790 ++++++ .../src/renderive/capture/Capture_Types.hpp | 20 + .../renderable/Renderable_Configuration.hpp | 8 + .../src/renderive/scene/Scene2D_Context.hpp | 18 +- .../scene/base/Frame_Render_Snapshot.hpp | 50 + .../renderive/scene/base/Frame_Viewport.hpp | 9 + .../renderive/scene/base/Render_Contexts.hpp | 27 + .../src/renderive/scene/base/Scene_Base.cpp | 13 +- .../src/renderive/scene/base/Scene_Base.hpp | 5 + .../scene/base/Scene_Render_Context.hpp | 2 + .../renderive/state/Double_State_Strategy.hpp | 5 +- .../renderive/scene/Scene2D_Context_Test.cpp | 23 + .../scene/Scene_State_Observer_Test.cpp | 4 +- .../renderive/scene/concept/Scene_Test.cpp | 2 +- Kernel/threading.md | 107 - Qt/bridge/Qt_Presentation_Sink.h | 2 +- Qt/plot/Explicit_Plot.cpp | 2 +- Qt/plot/Latency_Eager_Plot.cpp | 10 +- Qt/plot/Plot.cpp | 48 +- Qt/plot/Plot.h | 2 +- Qt/plot/Plot_p.h | 2 +- b.md | 2338 +++++++++++++++++ render_2D/axis/Abs_Axis.cpp | 2 +- render_2D/axis/Abs_Axis.h | 2 +- render_2D/axis/Axis_Builder.h | 5 +- render_2D/axis/Axis_State_Strategy.hpp | 4 +- render_2D/axis/Frequency_Axis.cpp | 4 +- render_2D/axis/Frequency_Axis.h | 2 +- render_2D/axis/Numeric_Axis.cpp | 4 +- render_2D/axis/Numeric_Axis.h | 2 +- render_2D/axis/Time_Axis.cpp | 4 +- render_2D/axis/Time_Axis.h | 2 +- render_2D/export.h | 2 +- render_2D/plot/Plot_Core.cpp | 591 ----- render_2D/plot/Plot_Core.h | 150 -- render_2D/plottable/Afterglow.cpp | 4 +- render_2D/plottable/Afterglow.h | 2 +- render_2D/plottable/Constellation_Diagram.cpp | 4 +- render_2D/plottable/Constellation_Diagram.h | 2 +- render_2D/plottable/Frequency_Trace.cpp | 4 +- render_2D/plottable/Frequency_Trace.h | 2 +- render_2D/plottable/Performance_Overlay.cpp | 38 - render_2D/plottable/Performance_Overlay.h | 59 +- render_2D/plottable/Plottable.h | 4 +- .../plottable/Selection_Rectangle_Overlay.cpp | 4 +- .../plottable/Selection_Rectangle_Overlay.h | 2 +- render_2D/plottable/Spectrum.cpp | 4 +- render_2D/plottable/Spectrum.h | 2 +- render_2D/plottable/Sweep_Spectrum.cpp | 4 +- render_2D/plottable/Sweep_Spectrum.h | 2 +- render_2D/plottable/Waterfall.cpp | 4 +- render_2D/plottable/Waterfall.h | 2 +- render_2D/renderable/Renderable.cpp | 33 +- render_2D/renderable/Renderable.h | 10 +- render_2D/renderable/Renderable_Builder.h | 13 +- render_2D/scene/Scene.cpp | 20 + render_2D/scene/Scene.h | 509 ++++ .../tests/render_2D_Frame_Pipeline_Tests.cpp | 68 +- .../tests/render_2D_Integration_Tests.cpp | 38 +- web_server/app/Gallery_Actions.h | 26 +- web_server/app/Gallery_Observer_Adminive.h | 2 +- web_server/app/Gallery_Plot_Session.cpp | 62 +- web_server/app/Gallery_Protocol.cpp | 40 +- web_server/app/Gallery_Session_Control.h | 25 +- .../app/Gallery_Session_Control_Adminive.h | 44 +- web_server/app/Web_Plot_Session.cpp | 2 +- 67 files changed, 4154 insertions(+), 1336 deletions(-) delete mode 100644 Kernel/readme.md create mode 100644 Kernel/renderive_threading_sync_draft.md create mode 100644 Kernel/src/renderive/capture/Capture_Types.hpp create mode 100644 Kernel/src/renderive/renderable/Renderable_Configuration.hpp create mode 100644 Kernel/src/renderive/scene/base/Frame_Render_Snapshot.hpp create mode 100644 Kernel/src/renderive/scene/base/Frame_Viewport.hpp create mode 100644 Kernel/src/renderive/scene/base/Render_Contexts.hpp delete mode 100644 Kernel/threading.md create mode 100644 b.md delete mode 100644 render_2D/plot/Plot_Core.cpp delete mode 100644 render_2D/plot/Plot_Core.h create mode 100644 render_2D/scene/Scene.cpp create mode 100644 render_2D/scene/Scene.h diff --git a/Kernel/readme.md b/Kernel/readme.md deleted file mode 100644 index cb91030..0000000 --- a/Kernel/readme.md +++ /dev/null @@ -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`,`Scene3D_Context` 通过 `Triple_State_Strategy` 继承 `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>` 保存待渲染帧,不引入 Boost.Lockfree 或其他新增队列依赖。多个 painter 可以并发创建 Frame,入队由 `state_mutex_` 串行化;多个 renderer 调用也安全,但实际消费由 `render_mutex_` 串行化,因此每个 Frame 最多被消费一次。多 producer 下的全局顺序以实际完成入队的线性化顺序为准。 - -Flow 的 observer 允许同线程重入 Observer_State;renderer observer 中再次申请同一个 Flow renderer 会立即得到空 lease,不会等待当前 renderer lease 自身释放。完整线程契约和多线程压力测试见 `threading.md`。 - -## 实时数据附件所有权 - -`With_Real_Time_Data` 接收 `std::shared_ptr`,`Attach_Real_Time_Data` 在自身生命周期内持有这些实时数据源,避免 Renderable 仍存活时外部提前销毁 Data 产生悬空指针: - -```cpp -auto data = std::make_shared>(); -auto renderable = std::make_shared>>( - 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(configuration); -scene.attach_renderable(renderable); -``` - -工厂只构造对象,不自动挂载,`attach_renderable()` 的原有语义不变。工厂分配器持有 Scene 内存域的共享所有权,因此 Renderable 析构时内存域仍然有效。 - -独立构造模式继续保留: - -```cpp -auto renderable = std::make_shared(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> 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 的核心对象,不承诺接管第三方库和用户类型的全部动态分配。 diff --git a/Kernel/renderive_threading_sync_draft.md b/Kernel/renderive_threading_sync_draft.md new file mode 100644 index 0000000..8c2a5fe --- /dev/null +++ b/Kernel/renderive_threading_sync_draft.md @@ -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 Data(RTD)生产、维护和读取。 + +在 `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 Data(RTD) + +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 建立顺序? +``` + +只有这些问题全部能从文档直接得到确定答案以后,才根据文档做下一轮锁清理。 diff --git a/Kernel/src/renderive/capture/Capture_Types.hpp b/Kernel/src/renderive/capture/Capture_Types.hpp new file mode 100644 index 0000000..6e0dc58 --- /dev/null +++ b/Kernel/src/renderive/capture/Capture_Types.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +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; + } +}; diff --git a/Kernel/src/renderive/renderable/Renderable_Configuration.hpp b/Kernel/src/renderive/renderable/Renderable_Configuration.hpp new file mode 100644 index 0000000..be2d01a --- /dev/null +++ b/Kernel/src/renderive/renderable/Renderable_Configuration.hpp @@ -0,0 +1,8 @@ +#pragma once + +struct Renderable_Configuration { + bool cache_enabled{false}; + + friend bool operator==(Renderable_Configuration, + Renderable_Configuration) = default; +}; diff --git a/Kernel/src/renderive/scene/Scene2D_Context.hpp b/Kernel/src/renderive/scene/Scene2D_Context.hpp index 1684ebc..157e729 100644 --- a/Kernel/src/renderive/scene/Scene2D_Context.hpp +++ b/Kernel/src/renderive/scene/Scene2D_Context.hpp @@ -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 +concept Scene2D_State_Value = State_Value && requires(State& state) { + requires std::same_as, Frame_Viewport>; }; template concept Scene2D_Frame_Control_Constructible = std::constructible_from || std::constructible_from; template , 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 && State_Value -class Scene2D_Context final : public Triple_State_Strategy, public Scene_Compositor { +requires Frame_Control_Strategy_For && Scene2D_State_Value +class Scene2D_Context : public Double_State_Strategy, public Scene_Compositor { public: - using Scene_State_Strategy = Triple_State_Strategy; + using Scene_State_Strategy = Double_State_Strategy; 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); diff --git a/Kernel/src/renderive/scene/base/Frame_Render_Snapshot.hpp b/Kernel/src/renderive/scene/base/Frame_Render_Snapshot.hpp new file mode 100644 index 0000000..ecd4a0c --- /dev/null +++ b/Kernel/src/renderive/scene/base/Frame_Render_Snapshot.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#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 render_graph; + std::vector dependency_parent_ids; + +private: + friend class Scene_Base; + std::shared_ptr owner_; + std::shared_ptr 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 renderables; + std::vector display_order; +}; diff --git a/Kernel/src/renderive/scene/base/Frame_Viewport.hpp b/Kernel/src/renderive/scene/base/Frame_Viewport.hpp new file mode 100644 index 0000000..99395ad --- /dev/null +++ b/Kernel/src/renderive/scene/base/Frame_Viewport.hpp @@ -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; +}; diff --git a/Kernel/src/renderive/scene/base/Render_Contexts.hpp b/Kernel/src/renderive/scene/base/Render_Contexts.hpp new file mode 100644 index 0000000..bdf3a71 --- /dev/null +++ b/Kernel/src/renderive/scene/base/Render_Contexts.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#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{}; +}; diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index 7d22f4e..2c413b2 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -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(this)) + state->publish(); Renderable_List renderables(&memory_resource()); { std::lock_guard lock(renderable_mutex_); renderables = *cache_renderables_; } for (const Renderable& renderable : renderables) { - if (auto* state = dynamic_cast(renderable.get())) { + if (auto* state = dynamic_cast(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 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 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); diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp index bfbf73b..10e35a6 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -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_commits; @@ -142,6 +145,7 @@ protected: virtual std::uint64_t observer_now_ns() const noexcept; std::unique_lock 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_; std::shared_ptr memory_domain_; + std::atomic_bool model_dirty_{true}; std::array renderable_states_; Renderable_List* render_renderables_; Renderable_List* cache_renderables_; diff --git a/Kernel/src/renderive/scene/base/Scene_Render_Context.hpp b/Kernel/src/renderive/scene/base/Scene_Render_Context.hpp index 2e65147..b91f63f 100644 --- a/Kernel/src/renderive/scene/base/Scene_Render_Context.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Render_Context.hpp @@ -1,6 +1,7 @@ #pragma once #include #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{}; diff --git a/Kernel/src/renderive/state/Double_State_Strategy.hpp b/Kernel/src/renderive/state/Double_State_Strategy.hpp index ec83d8d..b6acb20 100644 --- a/Kernel/src/renderive/state/Double_State_Strategy.hpp +++ b/Kernel/src/renderive/state/Double_State_Strategy.hpp @@ -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; diff --git a/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp b/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp index 5ed2146..299a154 100644 --- a/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp @@ -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(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})); +} diff --git a/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp b/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp index ab6d67d..dcd33ad 100644 --- a/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp @@ -6,7 +6,7 @@ #include #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; 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, Recording_Color_Cache, Scene_State_Observer_Test_State, Scene_State_Observer_Test_State_Observer, Scene_State_Observer_Test_Scene_Observer>; diff --git a/Kernel/tests/renderive/scene/concept/Scene_Test.cpp b/Kernel/tests/renderive/scene/concept/Scene_Test.cpp index 46bc3d0..d8e452e 100644 --- a/Kernel/tests/renderive/scene/concept/Scene_Test.cpp +++ b/Kernel/tests/renderive/scene/concept/Scene_Test.cpp @@ -8,5 +8,5 @@ static_assert(!Scene_3D>); TEST(scene_concept_test, accepts_2d_and_3d_contexts) { SUCCEED(); } -static_assert(std::is_final_v>); +static_assert(!std::is_final_v>); static_assert(std::is_final_v>); diff --git a/Kernel/threading.md b/Kernel/threading.md deleted file mode 100644 index 7fae074..0000000 --- a/Kernel/threading.md +++ /dev/null @@ -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`,所以返回以后即使对应对象从 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;两者不能互相替代。 diff --git a/Qt/bridge/Qt_Presentation_Sink.h b/Qt/bridge/Qt_Presentation_Sink.h index 0c7037c..7e072ae 100644 --- a/Qt/bridge/Qt_Presentation_Sink.h +++ b/Qt/bridge/Qt_Presentation_Sink.h @@ -1,5 +1,5 @@ #pragma once -#include "render_2D/plot/Plot_Core.h" +#include "render_2D/scene/Scene.h" #include #include class QWidget; diff --git a/Qt/plot/Explicit_Plot.cpp b/Qt/plot/Explicit_Plot.cpp index 950780d..4f59ef2 100644 --- a/Qt/plot/Explicit_Plot.cpp +++ b/Qt/plot/Explicit_Plot.cpp @@ -8,7 +8,7 @@ Explicit_Plot::Explicit_Plot() : Abs_Plot(new Abs_Plot_Private(false)) {} Render_Ticket Explicit_Plot::replot() { - return {d->core->render_frame(true)}; + return {d->scene->render_frame(true)}; } } // namespace renderive diff --git a/Qt/plot/Latency_Eager_Plot.cpp b/Qt/plot/Latency_Eager_Plot.cpp index 24feaed..f8c22f4 100644 --- a/Qt/plot/Latency_Eager_Plot.cpp +++ b/Qt/plot/Latency_Eager_Plot.cpp @@ -16,24 +16,24 @@ void Latency_Eager_Plot::pause() { } bool Latency_Eager_Plot::running() const { - return d->core->view_active(); + return d->scene->view_active(); } double Latency_Eager_Plot::max_render_fps() const { - return d->core->max_render_fps(); + return d->scene->max_render_fps(); } void Latency_Eager_Plot::set_max_render_fps(double fps) { - d->core->set_max_render_fps(fps); + d->scene->set_max_render_fps(fps); update_render_interval(); } Low_Latency_Diagnostics Latency_Eager_Plot::diagnostics() const { - return d->core->diagnostics(); + return d->scene->diagnostics(); } Refresh_Control_Snapshot Latency_Eager_Plot::refresh_feedback_snapshot() const { - return d->core->refresh_feedback_snapshot(); + return d->scene->refresh_feedback_snapshot(); } } // namespace renderive diff --git a/Qt/plot/Plot.cpp b/Qt/plot/Plot.cpp index d6a458b..ceb8059 100644 --- a/Qt/plot/Plot.cpp +++ b/Qt/plot/Plot.cpp @@ -38,18 +38,18 @@ std::string to_utf8_string(const QString& text) { } // namespace Abs_Plot_Private::Abs_Plot_Private(bool continuous_rendering) - : core(std::make_unique()), continuous(continuous_rendering) {} + : scene(std::make_unique()), continuous(continuous_rendering) {} Abs_Plot_Private::~Abs_Plot_Private() = default; void Abs_Plot_Private::attach_widget(Abs_Plot* plot) { presentation_sink = std::make_shared(plot); - core->set_presentation_sink(presentation_sink); + scene->set_presentation_sink(presentation_sink); timer = new QTimer(plot); timer->setTimerType(Qt::PreciseTimer); QObject::connect(timer, &QTimer::timeout, plot, [this] { - if (core->view_active()) - (void)core->render_frame(); + if (scene->view_active()) + (void)scene->render_frame(); }); } @@ -67,42 +67,42 @@ Abs_Plot::~Abs_Plot() { return; if (d->timer) d->timer->stop(); - d->core->deactivate_view(); + d->scene->deactivate_view(); delete d; d = nullptr; } void Abs_Plot::init() { - d->core->init(); + d->scene->init(); } -Plot_Core* Abs_Plot::core() const { - return d ? d->core.get() : nullptr; +Scene2D* Abs_Plot::scene() const { + return d ? d->scene.get() : nullptr; } std::shared_ptr Abs_Plot::root_renderable() const { - return d->core->root_renderable(); + return d->scene->root_renderable(); } std::shared_ptr Abs_Plot::create_renderable_node( const std::shared_ptr& parent, const QString& object_name) const { - return d->core->create_renderable_node(parent, to_utf8_string(object_name)); + return d->scene->create_renderable_node(parent, to_utf8_string(object_name)); } void Abs_Plot::remove_renderable(const std::shared_ptr& renderable) { - d->core->remove_renderable(renderable); + d->scene->remove_renderable(renderable); } QColor Abs_Plot::background_color() const { - return to_qcolor(d->core->background_color()); + return to_qcolor(d->scene->background_color()); } void Abs_Plot::set_background_color(const QColor& color) { - d->core->set_background_color(from_qcolor(color)); + d->scene->set_background_color(from_qcolor(color)); } void Abs_Plot::start_render_timer() { - d->core->activate_view(); + d->scene->activate_view(); update_render_interval(); if (d->timer && !d->timer->isActive()) d->timer->start(); @@ -111,13 +111,13 @@ void Abs_Plot::start_render_timer() { void Abs_Plot::stop_render_timer() { if (d->timer) d->timer->stop(); - d->core->deactivate_view(); + d->scene->deactivate_view(); } void Abs_Plot::update_render_interval() { if (!d->timer) return; - const double fps = std::max(1.0, d->core->max_render_fps()); + const double fps = std::max(1.0, d->scene->max_render_fps()); d->timer->setInterval(std::max(1, static_cast(std::lround(1000.0 / fps)))); } @@ -128,7 +128,7 @@ void Abs_Plot::paintEvent(QPaintEvent*) { void Abs_Plot_Private::paint_event(Abs_Plot* plot) { QPainter painter(plot); painter.fillRect(plot->rect(), plot->background_color()); - core->with_frame([&painter](Image_View view) { + scene->with_frame([&painter](Image_View view) { if (!view.empty()) painter.drawImage(QPoint(0, 0), Qt_Image_Adapter::to_qimage(view)); }); @@ -137,11 +137,11 @@ void Abs_Plot_Private::paint_event(Abs_Plot* plot) { } void Abs_Plot_Private::draw_performance_overlay(Abs_Plot* plot, QPainter& painter) { - const auto overlay = core->performance_overlay(); + const auto overlay = scene->performance_overlay(); if (!overlay || !overlay->enabled()) return; const auto snapshot = overlay->display_snapshot(); - if (!snapshot || snapshot->viewport_size != core->viewport_size()) + if (!snapshot || snapshot->viewport_size != scene->viewport_size()) return; const auto& options = snapshot->style; QFont font = painter.font(); @@ -186,7 +186,7 @@ void Abs_Plot_Private::draw_performance_overlay(Abs_Plot* plot, QPainter& painte bool Abs_Plot::event(QEvent* event) { const auto dispatch = [this, event](const Event& translated) { - d->core->dispatch_event(translated); + d->scene->dispatch_event(translated); if (!translated.is_accepted()) return false; event->accept(); @@ -195,17 +195,17 @@ bool Abs_Plot::event(QEvent* event) { switch (event->type()) { case QEvent::Resize: { auto* resize = static_cast(event); - d->core->set_viewport_size(Qt_Event_Adapter::to_size(resize->size())); - d->core->dispatch_event(Qt_Event_Adapter::to_resize_event(resize)); + d->scene->set_viewport_size(Qt_Event_Adapter::to_size(resize->size())); + d->scene->dispatch_event(Qt_Event_Adapter::to_resize_event(resize)); break; } case QEvent::Show: - d->core->dispatch_event(Event(Event_Type::Show)); + d->scene->dispatch_event(Event(Event_Type::Show)); if (d->continuous) start_render_timer(); break; case QEvent::Hide: - d->core->dispatch_event(Event(Event_Type::Hide)); + d->scene->dispatch_event(Event(Event_Type::Hide)); if (d->continuous) stop_render_timer(); break; diff --git a/Qt/plot/Plot.h b/Qt/plot/Plot.h index 32e3ec3..27fc73a 100644 --- a/Qt/plot/Plot.h +++ b/Qt/plot/Plot.h @@ -15,7 +15,7 @@ class LIB_DECL Abs_Plot : public QWidget { public: virtual void init(); [[nodiscard]] QColor background_color() const; - [[nodiscard]] Plot_Core* core() const; + [[nodiscard]] Scene2D* scene() const; [[nodiscard]] std::shared_ptr root_renderable() const; [[nodiscard]] std::shared_ptr create_renderable_node( const std::shared_ptr& parent, const QString& object_name = {}) const; diff --git a/Qt/plot/Plot_p.h b/Qt/plot/Plot_p.h index e76107f..a829f84 100644 --- a/Qt/plot/Plot_p.h +++ b/Qt/plot/Plot_p.h @@ -19,7 +19,7 @@ struct Abs_Plot_Private { void paint_event(Abs_Plot* plot); void draw_performance_overlay(Abs_Plot* plot, QPainter& painter); - std::unique_ptr core; + std::unique_ptr scene; std::shared_ptr presentation_sink; QTimer* timer{}; bool continuous{}; diff --git a/b.md b/b.md new file mode 100644 index 0000000..fe9855e --- /dev/null +++ b/b.md @@ -0,0 +1,2338 @@ +按这一版代码继续执行,不改 DAG 主体设计。 **后端只处理前面确认的 1~9,原来的第 10 项完全略过;同时重写 `Kernel/threading.md` +;前端整体迁移到 React + Vite + Material UI,DAG 使用 React Flow,但不使用 ELK、Dagre +或其他自动布局库,拓扑解析和布局算法自己实现。** + +前端职责明确分开:Material UI 负责页面、菜单、表单、Tabs、Dialog、Card、Table、Tooltip、Chip、Button、Input、Select、Accordion 等全部普通 +UI;React Flow 只作为 DAG 画布和节点/边交互层。React Flow 本身已经提供节点/边、缩放、平移、选择、自定义节点等基础能力,节点又直接是 +React component,适合和 MUI 组合; **拓扑层级、节点坐标、折叠和时序映射全部由我们自己的 parser 决定**。 ([MUI][1]) + +--- + +# Renderive Frame Publication + Lock Elimination + Web UI 执行计划 + +## 0. 最终目标 + +这轮改造完成以后,后端形成严格边界: + +```text +UI / Control / Producer + │ + ├── 修改 Renderable state + ├── 修改 RTD + ├── 修改 viewport + ├── 修改 visibility + ├── 修改 configuration + ├── rebuild Renderable graph + └── 修改 frame-control + │ + ▼ +┌──────────────────────────────┐ +│ Frame Publication Boundary │ +│ │ +│ 完成所有 mutable → immutable │ +│ snapshot / ownership 转移 │ +└──────────────────────────────┘ + │ +════════════════════════════════════ + │ 以下禁止访问 mutable + ▼ + Render Plan + │ + ▼ + ONE Taskflow + │ + ├── Prepare + ├── Paint + └── Composite + │ + ▼ + Frame Complete +``` + +双线以下必须满足: + +```text +不获取 Plot mutex +不获取 Renderable configuration mutex +不获取 Renderable graph mutex +不重新读取 Frame Control mutable state +不修改 RTD +不访问 Scene mutable API +不读取 live visibility +不持有 renderer mutex 跑完整帧 +``` + +允许的只有: + +```text +immutable Frame Snapshot +immutable Renderable Graph Snapshot +Prepare Buffer +Paint Buffer +Frame-local execution slots +Frame ownership +``` + +--- + +# 第一阶段:建立统一 `Frame_Render_Snapshot` + +这是 1~9 所有修改的基础。 + +不要针对每一个 mutex 单独打补丁。 + +新增一个明确的 Frame 级只读上下文,名称按当前项目命名规范确定,语义固定为: + +```cpp +Frame_Render_Snapshot +``` + +至少承载: + +```text +frame_id +render_sequence +scene_state_revision +viewport +frame_control_state +capture state/ticket + +per-renderable: + Renderable_Id + visibility + configuration snapshot + prepare revision + paint revision + immutable render graph +``` + +注意: + +```text +Frame_Render_Snapshot +``` + +不是把所有 Renderable 业务数据复制一次。 + +Renderable 的大块 State / RTD 继续使用已有: + +```text +published state +render state +immutable snapshot +``` + +Frame Snapshot 保存的是: + +```text +这一帧到底引用哪个 published version +``` + +以及渲染过程需要的小型 immutable metadata。 + +--- + +# 1. 删除 Paint 中的 viewport mutex + +## 当前问题 + +当前路径: + +```text +Paint Task + ↓ +Renderable::viewport_size() + ↓ +Plot_Core::viewport_size() + ↓ +Plot_Core::Impl::mutex +``` + +多个 Paint worker 会共同碰这个 mutex。 + +这是第一处直接从 worker 热路径删除的锁。 + +## 修改 + +Frame 发布时: + +```text +Plot viewport + ↓ +Frame_Render_Snapshot::viewport +``` + +Paint Context: + +```cpp +struct Paint_Render_Context { + ... + Size viewport; +}; +``` + +实际代码不要重复保存时,则引用: + +```cpp +const Frame_Render_Snapshot* frame; +``` + +Paint: + +```text +Painter + ↓ +context.frame.viewport +``` + +禁止再: + +```text +Painter + ↓ +Renderable::viewport_size() + ↓ +Plot_Core +``` + +## 旧接口语义 + +`Plot_Core::viewport_size()` 对外 API 不删除。 + +调用方普通控制线程仍然可以调用它。 + +但是: + +> Render DAG 内部不得调用它。 + +所以不是破坏旧 API,而是把渲染内部依赖切断。 + +## 测试 + +新增: + +```text +Frame snapshot 后修改 viewport +``` + +验证: + +```text +当前 Frame → 使用旧 viewport +下一 Frame → 使用新 viewport +``` + +同时保证多个并行 Paint 不进入 `Plot_Core::mutex`。 + +--- + +# 2. 拆分 Prepare / Paint / Composite Context + +这是这轮最重要的架构约束之一。 + +当前: + +```cpp +Scene_Render_Context +``` + +同时给 Prepare 和 Paint 使用,而且带: + +```cpp +Scene_Base* scene; +``` + +必须拆。 + +## 新结构 + +```text +Prepare_Render_Context +Paint_Render_Context +Composite_Render_Context +``` + +### Prepare 可以拥有 + +```text +Frame snapshot +自身 Renderable identity +prepare buffer +必要的 dependency published views +metrics +``` + +Prepare 可以解析已声明 dependency。 + +--- + +### Paint 只能拥有 + +```text +Frame immutable snapshot +自身 Prepare Buffer +自身 Paint Buffer / Color Cache +自身 paint-only immutable config +metrics +``` + +Paint Context **禁止存在**: + +```cpp +Scene_Base* scene; +Renderable_Base* dependency; +Plot_Core* plot; +Frame_Control_Strategy_Base* strategy; +``` + +这样用户就算想在 Paint 里面: + +```cpp +context.scene->xxx(); +``` + +也根本写不出来。 + +--- + +### Composite 只能拥有 + +```text +Frame snapshot +已完成 Paint Buffer +final target +layer/composite metadata +metrics +``` + +Composite 不允许重新访问 Renderable 原始状态。 + +## 执行 + +逐个修改: + +```text +Renderable +Waterfall +Spectrum +Afterglow +Axis +Overlay +其他内置 Renderable +``` + +所有: + +```text +prepare callback +paint callback +composite callback +``` + +签名。 + +旧的统一 `Scene_Render_Context` 实现直接删除。 + +不保留兼容 overload。 + +--- + +# 3. visibility 正式进入 Frame Snapshot + +当前: + +```cpp +visible_.load(...) +``` + +虽然无 mutex,但是仍然是 live state。 + +这个要改。 + +## 新语义 + +控制侧: + +```text +set_visible(false) +``` + +修改下一次 publication 使用的状态。 + +Frame 发布: + +```text +Renderable A visible = true +Renderable B visible = false +``` + +这一帧之后固定。 + +DAG 编译也直接基于这份 snapshot。 + +最好可以进一步: + +```text +visible == false +``` + +直接裁掉: + +```text +prepare +paint +composite +``` + +或者按现有缓存/图层语义裁剪所需节点。 + +## 禁止 + +Taskflow worker: + +```cpp +is_visible() +``` + +不要再 live load。 + +## 测试 + +```text +Frame A publish: visible=true + +Taskflow blocked + +control thread: +set_visible(false) + +release Taskflow +``` + +验证: + +```text +Frame A 仍绘制 +Frame B 不绘制 +``` + +--- + +# 4. `discard_real_time_data()` 移出 Prepare Worker + +这是现在 Prepare 热路径里最不应该存在的一类操作。 + +当前: + +```text +Prepare + ↓ +discard_real_time_data() + ↓ +Frame Control query + ↓ +RTD state + ↓ +RTD mutation/discard +``` + +必须取消。 + +## 正确边界 + +RTD 的: + +```text +update +discard +history maintenance +retention +``` + +属于: + +```text +producer/control/publication side +``` + +不属于 Render DAG。 + +Frame 建立之前: + +```text +RTD mutable storage + ↓ +maintenance / discard + ↓ +publish render snapshot +════════════════════════ + ↓ +Prepare 只读 +``` + +## Render DAG 内 + +Prepare 只允许: + +```text +read RTD render snapshot +``` + +禁止: + +```text +clear +discard +update +lock update storage +``` + +## Frequency + +如果 discard policy 需要: + +```text +frequency_hz +``` + +直接使用 publication 时捕获的: + +```text +frame_control_state.frequency +``` + +不要从 RTD Prepare 中再返回 Scene 查询。 + +## 测试 + +必须覆盖: + +```text +RTD update 与 Frame publication 并发 +RTD discard 与 publish +Frame publish 后 RTD 再变化 +``` + +当前 Frame 只能看到 publication 时确定的 snapshot。 + +--- + +# 5. `configuration_mutex_` 移出每帧 Render Plan 编译 + +当前 Render Plan 编译为了: + +```text +cache_enabled +prepare validity +paint validity +... +``` + +会读取: + +```cpp +configuration() +``` + +然后拿: + +```text +configuration_mutex_ +``` + +这要取消。 + +## 新设计 + +区分: + +```text +mutable/control configuration +``` + +和: + +```text +published render configuration +``` + +Scene 配置修改仍然通过当前: + +```text +set_renderable_configuration() +lock_render_idle() +``` + +保持原有外部线程安全语义。 + +Frame publication 时: + +```text +configuration + ↓ +Renderable_Frame_State.configuration +``` + +Render Plan compiler 之后只读: + +```text +snapshot.configuration +``` + +## `configuration_mutex_` + +如果控制 API 自身仍然需要 mutex,可以保留。 + +但它不能再进入: + +```text +compile_render_plan() +Taskflow +``` + +## 测试 + +同样使用 Frame A / Frame B 语义: + +```text +Frame A publication +改变 cache/config +Frame A 执行 +Frame B publication +``` + +Frame A 不允许中途改变。 + +--- + +# 6. Render Graph 改为 immutable published graph + +当前: + +```cpp +renderable->render_graph() +``` + +每帧经过: + +```text +render_graph_mutex_ +``` + +这个也清掉。 + +但不能直接删 mutex,因为当前允许: + +```text +render +|| +rebuild_render_graph() +``` + +并发。 + +因此要改 ownership,不是硬删锁。 + +## 新结构 + +Renderable 控制侧: + +```text +mutable graph builder state + │ + ▼ +rebuild + │ + ▼ +shared_ptr + │ + ▼ +atomic/publication +``` + +Frame publication: + +```text +Frame +└── shared_ptr +``` + +当前 Frame 一旦拿到: + +```text +Graph v17 +``` + +哪怕控制线程马上 rebuild: + +```text +Graph v18 +``` + +当前 Frame 仍继续读: + +```text +Graph v17 +``` + +不加锁。 + +下一 Frame 获得: + +```text +Graph v18 +``` + +## 生命周期 + +必须依靠 immutable graph ownership: + +```text +shared_ptr +``` + +或者现有项目已有等价 ownership。 + +不要重新做 generation pointer hack。 + +## `rebuild_render_graph()` + +锁只允许保护: + +```text +build/publish 新 Graph +``` + +绝不保护: + +```text +当前 Frame 读取旧 Graph +``` + +## 测试 + +保留并强化当前: + +```text +Scene render +|| +rebuild_render_graph() +``` + +测试。 + +新增验证: + +```text +Frame A = graph version 10 +rebuild → 11 +Frame A 仍然完整执行 version 10 +Frame B 使用 version 11 +``` + +--- + +# 7. Frame Control `swap + read` 合并成一次 publication + +现在类似: + +```text +swap() + ↓ +unlock + ↓ +frame_control_state() + ↓ +lock +``` + +第二次锁没有意义。 + +## 改造 + +Frame Control publication API 必须能够直接返回: + +```text +published State snapshot +``` + +语义类似: + +```cpp +State publish(); +``` + +或者符合现有命名: + +```text +swap_and_acquire_render_state() +``` + +具体函数名按现有接口风格确定。 + +但语义必须是: + +```text +lock +├── publish/swap +├── 得到当前 immutable State +└── unlock +``` + +一次完成。 + +Frame 以后只持: + +```text +State value +``` + +## 不做 + +不要为兼容保留: + +```text +swap() ++ +swap_and_get() +``` + +两套内部实现。 + +如果原接口属于公开旧语义,需要保留 public API 时: + +```text +public swap() +``` + +仍可存在。 + +但是 Renderive 内部新执行路径只使用单 publication operation。 + +--- + +# 8. `Render_Plan_History` 从 unchanged-frame 热路径移出 + +Plan History 是: + +```text +历史/分析冷路径 +``` + +不能每帧: + +```text +mutex +compare +unlock +``` + +## 新状态 + +Scene/Frame compiler 持有: + +```text +current_render_plan +``` + +同时维护能够判断 effective topology 是否变化的: + +```text +plan signature +``` + +这个 signature 是明确的结构字段组合。 + +不要使用字符串序列化。 + +例如覆盖: + +```text +active node IDs +node kind +dependency edges +composite edges +``` + +按照 canonical 顺序比较。 + +## 没变化 + +```text +reuse current shared_ptr +``` + +完全不进入 History mutex。 + +## 有变化 + +才: + +```text +new version + ↓ +new Render_Plan + ↓ +publish current + ↓ +append Render_Plan_History +``` + +History lock 只发生在: + +```text +plan version changed +``` + +这条冷路径。 + +## 注意 + +继续保持当前 `a.md` 的语义: + +> cache pruning 导致 active topology 改变,可以生成新的 Render Plan version。 + +不要因为这次锁优化重新改掉。 + +--- + +# 9. Render Lease 从“整帧持 mutex”改成短 ownership claim + +这个要非常谨慎,必须保留旧函数语义: + +> 多个 renderer caller 并发调用时,同一个 Frame 最多只能被一个 renderer 消费。 + +不能因为去锁导致重复消费。 + +## 当前 + +```text +acquire_renderer + ↓ +render_mutex LOCK + ↓ +Render Lease 整个生命周期 + ↓ +Taskflow + ↓ +wait + ↓ +Render Lease destructor + ↓ +render_mutex UNLOCK +``` + +要改成: + +```text +acquire_renderer + ↓ +短同步 ownership claim + ↓ +获得独占 Frame +════════════════════════ +下面不再持 Frame-Control mutex + ↓ +Taskflow + ↓ +complete +``` + +## ownership 状态 + +Frame 必须具有明确状态机,例如语义: + +```text +pending + ↓ +claimed + ↓ +rendering + ↓ +completed +``` + +ownership claim 可以: + +```text +短 mutex +``` + +或者符合当前数据结构时使用: + +```text +CAS +``` + +不要为了“lock-free”强行上复杂 atomic 状态机。 + +目标不是: + +```text +acquire_renderer 本身绝对无锁 +``` + +目标是: + +```text +ownership 确定以后不再持 mutex 执行整帧 +``` + +## Flow / Manual / Low Latency + +分别处理,但统一遵守: + +```text +Frame pointer/queue ownership transition +允许短同步 + +Frame 已被 renderer 独占后 +不持 mutex +``` + +不能破坏各 strategy 原来的语义。 + +## 测试 + +必须继续覆盖: + +```text +4/8 renderer callers +同一个 Frame 恰好消费一次 +无重复 +无撕裂 +无 ownership 丢失 +``` + +同时增加: + +```text +一个 renderer 正在执行长 Taskflow +另外 renderer 调用 acquire_renderer +``` + +验证: + +```text +不会因为第一个 Render Lease 持整帧 mutex +导致无关 Frame Control API 被长时间锁死 +``` + +--- + +# 原第 10 项 + +**完全略过。** + +不做: + +```text +TSan 执行计划 +性能阈值验收计划 +``` + +这轮不写进去。 + +--- + +# 11. 最终 Render Worker 的锁规则 + +完成 1~9 后,建立一条可以机械检查的规则。 + +进入: + +```text +Render Plan execution +``` + +以后,下列函数不得出现: + +```cpp +std::mutex::lock +std::recursive_mutex::lock +std::shared_mutex::lock +std::lock_guard +std::unique_lock +``` + +指的是 Kernel 自己的执行数据路径。 + +下面这种 Frame-local worker slot: + +```text +execution_slots[index] +``` + +继续直接独占写。 + +不增加锁。 + +最终应该是: + +```text +Taskflow worker +├── immutable Frame Snapshot readonly +├── immutable Render Graph readonly +├── Prepare Buffer owner-defined +├── Paint Buffer exclusive by DAG +├── final composite dependency DAG controlled +└── execution slot[index] exclusive index +``` + +--- + +# 12. 同步重写 `Kernel/threading.md` + +这次不要在现在的 `threading.md` 上简单补几行。 + +当前里面很多描述会因为此次改造失效,例如: + +```text +Double/Triple State render snapshot 都在 mutex 下读取 +render_graph_mutex_ 串行当前 render graph 访问 +Low Latency render_mutex_ 整个 Render Lease 生命周期持有 +``` + +这些都要按新模型重写。 + +--- + +# `threading.md` 必须采用下面的结构 + +## 1. 核心线程模型 + +第一段直接规定: + +```text +Renderive 的同步边界分为: + +1. Mutable Control Domain +2. Publication Domain +3. Immutable Frame Execution Domain +4. Cold Analysis Domain +``` + +--- + +## 2. Mutable Control Domain + +明确这里可以使用锁。 + +包括: + +```text +Renderable 属性修改 +Scene attach/detach +dependency/layer 修改 +RTD update +viewport 修改 +configuration 修改 +graph rebuild +Frame producer queue 操作 +``` + +这里的原则: + +```text +锁保护 mutable ownership +锁不泄漏到 Render DAG +``` + +--- + +## 3. Publication Domain + +专门说明: + +```text +publication 是 mutable → immutable 的唯一边界 +``` + +允许: + +```text +mutex +short critical section +pointer swap +shared_ptr publication +atomic ownership change +``` + +禁止: + +```text +持锁进入 Taskflow +``` + +描述: + +```text +Frame publication 完成以后, +当前 Frame 所需的数据版本必须全部固定。 +``` + +--- + +## 4. Immutable Frame Execution Domain + +这一节要写成最严格的规则。 + +明确: + +```text +Prepare/Paint/Composite worker 不获得 Kernel mutex。 +``` + +Worker 只允许: + +```text +读取 Frame Snapshot +读取 published Render State +读取 immutable Renderable Graph +访问 dependency snapshot +写自己的 Prepare Buffer +写自己的 Paint Buffer +写自己的 execution slot +``` + +禁止: + +```text +Plot_Core live getter +Scene control API +Frame Control mutable API +RTD mutation +configuration getter requiring lock +render_graph rebuild/getter requiring lock +live visibility +``` + +--- + +# 13. threading.md 增加“锁分类表” + +必须直接写表。 + +类似: + +| 锁/同步 | 所属域 | 是否允许跨 Frame Execution | 用途 | +|---------------------------|---------------------|---------------------------:|---------------------------------| +| Scene topology mutex | Control | 否 | attach/detach/topology mutation | +| State publish mutex | Publication | 否 | mutable→render state | +| RTD mutation mutex | Control | 否 | update + observer 顺序 | +| RTD publish mutex | Publication | 否 | render snapshot | +| Graph rebuild mutex | Control/Publication | 否 | 创建新 immutable graph | +| Frame queue mutex | Ownership | 否 | claim/publish frame | +| Render Plan History mutex | Cold | 不进入 worker | 保存历史版本 | +| Capture repository mutex | Cold | 不进入 worker | 保存成功 Snapshot | +| Taskflow worker data | Execution | 无锁 | immutable/exclusive data | + +这样以后审代码的时候直接对表查。 + +--- + +# 14. threading.md 增加锁顺序 + +虽然 Frame execution 不拿锁,控制侧仍然需要锁顺序。 + +必须明确规定: + +```text +Scene control + ↓ +Renderable control + ↓ +Frame control + ↓ +RTD / observer +``` + +但实际顺序需要按代码最后改完后的真实调用链重新核对后写。 + +**不能提前凭猜测写死。** + +最终 threading.md 必须列: + +```text +允许的 nested locking +禁止的 nested locking +callback 前必须释放哪些锁 +``` + +尤其强调: + +```text +用户 observer callback +用户 Renderable callback +Taskflow callback +``` + +调用前不得持有可能被 callback 重入的控制锁。 + +--- + +# 15. threading.md 增加 callback 规则 + +写明: + +```text +Observer callback 是同步 callback +``` + +但: + +```text +不得在持有 Frame ownership mutex 时调用 +不得在持有 Scene topology mutation mutex 时调用会重入 Scene 的 callback +不得在 publication mutex 下执行未知用户代码 +``` + +需要 state observation 时: + +```text +lock +生成 Observation value +unlock +callback(observation) +``` + +保持当前已经采用的正确模式。 + +--- + +# 16. threading.md 增加 Frame Ownership 章节 + +Flow / Manual / Low Latency 分别说明: + +```text +producer ownership +pending ownership +renderer claim +render ownership +completed ownership +recycle ownership +``` + +核心规则: + +> Frame ownership 转移可以同步,Frame ownership 一旦确定,Frame 内容执行不依赖互斥锁。 + +这句话作为 Frame Strategy 的总规则。 + +--- + +# 17. threading.md 增加 Snapshot Lifetime 章节 + +写清: + +```text +Frame 持有什么 shared ownership +什么时候释放 +graph version 生命周期 +RTD snapshot 生命周期 +renderable snapshot 生命周期 +``` + +特别说明: + +```text +Renderable rebuild Graph +``` + +不会使正在执行 Frame 的 Graph 失效。 + +--- + +# 18. threading.md 增加 Prepare/Paint 并发规则 + +保留现在正确的: + +```text +不同 Renderable 无依赖即可并发 +Renderable 内无 dependency edge 即可并发 +Prepare A 可以与 Paint B 并发 +``` + +并新增: + +```text +并发安全不是依赖 mutex 达成 +而是依赖: +immutable input +exclusive output +DAG dependency +``` + +这个很重要。 + +--- + +# 19. threading.md 增加“不应该看到的锁” + +直接写一个禁止清单。 + +在: + +```text +prepare callback +paint callback +composite callback +Taskflow wrapper +``` + +里面如果 Kernel 自己出现: + +```text +configuration_mutex_ +render_graph_mutex_ +Plot_Core::mutex +Frame Control state mutex +RTD mutable mutex +Render Lease mutex +``` + +视为架构错误。 + +不是“性能优化建议”。 + +是违反线程模型。 + +--- + +# 20. 前端整体废除当前手写 DOM 架构 + +当前: + +```text +webapp_gallery/ +├── index.html +├── app.js +└── styles.css +``` + +其中 `app.js` 自己维护: + +```text +GalleryCard class +DOM clone +querySelector +菜单状态 +Tabs +表单 +DAG SVG +Timeline SVG +选择状态 +``` + +这一版直接替换。 + +旧实现删除。 + +不保留: + +```text +legacy app.js +legacy page +React page +``` + +双实现。 + +--- + +# 21. 前端新技术栈 + +固定: + +```text +React +TypeScript +Vite +Material UI +@mui/icons-material +@xyflow/react +``` + +**不使用:** + +```text +ELK +elkjs +Dagre +D3 layout +Graphviz +Cytoscape +AMIS +手写 DOM UI +手写完整 DAG SVG +``` + +Material UI 是 React component library,可以把普通 UI 全部收敛为一致的组件模型。 ([MUI][1]) + +React Flow 只承担图画布能力: + +```text +node rendering +edge rendering +zoom +pan +selection +fit view +viewport +``` + +这些都是其现成能力。 ([React Flow][2]) + +--- + +# 22. DAG 布局自己解析 + +这里严格按你刚才的要求: + +**不要 ELK。** + +新增自己的纯 TypeScript parser/layout: + +```text +renderPlanToDagModel() + ↓ +layoutRenderDag() + ↓ +React Flow nodes/edges +``` + +--- + +# 23. DAG parser 输入 + +直接使用当前后端: + +```text +render_plan +├── version +├── nodes +└── edges +``` + +不要让 React component 自己理解后端 JSON。 + +首先解析成前端自己的强类型模型: + +```ts +RenderPlan +RenderNode +RenderEdge +RenderNodeKind +RenderDagModel +RenderDagLevel +``` + +所有后端 JSON 检查集中在: + +```text +protocol/ +``` + +React component 不做: + +```ts +data?.foo?.bar ?? +... +``` + +满页面 defensive parsing。 + +--- + +# 24. 自己实现 DAG level parser + +布局算法按 DAG 本身语义做。 + +第一步: + +```text +node indegree +``` + +然后 Kahn topological traversal。 + +计算: + +```text +level[node] = +max(level[parent] + 1) +``` + +无父节点: + +```text +level = 0 +``` + +得到: + +```text +Level 0 +├── A.prepare +├── B.prepare +└── C.prepare + +Level 1 +├── A.paint +└── C.paint + +Level 2 +├── ... +``` + +--- + +# 25. 同 level 节点排序 + +不能随机。 + +排序规则固定,保证同一 plan 刷新后节点不乱跳。 + +依次: + +```text +owner/renderable order +kind +logical execution order +node_id +``` + +如果前一 version 有相同 Node ID: + +```text +优先保持上一个位置 +``` + +这样: + +```text +Plan v17 +→ +Plan v18 +``` + +只新增一个 node 时,不应该全图重排。 + +--- + +# 26. DAG 布局增加 barycenter pass + +基础 level 完成之后,自己实现简单交叉减少。 + +两遍: + +```text +left → right +right → left +``` + +对每层节点根据邻接节点平均位置排序。 + +不引入第三方 layout engine。 + +复杂度保持可控。 + +如果数据量增长,最多增加几轮 deterministic pass。 + +不做无限迭代优化。 + +--- + +# 27. DAG Node 使用 MUI 组件 + +React Flow custom node 内部直接用 MUI: + +```text +Paper +Stack +Typography +Chip +Tooltip +Box +``` + +例如: + +```text +┌────────────────────────────┐ +│ Waterfall │ +│ chunk.paint.4 │ +│ │ +│ PAINT 1.82ms 23.1% │ +└────────────────────────────┘ +``` + +Node 数据: + +```text +owner +name +kind +duration +critical +cache state +worker +``` + +React Flow custom node 本身就是普通 React component,因此这种组合是自然的。 ([React Flow][3]) + +--- + +# 28. DAG Edge 不自己写 SVG Path 算法 + +这个和“自己写 parser”区分开。 + +自己写的是: + +```text +DAG topology parser +level layout +position +selection mapping +``` + +不是重新实现图形库。 + +Edge 使用 React Flow 自带: + +```text +straight +step +smoothstep +``` + +当前更适合执行 DAG 的默认使用: + +```text +smoothstep +``` + +dependency / composite 用不同: + +```text +edge data / style +``` + +表达。 + +React Flow 已经提供这些 edge 类型以及自定义 edge 能力,不需要我们重新维护点击命中、SVG 路径等基础设施。 ([React Flow][4]) + +--- + +# 29. 前端目录重构 + +改成: + +```text +webapp_gallery/ +├── index.html +├── package.json +├── tsconfig.json +├── vite.config.ts +└── src/ + ├── main.tsx + ├── App.tsx + ├── theme.ts + ├── protocol/ + │ ├── gallery.ts + │ ├── renderPlan.ts + │ └── capture.ts + ├── websocket/ + │ └── GallerySocket.ts + ├── state/ + │ └── galleryState.ts + ├── components/ + │ ├── GalleryCard.tsx + │ ├── GalleryToolbar.tsx + │ ├── ControlPanel.tsx + │ ├── ObserverPanel.tsx + │ ├── PerformancePanel.tsx + │ └── capture/ + │ ├── CapturePanel.tsx + │ ├── CaptureControls.tsx + │ ├── FrameBrowser.tsx + │ ├── RenderDag.tsx + │ ├── RenderDagNode.tsx + │ ├── WorkerTimeline.tsx + │ ├── NodeDetail.tsx + │ └── PlanComparison.tsx + └── dag/ + ├── model.ts + ├── parseRenderPlan.ts + └── layoutRenderDag.ts +``` + +不要搞一个: + +```text +utils.ts +``` + +塞几千行。 + +--- + +# 30. Gallery 主页面 Material UI 化 + +现在的: + +```text +topbar +hero +mode tabs +category filters +cards +context menu +toast +``` + +分别改成 MUI: + +```text +AppBar +Toolbar +Tabs +ToggleButtonGroup / Chip +Grid +Card +Drawer / Dialog +Snackbar +``` + +所有按钮和 input 都不再自己手写 HTML styling。 + +--- + +# 31. 原右键菜单改成 MUI Drawer + +当前性能控制区内容很多。 + +不要继续使用自己计算定位的 context menu。 + +右键 Renderable 后打开: + +```text +Drawer +``` + +PC 端右侧。 + +里面: + +```text +Tabs +├── 属性 +├── 专属 API +├── Observer +├── Performance +├── Performance Capture +└── Render DAG +``` + +保持现有功能语义。 + +--- + +# 32. 表单全部 MUI controlled component + +后端字段类型解析成: + +```text +boolean → Switch +enum → Select +number → TextField type=number +action → Button +readonly → Typography/TextField readonly +``` + +解决之前手写表单容易: + +```text +后端刷新覆盖正在输入的值 +Enter 后值恢复 +dirty state 不明确 +``` + +的问题。 + +每个字段维护: + +```text +serverValue +draftValue +dirty +submitting +error +``` + +**后端 telemetry refresh 不允许覆盖 dirty draft。** + +只有: + +```text +submit 成功 +manual reset +``` + +才同步。 + +--- + +# 33. Performance Capture 页面重新组件化 + +布局: + +```text +┌────────────────────────────────────────────────────┐ +│ Capture Controls │ +├──────────────┬─────────────────────────────────────┤ +│ Frame List │ DAG │ +│ │ │ +│ ├─────────────────────────────────────┤ +│ │ Worker Timeline │ +├──────────────┴─────────────────────────────────────┤ +│ Selected Node Detail │ +├────────────────────────────────────────────────────┤ +│ Statistics / Plan Comparison │ +└────────────────────────────────────────────────────┘ +``` + +使用 MUI: + +```text +Paper +Stack +Grid +List +ListItemButton +Tabs +Table +Chip +Tooltip +Divider +``` + +--- + +# 34. Timeline 不再手写 SVG + +Timeline 不需要另外一个图框架。 + +自己解析: + +```text +worker_id +start_offset_ns +duration_ns +``` + +然后使用: + +```text +MUI Box +``` + +做 CSS absolute positioning。 + +每一个 execution block: + +```text +Box +``` + +位置: + +```text +left = start / frame_duration +width = duration / frame_duration +``` + +Worker 一行一个: + +```text +Stack / Box +``` + +这样: + +```text +selection +hover +tooltip +critical path +``` + +全部是标准 React DOM,而不是手工 `createElementNS()`。 + +--- + +# 35. DAG 和 Timeline 共享 selection store + +当前: + +```text +selectedCaptureNodeId +``` + +保留这个概念,但放到 React state。 + +```text +selectedNodeId +selectedFrameId +selectedPlanVersion +``` + +点击 DAG: + +```text +selectedNodeId = X +``` + +Timeline 对应 block 高亮。 + +点击 Timeline: + +```text +selectedNodeId = X +``` + +DAG 对应 Node 高亮。 + +Detail 自动更新。 + +不要两套独立选择状态。 + +--- + +# 36. DAG parser 不关心 Capture + +建立: + +```ts +parseRenderPlan(plan) +``` + +只处理 topology。 + +Capture overlay 单独: + +```ts +applyFrameExecution(dag, frame) +``` + +这样同一套 DAG 可以用于: + +```text +Current Render Plan +Capture Frame +Plan Comparison +``` + +避免现在 SVG renderer 里: + +```text +plan + frame + statistics +``` + +全部搅在一起。 + +--- + +# 37. Plan version 变化的前端处理 + +利用稳定: + +```text +node_id +``` + +做增量视觉稳定。 + +当: + +```text +v17 → v18 +``` + +parser 输出: + +```text +same nodes +added nodes +removed nodes +changed edges +``` + +UI 可以: + +```text +新增节点标记 +删除节点列表 +边变化 +``` + +并尽可能保持原 Node 位置。 + +这非常适合你后面看: + +```text +Waterfall partition +cache pruning +``` + +到底怎么改变 DAG。 + +--- + +# 38. 前端不要修改后端协议语义 + +这一轮: + +```text +前端技术实现替换 +``` + +不是重新设计 Gallery protocol。 + +当前已有: + +```text +render_plan +performance_capture +node_statistics +plan statistics +frame snapshots +``` + +继续消费。 + +只有在 React 强类型化时发现: + +```text +数据本身缺关键 identity +``` + +才修改协议。 + +不要为了前端组件化随便改 C++ JSON 字段。 + +--- + +# 39. Vite 集成方式 + +`webapp_gallery` 自己作为 frontend source。 + +Vite build 输出固定到现有 Web Server 使用的静态目录。 + +路径计算全部基于: + +```text +vite.config.ts 自身目录 +``` + +不做随机输出目录。 + +不改变现有构建库的文件路径语义。 + +CMake 只负责: + +```text +调用 frontend build +复制/嵌入确定的 dist +``` + +不把前端源文件逻辑塞进 CMake。 + +--- + +# 40. 前端迁移顺序 + +严格按: + +```text +1. Vite + React + TS 能构建 +2. WebSocket protocol 强类型化 +3. Gallery 首页 +4. Pixel Canvas +5. Controls +6. Actions +7. Observer +8. Performance +9. Performance Capture +10. Render DAG +11. Worker Timeline +12. Plan Comparison +13. 删除 app.js +14. 删除旧 DOM templates +15. 删除不再使用的 styles.css +``` + +不要边迁移边留旧页面 fallback。 + +最后直接一套实现。 + +--- + +# 41. 后端实际执行顺序 + +这部分严格按依赖执行: + +```text +A. Frame_Render_Snapshot 基础结构 + ↓ +B. viewport snapshot + ↓ +C. Prepare/Paint/Composite Context 拆分 + ↓ +D. visibility snapshot + ↓ +E. RTD discard 移出 DAG + ↓ +F. configuration snapshot + ↓ +G. immutable Render Graph publication + ↓ +H. Frame Control publish/read 合并 + ↓ +I. Render Plan current/history 分离 + ↓ +J. Render Lease 短 ownership claim + ↓ +K. threading.md 重写 +``` + +原因是后面的锁删除都依赖前面的 snapshot ownership。 + +不要反过来先删 mutex。 + +--- + +# 42. 每一步测试要求 + +每一个步骤都采用: + +```text +先补对应测试 +↓ +修改实现 +↓ +跑对应测试 +↓ +跑 Kernel/render_2D/web_server 全量相关测试 +↓ +进入下一项 +``` + +不是全部改完最后才测。 + +尤其不能出现: + +```text +先把 mutex 全删掉 +然后看看有没有 crash +``` + +--- + +# 43. 最终锁审计标准 + +改完以后重新全局搜索: + +```text +mutex +lock_guard +unique_lock +shared_lock +recursive_mutex +``` + +逐个给它分类: + +```text +Control +Publication +Ownership +Cold +``` + +**不存在第五类。** + +凡是属于: + +```text +Frame Execution +``` + +的 Kernel mutex,继续修改,直到为 0。 + +--- + +# 44. 最终架构 + +最终形成: + +```text + MUTABLE DOMAIN + │ + ┌──────────────────────┼───────────────────────┐ + │ │ │ +Renderable State RTD / Config Graph Builder + │ │ │ + └──────────────────────┼───────────────────────┘ + ▼ + FRAME PUBLICATION + │ + ┌────────────┴────────────┐ + │ Frame Render Snapshot │ + │ Immutable Graphs │ + │ Published RTD Views │ + │ Render Plan │ + └────────────┬────────────┘ + │ +═══════════════════════════╪════════════════════════════ + NO KERNEL LOCKS + │ + ▼ + ONE TASKFLOW + ┌────────────┼────────────┐ + ▼ ▼ ▼ + Prepare Paint Composite + │ │ │ + └────────────┼────────────┘ + ▼ + Frame Snapshot + │ + COLD ANALYSIS DOMAIN + │ + Repository / Statistics / API + │ + ▼ + React + Material UI + │ + React Flow Render DAG + │ + Own DAG Layout Parser +``` + +## 最终验收只有一句话 + +> **控制侧允许锁,publication 允许短锁,Frame ownership 转移允许短同步;一旦 Frame 的 immutable execution snapshot 发布完成,直到 +Prepare/Paint/Composite 完成,Kernel 不再通过 mutex 获取任何渲染输入,也不持有 Frame-Control mutex 执行整帧。** + +前端则执行: + +> **所有常规 UI 使用 Material UI;DAG 使用 React Flow 的画布/节点/边能力;DAG 拓扑解析、分层、排序、交叉减少、版本位置稳定全部由 +Renderive 自己的 TypeScript parser 完成;不引入 ELK/Dagre。** ([React Flow][2]) + +这版计划不会动你已经完成的全局 DAG 核心,主要是在它外面补上最后一道真正严格的 **Frame immutable publication boundary** +。完成后,“交换以后渲染执行无锁”就不再是一种实现倾向,而会成为整个 Kernel 明文规定的线程契约。 + +[1]: https://mui.com/material-ui/?utm_source=chatgpt.com "React components that implement Material Design" +[2]: https://reactflow.dev/api-reference/react-flow?utm_source=chatgpt.com "The ReactFlow component" +[3]: https://reactflow.dev/learn/customization/custom-nodes?utm_source=chatgpt.com "Custom Nodes - React Flow" +[4]: https://reactflow.dev/examples/edges/custom-edges?utm_source=chatgpt.com "Custom Edges - React Flow" diff --git a/render_2D/axis/Abs_Axis.cpp b/render_2D/axis/Abs_Axis.cpp index 9b2998a..262c221 100644 --- a/render_2D/axis/Abs_Axis.cpp +++ b/render_2D/axis/Abs_Axis.cpp @@ -5,7 +5,7 @@ namespace renderive { -Abs_Axis::Abs_Axis(Plot_Core& plot) : Renderable(plot, true) {} +Abs_Axis::Abs_Axis(::Scene_Base& scene) : Renderable(scene, true) {} Abs_Axis::~Abs_Axis() = default; diff --git a/render_2D/axis/Abs_Axis.h b/render_2D/axis/Abs_Axis.h index 22bccbe..4ae3eca 100644 --- a/render_2D/axis/Abs_Axis.h +++ b/render_2D/axis/Abs_Axis.h @@ -9,7 +9,7 @@ namespace renderive { class LIB_DECL Abs_Axis : public Renderable { public: - explicit Abs_Axis(Plot_Core& plot); + explicit Abs_Axis(::Scene_Base& scene); ~Abs_Axis() override; [[nodiscard]] virtual Axis_Transform transform() const = 0; diff --git a/render_2D/axis/Axis_Builder.h b/render_2D/axis/Axis_Builder.h index 8a76804..dbaed91 100644 --- a/render_2D/axis/Axis_Builder.h +++ b/render_2D/axis/Axis_Builder.h @@ -2,6 +2,7 @@ #include "../renderable/Renderable.h" #include "Axis_Types.h" +#include #include #include @@ -72,7 +73,9 @@ public: std::shared_ptr build() const { if (!parent_) return {}; - return parent_->plot().template make_renderable(parent_, properties_); + auto result = parent_->scene().template make_renderable(properties_); + attach_renderable_child(parent_->scene(), result, parent_); + return result; } private: diff --git a/render_2D/axis/Axis_State_Strategy.hpp b/render_2D/axis/Axis_State_Strategy.hpp index d47f0bb..2c2a3c3 100644 --- a/render_2D/axis/Axis_State_Strategy.hpp +++ b/render_2D/axis/Axis_State_Strategy.hpp @@ -25,10 +25,10 @@ class Axis_State_Strategy using Base = Double_State_Strategy; public: - Axis_State_Strategy(Plot_Core& plot, State state) + Axis_State_Strategy(::Scene_Base& scene, State state) : Base(state, With_Observer{State_Observer(Renderable_State_Observer(this))}, - plot) {} + scene) {} template Value> Axis_State_Strategy& set(Value&& value) { diff --git a/render_2D/axis/Frequency_Axis.cpp b/render_2D/axis/Frequency_Axis.cpp index 446dae2..e6bd5d2 100644 --- a/render_2D/axis/Frequency_Axis.cpp +++ b/render_2D/axis/Frequency_Axis.cpp @@ -6,8 +6,8 @@ namespace renderive::detail { -Frequency_Axis_Control::Frequency_Axis_Control(Plot_Core& plot, const Axis_Properties& properties) - : Axis(plot, properties) {} +Frequency_Axis_Control::Frequency_Axis_Control(::Scene_Base& scene, const Axis_Properties& properties) + : Axis(scene, properties) {} std::string Frequency_Axis_Control::format_tick_label(double tick, const Axis_Properties& state) const { diff --git a/render_2D/axis/Frequency_Axis.h b/render_2D/axis/Frequency_Axis.h index 1eb9a06..d10e155 100644 --- a/render_2D/axis/Frequency_Axis.h +++ b/render_2D/axis/Frequency_Axis.h @@ -8,7 +8,7 @@ namespace detail { class LIB_DECL Frequency_Axis_Control : public Axis { public: - Frequency_Axis_Control(Plot_Core& plot, const Axis_Properties& properties); + Frequency_Axis_Control(::Scene_Base& scene, const Axis_Properties& properties); protected: [[nodiscard]] std::string format_tick_label(double tick, const Axis_Properties& state) const override; }; diff --git a/render_2D/axis/Numeric_Axis.cpp b/render_2D/axis/Numeric_Axis.cpp index 6f4cfb5..34b930f 100644 --- a/render_2D/axis/Numeric_Axis.cpp +++ b/render_2D/axis/Numeric_Axis.cpp @@ -2,8 +2,8 @@ namespace renderive::detail { -Axis_Control::Axis_Control(Plot_Core& plot, const Axis_Properties& properties) - : Axis_State_Strategy(plot, properties) {} +Axis_Control::Axis_Control(::Scene_Base& scene, const Axis_Properties& properties) + : Axis_State_Strategy(scene, properties) {} void Axis_Control::handle_event(const Event& event) { if (event.type == Event_Type::Wheel && get<&Axis_Properties::wheel>()) { diff --git a/render_2D/axis/Numeric_Axis.h b/render_2D/axis/Numeric_Axis.h index 2cf7c5f..fcfbe44 100644 --- a/render_2D/axis/Numeric_Axis.h +++ b/render_2D/axis/Numeric_Axis.h @@ -34,7 +34,7 @@ struct Axis_Interaction_State { class LIB_DECL Axis_Control : public Axis_State_Strategy, public Event_Handler { public: - Axis_Control(Plot_Core& plot, const Axis_Properties& properties); + Axis_Control(::Scene_Base& scene, const Axis_Properties& properties); void handle_event(const Event& event) override; void publish() override; diff --git a/render_2D/axis/Time_Axis.cpp b/render_2D/axis/Time_Axis.cpp index dd2fdf0..0388d68 100644 --- a/render_2D/axis/Time_Axis.cpp +++ b/render_2D/axis/Time_Axis.cpp @@ -16,8 +16,8 @@ Time_Axis_State time_state_from(const Time_Axis_Properties& properties) { } // namespace -Time_Axis_Control::Time_Axis_Control(Plot_Core& plot, const Time_Axis_Properties& properties) - : Axis_State_Strategy(plot, time_state_from(properties)) {} +Time_Axis_Control::Time_Axis_Control(::Scene_Base& scene, const Time_Axis_Properties& properties) + : Axis_State_Strategy(scene, time_state_from(properties)) {} std::size_t Time_Axis_Control::time_point_count() const { return read([](const Time_Axis_State& state) { return state.samples.size(); }); diff --git a/render_2D/axis/Time_Axis.h b/render_2D/axis/Time_Axis.h index a6d13ac..db24129 100644 --- a/render_2D/axis/Time_Axis.h +++ b/render_2D/axis/Time_Axis.h @@ -27,7 +27,7 @@ struct Time_Axis_State : Time_Axis_Properties { class LIB_DECL Time_Axis_Control : public Axis_State_Strategy { public: - Time_Axis_Control(Plot_Core& plot, const Time_Axis_Properties& properties); + Time_Axis_Control(::Scene_Base& scene, const Time_Axis_Properties& properties); [[nodiscard]] std::size_t time_point_count() const; int append_time(Time_Of_Day time); [[nodiscard]] Time_Of_Day tick_to_time(int tick) const; diff --git a/render_2D/export.h b/render_2D/export.h index 6549629..39fd54e 100644 --- a/render_2D/export.h +++ b/render_2D/export.h @@ -3,7 +3,7 @@ #include "base/Types.h" #include "event/Event.h" #include "renderable/Renderable.h" -#include "plot/Plot_Core.h" +#include "scene/Scene.h" #include "axis/Axis.h" #include "plottable/Hover_Tooltip.h" #include "plottable/Plottables.h" diff --git a/render_2D/plot/Plot_Core.cpp b/render_2D/plot/Plot_Core.cpp deleted file mode 100644 index a1b8df5..0000000 --- a/render_2D/plot/Plot_Core.cpp +++ /dev/null @@ -1,591 +0,0 @@ -#include "Plot_Core.h" - -#include "../plottable/Performance_Overlay.h" -#include "../render/Blend2D_Cache.h" -#include "../renderable/Renderable.h" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace renderive { -namespace { - -struct Frame_Observer_Data { - mutable std::mutex mutex; - std::uint32_t last_event{}; - std::uint64_t observation_count{}; - std::uint64_t produced_frame_count{}; - std::uint64_t consumed_frame_count{}; - std::uint64_t dropped_frame_count{}; - std::uint64_t failed_operation_count{}; - std::uint64_t pending_frame_count{}; - std::uint64_t latest_sequence{}; - std::uint64_t paint_duration_ns{}; - std::uint64_t render_duration_ns{}; - std::uint64_t paint_lease_wait_ns{}; - std::uint64_t paint_state_wait_ns{}; - std::uint64_t publish_state_wait_ns{}; - std::uint64_t ready_wait_ns{}; - std::uint64_t frame_age_at_render_ns{}; - std::uint64_t render_lease_wait_ns{}; - std::uint64_t render_state_wait_ns{}; - std::uint64_t render_finish_state_wait_ns{}; - std::uint64_t queue_wait_ns{}; -}; - -struct Frame_Observer { - static constexpr bool enabled = true; - std::shared_ptr data; - - template - void observe(const Observation& observation) noexcept { - std::lock_guard lock(data->mutex); - data->last_event = static_cast(observation.event); - ++data->observation_count; - if (observation.statistics.sequence != 0) - data->latest_sequence = observation.statistics.sequence; - if constexpr (requires { observation.statistics.timing.paint_duration_ns; }) { - if (observation.statistics.sequence != 0) { - const auto& timing = observation.statistics.timing; - data->paint_lease_wait_ns = timing.paint_lease_wait_ns; - data->paint_state_wait_ns = timing.paint_state_wait_ns; - data->publish_state_wait_ns = timing.publish_state_wait_ns; - data->ready_wait_ns = timing.ready_wait_ns; - data->frame_age_at_render_ns = timing.frame_age_at_render_ns; - data->render_lease_wait_ns = timing.render_lease_wait_ns; - data->render_state_wait_ns = timing.render_state_wait_ns; - data->render_finish_state_wait_ns = timing.render_finish_state_wait_ns; - } - } else { - if (observation.statistics.sequence != 0) { - data->paint_duration_ns = observation.statistics.paint_duration_ns; - data->render_duration_ns = observation.statistics.render_duration_ns; - } - } - if constexpr (requires { observation.statistics.queue_wait_ns; }) { - if (observation.statistics.sequence != 0) - data->queue_wait_ns = observation.statistics.queue_wait_ns; - } - - if constexpr (requires { observation.state.prepared_frame_count; }) { - data->produced_frame_count = observation.state.prepared_frame_count; - data->consumed_frame_count = observation.state.render_count; - data->dropped_frame_count = observation.state.replaced_prepared_frame_count + - observation.state.discarded_prepared_frame_count; - data->failed_operation_count = observation.state.failed_refresh_count; - data->pending_frame_count = observation.state.pending_frame ? 1U : 0U; - } else if constexpr (requires { observation.state.limit_state; }) { - data->produced_frame_count = observation.statistics.counters.published_frame_count; - data->consumed_frame_count = observation.state.completed_lifecycle_count; - data->dropped_frame_count = - observation.statistics.counters.abandoned_frame_count + - observation.statistics.counters.manually_discarded_frame_count; - data->failed_operation_count = observation.statistics.counters.swap_failure_count; - const auto retired_frame_count = - observation.state.completed_lifecycle_count + - observation.statistics.counters.abandoned_frame_count + - observation.statistics.counters.manually_discarded_frame_count; - data->pending_frame_count = - observation.statistics.counters.published_frame_count > retired_frame_count ? 1U : 0U; - } else { - data->produced_frame_count = observation.state.enqueued_frame_count; - data->consumed_frame_count = observation.state.rendered_frame_count; - data->failed_operation_count = observation.state.empty_acquire_count; - data->pending_frame_count = observation.state.pending_frame_count; - } - } -}; - -using Frame_Observer_State = ::Observer_State; -using Manual_Frame_Control = - ::Manual_Refresh_Strategy<::Scene2D_Frame_Data, std::mutex, Frame_Observer_State>; -using Low_Latency_Frame_Control = - ::Low_Latency_Strategy<::Scene2D_Frame_Data, std::mutex, Frame_Observer_State>; -using Playback_Frame_Control = - ::Flow_Refresh_Strategy<::Scene2D_Frame_Data, std::mutex, Frame_Observer_State>; -using Manual_Plot_Scene = - ::Scene2D_Context; -using Low_Latency_Plot_Scene = - ::Scene2D_Context; -using Playback_Plot_Scene = - ::Scene2D_Context; -using Plot_Scene = std::variant, - std::unique_ptr, - std::unique_ptr>; - -template -decltype(auto) with_scene(Plot_Scene& scene, Function&& function) { - return std::visit( - [&function](auto& concrete) -> decltype(auto) { - return std::forward(function)(*concrete); - }, - scene); -} - -template -decltype(auto) with_scene(const Plot_Scene& scene, Function&& function) { - return std::visit( - [&function](const auto& concrete) -> decltype(auto) { - return std::forward(function)(*concrete); - }, - scene); -} - -std::string observer_event_name(Frame_Control_Mode mode, std::uint32_t event) { - if (mode == Frame_Control_Mode::Manual) { - static constexpr const char* names[]{"prepared", "prepared_replaced", "refresh_succeeded", - "refresh_failed", "rendered", "real_time_data_updated", - "manually_discarded"}; - return event < std::size(names) ? names[event] : "none"; - } - if (mode == Frame_Control_Mode::Playback) { - static constexpr const char* names[]{"enqueued", "dequeued", "queue_empty", "rendered", - "real_time_data_updated"}; - return event < std::size(names) ? names[event] : "none"; - } - static constexpr const char* names[]{"published", "abandoned", "manually_discarded", - "swap_failed", "rendered", "lifecycle_completed", - "real_time_data_updated"}; - return event < std::size(names) ? names[event] : "none"; -} - -std::string limit_state_name(bool available, std::uint32_t state) { - if (!available) - return "not_applicable"; - static constexpr const char* names[]{"frequency_limited", "paint_limited", "render_limited", - "consumer_limited", "unlimited"}; - return state < std::size(names) ? names[state] : "unknown"; -} - -Rect full_rect(Size size) { - return {0, 0, std::max(0, size.width), std::max(0, size.height)}; -} - -class Renderable_Group final : public Renderable { -public: - using Renderable::Renderable; - -private: - void prepare_frame(const Render_State_View&, - const Scene_Render_Context&) override {} - void paint(detail::Painter&, const Scene_Render_Context&) override {} -}; - -} // namespace - -struct Plot_Core::Impl { - explicit Impl(Frame_Control_Mode selected_mode) - : mode(selected_mode), observer(std::make_shared()), - scene(make_scene(selected_mode, observer)) {} - - static Plot_Scene make_scene(Frame_Control_Mode mode, - const std::shared_ptr& observer) { - auto& resource = *memory_resource(Memory_Domain::Plot_Frame); - Frame_Observer_State observer_state(Frame_Observer{observer}); - switch (mode) { - case Frame_Control_Mode::Manual: - return std::make_unique(resource, std::move(observer_state)); - case Frame_Control_Mode::Playback: - return std::make_unique(resource, std::move(observer_state)); - case Frame_Control_Mode::Low_Latency: - return std::make_unique(resource, std::move(observer_state)); - } - throw std::invalid_argument("unknown frame control mode"); - } - - Frame_Control_Mode mode; - std::shared_ptr observer; - Plot_Scene scene; - mutable std::mutex mutex; - std::mutex initialization_mutex; - std::weak_ptr presentation_sink; - std::shared_ptr performance; - Color background = Color::black(); - Size viewport; - std::atomic_bool active{}; - std::atomic_bool dirty{true}; -}; - -Plot_Core::Plot_Core() - : Plot_Core(Frame_Control_Mode::Low_Latency) {} - -Plot_Core::Plot_Core(Frame_Control_Mode mode) - : impl_(std::make_unique(mode)) {} - -Plot_Core::~Plot_Core() = default; - -void Plot_Core::init() { - std::lock_guard initialization_lock(impl_->initialization_mutex); - if (root_renderable()) - return; - auto root = renderive::make_shared(Memory_Domain::Renderable, *this, true); - root->set_object_name("root"); - with_scene(impl_->scene, [&root](auto& scene) { scene.attach_renderable(root); }); - notify_model_dirty(); -} - -std::shared_ptr Plot_Core::root_renderable() const { - const auto topology = with_scene(impl_->scene, [](const auto& scene) { - return scene.topology_snapshot(); - }); - for (const auto& relationship : topology.display) { - if (relationship.parent) - continue; - auto base = std::const_pointer_cast<::Renderable_Base>(relationship.child); - if (auto renderable = std::dynamic_pointer_cast(base)) - return renderable; - } - return {}; -} - -std::shared_ptr Plot_Core::create_renderable_node( - const std::shared_ptr& parent, std::string object_name) { - auto renderable = renderive::make_shared( - Memory_Domain::Renderable, *this, true); - renderable->set_object_name(std::move(object_name)); - attach_renderable(renderable, parent); - return renderable; -} - -void Plot_Core::attach_renderable(const std::shared_ptr& renderable, - const std::shared_ptr& parent) { - if (!renderable) - throw std::invalid_argument("renderable is null"); - Renderable* parent_pointer = parent.get(); - if (!parent_pointer) { - auto root = root_renderable(); - if (!root) - throw std::logic_error("Plot_Core::init must be called before adding renderables"); - parent_pointer = root.get(); - } - with_scene(impl_->scene, [&renderable](auto& scene) { scene.attach_renderable(renderable); }); - try { - with_scene(impl_->scene, [&renderable, parent_pointer](auto& scene) { - scene.set_display_parent(*renderable, parent_pointer); - scene.set_dependency_parent(*renderable, parent_pointer); - }); - } catch (...) { - with_scene(impl_->scene, [&renderable](auto& scene) { scene.detach_renderable(*renderable); }); - throw; - } - notify_model_dirty(); -} - -void Plot_Core::remove_renderable(const std::shared_ptr& renderable) { - if (!renderable || renderable == root_renderable()) - return; - with_scene(impl_->scene, [&renderable](auto& scene) { scene.detach_renderable(*renderable); }); - notify_model_dirty(); -} - -void Plot_Core::set_background_color(Color color) { - { - std::lock_guard lock(impl_->mutex); - if (impl_->background == color) - return; - impl_->background = color; - } - notify_model_dirty(); -} - -Color Plot_Core::background_color() const noexcept { - std::lock_guard lock(impl_->mutex); - return impl_->background; -} - -void Plot_Core::set_viewport_size(Size size) { - size.width = std::max(0, size.width); - size.height = std::max(0, size.height); - { - std::lock_guard lock(impl_->mutex); - if (impl_->viewport == size) - return; - impl_->viewport = size; - } - const auto topology = with_scene(impl_->scene, [](const auto& scene) { - return scene.topology_snapshot(); - }); - for (const auto& renderable : topology.renderables) - const_cast<::Renderable_Base&>(*renderable).invalidate_prepare(); - notify_model_dirty(); -} - -Size Plot_Core::viewport_size() const noexcept { - std::lock_guard lock(impl_->mutex); - return impl_->viewport; -} - -void Plot_Core::dispatch_event(const Event& event) { - const auto paint_order = with_scene(impl_->scene, [](const auto& scene) { - return scene.paint_order_snapshot(); - }); - for (auto iterator = paint_order.rbegin(); iterator != paint_order.rend(); ++iterator) { - auto base = std::const_pointer_cast<::Renderable_Base>(*iterator); - if (auto renderable = std::dynamic_pointer_cast(base)) { - if (renderable->is_visible()) { - if (auto handler = std::dynamic_pointer_cast(base)) - handler->handle_event(event); - } - if (event.is_accepted()) - break; - } - } -} - -void Plot_Core::notify_model_dirty() noexcept { - impl_->dirty.store(true, std::memory_order_release); -} - -bool Plot_Core::prepare_frame() { - return with_scene(impl_->scene, [](auto& scene) { - auto paint_frame = scene.frame_control.acquire_painter(); - if (!paint_frame) - return false; - scene.publish_frame_state(); - auto& scene_state = static_cast::Scene_State_Strategy&>(scene); - scene_state.template set<&::Scene2D_State::revision>(scene_state.state_revision() + 1); - scene_state.publish(); - return true; - }); -} - -bool Plot_Core::refresh_manual_frame() { - if (impl_->mode != Frame_Control_Mode::Manual) - return false; - return std::get>(impl_->scene)->frame_control.refresh(); -} - -bool Plot_Core::render_prepared_frame() { - return with_scene(impl_->scene, [](auto& scene) { - auto render_frame = scene.frame_control.acquire_renderer(); - if (!render_frame) - return false; - scene.render(*render_frame); - scene.wait_for_render(); - return true; - }); -} - -bool Plot_Core::discard_pending_frame() { - return with_scene(impl_->scene, [](auto& scene) { - using Scene = std::remove_reference_t; - if constexpr (std::is_same_v) - return false; - else - return scene.frame_control.discard_pending_frame(); - }); -} - -bool Plot_Core::render_frame(bool force) { - if (!view_active() && !force) - return false; - const Size viewport = viewport_size(); - if (viewport.empty()) - return false; - if (!impl_->dirty.exchange(false, std::memory_order_acq_rel) && !force) - return false; - - const auto started = std::chrono::steady_clock::now(); - try { - if (!prepare_frame()) - return false; - if (impl_->mode == Frame_Control_Mode::Manual && !refresh_manual_frame()) - return false; - if (!render_prepared_frame()) - return false; - } catch (...) { - notify_model_dirty(); - throw; - } - const auto finished = std::chrono::steady_clock::now(); - const double duration_ms = std::chrono::duration(finished - started).count(); - - std::shared_ptr sink; - std::shared_ptr overlay; - { - std::lock_guard lock(impl_->mutex); - sink = impl_->presentation_sink.lock(); - overlay = impl_->performance; - } - if (overlay) - overlay->record_frame(duration_ms, diagnostics(), - with_scene(impl_->scene, [](const auto& scene) { - return scene.renderable_count(); - }), - viewport); - if (sink) - sink->request_present(full_rect(viewport)); - return true; -} - -void Plot_Core::with_frame(const std::function& consumer) { - if (!consumer) - return; - with_scene(impl_->scene, [&consumer](auto& scene) { - scene.with_final_color_cache([&consumer](const detail::Blend2D_Color_Cache& cache) { - consumer(cache.view()); - }); - }); -} - -void Plot_Core::activate_view() noexcept { - impl_->active.store(true, std::memory_order_release); - notify_model_dirty(); -} - -void Plot_Core::deactivate_view() noexcept { - impl_->active.store(false, std::memory_order_release); -} - -bool Plot_Core::view_active() const noexcept { - return impl_->active.load(std::memory_order_acquire); -} - -void Plot_Core::set_max_render_fps(double fps) { - if (!std::isfinite(fps) || fps <= 0.0) - throw std::invalid_argument("maximum render FPS must be finite and positive"); - if (impl_->mode != Frame_Control_Mode::Low_Latency) - throw std::logic_error("maximum render FPS is only available in low-latency mode"); - std::get>(impl_->scene)->frame_control.set_frequency_hz(fps); - notify_model_dirty(); -} - -void Plot_Core::clear_max_render_fps() { - if (impl_->mode != Frame_Control_Mode::Low_Latency) - throw std::logic_error("maximum render FPS is only available in low-latency mode"); - std::get>(impl_->scene)->frame_control.clear_frequency_limit(); - notify_model_dirty(); -} - -void Plot_Core::set_consumer_feedback(Frame_Consumer_Feedback feedback) { - if (impl_->mode != Frame_Control_Mode::Low_Latency) - throw std::logic_error("consumer feedback is only available in low-latency mode"); - std::get>(impl_->scene)->frame_control.set_consumer_feedback(feedback); -} - -void Plot_Core::clear_consumer_feedback() { - if (impl_->mode != Frame_Control_Mode::Low_Latency) - throw std::logic_error("consumer feedback is only available in low-latency mode"); - std::get>(impl_->scene)->frame_control.clear_consumer_feedback(); -} - -double Plot_Core::max_render_fps() const noexcept { - if (impl_->mode != Frame_Control_Mode::Low_Latency) - return std::numeric_limits::quiet_NaN(); - return std::get>(impl_->scene)->frame_control.state().frequency_hz; -} - -Low_Latency_Diagnostics Plot_Core::diagnostics() const { - if (impl_->mode == Frame_Control_Mode::Low_Latency) { - const auto state = std::get>(impl_->scene)->frame_control.state(); - return {view_active(), - {state.frequency_hz, state.completed_lifecycle_count, - state.next_refresh_interval_ns}}; - } - const auto observed = frame_observer_snapshot(); - return {view_active(), - {std::numeric_limits::quiet_NaN(), observed.consumed_frame_count, 0}}; -} - -Refresh_Control_Snapshot Plot_Core::refresh_feedback_snapshot() const { - return diagnostics().refresh; -} - -Frame_Control_Mode Plot_Core::frame_control_mode() const noexcept { - return impl_->mode; -} - -Frame_Observer_Snapshot Plot_Core::frame_observer_snapshot() const { - Frame_Observer_Snapshot snapshot; - { - std::lock_guard lock(impl_->observer->mutex); - snapshot.mode = impl_->mode; - snapshot.last_event = impl_->observer->observation_count == 0 ? - std::string("none") : - observer_event_name(impl_->mode, impl_->observer->last_event); - snapshot.limit_state = "not_applicable"; - snapshot.observation_count = impl_->observer->observation_count; - snapshot.produced_frame_count = impl_->observer->produced_frame_count; - snapshot.consumed_frame_count = impl_->observer->consumed_frame_count; - snapshot.dropped_frame_count = impl_->observer->dropped_frame_count; - snapshot.failed_operation_count = impl_->observer->failed_operation_count; - snapshot.pending_frame_count = impl_->observer->pending_frame_count; - snapshot.latest_sequence = impl_->observer->latest_sequence; - snapshot.paint_duration_ns = impl_->observer->paint_duration_ns; - snapshot.render_duration_ns = impl_->observer->render_duration_ns; - snapshot.paint_lease_wait_ns = impl_->observer->paint_lease_wait_ns; - snapshot.paint_state_wait_ns = impl_->observer->paint_state_wait_ns; - snapshot.publish_state_wait_ns = impl_->observer->publish_state_wait_ns; - snapshot.ready_wait_ns = impl_->observer->ready_wait_ns; - snapshot.frame_age_at_render_ns = impl_->observer->frame_age_at_render_ns; - snapshot.render_lease_wait_ns = impl_->observer->render_lease_wait_ns; - snapshot.render_state_wait_ns = impl_->observer->render_state_wait_ns; - snapshot.render_finish_state_wait_ns = impl_->observer->render_finish_state_wait_ns; - snapshot.queue_wait_ns = impl_->observer->queue_wait_ns; - } - if (impl_->mode == Frame_Control_Mode::Low_Latency) { - const auto state = std::get>(impl_->scene)->frame_control.state(); - snapshot.limit_state = limit_state_name(true, static_cast(state.limit_state)); - snapshot.frequency_hz = state.frequency_hz; - snapshot.paint_duration_ns = state.paint_duration_ns; - snapshot.render_duration_ns = state.render_duration_ns; - snapshot.target_interval_ns = state.target_interval_ns; - snapshot.frequency_limit_enabled = state.frequency_limit_enabled; - snapshot.consumer_feedback_enabled = state.consumer_feedback_enabled; - snapshot.bottleneck_duration_ns = state.bottleneck_duration_ns; - snapshot.consumer_sample_interval_ns = state.consumer_sample_interval_ns; - snapshot.consumer_smoothed_interval_ns = state.consumer_smoothed_interval_ns; - snapshot.consumer_variation_ns = state.consumer_variation_ns; - snapshot.consumer_safety_interval_ns = state.consumer_safety_interval_ns; - snapshot.consumer_interval_ns = state.consumer_interval_ns; - snapshot.next_refresh_interval_ns = state.next_refresh_interval_ns; - snapshot.end_to_end_ns = state.end_to_end_ns; - } - return snapshot; -} - -void Plot_Core::set_presentation_sink(std::weak_ptr sink) { - std::lock_guard lock(impl_->mutex); - impl_->presentation_sink = std::move(sink); -} - -std::shared_ptr Plot_Core::performance_overlay() const { - std::lock_guard lock(impl_->mutex); - return impl_->performance; -} - -void Plot_Core::set_performance_overlay(std::shared_ptr overlay) { - { - std::lock_guard lock(impl_->mutex); - impl_->performance = std::move(overlay); - } - notify_model_dirty(); -} - -::Scene_Base& Plot_Core::kernel_scene() const noexcept { - return with_scene(impl_->scene, [](auto& scene) -> ::Scene_Base& { return scene; }); -} - -void Plot_Core::set_renderable_cache(Renderable& renderable, Renderable_Cache_Mode mode) { - with_scene(impl_->scene, [&renderable, mode](auto& scene) { - scene.set_renderable_configuration( - renderable, {.cache_enabled = mode == Renderable_Cache_Mode::Local_Pixel}); - }); - notify_model_dirty(); -} - -} // namespace renderive diff --git a/render_2D/plot/Plot_Core.h b/render_2D/plot/Plot_Core.h deleted file mode 100644 index c10a5b4..0000000 --- a/render_2D/plot/Plot_Core.h +++ /dev/null @@ -1,150 +0,0 @@ -#pragma once - -#include "../base/Types.h" -#include "../event/Event.h" -#include - -#include -#include -#include -#include -#include - -class Scene_Base; - -namespace renderive { - -class Performance_Overlay; -struct Performance_Overlay_Options; -class Renderable; - -enum class Frame_Control_Mode : std::uint8_t { - Manual, - Low_Latency, - Playback -}; - -struct Refresh_Control_Snapshot { - double frequency_hz{}; - std::uint64_t frame_count{}; - std::uint64_t next_refresh_interval_ns{}; -}; - -struct Low_Latency_Diagnostics { - bool active{}; - Refresh_Control_Snapshot refresh; -}; - -struct Frame_Observer_Snapshot { - Frame_Control_Mode mode = Frame_Control_Mode::Low_Latency; - std::string last_event; - std::string limit_state; - double frequency_hz{}; - std::uint64_t observation_count{}; - std::uint64_t produced_frame_count{}; - std::uint64_t consumed_frame_count{}; - std::uint64_t dropped_frame_count{}; - std::uint64_t failed_operation_count{}; - std::uint64_t pending_frame_count{}; - std::uint64_t latest_sequence{}; - std::uint64_t paint_duration_ns{}; - std::uint64_t render_duration_ns{}; - std::uint64_t target_interval_ns{}; - bool frequency_limit_enabled{}; - bool consumer_feedback_enabled{}; - std::uint64_t bottleneck_duration_ns{}; - std::uint64_t consumer_sample_interval_ns{}; - std::uint64_t consumer_smoothed_interval_ns{}; - std::uint64_t consumer_variation_ns{}; - std::uint64_t consumer_safety_interval_ns{}; - std::uint64_t consumer_interval_ns{}; - std::uint64_t next_refresh_interval_ns{}; - std::uint64_t paint_lease_wait_ns{}; - std::uint64_t paint_state_wait_ns{}; - std::uint64_t publish_state_wait_ns{}; - std::uint64_t ready_wait_ns{}; - std::uint64_t frame_age_at_render_ns{}; - std::uint64_t render_lease_wait_ns{}; - std::uint64_t render_state_wait_ns{}; - std::uint64_t render_finish_state_wait_ns{}; - std::uint64_t queue_wait_ns{}; - std::uint64_t end_to_end_ns{}; -}; - -class Presentation_Sink { -public: - virtual ~Presentation_Sink() = default; - virtual void request_present(Rect dirty_rect) = 0; -}; - -class LIB_DECL Plot_Core { -public: - Plot_Core(); - explicit Plot_Core(Frame_Control_Mode mode); - ~Plot_Core(); - Plot_Core(const Plot_Core&) = delete; - Plot_Core& operator=(const Plot_Core&) = delete; - - void init(); - [[nodiscard]] std::shared_ptr root_renderable() const; - [[nodiscard]] std::shared_ptr create_renderable_node( - const std::shared_ptr& parent, - std::string object_name = {}); - - template - requires std::derived_from && std::constructible_from - std::shared_ptr make_renderable(const std::shared_ptr& parent, Args&&... args) { - auto renderable = renderive::make_shared(Memory_Domain::Renderable, *this, - std::forward(args)...); - attach_renderable(renderable, parent); - return renderable; - } - - void remove_renderable(const std::shared_ptr& renderable); - void set_background_color(Color color); - [[nodiscard]] Color background_color() const noexcept; - void set_viewport_size(Size size); - [[nodiscard]] Size viewport_size() const noexcept; - - void dispatch_event(const Event& event); - void notify_model_dirty() noexcept; - [[nodiscard]] bool prepare_frame(); - [[nodiscard]] bool refresh_manual_frame(); - [[nodiscard]] bool render_prepared_frame(); - [[nodiscard]] bool discard_pending_frame(); - [[nodiscard]] bool render_frame(bool force = false); - void with_frame(const std::function& consumer); - - void activate_view() noexcept; - void deactivate_view() noexcept; - [[nodiscard]] bool view_active() const noexcept; - - void set_max_render_fps(double fps); - void clear_max_render_fps(); - void set_consumer_feedback(Frame_Consumer_Feedback feedback); - void clear_consumer_feedback(); - [[nodiscard]] double max_render_fps() const noexcept; - [[nodiscard]] Low_Latency_Diagnostics diagnostics() const; - [[nodiscard]] Refresh_Control_Snapshot refresh_feedback_snapshot() const; - [[nodiscard]] Frame_Control_Mode frame_control_mode() const noexcept; - [[nodiscard]] Frame_Observer_Snapshot frame_observer_snapshot() const; - - void set_presentation_sink(std::weak_ptr sink); - [[nodiscard]] std::shared_ptr performance_overlay() const; - -private: - friend class Renderable; - friend std::shared_ptr attach_performance_overlay(Plot_Core&); - friend std::shared_ptr attach_performance_overlay( - Plot_Core&, const Performance_Overlay_Options&); - struct Impl; - std::unique_ptr impl_; - - [[nodiscard]] ::Scene_Base& kernel_scene() const noexcept; - void attach_renderable(const std::shared_ptr& renderable, - const std::shared_ptr& parent); - void set_renderable_cache(Renderable& renderable, Renderable_Cache_Mode mode); - void set_performance_overlay(std::shared_ptr overlay); -}; - -} // namespace renderive diff --git a/render_2D/plottable/Afterglow.cpp b/render_2D/plottable/Afterglow.cpp index 828fa6b..af7e3a5 100644 --- a/render_2D/plottable/Afterglow.cpp +++ b/render_2D/plottable/Afterglow.cpp @@ -31,8 +31,8 @@ struct Afterglow_Control::Impl { Adaptive_Render_Partitioner partitioner; Afterglow_Prepare_Buffer prepare_buffer; }; -Afterglow_Control::Afterglow_Control(Plot_Core& plot, const Afterglow_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) - : Plottable_State(plot, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} +Afterglow_Control::Afterglow_Control(::Scene_Base& scene, const Afterglow_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) + : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} Afterglow_Control::~Afterglow_Control() = default; std::size_t Afterglow_Control::history_count() const { return impl_->history.size(); diff --git a/render_2D/plottable/Afterglow.h b/render_2D/plottable/Afterglow.h index 5a02bb3..fd1991a 100644 --- a/render_2D/plottable/Afterglow.h +++ b/render_2D/plottable/Afterglow.h @@ -21,7 +21,7 @@ namespace detail { class LIB_DECL Afterglow_Control : public Plottable_State, public ::Render_Frame_Completion { public: - Afterglow_Control(Plot_Core& plot, const Afterglow_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis); + Afterglow_Control(::Scene_Base& scene, const Afterglow_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis); ~Afterglow_Control() override; [[nodiscard]] std::size_t history_count() const; [[nodiscard]] std::size_t latest_spectrum_point_count() const; diff --git a/render_2D/plottable/Constellation_Diagram.cpp b/render_2D/plottable/Constellation_Diagram.cpp index 9ad17cf..7b55946 100644 --- a/render_2D/plottable/Constellation_Diagram.cpp +++ b/render_2D/plottable/Constellation_Diagram.cpp @@ -28,8 +28,8 @@ struct Constellation_Diagram_Control::Impl { Constellation_History points; Prepare_Buffer prepare_buffer; }; -Constellation_Diagram_Control::Constellation_Diagram_Control(Plot_Core& plot, const Constellation_Diagram_Properties& properties, std::shared_ptr i_axis, std::shared_ptr q_axis) - : Plottable_State(plot, properties), impl_(std::make_unique(*this, std::move(i_axis), std::move(q_axis))) {} +Constellation_Diagram_Control::Constellation_Diagram_Control(::Scene_Base& scene, const Constellation_Diagram_Properties& properties, std::shared_ptr i_axis, std::shared_ptr q_axis) + : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(i_axis), std::move(q_axis))) {} Constellation_Diagram_Control::~Constellation_Diagram_Control() = default; void Constellation_Diagram_Control::append_point(PointF point) { const auto now = std::chrono::steady_clock::now(); diff --git a/render_2D/plottable/Constellation_Diagram.h b/render_2D/plottable/Constellation_Diagram.h index c2bb19e..0127ef7 100644 --- a/render_2D/plottable/Constellation_Diagram.h +++ b/render_2D/plottable/Constellation_Diagram.h @@ -19,7 +19,7 @@ struct Constellation_Diagram_Properties { namespace detail { class LIB_DECL Constellation_Diagram_Control : public Plottable_State { public: - Constellation_Diagram_Control(Plot_Core& plot, const Constellation_Diagram_Properties& properties, std::shared_ptr i_axis, std::shared_ptr q_axis); + Constellation_Diagram_Control(::Scene_Base& scene, const Constellation_Diagram_Properties& properties, std::shared_ptr i_axis, std::shared_ptr q_axis); ~Constellation_Diagram_Control() override; void append_point(PointF point); [[nodiscard]] std::size_t point_count() const; diff --git a/render_2D/plottable/Frequency_Trace.cpp b/render_2D/plottable/Frequency_Trace.cpp index 8dad1d2..ade9317 100644 --- a/render_2D/plottable/Frequency_Trace.cpp +++ b/render_2D/plottable/Frequency_Trace.cpp @@ -21,8 +21,8 @@ struct Frequency_Trace_Control::Impl { Frequency_Trace_History samples; Prepare_Buffer prepare_buffer; }; -Frequency_Trace_Control::Frequency_Trace_Control(Plot_Core& plot, const Frequency_Trace_Properties& properties, std::shared_ptr time_axis, std::shared_ptr value_axis) - : Plottable_State(plot, properties), impl_(std::make_unique(*this, std::move(time_axis), std::move(value_axis))) {} +Frequency_Trace_Control::Frequency_Trace_Control(::Scene_Base& scene, const Frequency_Trace_Properties& properties, std::shared_ptr time_axis, std::shared_ptr value_axis) + : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(time_axis), std::move(value_axis))) {} Frequency_Trace_Control::~Frequency_Trace_Control() = default; void Frequency_Trace_Control::append_sample(int tick, double value) { const int limit = std::max(2, impl_->time_axis->get<&Time_Axis_Properties::visible_count>()); diff --git a/render_2D/plottable/Frequency_Trace.h b/render_2D/plottable/Frequency_Trace.h index e59a833..ee09186 100644 --- a/render_2D/plottable/Frequency_Trace.h +++ b/render_2D/plottable/Frequency_Trace.h @@ -8,7 +8,7 @@ struct Frequency_Trace_Properties { namespace detail { class LIB_DECL Frequency_Trace_Control : public Plottable_State { public: - Frequency_Trace_Control(Plot_Core& plot, const Frequency_Trace_Properties& properties, std::shared_ptr time_axis, std::shared_ptr value_axis); + Frequency_Trace_Control(::Scene_Base& scene, const Frequency_Trace_Properties& properties, std::shared_ptr time_axis, std::shared_ptr value_axis); ~Frequency_Trace_Control() override; void append_sample(int tick, double value); void append_sample(Time_Of_Day time, double value); diff --git a/render_2D/plottable/Performance_Overlay.cpp b/render_2D/plottable/Performance_Overlay.cpp index b215554..2d6fdba 100644 --- a/render_2D/plottable/Performance_Overlay.cpp +++ b/render_2D/plottable/Performance_Overlay.cpp @@ -150,42 +150,4 @@ void Performance_Overlay::record_frame(double render_duration_ms, } } -std::shared_ptr attach_performance_overlay(Plot_Core& plot) { - if (auto existing = plot.performance_overlay()) - return existing; - auto overlay = std::make_shared(); - plot.set_performance_overlay(overlay); - return overlay; -} - -std::shared_ptr attach_performance_overlay( - Plot_Core& plot, const Performance_Overlay_Options& options) { - if (auto existing = plot.performance_overlay()) { - existing->set_options(options); - plot.notify_model_dirty(); - return existing; - } - auto overlay = std::make_shared(options); - plot.set_performance_overlay(overlay); - return overlay; -} - -void set_performance_plot_name(Plot_Core& plot, std::string name) { - auto overlay = plot.performance_overlay(); - if (!overlay) - overlay = attach_performance_overlay(plot); - overlay->set_plot_name(std::move(name)); - plot.notify_model_dirty(); -} - -void set_performance_overlay_enabled(Plot_Core& plot, bool enabled) { - auto overlay = plot.performance_overlay(); - if (!overlay && !enabled) - return; - if (!overlay) - overlay = attach_performance_overlay(plot); - overlay->set_enabled(enabled); - plot.notify_model_dirty(); -} - } // namespace renderive diff --git a/render_2D/plottable/Performance_Overlay.h b/render_2D/plottable/Performance_Overlay.h index 63945e5..b589079 100644 --- a/render_2D/plottable/Performance_Overlay.h +++ b/render_2D/plottable/Performance_Overlay.h @@ -1,7 +1,7 @@ #pragma once #include "../base/Types.h" -#include "../plot/Plot_Core.h" +#include "../scene/Scene.h" #include #include @@ -51,12 +51,14 @@ public: [[nodiscard]] std::shared_ptr display_snapshot() const; private: - friend class Plot_Core; - friend std::shared_ptr attach_performance_overlay(Plot_Core&); - friend std::shared_ptr attach_performance_overlay( - Plot_Core&, const Performance_Overlay_Options&); - friend void set_performance_plot_name(Plot_Core&, std::string); - friend void set_performance_overlay_enabled(Plot_Core&, bool); + template + friend std::shared_ptr attach_performance_overlay(Basic_Scene2D&); + template + friend std::shared_ptr attach_performance_overlay(Basic_Scene2D&, const Performance_Overlay_Options&); + template + friend void set_performance_plot_name(Basic_Scene2D&, std::string); + template + friend void set_performance_overlay_enabled(Basic_Scene2D&, bool); struct Impl; std::unique_ptr impl_; @@ -64,16 +66,49 @@ private: void set_enabled(bool enabled); void set_plot_name(std::string name); void set_options(Performance_Overlay_Options options); + friend void detail::record_performance_frame(Performance_Overlay&, double, const Low_Latency_Diagnostics&, std::size_t, Size); void record_frame(double render_duration_ms, const Low_Latency_Diagnostics& diagnostics, std::size_t renderable_count, Size viewport); }; -LIB_DECL std::shared_ptr attach_performance_overlay(Plot_Core& plot); -LIB_DECL std::shared_ptr attach_performance_overlay( - Plot_Core& plot, const Performance_Overlay_Options& options); -LIB_DECL void set_performance_plot_name(Plot_Core& plot, std::string name); -LIB_DECL void set_performance_overlay_enabled(Plot_Core& plot, bool enabled); +template +std::shared_ptr attach_performance_overlay(Basic_Scene2D& scene) { + if (auto existing = scene.performance_overlay()) + return existing; + auto overlay = std::make_shared(); + scene.set_performance_overlay(overlay); + return overlay; +} +template +std::shared_ptr attach_performance_overlay(Basic_Scene2D& scene, const Performance_Overlay_Options& options) { + if (auto existing = scene.performance_overlay()) { + existing->set_options(options); + scene.notify_model_dirty(); + return existing; + } + auto overlay = std::make_shared(options); + scene.set_performance_overlay(overlay); + return overlay; +} +template +void set_performance_plot_name(Basic_Scene2D& scene, std::string name) { + auto overlay = scene.performance_overlay(); + if (!overlay) + overlay = attach_performance_overlay(scene); + overlay->set_plot_name(std::move(name)); + scene.notify_model_dirty(); +} +template +void set_performance_overlay_enabled(Basic_Scene2D& scene, bool enabled) { + auto overlay = scene.performance_overlay(); + if (!overlay && !enabled) + return; + if (!overlay) + overlay = attach_performance_overlay(scene); + overlay->set_enabled(enabled); + scene.notify_model_dirty(); +} } // namespace renderive diff --git a/render_2D/plottable/Plottable.h b/render_2D/plottable/Plottable.h index f0df455..ca6aaa2 100644 --- a/render_2D/plottable/Plottable.h +++ b/render_2D/plottable/Plottable.h @@ -60,11 +60,11 @@ public: using State_Observer = Observer_State; using Base = Double_State_Strategy; - Plottable_State(Plot_Core& plot, const Properties& properties) + Plottable_State(::Scene_Base& scene, const Properties& properties) : Base(properties, With_Observer{ State_Observer(Renderable_State_Observer(this))}, - plot) {} + scene) {} template Value> Plottable_State& set(Value&& value) { Base::template set(std::forward(value)); diff --git a/render_2D/plottable/Selection_Rectangle_Overlay.cpp b/render_2D/plottable/Selection_Rectangle_Overlay.cpp index bb5e017..4913c62 100644 --- a/render_2D/plottable/Selection_Rectangle_Overlay.cpp +++ b/render_2D/plottable/Selection_Rectangle_Overlay.cpp @@ -37,8 +37,8 @@ struct Selection_Rectangle_Overlay_Control::Impl { Selection_Interaction_State interaction; Prepare_Buffer prepare_buffer; }; -Selection_Rectangle_Overlay_Control::Selection_Rectangle_Overlay_Control(Plot_Core& plot, const Selection_Rectangle_Overlay_Properties& properties, std::shared_ptr horizontal_axis, std::shared_ptr vertical_axis) - : Plottable_State(plot, properties), impl_(std::make_unique(*this, std::move(horizontal_axis), std::move(vertical_axis))) {} +Selection_Rectangle_Overlay_Control::Selection_Rectangle_Overlay_Control(::Scene_Base& scene, const Selection_Rectangle_Overlay_Properties& properties, std::shared_ptr horizontal_axis, std::shared_ptr vertical_axis) + : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(horizontal_axis), std::move(vertical_axis))) {} Selection_Rectangle_Overlay_Control::~Selection_Rectangle_Overlay_Control() = default; std::vector Selection_Rectangle_Overlay_Control::selected_regions() const { return impl_->regions.snapshot(); diff --git a/render_2D/plottable/Selection_Rectangle_Overlay.h b/render_2D/plottable/Selection_Rectangle_Overlay.h index b527c88..83fecbe 100644 --- a/render_2D/plottable/Selection_Rectangle_Overlay.h +++ b/render_2D/plottable/Selection_Rectangle_Overlay.h @@ -11,7 +11,7 @@ struct Selection_Rectangle_Overlay_Properties { namespace detail { class LIB_DECL Selection_Rectangle_Overlay_Control : public Plottable_State, public Paint_Overlay, public Event_Handler { public: - Selection_Rectangle_Overlay_Control(Plot_Core& plot, const Selection_Rectangle_Overlay_Properties& properties, std::shared_ptr horizontal_axis, std::shared_ptr vertical_axis); + Selection_Rectangle_Overlay_Control(::Scene_Base& scene, const Selection_Rectangle_Overlay_Properties& properties, std::shared_ptr horizontal_axis, std::shared_ptr vertical_axis); ~Selection_Rectangle_Overlay_Control() override; [[nodiscard]] std::vector selected_regions() const; void clear_selected_regions(); diff --git a/render_2D/plottable/Spectrum.cpp b/render_2D/plottable/Spectrum.cpp index 48990d1..fe5aa4b 100644 --- a/render_2D/plottable/Spectrum.cpp +++ b/render_2D/plottable/Spectrum.cpp @@ -147,8 +147,8 @@ struct Spectrum_Control::Impl { Adaptive_Render_Partitioner partitioner; Spectrum_Prepare_Buffer prepare_buffer; }; -Spectrum_Control::Spectrum_Control(Plot_Core& plot, const Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) - : Plottable_State(plot, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} +Spectrum_Control::Spectrum_Control(::Scene_Base& scene, const Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) + : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} Spectrum_Control::~Spectrum_Control() = default; void Spectrum_Control::update_samples(std::span values) { if(get<&Spectrum_Properties::frequency_point_size>() <= 0) diff --git a/render_2D/plottable/Spectrum.h b/render_2D/plottable/Spectrum.h index 83d7556..aa8935f 100644 --- a/render_2D/plottable/Spectrum.h +++ b/render_2D/plottable/Spectrum.h @@ -38,7 +38,7 @@ class LIB_DECL Spectrum_Control : public Plottable_State, public Event_Handler, public ::Render_Frame_Completion { public: - Spectrum_Control(Plot_Core& plot, const Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis); + Spectrum_Control(::Scene_Base& scene, const Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis); ~Spectrum_Control() override; void update_samples(std::span values); void update_samples(std::pmr::vector&& values); diff --git a/render_2D/plottable/Sweep_Spectrum.cpp b/render_2D/plottable/Sweep_Spectrum.cpp index 3d8a9ac..bdef379 100644 --- a/render_2D/plottable/Sweep_Spectrum.cpp +++ b/render_2D/plottable/Sweep_Spectrum.cpp @@ -30,8 +30,8 @@ struct Sweep_Spectrum_Control::Impl { Sweep_Spectrum_History blocks; Prepare_Buffer prepare_buffer; }; -Sweep_Spectrum_Control::Sweep_Spectrum_Control(Plot_Core& plot, const Sweep_Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) - : Plottable_State(plot, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} +Sweep_Spectrum_Control::Sweep_Spectrum_Control(::Scene_Base& scene, const Sweep_Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) + : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} Sweep_Spectrum_Control::~Sweep_Spectrum_Control() = default; void Sweep_Spectrum_Control::append_block(std::span values) { if(get<&Sweep_Spectrum_Properties::bins_per_block>() <= 0) diff --git a/render_2D/plottable/Sweep_Spectrum.h b/render_2D/plottable/Sweep_Spectrum.h index 9a43796..01a2584 100644 --- a/render_2D/plottable/Sweep_Spectrum.h +++ b/render_2D/plottable/Sweep_Spectrum.h @@ -16,7 +16,7 @@ struct Sweep_Spectrum_Properties { namespace detail { class LIB_DECL Sweep_Spectrum_Control : public Plottable_State { public: - Sweep_Spectrum_Control(Plot_Core& plot, const Sweep_Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis); + Sweep_Spectrum_Control(::Scene_Base& scene, const Sweep_Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis); ~Sweep_Spectrum_Control() override; void append_block(std::span values); void append_block(std::pmr::vector&& values); diff --git a/render_2D/plottable/Waterfall.cpp b/render_2D/plottable/Waterfall.cpp index 2cb3e93..242c0c2 100644 --- a/render_2D/plottable/Waterfall.cpp +++ b/render_2D/plottable/Waterfall.cpp @@ -66,8 +66,8 @@ struct Waterfall_Control::Impl { Adaptive_Render_Partitioner partitioner; Waterfall_Prepare_Buffer prepare_buffer; }; -Waterfall_Control::Waterfall_Control(Plot_Core& plot, const Waterfall_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr time_axis) - : Plottable_State(plot, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(time_axis))) {} +Waterfall_Control::Waterfall_Control(::Scene_Base& scene, const Waterfall_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr time_axis) + : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(time_axis))) {} Waterfall_Control::~Waterfall_Control() = default; void Waterfall_Control::append_row(int tick, std::span values) { const std::size_t limit = static_cast( diff --git a/render_2D/plottable/Waterfall.h b/render_2D/plottable/Waterfall.h index 5e99ee2..599e5f3 100644 --- a/render_2D/plottable/Waterfall.h +++ b/render_2D/plottable/Waterfall.h @@ -22,7 +22,7 @@ class LIB_DECL Waterfall_Control : public Plottable_State, public Event_Handler, public ::Render_Frame_Completion { public: - Waterfall_Control(Plot_Core& plot, const Waterfall_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr time_axis); + Waterfall_Control(::Scene_Base& scene, const Waterfall_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr time_axis); ~Waterfall_Control() override; void append_row(int tick, std::span values); void append_row(int tick, std::pmr::vector&& values); diff --git a/render_2D/renderable/Renderable.cpp b/render_2D/renderable/Renderable.cpp index 36c7ea1..39ea197 100644 --- a/render_2D/renderable/Renderable.cpp +++ b/render_2D/renderable/Renderable.cpp @@ -1,6 +1,6 @@ #include "Renderable.h" +#include -#include "../plot/Plot_Core.h" #include "../render/Blend2D_Cache.h" #include @@ -24,11 +24,23 @@ void Renderable_Observer::observe_state(Renderable_Observer_Event event, observation_.publish_count = publish_count; } -Renderable::Renderable(Plot_Core& plot, bool cache_enabled) - : ::Renderable_Base(plot.kernel_scene(), {.cache_enabled = cache_enabled}), plot_(plot) {} +Renderable::Renderable(::Scene_Base& scene, bool cache_enabled) + : ::Renderable_Base(scene, {.cache_enabled = cache_enabled}) {} Renderable::~Renderable() = default; +void attach_renderable_child(::Scene_Base& scene, const std::shared_ptr& renderable, const std::shared_ptr& parent) { + scene.attach_renderable(renderable); + try { + scene.set_display_parent(*renderable, parent.get()); + scene.set_dependency_parent(*renderable, parent.get()); + } catch (...) { + scene.detach_renderable(*renderable); + throw; + } + scene.notify_model_dirty(); +} + Renderable_Cache_Mode Renderable::get_cache_mode() const noexcept { return configuration().cache_enabled ? Renderable_Cache_Mode::Local_Pixel @@ -36,7 +48,8 @@ Renderable_Cache_Mode Renderable::get_cache_mode() const noexcept { } void Renderable::set_cache_mode(Renderable_Cache_Mode mode) { - plot_.set_renderable_cache(*this, mode); + scene().set_renderable_configuration(*this, {.cache_enabled = mode == Renderable_Cache_Mode::Local_Pixel}); + scene().notify_model_dirty(); } std::string Renderable::object_name() const { @@ -109,7 +122,7 @@ Renderable_Graph_Builder::Task Renderable::add_paint_task( auto* cache = dynamic_cast(context.color_cache); if (!cache) return; - detail::Painter painter(*cache, viewport_size()); + detail::Painter painter(*cache, {context.viewport.width, context.viewport.height}); if (painter) function(painter, context); }); @@ -117,21 +130,17 @@ Renderable_Graph_Builder::Task Renderable::add_paint_task( void Renderable::changed() noexcept { invalidate_prepare(); - plot_.notify_model_dirty(); + scene().notify_model_dirty(); } void Renderable::paint_changed() noexcept { invalidate_paint(); - plot_.notify_model_dirty(); + scene().notify_model_dirty(); } void Renderable::render_graph_changed() noexcept { rebuild_render_graph(); - plot_.notify_model_dirty(); -} - -Size Renderable::viewport_size() const noexcept { - return plot_.viewport_size(); + scene().notify_model_dirty(); } } // namespace renderive diff --git a/render_2D/renderable/Renderable.h b/render_2D/renderable/Renderable.h index dd7a7ac..645e0d0 100644 --- a/render_2D/renderable/Renderable.h +++ b/render_2D/renderable/Renderable.h @@ -8,13 +8,14 @@ #include #include #include +#include #include #include #include namespace renderive { -class Plot_Core; +class Renderable; namespace detail { class Painter; class Renderable_State_Observer; @@ -50,12 +51,13 @@ private: Renderable_Observation observation_; }; +LIB_DECL void attach_renderable_child(::Scene_Base& scene, const std::shared_ptr& renderable, const std::shared_ptr& parent); + class LIB_DECL Renderable : public ::Renderable_Base { public: - explicit Renderable(Plot_Core& plot, bool cache_enabled = true); + explicit Renderable(::Scene_Base& scene, bool cache_enabled = true); ~Renderable() override; - [[nodiscard]] Plot_Core& plot() const noexcept { return plot_; } [[nodiscard]] Renderable_Cache_Mode get_cache_mode() const noexcept; void set_cache_mode(Renderable_Cache_Mode mode); @@ -88,14 +90,12 @@ protected: void changed() noexcept; void paint_changed() noexcept; void render_graph_changed() noexcept; - [[nodiscard]] Size viewport_size() const noexcept; private: void observe_state(Renderable_Observer_Event event, std::uint64_t time_ns, std::uint64_t cache_update_count, std::uint64_t publish_count) noexcept; friend class detail::Renderable_State_Observer; - Plot_Core& plot_; mutable std::mutex metadata_mutex_; std::string object_name_; std::atomic visible_{true}; diff --git a/render_2D/renderable/Renderable_Builder.h b/render_2D/renderable/Renderable_Builder.h index b608fe6..d1e6635 100644 --- a/render_2D/renderable/Renderable_Builder.h +++ b/render_2D/renderable/Renderable_Builder.h @@ -1,8 +1,8 @@ #pragma once #include "Renderable.h" -#include "../plot/Plot_Core.h" #include #include +#include #include #include #include @@ -33,15 +33,16 @@ public: return properties; } template - requires std::derived_from && std::constructible_from + requires std::derived_from && std::constructible_from std::shared_ptr build(const std::shared_ptr& parent, Args&&... args) const { - if(!parent || !(valid_argument(args) && ...)) + if (!parent || !(valid_argument(args) && ...)) return {}; validator(properties); if constexpr (requires(Product& product) { attach_renderable_dependencies(product, args...); } && (std::copy_constructible> && ...)) { auto dependency_args = std::make_tuple(args...); - auto result = parent->plot().make_renderable(parent, properties, std::forward(args)...); + auto result = parent->scene().template make_renderable(properties, std::forward(args)...); + attach_renderable_child(parent->scene(), result, parent); try { std::apply([&result](const auto&... values) { attach_renderable_dependencies(*result, values...); @@ -52,7 +53,9 @@ public: } return result; } - return parent->plot().make_renderable(parent, properties, std::forward(args)...); + auto result = parent->scene().template make_renderable(properties, std::forward(args)...); + attach_renderable_child(parent->scene(), result, parent); + return result; } private: template diff --git a/render_2D/scene/Scene.cpp b/render_2D/scene/Scene.cpp new file mode 100644 index 0000000..2abb40b --- /dev/null +++ b/render_2D/scene/Scene.cpp @@ -0,0 +1,20 @@ +#include "Scene.h" +#include "../plottable/Performance_Overlay.h" +#include "../renderable/Renderable.h" +namespace renderive::detail { +namespace { +class Renderable_Group final : public Renderable { +public: + using Renderable::Renderable; +private: + void prepare_frame(const Render_State_View&, const Scene_Render_Context&) override {} + void paint(Painter&, const Scene_Render_Context&) override {} +}; +} +std::shared_ptr make_renderable_group(::Scene_Base& scene, bool cache_enabled) { + return scene.make_renderable(cache_enabled); +} +void record_performance_frame(Performance_Overlay& overlay, double duration_ms, const Low_Latency_Diagnostics& diagnostics, std::size_t renderable_count, Size viewport) { + overlay.record_frame(duration_ms, diagnostics, renderable_count, viewport); +} +} diff --git a/render_2D/scene/Scene.h b/render_2D/scene/Scene.h new file mode 100644 index 0000000..a29ef0b --- /dev/null +++ b/render_2D/scene/Scene.h @@ -0,0 +1,509 @@ +#pragma once +#include "../base/Types.h" +#include "../event/Event.h" +#include "../render/Blend2D_Cache.h" +#include "../renderable/Renderable.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace renderive { +class Performance_Overlay; +class Renderable; +class Event_Handler; +enum class Frame_Control_Mode : std::uint8_t { + Manual, + Low_Latency, + Playback +}; +struct Refresh_Control_Snapshot { + double frequency_hz{}; + std::uint64_t frame_count{}; + std::uint64_t next_refresh_interval_ns{}; +}; +struct Low_Latency_Diagnostics { + bool active{}; + Refresh_Control_Snapshot refresh; +}; +struct Frame_Observer_Snapshot { + Frame_Control_Mode mode = Frame_Control_Mode::Low_Latency; + std::string last_event; + std::string limit_state; + double frequency_hz{}; + std::uint64_t observation_count{}; + std::uint64_t produced_frame_count{}; + std::uint64_t consumed_frame_count{}; + std::uint64_t dropped_frame_count{}; + std::uint64_t failed_operation_count{}; + std::uint64_t pending_frame_count{}; + std::uint64_t latest_sequence{}; + std::uint64_t paint_duration_ns{}; + std::uint64_t render_duration_ns{}; + std::uint64_t target_interval_ns{}; + bool frequency_limit_enabled{}; + bool consumer_feedback_enabled{}; + std::uint64_t bottleneck_duration_ns{}; + std::uint64_t consumer_sample_interval_ns{}; + std::uint64_t consumer_smoothed_interval_ns{}; + std::uint64_t consumer_variation_ns{}; + std::uint64_t consumer_safety_interval_ns{}; + std::uint64_t consumer_interval_ns{}; + std::uint64_t next_refresh_interval_ns{}; + std::uint64_t paint_lease_wait_ns{}; + std::uint64_t paint_state_wait_ns{}; + std::uint64_t publish_state_wait_ns{}; + std::uint64_t ready_wait_ns{}; + std::uint64_t frame_age_at_render_ns{}; + std::uint64_t render_lease_wait_ns{}; + std::uint64_t render_state_wait_ns{}; + std::uint64_t render_finish_state_wait_ns{}; + std::uint64_t queue_wait_ns{}; + std::uint64_t end_to_end_ns{}; +}; +class Presentation_Sink { +public: + virtual ~Presentation_Sink() = default; + virtual void request_present(Rect dirty_rect) = 0; +}; +struct Scene_State : Scene2D_State { + Color background = Color::black(); +}; +namespace detail { +struct Frame_Observer_Data { + mutable std::mutex mutex; + std::uint32_t last_event{}; + std::uint64_t observation_count{}; + std::uint64_t produced_frame_count{}; + std::uint64_t consumed_frame_count{}; + std::uint64_t dropped_frame_count{}; + std::uint64_t failed_operation_count{}; + std::uint64_t pending_frame_count{}; + std::uint64_t latest_sequence{}; + std::uint64_t paint_duration_ns{}; + std::uint64_t render_duration_ns{}; + std::uint64_t paint_lease_wait_ns{}; + std::uint64_t paint_state_wait_ns{}; + std::uint64_t publish_state_wait_ns{}; + std::uint64_t ready_wait_ns{}; + std::uint64_t frame_age_at_render_ns{}; + std::uint64_t render_lease_wait_ns{}; + std::uint64_t render_state_wait_ns{}; + std::uint64_t render_finish_state_wait_ns{}; + std::uint64_t queue_wait_ns{}; +}; +struct Frame_Observer { + static constexpr bool enabled = true; + std::shared_ptr data; + template + void observe(const Observation& observation) noexcept { + std::lock_guard lock(data->mutex); + data->last_event = static_cast(observation.event); + ++data->observation_count; + if (observation.statistics.sequence != 0) + data->latest_sequence = observation.statistics.sequence; + if constexpr (requires { observation.statistics.timing.paint_duration_ns; }) { + if (observation.statistics.sequence != 0) { + const auto& timing = observation.statistics.timing; + data->paint_lease_wait_ns = timing.paint_lease_wait_ns; + data->paint_state_wait_ns = timing.paint_state_wait_ns; + data->publish_state_wait_ns = timing.publish_state_wait_ns; + data->ready_wait_ns = timing.ready_wait_ns; + data->frame_age_at_render_ns = timing.frame_age_at_render_ns; + data->render_lease_wait_ns = timing.render_lease_wait_ns; + data->render_state_wait_ns = timing.render_state_wait_ns; + data->render_finish_state_wait_ns = timing.render_finish_state_wait_ns; + } + } else if (observation.statistics.sequence != 0) { + data->paint_duration_ns = observation.statistics.paint_duration_ns; + data->render_duration_ns = observation.statistics.render_duration_ns; + } + if constexpr (requires { observation.statistics.queue_wait_ns; }) { + if (observation.statistics.sequence != 0) + data->queue_wait_ns = observation.statistics.queue_wait_ns; + } + if constexpr (requires { observation.state.prepared_frame_count; }) { + data->produced_frame_count = observation.state.prepared_frame_count; + data->consumed_frame_count = observation.state.render_count; + data->dropped_frame_count = observation.state.replaced_prepared_frame_count + observation.state.discarded_prepared_frame_count; + data->failed_operation_count = observation.state.failed_refresh_count; + data->pending_frame_count = observation.state.pending_frame ? 1U : 0U; + } else if constexpr (requires { observation.state.limit_state; }) { + data->produced_frame_count = observation.statistics.counters.published_frame_count; + data->consumed_frame_count = observation.state.completed_lifecycle_count; + data->dropped_frame_count = observation.statistics.counters.abandoned_frame_count + observation.statistics.counters.manually_discarded_frame_count; + data->failed_operation_count = observation.statistics.counters.swap_failure_count; + const auto retired = observation.state.completed_lifecycle_count + observation.statistics.counters.abandoned_frame_count + observation.statistics.counters.manually_discarded_frame_count; + data->pending_frame_count = observation.statistics.counters.published_frame_count > retired ? 1U : 0U; + } else { + data->produced_frame_count = observation.state.enqueued_frame_count; + data->consumed_frame_count = observation.state.rendered_frame_count; + data->failed_operation_count = observation.state.empty_acquire_count; + data->pending_frame_count = observation.state.pending_frame_count; + } + } +}; +using Frame_Observer_State = ::Observer_State; +using Manual_Frame_Control = ::Manual_Refresh_Strategy<::Scene2D_Frame_Data, std::mutex, Frame_Observer_State>; +using Low_Latency_Frame_Control = ::Low_Latency_Strategy<::Scene2D_Frame_Data, std::mutex, Frame_Observer_State>; +using Playback_Frame_Control = ::Flow_Refresh_Strategy<::Scene2D_Frame_Data, std::mutex, Frame_Observer_State>; +template +concept Scene_Frame_Control = std::same_as || std::same_as || std::same_as; +template +constexpr Frame_Control_Mode frame_control_mode() noexcept { + if constexpr (std::same_as) + return Frame_Control_Mode::Manual; + if constexpr (std::same_as) + return Frame_Control_Mode::Low_Latency; + return Frame_Control_Mode::Playback; +} +LIB_DECL std::shared_ptr make_renderable_group(::Scene_Base& scene, bool cache_enabled); +LIB_DECL void record_performance_frame(Performance_Overlay& overlay, double duration_ms, const Low_Latency_Diagnostics& diagnostics, std::size_t renderable_count, Size viewport); +inline std::string observer_event_name(Frame_Control_Mode mode, std::uint32_t event) { + if (mode == Frame_Control_Mode::Manual) { + static constexpr const char* names[]{"prepared", "prepared_replaced", "refresh_succeeded", "refresh_failed", "rendered", "real_time_data_updated", "manually_discarded"}; + return event < std::size(names) ? names[event] : "none"; + } + if (mode == Frame_Control_Mode::Playback) { + static constexpr const char* names[]{"enqueued", "dequeued", "queue_empty", "rendered", "real_time_data_updated"}; + return event < std::size(names) ? names[event] : "none"; + } + static constexpr const char* names[]{"published", "abandoned", "manually_discarded", "swap_failed", "rendered", "lifecycle_completed", "real_time_data_updated"}; + return event < std::size(names) ? names[event] : "none"; +} +inline std::string limit_state_name(std::uint32_t state) { + static constexpr const char* names[]{"frequency_limited", "paint_limited", "render_limited", "consumer_limited", "unlimited"}; + return state < std::size(names) ? names[state] : "unknown"; +} +inline Rect full_rect(Size size) { + return {0, 0, std::max(0, size.width), std::max(0, size.height)}; +} +} +template +class Basic_Scene2D final : public ::Scene2D_Context { + using Kernel_Scene = ::Scene2D_Context; + using Scene_State_Strategy = typename Kernel_Scene::Scene_State_Strategy; + static constexpr Frame_Control_Mode Mode = detail::frame_control_mode(); +public: + Basic_Scene2D() : Basic_Scene2D(std::make_shared()) {} + ~Basic_Scene2D() override = default; + Basic_Scene2D(const Basic_Scene2D&) = delete; + Basic_Scene2D& operator=(const Basic_Scene2D&) = delete; + void init() { + std::lock_guard lock(initialization_mutex_); + if (root_renderable()) + return; + auto root = detail::make_renderable_group(*this, true); + root->set_object_name("root"); + this->attach_renderable(root); + this->notify_model_dirty(); + } + [[nodiscard]] std::shared_ptr root_renderable() const { + const auto topology = this->topology_snapshot(); + for (const auto& relationship : topology.display) { + if (relationship.parent) + continue; + auto base = std::const_pointer_cast<::Renderable_Base>(relationship.child); + if (auto renderable = std::dynamic_pointer_cast(base)) + return renderable; + } + return {}; + } + [[nodiscard]] std::shared_ptr create_renderable_node(const std::shared_ptr& parent, std::string object_name = {}) { + auto renderable = detail::make_renderable_group(*this, true); + renderable->set_object_name(std::move(object_name)); + attach_renderable_node(renderable, parent); + return renderable; + } + template + requires std::derived_from && std::constructible_from + std::shared_ptr make_renderable(const std::shared_ptr& parent, Args&&... args) { + auto renderable = Kernel_Scene::template make_renderable(std::forward(args)...); + attach_renderable_node(renderable, parent); + return renderable; + } + void remove_renderable(const std::shared_ptr& renderable) { + if (!renderable || renderable == root_renderable()) + return; + this->detach_renderable(*renderable); + this->notify_model_dirty(); + } + void set_background_color(Color color) { + if (background_color() == color) + return; + static_cast(*this).template set<&Scene_State::background>(color); + this->notify_model_dirty(); + } + [[nodiscard]] Color background_color() const noexcept { + return static_cast(*this).template get<&Scene_State::background>(); + } + void set_viewport_size(Size size) { + size.width = std::max(0, size.width); + size.height = std::max(0, size.height); + const Frame_Viewport viewport{size.width, size.height}; + if (static_cast(*this).template get<&Scene_State::viewport>() == viewport) + return; + static_cast(*this).template set<&Scene_State::viewport>(viewport); + const auto topology = this->topology_snapshot(); + for (const auto& renderable : topology.renderables) + const_cast<::Renderable_Base&>(*renderable).invalidate_prepare(); + this->notify_model_dirty(); + } + [[nodiscard]] Size viewport_size() const noexcept { + const auto viewport = static_cast(*this).template get<&Scene_State::viewport>(); + return {viewport.width, viewport.height}; + } + void dispatch_event(const Event& event) { + const auto paint_order = this->paint_order_snapshot(); + for (auto iterator = paint_order.rbegin(); iterator != paint_order.rend(); ++iterator) { + auto base = std::const_pointer_cast<::Renderable_Base>(*iterator); + if (auto renderable = std::dynamic_pointer_cast(base)) { + if (renderable->is_visible()) { + if (auto handler = std::dynamic_pointer_cast(base)) + handler->handle_event(event); + } + if (event.is_accepted()) + break; + } + } + } + [[nodiscard]] bool prepare_frame() { + auto paint_frame = this->frame_control.acquire_painter(); + if (!paint_frame) + return false; + this->publish_frame_state(); + return true; + } + [[nodiscard]] bool refresh_manual_frame() { + if constexpr (Mode == Frame_Control_Mode::Manual) + return this->frame_control.refresh(); + return false; + } + [[nodiscard]] bool render_prepared_frame() { + auto render_frame = this->frame_control.acquire_renderer(); + if (!render_frame) + return false; + this->render(*render_frame); + this->wait_for_render(); + return true; + } + [[nodiscard]] bool discard_pending_frame() { + if constexpr (Mode == Frame_Control_Mode::Playback) + return false; + else + return this->frame_control.discard_pending_frame(); + } + [[nodiscard]] bool render_frame(bool force = false) { + if (!view_active() && !force) + return false; + if (viewport_size().empty()) + return false; + if (!this->consume_model_dirty() && !force) + return false; + const auto started = std::chrono::steady_clock::now(); + Size viewport; + try { + if (!prepare_frame()) + return false; + const auto frame_viewport = this->Scene_State_Strategy::render_state_value().viewport; + viewport = {frame_viewport.width, frame_viewport.height}; + if constexpr (Mode == Frame_Control_Mode::Manual) { + if (!refresh_manual_frame()) + return false; + } + if (!render_prepared_frame()) + return false; + } catch (...) { + this->notify_model_dirty(); + throw; + } + const auto finished = std::chrono::steady_clock::now(); + const double duration_ms = std::chrono::duration(finished - started).count(); + std::shared_ptr sink; + std::shared_ptr overlay; + { + std::lock_guard lock(control_mutex_); + sink = presentation_sink_.lock(); + overlay = performance_; + } + if (overlay) + detail::record_performance_frame(*overlay, duration_ms, diagnostics(), this->renderable_count(), viewport); + if (sink) + sink->request_present(detail::full_rect(viewport)); + return true; + } + void with_frame(const std::function& consumer) { + if (!consumer) + return; + this->with_final_color_cache([&consumer](const detail::Blend2D_Color_Cache& cache) { + consumer(cache.view()); + }); + } + void activate_view() noexcept { + active_.store(true, std::memory_order_release); + this->notify_model_dirty(); + } + void deactivate_view() noexcept { + active_.store(false, std::memory_order_release); + } + [[nodiscard]] bool view_active() const noexcept { + return active_.load(std::memory_order_acquire); + } + void set_max_render_fps(double fps) { + if constexpr (Mode != Frame_Control_Mode::Low_Latency) { + throw std::logic_error("maximum render FPS is only available in low-latency mode"); + } else { + if (!std::isfinite(fps) || fps <= 0.0) + throw std::invalid_argument("maximum render FPS must be finite and positive"); + this->frame_control.set_frequency_hz(fps); + this->notify_model_dirty(); + } + } + void clear_max_render_fps() { + if constexpr (Mode != Frame_Control_Mode::Low_Latency) { + throw std::logic_error("maximum render FPS is only available in low-latency mode"); + } else { + this->frame_control.clear_frequency_limit(); + this->notify_model_dirty(); + } + } + void set_consumer_feedback(Frame_Consumer_Feedback feedback) { + if constexpr (Mode != Frame_Control_Mode::Low_Latency) + throw std::logic_error("consumer feedback is only available in low-latency mode"); + else + this->frame_control.set_consumer_feedback(feedback); + } + void clear_consumer_feedback() { + if constexpr (Mode != Frame_Control_Mode::Low_Latency) + throw std::logic_error("consumer feedback is only available in low-latency mode"); + else + this->frame_control.clear_consumer_feedback(); + } + [[nodiscard]] double max_render_fps() const noexcept { + if constexpr (Mode == Frame_Control_Mode::Low_Latency) + return this->frame_control.state().frequency_hz; + return std::numeric_limits::quiet_NaN(); + } + [[nodiscard]] Low_Latency_Diagnostics diagnostics() const { + if constexpr (Mode == Frame_Control_Mode::Low_Latency) { + const auto state = this->frame_control.state(); + return {view_active(), {state.frequency_hz, state.completed_lifecycle_count, state.next_refresh_interval_ns}}; + } + const auto observed = frame_observer_snapshot(); + return {view_active(), {std::numeric_limits::quiet_NaN(), observed.consumed_frame_count, 0}}; + } + [[nodiscard]] Refresh_Control_Snapshot refresh_feedback_snapshot() const { + return diagnostics().refresh; + } + [[nodiscard]] static constexpr Frame_Control_Mode frame_control_mode() noexcept { + return Mode; + } + [[nodiscard]] Frame_Observer_Snapshot frame_observer_snapshot() const { + Frame_Observer_Snapshot snapshot; + { + std::lock_guard lock(observer_->mutex); + snapshot.mode = Mode; + snapshot.last_event = observer_->observation_count == 0 ? std::string("none") : detail::observer_event_name(Mode, observer_->last_event); + snapshot.limit_state = "not_applicable"; + snapshot.observation_count = observer_->observation_count; + snapshot.produced_frame_count = observer_->produced_frame_count; + snapshot.consumed_frame_count = observer_->consumed_frame_count; + snapshot.dropped_frame_count = observer_->dropped_frame_count; + snapshot.failed_operation_count = observer_->failed_operation_count; + snapshot.pending_frame_count = observer_->pending_frame_count; + snapshot.latest_sequence = observer_->latest_sequence; + snapshot.paint_duration_ns = observer_->paint_duration_ns; + snapshot.render_duration_ns = observer_->render_duration_ns; + snapshot.paint_lease_wait_ns = observer_->paint_lease_wait_ns; + snapshot.paint_state_wait_ns = observer_->paint_state_wait_ns; + snapshot.publish_state_wait_ns = observer_->publish_state_wait_ns; + snapshot.ready_wait_ns = observer_->ready_wait_ns; + snapshot.frame_age_at_render_ns = observer_->frame_age_at_render_ns; + snapshot.render_lease_wait_ns = observer_->render_lease_wait_ns; + snapshot.render_state_wait_ns = observer_->render_state_wait_ns; + snapshot.render_finish_state_wait_ns = observer_->render_finish_state_wait_ns; + snapshot.queue_wait_ns = observer_->queue_wait_ns; + } + if constexpr (Mode == Frame_Control_Mode::Low_Latency) { + const auto state = this->frame_control.state(); + snapshot.limit_state = detail::limit_state_name(static_cast(state.limit_state)); + snapshot.frequency_hz = state.frequency_hz; + snapshot.paint_duration_ns = state.paint_duration_ns; + snapshot.render_duration_ns = state.render_duration_ns; + snapshot.target_interval_ns = state.target_interval_ns; + snapshot.frequency_limit_enabled = state.frequency_limit_enabled; + snapshot.consumer_feedback_enabled = state.consumer_feedback_enabled; + snapshot.bottleneck_duration_ns = state.bottleneck_duration_ns; + snapshot.consumer_sample_interval_ns = state.consumer_sample_interval_ns; + snapshot.consumer_smoothed_interval_ns = state.consumer_smoothed_interval_ns; + snapshot.consumer_variation_ns = state.consumer_variation_ns; + snapshot.consumer_safety_interval_ns = state.consumer_safety_interval_ns; + snapshot.consumer_interval_ns = state.consumer_interval_ns; + snapshot.next_refresh_interval_ns = state.next_refresh_interval_ns; + snapshot.end_to_end_ns = state.end_to_end_ns; + } + return snapshot; + } + void set_presentation_sink(std::weak_ptr sink) { + std::lock_guard lock(control_mutex_); + presentation_sink_ = std::move(sink); + } + [[nodiscard]] std::shared_ptr performance_overlay() const { + std::lock_guard lock(control_mutex_); + return performance_; + } + void set_performance_overlay(std::shared_ptr overlay) { + { + std::lock_guard lock(control_mutex_); + performance_ = std::move(overlay); + } + this->notify_model_dirty(); + } +private: + explicit Basic_Scene2D(std::shared_ptr observer) + : Kernel_Scene(*memory_resource(Memory_Domain::Plot_Frame), detail::Frame_Observer_State(detail::Frame_Observer{observer})), observer_(std::move(observer)) {} + void attach_renderable_node(const std::shared_ptr& renderable, const std::shared_ptr& parent) { + if (!renderable) + throw std::invalid_argument("renderable is null"); + Renderable* parent_pointer = parent.get(); + if (!parent_pointer) { + auto root = root_renderable(); + if (!root) + throw std::logic_error("Scene2D::init must be called before adding renderables"); + parent_pointer = root.get(); + } + this->attach_renderable(renderable); + try { + this->set_display_parent(*renderable, parent_pointer); + this->set_dependency_parent(*renderable, parent_pointer); + } catch (...) { + this->detach_renderable(*renderable); + throw; + } + this->notify_model_dirty(); + } + std::shared_ptr observer_; + mutable std::mutex control_mutex_; + std::mutex initialization_mutex_; + std::weak_ptr presentation_sink_; + std::shared_ptr performance_; + std::atomic_bool active_{}; +}; +using Manual_Scene2D = Basic_Scene2D; +using Scene2D = Basic_Scene2D; +using Playback_Scene2D = Basic_Scene2D; +} diff --git a/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp b/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp index 997fb8e..06e07b5 100644 --- a/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp +++ b/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp @@ -7,7 +7,7 @@ namespace renderive { namespace { TEST(Renderive_Core2_Frame_Pipeline, ManualLifecycleSeparatesPrepareRefreshAndRender) { - Plot_Core plot(Frame_Control_Mode::Manual); + Manual_Scene2D plot; plot.init(); plot.set_viewport_size({96, 54}); ASSERT_TRUE(plot.prepare_frame()); @@ -36,7 +36,7 @@ TEST(Renderive_Core2_Frame_Pipeline, ManualLifecycleSeparatesPrepareRefreshAndRe EXPECT_EQ(plot.diagnostics().refresh.frame_count, 1u); } TEST(Renderive_Core2_Frame_Pipeline, HighFrequencyLowLatencyLifecycleExposesKernelTiming) { - Plot_Core plot(Frame_Control_Mode::Low_Latency); + Scene2D plot; plot.init(); plot.set_viewport_size({96, 54}); plot.set_max_render_fps(1'000'000'000.0); @@ -69,7 +69,7 @@ TEST(Renderive_Core2_Frame_Pipeline, HighFrequencyLowLatencyLifecycleExposesKern EXPECT_EQ(refresh.next_refresh_interval_ns, snapshot.next_refresh_interval_ns); } TEST(Renderive_Core2_Frame_Pipeline, LowLatencyConsumerFeedbackFlowsThroughCore2IntoKernelScheduling) { - Plot_Core plot(Frame_Control_Mode::Low_Latency); + Scene2D plot; plot.init(); plot.set_viewport_size({96, 54}); plot.set_max_render_fps(100.0); @@ -90,7 +90,7 @@ TEST(Renderive_Core2_Frame_Pipeline, LowLatencyConsumerFeedbackFlowsThroughCore2 EXPECT_EQ(plot.refresh_feedback_snapshot().next_refresh_interval_ns, 40'000'000u); } TEST(Renderive_Core2_Frame_Pipeline, LowLatencyLimitsCanBeClearedIndependently) { - Plot_Core plot(Frame_Control_Mode::Low_Latency); + Scene2D plot; plot.init(); plot.set_viewport_size({96, 54}); plot.set_max_render_fps(100.0); @@ -108,7 +108,7 @@ TEST(Renderive_Core2_Frame_Pipeline, LowLatencyLimitsCanBeClearedIndependently) EXPECT_TRUE(snapshot.limit_state == "paint_limited" || snapshot.limit_state == "render_limited"); } TEST(Renderive_Core2_Frame_Pipeline, LowLatencyReplacementRetiresOldFrameWithoutPhantomPendingFrame) { - Plot_Core plot(Frame_Control_Mode::Low_Latency); + Scene2D plot; plot.init(); plot.set_viewport_size({64, 40}); ASSERT_TRUE(plot.prepare_frame()); @@ -129,7 +129,7 @@ TEST(Renderive_Core2_Frame_Pipeline, LowLatencyReplacementRetiresOldFrameWithout EXPECT_EQ(snapshot.latest_sequence, 2u); } TEST(Renderive_Core2_Frame_Pipeline, LowLatencyDiscardAndEmptyRenderRemainObservable) { - Plot_Core plot(Frame_Control_Mode::Low_Latency); + Scene2D plot; plot.init(); plot.set_viewport_size({64, 40}); ASSERT_TRUE(plot.prepare_frame()); @@ -149,7 +149,7 @@ TEST(Renderive_Core2_Frame_Pipeline, LowLatencyDiscardAndEmptyRenderRemainObserv EXPECT_EQ(snapshot.latest_sequence, 1u); } TEST(Renderive_Core2_Frame_Pipeline, ManualDiscardAndFailedRefreshUpdateObserverState) { - Plot_Core plot(Frame_Control_Mode::Manual); + Manual_Scene2D plot; plot.init(); plot.set_viewport_size({64, 40}); ASSERT_TRUE(plot.prepare_frame()); @@ -170,7 +170,7 @@ TEST(Renderive_Core2_Frame_Pipeline, ManualDiscardAndFailedRefreshUpdateObserver EXPECT_EQ(snapshot.paint_duration_ns, last_paint_duration_ns); } TEST(Renderive_Core2_Frame_Pipeline, PlaybackPreservesQueueDepthAndEmptyAcquireFailure) { - Plot_Core plot(Frame_Control_Mode::Playback); + Playback_Scene2D plot; plot.init(); plot.set_viewport_size({64, 40}); ASSERT_TRUE(plot.prepare_frame()); @@ -202,7 +202,7 @@ TEST(Renderive_Core2_Frame_Pipeline, PlaybackPreservesQueueDepthAndEmptyAcquireF EXPECT_EQ(snapshot.queue_wait_ns, last_queue_wait_ns); } TEST(Renderive_Core2_Frame_Pipeline, PlaybackFullCyclePreservesPreviouslyQueuedFrames) { - Plot_Core plot(Frame_Control_Mode::Playback); + Playback_Scene2D plot; plot.init(); plot.set_viewport_size({96, 54}); ASSERT_TRUE(plot.prepare_frame()); @@ -224,34 +224,32 @@ TEST(Renderive_Core2_Frame_Pipeline, PlaybackFullCyclePreservesPreviouslyQueuedF EXPECT_EQ(plot.frame_observer_snapshot().pending_frame_count, 3u); } TEST(Renderive_Core2_Frame_Pipeline, EveryModeHonorsActiveDirtyAndForceRenderGates) { - constexpr std::array modes{ - Frame_Control_Mode::Manual, - Frame_Control_Mode::Low_Latency, - Frame_Control_Mode::Playback - }; - for (const auto mode : modes) { - Plot_Core plot(mode); - plot.init(); - plot.set_viewport_size({80, 45}); - EXPECT_FALSE(plot.view_active()); - EXPECT_FALSE(plot.render_frame(false)); - EXPECT_EQ(plot.frame_observer_snapshot().observation_count, 0u); - plot.activate_view(); - ASSERT_TRUE(plot.render_frame(false)); - const auto completed = plot.frame_observer_snapshot(); + const auto verify = [](Frame_Control_Mode mode) { + Scene_Type scene; + scene.init(); + scene.set_viewport_size({80, 45}); + EXPECT_FALSE(scene.view_active()); + EXPECT_FALSE(scene.render_frame(false)); + EXPECT_EQ(scene.frame_observer_snapshot().observation_count, 0u); + scene.activate_view(); + ASSERT_TRUE(scene.render_frame(false)); + const auto completed = scene.frame_observer_snapshot(); EXPECT_EQ(completed.mode, mode); EXPECT_EQ(completed.consumed_frame_count, 1u); - EXPECT_FALSE(plot.render_frame(false)); - EXPECT_EQ(plot.frame_observer_snapshot().consumed_frame_count, 1u); - plot.deactivate_view(); - plot.set_background_color(Color{8, 16, 24, 255}); - EXPECT_FALSE(plot.render_frame(false)); - ASSERT_TRUE(plot.render_frame(true)); - EXPECT_EQ(plot.frame_observer_snapshot().consumed_frame_count, 2u); - } + EXPECT_FALSE(scene.render_frame(false)); + EXPECT_EQ(scene.frame_observer_snapshot().consumed_frame_count, 1u); + scene.deactivate_view(); + scene.set_background_color(Color{8, 16, 24, 255}); + EXPECT_FALSE(scene.render_frame(false)); + ASSERT_TRUE(scene.render_frame(true)); + EXPECT_EQ(scene.frame_observer_snapshot().consumed_frame_count, 2u); + }; + verify.template operator()(Frame_Control_Mode::Manual); + verify.template operator()(Frame_Control_Mode::Low_Latency); + verify.template operator()(Frame_Control_Mode::Playback); } TEST(Renderive_Core2_Frame_Pipeline, FrequencyConfigurationRejectsInvalidValuesAndWrongModes) { - Plot_Core low_latency(Frame_Control_Mode::Low_Latency); + Scene2D low_latency; EXPECT_THROW(low_latency.set_max_render_fps(0.0), std::invalid_argument); EXPECT_THROW(low_latency.set_max_render_fps(-1.0), std::invalid_argument); EXPECT_THROW(low_latency.set_max_render_fps(std::numeric_limits::quiet_NaN()), @@ -260,8 +258,8 @@ TEST(Renderive_Core2_Frame_Pipeline, FrequencyConfigurationRejectsInvalidValuesA std::invalid_argument); low_latency.set_max_render_fps(1'000.0); EXPECT_DOUBLE_EQ(low_latency.max_render_fps(), 1'000.0); - Plot_Core manual(Frame_Control_Mode::Manual); - Plot_Core playback(Frame_Control_Mode::Playback); + Manual_Scene2D manual; + Playback_Scene2D playback; EXPECT_THROW(manual.set_max_render_fps(60.0), std::logic_error); EXPECT_THROW(playback.set_max_render_fps(60.0), std::logic_error); EXPECT_FALSE(playback.refresh_manual_frame()); diff --git a/render_2D/tests/render_2D_Integration_Tests.cpp b/render_2D/tests/render_2D_Integration_Tests.cpp index 21a1696..4618326 100644 --- a/render_2D/tests/render_2D_Integration_Tests.cpp +++ b/render_2D/tests/render_2D_Integration_Tests.cpp @@ -33,7 +33,7 @@ static_assert(!Legacy_Axis_Property_Api); static_assert(!Legacy_Time_Axis_Property_Api); TEST(Renderive_Core2, RootIdentityAndRefreshDiagnosticsComeFromKernelState) { - Plot_Core plot; + Scene2D plot; plot.init(); const auto root = plot.root_renderable(); ASSERT_TRUE(root); @@ -115,7 +115,7 @@ TEST(Renderive_Core2, BicubicHeatmapUsesRealCubicResampling) { EXPECT_NE(bilinear, bicubic); } TEST(Renderive_Core2, KernelSceneRendersBusinessObjectsIntoBlend2DFrame) { - Plot_Core plot; + Scene2D plot; plot.init(); plot.set_viewport_size({320, 180}); plot.activate_view(); @@ -184,7 +184,7 @@ TEST(Renderive_Core2, KernelSceneRendersBusinessObjectsIntoBlend2DFrame) { EXPECT_FALSE(plot.render_frame()); } TEST(Renderive_Core2, TimeAxisUsesOneFontStateAndFormatsConfiguredLabels) { - Plot_Core plot; + Scene2D plot; plot.init(); const auto root = plot.root_renderable(); auto axis = Time_Axis::Builder(root, Orientation::Horizontal) @@ -199,7 +199,7 @@ TEST(Renderive_Core2, TimeAxisUsesOneFontStateAndFormatsConfiguredLabels) { EXPECT_EQ(axis->get<&Time_Axis_Properties::tick_label_spacing_px>(), 17); } TEST(Renderive_Core2, PerformanceOverlayConsumesKernelFrameDiagnostics) { - Plot_Core plot; + Scene2D plot; plot.init(); plot.set_viewport_size({160, 90}); plot.activate_view(); @@ -220,7 +220,7 @@ TEST(Renderive_Core2, PerformanceOverlayConsumesKernelFrameDiagnostics) { EXPECT_DOUBLE_EQ(plot.max_render_fps(), 120.0); } TEST(Renderive_Core2, AllFrameControlModesUseKernelStrategiesAndExposeObservers) { - Plot_Core manual(Frame_Control_Mode::Manual); + Manual_Scene2D manual; manual.init(); manual.set_viewport_size({64, 40}); EXPECT_EQ(manual.frame_control_mode(), Frame_Control_Mode::Manual); @@ -233,7 +233,7 @@ TEST(Renderive_Core2, AllFrameControlModesUseKernelStrategiesAndExposeObservers) manual_observer = manual.frame_observer_snapshot(); EXPECT_EQ(manual_observer.last_event, "rendered"); EXPECT_EQ(manual_observer.consumed_frame_count, 1u); - Plot_Core low_latency(Frame_Control_Mode::Low_Latency); + Scene2D low_latency; low_latency.init(); low_latency.set_viewport_size({64, 40}); low_latency.activate_view(); @@ -242,7 +242,7 @@ TEST(Renderive_Core2, AllFrameControlModesUseKernelStrategiesAndExposeObservers) EXPECT_EQ(low_latency.frame_control_mode(), Frame_Control_Mode::Low_Latency); EXPECT_EQ(low_observer.last_event, "lifecycle_completed"); EXPECT_EQ(low_observer.consumed_frame_count, 1u); - Plot_Core playback(Frame_Control_Mode::Playback); + Playback_Scene2D playback; playback.init(); playback.set_viewport_size({64, 40}); ASSERT_TRUE(playback.prepare_frame()); @@ -259,7 +259,7 @@ TEST(Renderive_Core2, AllFrameControlModesUseKernelStrategiesAndExposeObservers) EXPECT_EQ(playback_observer.consumed_frame_count, 1u); } TEST(Renderive_Core2, RetainedAxisAndTimeAxisApisRoundTripWithoutWebAdapters) { - Plot_Core plot; + Scene2D plot; plot.init(); const auto root = plot.root_renderable(); ASSERT_TRUE(root); @@ -336,7 +336,7 @@ TEST(Renderive_Core2, RetainedAxisAndTimeAxisApisRoundTripWithoutWebAdapters) { EXPECT_EQ(time_axis->tick_label(first), "01:02:03.004"); } TEST(Renderive_Core2, RetainedSpectrumApisRoundTripAndMarkersRemainObservable) { - Plot_Core plot; + Scene2D plot; plot.init(); plot.set_viewport_size({360, 240}); plot.activate_view(); @@ -414,7 +414,7 @@ TEST(Renderive_Core2, RetainedSpectrumApisRoundTripAndMarkersRemainObservable) { EXPECT_TRUE(plot.render_frame(true)); } TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { - Plot_Core plot; + Scene2D plot; plot.init(); plot.set_viewport_size({420, 280}); plot.activate_view(); @@ -507,7 +507,7 @@ TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { EXPECT_TRUE(plot.render_frame(true)); } TEST(Renderive_Core2, RetainedSelectionAndConstellationApisDriveInteractionAndLayout) { - Plot_Core plot; + Scene2D plot; plot.init(); plot.set_viewport_size({360, 260}); plot.activate_view(); @@ -573,7 +573,7 @@ TEST(Renderive_Core2, RetainedSelectionAndConstellationApisDriveInteractionAndLa EXPECT_TRUE(plot.render_frame(true)); } TEST(Renderive_Core2, PlottablePropertiesPublishOnlyAtFrameBoundary) { - Plot_Core plot; + Scene2D plot; plot.init(); const auto root = plot.root_renderable(); const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal).build(); @@ -591,7 +591,7 @@ TEST(Renderive_Core2, PlottablePropertiesPublishOnlyAtFrameBoundary) { ASSERT_TRUE(plot.render_prepared_frame()); } TEST(Renderive_Core2, EveryStatefulRenderableOwnsItsStateObserver) { - Plot_Core plot; + Scene2D plot; plot.init(); const auto root = plot.root_renderable(); const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal).build(); @@ -635,7 +635,7 @@ TEST(Renderive_Core2, EveryStatefulRenderableOwnsItsStateObserver) { EXPECT_EQ(spectrum->observation().event, Renderable_Observer_Event::Published); } TEST(Renderive_Core2, PlottableDataUpdatesReachKernelRealTimeDataStrategy) { - Plot_Core plot; + Scene2D plot; plot.init(); const auto root = plot.root_renderable(); const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal).build(); @@ -700,7 +700,7 @@ TEST(Renderive_Core2, AxisRasterLayoutCoversAxisSwapAndEveryReversal) { EXPECT_DOUBLE_EQ(points.back().y, 30.0); } TEST(Renderive_Core2, PartitionedPlotsBuildPropertyDrivenRenderGraphs) { - Plot_Core plot; + Scene2D plot; plot.init(); plot.set_viewport_size({320, 180}); const auto root = plot.root_renderable(); @@ -815,7 +815,7 @@ TEST(Renderive_Core2, AutomaticPartitioningSplitsOnlyForMeasuredBottlenecks) { } TEST(Renderive_Core2, DynamicWaterfallCaptureStressPreservesPlansSlotsAndExactSessions) { - Plot_Core plot; + Scene2D plot; plot.init(); plot.set_viewport_size({640, 360}); const auto root = plot.root_renderable(); @@ -873,7 +873,7 @@ TEST(Renderive_Core2, DynamicWaterfallCaptureStressPreservesPlansSlotsAndExactSe } TEST(Renderive_Core2, PaintOnlyStyleChangesPreservePrepareCache) { - Plot_Core plot; + Scene2D plot; plot.init(); plot.set_viewport_size({320, 180}); const auto root = plot.root_renderable(); @@ -921,7 +921,7 @@ TEST(Renderive_Core2, PaintOnlyStyleChangesPreservePrepareCache) { })); } TEST(Renderive_Core2, PlottableAxesAreDataDependenciesAndPaintOverlays) { - Plot_Core plot; + Scene2D plot; plot.init(); const auto root = plot.root_renderable(); const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal).build(); @@ -955,7 +955,7 @@ TEST(Renderive_Core2, PlottableAxesAreDataDependenciesAndPaintOverlays) { EXPECT_LT(spectrum_position, power_position); } TEST(Renderive_Core2, InteractionOverlayPaintsAboveItsAxes) { - Plot_Core plot; + Scene2D plot; plot.init(); const auto root = plot.root_renderable(); const auto horizontal = Axis::Builder(root, Orientation::Horizontal).build(); diff --git a/web_server/app/Gallery_Actions.h b/web_server/app/Gallery_Actions.h index c544c31..3461a6e 100644 --- a/web_server/app/Gallery_Actions.h +++ b/web_server/app/Gallery_Actions.h @@ -88,19 +88,19 @@ struct Type_Descriptor> { using renderive::web::gallery_action; return object(type_metadata( gallery_action("mode_prepare", "准备帧", - "Manual_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame", + "Manual_Refresh_Strategy::acquire_painter / Basic_Scene2D::prepare_frame", "Manual_Refresh_Strategy"), gallery_action("mode_refresh", "提交手动刷新", - "Manual_Refresh_Strategy::refresh / Plot_Core::refresh_manual_frame", + "Manual_Refresh_Strategy::refresh / Basic_Scene2D::refresh_manual_frame", "Manual_Refresh_Strategy"), gallery_action("mode_render", "渲染已刷新帧", - "Manual_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", + "Manual_Refresh_Strategy::acquire_renderer / Basic_Scene2D::render_prepared_frame", "Manual_Refresh_Strategy", {}, {}, {}, 0.0, true), gallery_action("mode_discard", "丢弃待刷新帧", - "Plot_Core::discard_pending_frame", + "Basic_Scene2D::discard_pending_frame", "Manual_Refresh_Strategy"), gallery_action("mode_cycle", "执行完整手动帧周期", - "Plot_Core::render_frame", "Manual_Refresh_Strategy", + "Basic_Scene2D::render_frame", "Manual_Refresh_Strategy", {}, {}, {}, 0.0, true))); } }; @@ -112,15 +112,15 @@ struct Type_Descriptor> { using renderive::web::gallery_action; return object(type_metadata( gallery_action("mode_prepare", "发布低延迟帧", - "Low_Latency_Strategy::acquire_painter / Plot_Core::prepare_frame", + "Low_Latency_Strategy::acquire_painter / Basic_Scene2D::prepare_frame", "Low_Latency_Strategy"), gallery_action("mode_render", "消费最新帧", - "Low_Latency_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", + "Low_Latency_Strategy::acquire_renderer / Basic_Scene2D::render_prepared_frame", "Low_Latency_Strategy", {}, {}, {}, 0.0, true), gallery_action("mode_discard", "丢弃陈旧帧", - "Plot_Core::discard_pending_frame", "Low_Latency_Strategy"), + "Basic_Scene2D::discard_pending_frame", "Low_Latency_Strategy"), gallery_action("mode_cycle", "执行低延迟帧周期", - "Plot_Core::render_frame", "Low_Latency_Strategy", + "Basic_Scene2D::render_frame", "Low_Latency_Strategy", {}, {}, {}, 0.0, true))); } }; @@ -132,16 +132,16 @@ struct Type_Descriptor> { using renderive::web::gallery_action; return object(type_metadata( gallery_action("mode_enqueue", "压入一帧", - "Flow_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame", + "Flow_Refresh_Strategy::acquire_painter / Basic_Scene2D::prepare_frame", "Flow_Refresh_Strategy"), gallery_action("mode_enqueue_burst", "批量压入回放队列", - "Flow_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame x N", + "Flow_Refresh_Strategy::acquire_painter / Basic_Scene2D::prepare_frame x N", "Flow_Refresh_Strategy", {}, "number", "帧数", 8.0), gallery_action("mode_dequeue", "消费队首帧", - "Flow_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", + "Flow_Refresh_Strategy::acquire_renderer / Basic_Scene2D::render_prepared_frame", "Flow_Refresh_Strategy", {}, {}, {}, 0.0, true), gallery_action("mode_cycle", "执行回放帧周期", - "Plot_Core::render_frame", "Flow_Refresh_Strategy", + "Basic_Scene2D::render_frame", "Flow_Refresh_Strategy", {}, {}, {}, 0.0, true))); } }; diff --git a/web_server/app/Gallery_Observer_Adminive.h b/web_server/app/Gallery_Observer_Adminive.h index df1a642..31f6935 100644 --- a/web_server/app/Gallery_Observer_Adminive.h +++ b/web_server/app/Gallery_Observer_Adminive.h @@ -1,7 +1,7 @@ #pragma once #include "Gallery_Enum.h" -#include "render_2D/plot/Plot_Core.h" +#include "render_2D/scene/Scene.h" #include "render_2D/renderable/Renderable.h" #include "adminive/adminive.hpp" diff --git a/web_server/app/Gallery_Plot_Session.cpp b/web_server/app/Gallery_Plot_Session.cpp index e27ae7d..e7bcabb 100644 --- a/web_server/app/Gallery_Plot_Session.cpp +++ b/web_server/app/Gallery_Plot_Session.cpp @@ -77,21 +77,32 @@ std::optional action_number(const Gallery_Action_Request& request) { return std::nullopt; return std::get(*request.argument); } -Frame_Control_Mode core_frame_mode(Gallery_Frame_Mode mode) { - const auto result = magic_enum::enum_cast( - magic_enum::enum_name(mode)); - if (!result) - throw std::logic_error("Gallery frame mode has no Kernel strategy"); - return *result; -} } // namespace -class Gallery_Scene final { +class Gallery_Scene_Interface { public: - Gallery_Scene(std::uint64_t session_id, std::string case_id, - Gallery_Frame_Mode frame_mode, bool automatic_low_latency) - : session_id_(session_id), case_id_(std::move(case_id)), - frame_mode_(frame_mode), automatic_low_latency_(automatic_low_latency), - plot_(core_frame_mode(frame_mode)), performance_started_(std::chrono::steady_clock::now()) { + virtual ~Gallery_Scene_Interface() = default; + [[nodiscard]] virtual const std::string& case_id() const noexcept = 0; + [[nodiscard]] virtual nlohmann::json controls() const = 0; + [[nodiscard]] virtual Gallery_Frame_Mode frame_mode() const noexcept = 0; + virtual void reset_monitoring() = 0; + [[nodiscard]] virtual bool can_render_automatically() const noexcept = 0; + [[nodiscard]] virtual std::uint64_t kernel_refresh_interval_ns() const = 0; + virtual void set_client_metrics(Gallery_Client_Performance metrics) noexcept = 0; + [[nodiscard]] virtual adminive::Update_Result apply_patch(std::string_view target, const nlohmann::json& patch) = 0; + virtual void resize(Size size) = 0; + virtual void dispatch(const Event& event) = 0; + virtual bool render_latest_frame() = 0; + [[nodiscard]] virtual std::optional encode_latest_pixels() = 0; + virtual void record_pixel_response(std::chrono::steady_clock::time_point request_started, std::chrono::steady_clock::time_point encode_started, std::chrono::steady_clock::time_point encode_finished, std::size_t pixel_bytes) = 0; + [[nodiscard]] virtual std::string action(const Gallery_Action_Request& request, bool& recognized) = 0; + virtual void record_action_notice_if_empty(std::string_view notice) = 0; + [[nodiscard]] virtual std::string telemetry_json() const = 0; +}; +template +class Gallery_Scene final : public Gallery_Scene_Interface { +public: + Gallery_Scene(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode frame_mode, bool automatic_low_latency) + : session_id_(session_id), case_id_(std::move(case_id)), frame_mode_(frame_mode), automatic_low_latency_(automatic_low_latency), performance_started_(std::chrono::steady_clock::now()) { plot_.init(); plot_.set_viewport_size({560, 320}); root_ = plot_.root_renderable(); @@ -104,7 +115,7 @@ public: build_axes(); build_primary(); apply_layout(); - session_control_ = std::make_unique( + session_control_ = std::make_unique>( plot_, *primary_, feedback_policy_); register_controls(); set_performance_plot_name(plot_, case_id_ + "/" + gallery_enum_id(frame_mode_)); @@ -1236,9 +1247,9 @@ private: std::string case_id_; Gallery_Frame_Mode frame_mode_ = Gallery_Frame_Mode::Low_Latency; bool automatic_low_latency_{}; - Plot_Core plot_; + Scene_Type plot_; Gallery_Feedback_Policy feedback_policy_; - std::unique_ptr session_control_; + std::unique_ptr> session_control_; Gallery_Controls controls_; std::chrono::steady_clock::time_point performance_started_; std::chrono::steady_clock::time_point last_performance_log_; @@ -1280,6 +1291,17 @@ private: bool rendered_since_last_pixel_{}; std::string last_action_result_; }; +std::unique_ptr make_gallery_scene(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode, bool automatic_low_latency) { + switch (mode) { + case Gallery_Frame_Mode::Manual: + return std::make_unique>(session_id, std::move(case_id), mode, automatic_low_latency); + case Gallery_Frame_Mode::Low_Latency: + return std::make_unique>(session_id, std::move(case_id), mode, automatic_low_latency); + case Gallery_Frame_Mode::Playback: + return std::make_unique>(session_id, std::move(case_id), mode, automatic_low_latency); + } + throw std::invalid_argument("unknown gallery frame mode"); +} struct Gallery_Plot_Session::Impl { explicit Impl(bool enable_automatic_low_latency) : automatic_low_latency(enable_automatic_low_latency), @@ -1433,7 +1455,7 @@ struct Gallery_Plot_Session::Impl { }, event); } - std::unique_ptr scene; + std::unique_ptr scene; std::mutex mutex; std::condition_variable scheduler_condition; std::uint64_t scheduler_revision{}; @@ -1452,8 +1474,7 @@ struct Gallery_Plot_Session::Impl { Web_Response_Type::Json, Gallery_Protocol::error_json("未知的 gallery case 或 frame_mode", "case") }; - scene = std::make_unique( - session_id, open->case_id, open->frame_mode, automatic_low_latency); + scene = make_gallery_scene(session_id, open->case_id, open->frame_mode, automatic_low_latency); ++scene_generation; if (scene->frame_mode() == Gallery_Frame_Mode::Low_Latency) ensure_render_worker(); @@ -1538,8 +1559,7 @@ struct Gallery_Plot_Session::Impl { if (action->id == "reset") { const std::string id = scene->case_id(); const auto mode = scene->frame_mode(); - scene = std::make_unique( - session_id, id, mode, automatic_low_latency); + scene = make_gallery_scene(session_id, id, mode, automatic_low_latency); ++scene_generation; return Web_Response{ Web_Response_Type::Json, diff --git a/web_server/app/Gallery_Protocol.cpp b/web_server/app/Gallery_Protocol.cpp index 93d7f81..e5e53be 100644 --- a/web_server/app/Gallery_Protocol.cpp +++ b/web_server/app/Gallery_Protocol.cpp @@ -64,13 +64,38 @@ const Case_Model* find_case(std::string_view id) { return found == all.end() ? nullptr : &*found; } +void append_session_actions(Gallery_Frame_Mode frame_mode, + std::vector& result) { + switch (frame_mode) { + case Gallery_Frame_Mode::Manual: + append_gallery_actions>(result); + return; + case Gallery_Frame_Mode::Low_Latency: + append_gallery_actions>(result); + return; + case Gallery_Frame_Mode::Playback: + append_gallery_actions>(result); + return; + } + throw std::invalid_argument("unknown gallery frame mode"); +} +std::size_t session_control_count(Gallery_Frame_Mode frame_mode) { + switch (frame_mode) { + case Gallery_Frame_Mode::Manual: + return gallery_control_count>(); + case Gallery_Frame_Mode::Low_Latency: + return gallery_control_count>(); + case Gallery_Frame_Mode::Playback: + return gallery_control_count>(); + } + throw std::invalid_argument("unknown gallery frame mode"); +} std::vector registered_actions(std::string_view case_id, Gallery_Frame_Mode frame_mode) { std::vector result; - append_gallery_actions(result); - for (auto& action : gallery_frame_actions(frame_mode)) { + append_session_actions(frame_mode, result); + for (auto& action : gallery_frame_actions(frame_mode)) result.push_back(std::move(action)); - } append_gallery_renderable_actions(case_id, result); return result; } @@ -492,10 +517,11 @@ std::string Gallery_Protocol::catalog_json() { Json entry = adminive::to_frontend_json(item); entry["control_count_by_mode"] = Json::object(); entry["action_count_by_mode"] = Json::object(); - const std::size_t controls = gallery_control_count() + - gallery_renderable_control_count(item.id); + const std::size_t renderable_controls = gallery_renderable_control_count(item.id); for (const auto mode : frame_modes) { const std::string key = gallery_enum_id(mode); + const std::size_t controls = + gallery_detail::session_control_count(mode) + renderable_controls; const std::size_t actions = gallery_detail::registered_actions(item.id, mode).size(); entry["control_count_by_mode"][key] = controls; @@ -503,7 +529,9 @@ std::string Gallery_Protocol::catalog_json() { controls_total += controls; actions_total += actions; } - entry["control_count"] = controls; + entry["control_count"] = + gallery_detail::session_control_count(Gallery_Frame_Mode::Low_Latency) + + renderable_controls; entry["action_count"] = gallery_detail::registered_actions( item.id, Gallery_Frame_Mode::Low_Latency).size(); diff --git a/web_server/app/Gallery_Session_Control.h b/web_server/app/Gallery_Session_Control.h index 3227814..e79164b 100644 --- a/web_server/app/Gallery_Session_Control.h +++ b/web_server/app/Gallery_Session_Control.h @@ -1,43 +1,32 @@ #pragma once - -#include "render_2D/plot/Plot_Core.h" #include "render_2D/plottable/Performance_Overlay.h" #include "render_2D/renderable/Renderable.h" - +#include "render_2D/scene/Scene.h" #include #include - namespace adminive { template struct Type_Descriptor; } - namespace renderive::web { - struct Gallery_Feedback_Policy { bool enabled{true}; bool pixel{}; bool presentation{true}; bool manual{}; double manual_fps{60.0}; - - friend bool operator==(const Gallery_Feedback_Policy&, - const Gallery_Feedback_Policy&) = default; + friend bool operator==(const Gallery_Feedback_Policy&, const Gallery_Feedback_Policy&) = default; }; - +template class Gallery_Session_Control final { public: - Gallery_Session_Control(Plot_Core& plot, Renderable& renderable, - Gallery_Feedback_Policy& feedback) noexcept - : plot_(plot), renderable_(renderable), feedback_(feedback) {} - + Gallery_Session_Control(Scene_Type& scene, Renderable& renderable, Gallery_Feedback_Policy& feedback) noexcept + : scene_(scene), renderable_(renderable), feedback_(feedback) {} private: - Plot_Core& plot_; + Scene_Type& scene_; Renderable& renderable_; Gallery_Feedback_Policy& feedback_; - template friend struct ::adminive::Type_Descriptor; }; - -} // namespace renderive::web +} diff --git a/web_server/app/Gallery_Session_Control_Adminive.h b/web_server/app/Gallery_Session_Control_Adminive.h index 149aaee..0dc61e9 100644 --- a/web_server/app/Gallery_Session_Control_Adminive.h +++ b/web_server/app/Gallery_Session_Control_Adminive.h @@ -38,28 +38,28 @@ struct Type_Descriptor { } }; -template <> -struct Type_Descriptor { +template +struct Type_Descriptor> { static auto get() { - using T = renderive::web::Gallery_Session_Control; + using T = renderive::web::Gallery_Session_Control; using namespace renderive::web::gallery_adminive; - const auto dirty = [](T& value) { value.plot_.notify_model_dirty(); }; + const auto dirty = [](T& value) { value.scene_.notify_model_dirty(); }; return adminive::object("scene", "场景", callback_property( "background_color", "背景颜色", Field_Control::color, - [](const T& value) { return value.plot_.background_color(); }, + [](const T& value) { return value.scene_.background_color(); }, [dirty](T& value, renderive::Color color) { - value.plot_.set_background_color(color); + value.scene_.set_background_color(color); dirty(value); }), callback_property( "performance_overlay", "性能叠加层", Field_Control::boolean, [](const T& value) { - const auto overlay = value.plot_.performance_overlay(); + const auto overlay = value.scene_.performance_overlay(); return overlay && overlay->enabled(); }, [dirty](T& value, bool enabled) { - renderive::set_performance_overlay_enabled(value.plot_, enabled); + renderive::set_performance_overlay_enabled(value.scene_, enabled); dirty(value); }), callback_property( @@ -76,8 +76,8 @@ struct Type_Descriptor { value.renderable_.set_cache_mode(mode); dirty(value); }) - .enum_label("直接绘制") - .enum_label("本地像素缓存"), + .template enum_label("直接绘制") + .template enum_label("本地像素缓存"), callback_property( "object_name", "对象名称", Field_Control::text, [](const T& value) { return value.renderable_.object_name(); }, @@ -87,25 +87,25 @@ struct Type_Descriptor { }), callback_property( "frequency_limit_enabled", "启用帧率限制", Field_Control::boolean, - [](const T& value) { return value.plot_.max_render_fps() > 0.0; }, + [](const T& value) { return value.scene_.max_render_fps() > 0.0; }, [dirty](T& value, bool enabled) { if (enabled) { - if (value.plot_.max_render_fps() <= 0.0) - value.plot_.set_max_render_fps(30.0); + if (value.scene_.max_render_fps() <= 0.0) + value.scene_.set_max_render_fps(30.0); } else { - value.plot_.clear_max_render_fps(); + value.scene_.clear_max_render_fps(); } dirty(value); }), callback_property( "max_render_fps", "最大渲染帧率", Field_Control::number, [](const T& value) { - const double fps = value.plot_.max_render_fps(); + const double fps = value.scene_.max_render_fps(); return fps > 0.0 ? fps : 30.0; }, [dirty](T& value, double fps) { - if (value.plot_.max_render_fps() > 0.0) - value.plot_.set_max_render_fps(fps); + if (value.scene_.max_render_fps() > 0.0) + value.scene_.set_max_render_fps(fps); dirty(value); }), callback_property( @@ -118,22 +118,20 @@ struct Type_Descriptor { } }; -static_assert(Described_Type); -static_assert(Direct_Described_Object); } // namespace adminive namespace structive { -template <> -struct Type_Descriptor { +template +struct Type_Descriptor> { static auto get() { - using T = renderive::web::Gallery_Session_Control; + using T = renderive::web::Gallery_Session_Control; using renderive::web::gallery_action; return object(type_metadata( gallery_action("reset", "恢复本图默认值", "Gallery_Scene::rebuild", "场景"), gallery_action("toggle_view", "切换 View 生命周期", - "Plot_Core::activate_view / deactivate_view", "场景"), + "Basic_Scene2D::activate_view / deactivate_view", "场景"), gallery_action("capture_next_frame", "Capture next frame", "Scene_Base::capture_next_frame", "Performance Capture", "Capture the next completed render frame", {}, {}, 0.0, true), diff --git a/web_server/app/Web_Plot_Session.cpp b/web_server/app/Web_Plot_Session.cpp index 025bd95..0993ab0 100644 --- a/web_server/app/Web_Plot_Session.cpp +++ b/web_server/app/Web_Plot_Session.cpp @@ -49,7 +49,7 @@ double gaussian(double x, double center, double width) { } } // namespace struct Web_Plot_Session::Impl { - Plot_Core plot; + Scene2D plot; std::shared_ptr spectrum_frequency_axis; std::shared_ptr spectrum_power_axis; std::shared_ptr waterfall_frequency_axis;