From 302b1dcdfd4949868e9f53c2d2f5bb02740290a8 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Mon, 10 Aug 2026 13:50:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=85=E6=A0=B8=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Kernel/CMakeLists.txt | 91 +++--- Kernel/readme.md | 49 ++-- .../src/renderive/base/node/Multiway_Node.hpp | 63 +++- .../base/node/concept/Multiway_Node.hpp | 11 +- .../base/observer/Observer_State.hpp | 14 +- .../renderive/compat/taskflow/taskflow.hpp | 129 +++++++++ .../base/Frame_Control_Strategy_Base.hpp | 11 + .../strategy/flow/Flow_Refresh_Strategy.hpp | 4 +- .../strategy/flow/Flow_Refresh_Strategy.inl | 20 +- .../low_latency/Low_Latency_Strategy.hpp | 3 +- .../low_latency/Low_Latency_Strategy.inl | 12 +- .../real_time_data/Attach_Real_Time_Data.hpp | 55 ++-- .../Frame_Strategy_Observer.hpp | 127 +++++++- .../real_time_data/History_Real_Time_Data.hpp | 6 +- .../real_time_data/Latest_Real_Time_Data.hpp | 6 +- .../renderable/Renderable_Task_Graph.cpp | 45 ++- .../renderable/Renderable_Task_Graph.hpp | 6 + .../renderable/base/Renderable_Base.cpp | 52 +++- .../renderable/base/Renderable_Base.hpp | 42 ++- .../renderable/concept/Renderable.hpp | 8 +- .../src/renderive/scene/Scene2D_Context.hpp | 90 +++--- .../src/renderive/scene/Scene3D_Context.hpp | 17 +- .../src/renderive/scene/base/Scene_Base.cpp | 272 ++++++++++++++---- .../src/renderive/scene/base/Scene_Base.hpp | 52 +++- .../renderive/scene/base/Scene_Lifetime.hpp | 35 +++ Kernel/src/renderive/scene/concept/Scene.hpp | 5 +- Kernel/src/renderive/state/Concepts.hpp | 2 +- .../renderive/state/Double_State_Strategy.hpp | 2 +- .../renderive/state/Triple_State_Strategy.hpp | 6 +- Kernel/tests/compat/taskflow/taskflow.hpp | 125 -------- Kernel/tests/main.cpp | 76 +---- .../base/node/Multiway_Node_Test.cpp | 31 +- .../flow/Flow_Refresh_Strategy_Test.cpp | 154 ++++++++++ .../low_latency/Low_Latency_Strategy_Test.cpp | 26 ++ .../real_time_data/Real_Time_Data_Test.cpp | 133 ++++++++- .../renderable/Renderable_Task_Graph_Test.cpp | 104 ++++++- .../renderable/base/Renderable_Base_Test.cpp | 159 +++++++++- .../renderable/concept/Renderable_Test.cpp | 8 +- .../renderive/scene/Scene2D_Context_Test.cpp | 97 ++++++- .../scene/Scene2D_Render_Order_Test.cpp | 14 +- .../renderive/scene/Scene3D_Context_Test.cpp | 29 ++ .../scene/Scene_Memory_Resource_Test.cpp | 14 + .../scene/Scene_State_Observer_Test.cpp | 31 ++ .../renderive/scene/base/Scene_Base_Test.cpp | 194 +++++++++++++ .../state/Double_State_Strategy_Test.cpp | 8 + .../state/Triple_State_Strategy_Test.cpp | 32 +++ 46 files changed, 1955 insertions(+), 515 deletions(-) create mode 100644 Kernel/src/renderive/compat/taskflow/taskflow.hpp create mode 100644 Kernel/src/renderive/scene/base/Scene_Lifetime.hpp delete mode 100644 Kernel/tests/compat/taskflow/taskflow.hpp diff --git a/Kernel/CMakeLists.txt b/Kernel/CMakeLists.txt index fa25e58..a38a5ac 100644 --- a/Kernel/CMakeLists.txt +++ b/Kernel/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.20) project(Renderive_Scene_Taskflow LANGUAGES CXX) set(RENDERIVE_TASKFLOW_ROOT "" CACHE PATH "Taskflow 4.1.0 root") +set(_renderive_builtin_taskflow_dir "${CMAKE_CURRENT_LIST_DIR}/src/renderive/compat") find_path(RENDERIVE_TASKFLOW_INCLUDE_DIR NAMES taskflow/taskflow.hpp PATHS @@ -10,8 +11,14 @@ find_path(RENDERIVE_TASKFLOW_INCLUDE_DIR "${CMAKE_CURRENT_LIST_DIR}/third_party/taskflow/include" ) if (NOT RENDERIVE_TASKFLOW_INCLUDE_DIR) - set(RENDERIVE_TASKFLOW_INCLUDE_DIR "${CMAKE_CURRENT_LIST_DIR}/tests/compat") - message(STATUS "Taskflow 4.1.0 not found; using the private dependency-graph compatibility layer") + set(RENDERIVE_TASKFLOW_INCLUDE_DIR "${_renderive_builtin_taskflow_dir}") +endif () +get_filename_component(_renderive_taskflow_include_dir_real "${RENDERIVE_TASKFLOW_INCLUDE_DIR}" REALPATH) +get_filename_component(_renderive_builtin_taskflow_dir_real "${_renderive_builtin_taskflow_dir}" REALPATH) +set(RENDERIVE_TASKFLOW_MOCK OFF) +if (_renderive_taskflow_include_dir_real STREQUAL _renderive_builtin_taskflow_dir_real) + set(RENDERIVE_TASKFLOW_MOCK ON) + message(STATUS "Using the built-in dependency-graph executor") endif () add_library(renderive_scene STATIC "${CMAKE_CURRENT_LIST_DIR}/src/renderive/renderable/Renderable_Task_Graph.cpp" @@ -19,47 +26,49 @@ add_library(renderive_scene STATIC "${CMAKE_CURRENT_LIST_DIR}/src/renderive/scene/base/Scene_Base.cpp" ) target_include_directories(renderive_scene - PUBLIC "$" + PUBLIC + "$" PRIVATE "${RENDERIVE_TASKFLOW_INCLUDE_DIR}" ) target_compile_features(renderive_scene PUBLIC cxx_std_20) +if (RENDERIVE_TASKFLOW_MOCK) + target_compile_definitions(renderive_scene PRIVATE RENDERIVE_TASKFLOW_MOCK=1) +endif () find_package(Threads REQUIRED) target_link_libraries(renderive_scene PRIVATE Threads::Threads) -set(renderive_test_sources - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/Concepts_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/node/Multiway_Node_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/observer/Observer_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/observer/time_source/Steady_Time_Source_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/property/Concepts_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/property/Property_Builder_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/property/Validated_Value_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/base/Frame_Control_Strategy_Base_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/concept/Concepts_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/real_time_data/base/Real_Time_Data_Base_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/real_time_data/Real_Time_Data_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/renderable/base/Renderable_Base_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/renderable/color/Color_Cache_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/renderable/concept/Renderable_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/base/Scene_Base_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene_Memory_Resource_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/concept/Scene_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene2D_Context_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene2D_Render_Order_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene3D_Context_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene_State_Observer_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/state/base/State_Strategy_Base_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/state/Concepts_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/state/Double_State_Strategy_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/state/Triple_State_Strategy_Test.cpp" -) -add_executable(renderive_scene_app "tests/main.cpp") -target_link_libraries(renderive_scene_app PRIVATE renderive_scene) include(CTest) if (BUILD_TESTING) + set(renderive_test_sources + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/Concepts_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/node/Multiway_Node_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/observer/Observer_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/observer/time_source/Steady_Time_Source_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/property/Concepts_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/property/Property_Builder_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/property/Validated_Value_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/base/Frame_Control_Strategy_Base_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/concept/Concepts_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/real_time_data/base/Real_Time_Data_Base_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/real_time_data/Real_Time_Data_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/renderable/base/Renderable_Base_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/renderable/color/Color_Cache_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/renderable/concept/Renderable_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/base/Scene_Base_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene_Memory_Resource_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/concept/Scene_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene2D_Context_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene2D_Render_Order_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene3D_Context_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene_State_Observer_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/state/base/State_Strategy_Base_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/state/Concepts_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/state/Double_State_Strategy_Test.cpp" + "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/state/Triple_State_Strategy_Test.cpp" + ) find_package(GTest CONFIG QUIET) if (NOT GTest_FOUND) add_library(renderive_gtest_compat INTERFACE) @@ -67,8 +76,10 @@ if (BUILD_TESTING) add_library(GTest::gtest ALIAS renderive_gtest_compat) message(STATUS "GTest not found; using the test-only compatibility runner") endif () - target_sources(renderive_scene_app PRIVATE ${renderive_test_sources}) - target_compile_definitions(renderive_scene_app PRIVATE RENDERIVE_WITH_GTEST=1) - target_link_libraries(renderive_scene_app PRIVATE GTest::gtest) - add_test(NAME renderive_scene_tests COMMAND renderive_scene_app --gtest) + add_executable(renderive_scene_tests "${CMAKE_CURRENT_LIST_DIR}/tests/main.cpp" ${renderive_test_sources}) + if (RENDERIVE_TASKFLOW_MOCK) + target_compile_definitions(renderive_scene_tests PRIVATE RENDERIVE_TASKFLOW_MOCK=1) + endif () + target_link_libraries(renderive_scene_tests PRIVATE renderive_scene GTest::gtest) + add_test(NAME renderive_scene_tests COMMAND renderive_scene_tests) endif () diff --git a/Kernel/readme.md b/Kernel/readme.md index 8aea676..46b59b0 100644 --- a/Kernel/readme.md +++ b/Kernel/readme.md @@ -22,7 +22,7 @@ scene.Scene_State_Strategy::publish(); scene.Scene_State_Strategy::render_use_state(); ``` -三缓冲分别承担正在渲染、已发布待获取、前台写入三种职责。`render()` 提交任务前调用 `acquire_render_state()`,后台帧获得稳定的状态版本。 +三缓冲分别承担正在渲染、已发布待获取、前台写入三种职责。`render()` 提交任务前调用 `acquire_render_state()`,后台帧获得稳定的状态版本。`render_use_state()` 返回状态快照,不返回内部缓冲区引用。 ## Scene 观察者 @@ -48,7 +48,7 @@ struct Spectrum : Renderable_Base { `Renderable_Configuration::cache_enabled` 只控制渲染结果是否跨帧复用。默认关闭,以保持每次 Scene 提交都重新渲染的旧语义。Scene2D 仍为每个 Renderable 维护颜色缓存,以支持渲染依赖顺序和显示合成顺序相互独立。 -缓存开启后,首次渲染或调用 `invalidate_cache()` 后执行任务图;缓存有效时跳过内部任务图,直接参与最终颜色合成。缓存关闭时每帧执行。 +缓存开启后,首次渲染或调用 `invalidate_cache()` 后执行任务图;缓存有效时跳过内部任务图,直接参与最终颜色合成。缓存关闭时每帧执行。`configuration()` 返回线程安全的配置快照,配置修改统一通过 `Scene_Base::set_renderable_configuration()` 完成。 ## Renderable 内部任务图 @@ -66,38 +66,48 @@ void build_task_graph(Renderable_Task_Graph& graph) override { } ``` -未覆盖 `build_task_graph` 时,基类自动创建一个调用虚函数 `render()` 的单节点图。 +未覆盖 `build_task_graph` 时,基类自动创建一个调用虚函数 `render()` 的单节点图。`task_graph()` 返回不可变共享快照;`rebuild_task_graph()` 与并发读取串行化,旧快照在调用方释放前保持有效。 ## 外部依赖与总图组合 -`dependency_node` 表达 Renderable 之间的外部依赖,父节点先于子节点。每次后台渲染会: +依赖树表达 Renderable 之间的外部依赖,父节点先于子节点。关系修改统一通过 Scene 完成,例如 `scene.set_dependency_parent(child, parent)`,不会绕过 Scene 的渲染同步边界。每次后台渲染会: 1. 为每个 Renderable 构造或复用其内部图描述。 2. 将内部图转换为一个 Taskflow module task。 -3. 按 `dependency_node` 连接 module task。 +3. 按依赖树连接 module task。 4. 将所有 module task 连接到最终颜色缓存生成任务。 -5. 交给 `tf::Executor` 执行并等待完成。 +5. 交给进程内共享的长期 `tf::Executor` 执行并等待完成。 -Taskflow 只在 `Scene_Base.cpp` 中包含。`renderive_scene` 对 Taskflow include 路径使用 `PRIVATE`,外部头文件不会受到 Taskflow 宏、类型或包含链污染。 +Taskflow 只在 `Scene_Base.cpp` 中包含。`Scene_Base` 通过内部执行上下文访问进程内共享的 `tf::Executor`,Executor 跨 Scene、跨帧复用。`renderive_scene` 对 Taskflow include 路径使用 `PRIVATE`,外部头文件不会受到 Taskflow 宏、类型或包含链污染。 ## 显示树与依赖树 -`display_root` 只决定最终颜色缓存合成顺序。`dependency_root` 只决定 Renderable module task 的执行依赖。两棵树继续保持独立,轴可以先计算但最后合成。 +显示树只决定最终颜色缓存合成顺序,依赖树只决定 Renderable module task 的执行依赖。两棵树继续保持独立,轴可以先计算但最后合成。节点对象不再作为公开拓扑读接口;调用方通过 `topology_snapshot()` 获取线程安全关系快照,父子关系只通过 `set_display_parent()` 和 `set_dependency_parent()` 修改。参与显示树或依赖树的 Renderable 必须先 attach 到所属 Scene,避免拓扑引用未被 Scene 持有的对象。 -## 主程序入口 +## 测试入口 -同一个 `main()` 支持两种模式: - -```text -renderive_scene_app -renderive_scene_app --gtest -``` - -不带参数执行主流程;传入 `--gtest` 后初始化 GTest 并运行测试。其他 GTest 参数可以放在 `--gtest` 后面。 +`BUILD_TESTING=ON` 时构建 `renderive_scene_tests` 并通过 CTest 运行;`BUILD_TESTING=OFF` 时只构建 `renderive_scene` 库,不再编译 `tests/main.cpp` 或任何测试源文件。 ## Taskflow 查找 -CMake 通过 `RENDERIVE_TASKFLOW_ROOT` 或系统 include 路径查找 `taskflow/taskflow.hpp`。Taskflow 不存在时,仅测试和沙盒构建使用 `tests/compat/taskflow/taskflow.hpp` 验证图生成与依赖语义;正式集成时把 `RENDERIVE_TASKFLOW_ROOT` 指向 Taskflow 4.1.0 根目录或 include 目录。 +CMake 通过 `RENDERIVE_TASKFLOW_ROOT` 或系统 include 路径查找 `taskflow/taskflow.hpp`。找到 Taskflow 4.1.0 时使用真实执行器;找不到时使用 `src/renderive/compat/taskflow/taskflow.hpp` 的内置顺序依赖图执行器,使工程仍可完成 CMake 配置、编译并运行依赖图测试。兼容执行器只实现 Renderive 当前使用的 Taskflow 子集,用于验证内部 DAG、Renderable 外部依赖和 module 完成关系是否被正确转换,不模拟并行调度能力。真实 Taskflow 下额外运行并行执行测试。 + + +## Flow MPSC 队列 + +`Flow_Refresh_Strategy` 使用 `boost::lockfree::queue` 作为多生产者、单消费者帧队列。多个 painter 线程可以并发发布帧,renderer 按队列顺序逐个取得所有已发布帧。Frame 对象本身继续由传入的 `std::pmr::memory_resource` 分配,Boost.Lockfree 队列内部节点由 Boost 自己的 allocator 管理。 + +Flow 的 observer 允许同线程重入 Observer_State;renderer observer 中再次申请同一个 Flow renderer 会立即得到空 lease,不会等待当前 renderer lease 自身释放。 + +## 实时数据附件所有权 + +`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 内存域 @@ -140,7 +150,7 @@ auto renderable = std::make_shared(scene, configuration); Spectrum_Renderable stack_renderable(scene, configuration); ``` -独立模式下,Renderable 对象本身和 `shared_ptr` 控制块由调用方选择的机制分配;`Multiway_Node`、`Renderable_Task_Graph`、任务名称和依赖边仍使用所属 Scene 的内存域。 +独立模式下,Renderable 对象本身和 `shared_ptr` 控制块由调用方选择的机制分配;`Multiway_Node`、`Renderable_Task_Graph`、任务名称和依赖边仍使用所属 Scene 的内存域。Renderable 可以晚于 Scene 析构以完成自身释放;Scene 销毁后实时数据绑定自动失效,继续调用 `scene()` 会抛出 `std::logic_error`,不会返回悬空 Scene 引用。 ## 独立 allocator-aware 组件 @@ -165,6 +175,7 @@ History_Real_Time_Data> history(memory_resource); 以下内存不属于 Renderive 自己的 allocator-aware 场景域: - Taskflow 4.1.0 内部任务节点、执行队列和 executor 工作资源 +- Boost.Lockfree Flow 队列内部节点 - `std::function` 超出小对象优化后的内部存储 - `std::thread` 的系统线程对象和线程栈 - 异常运行时对象 diff --git a/Kernel/src/renderive/base/node/Multiway_Node.hpp b/Kernel/src/renderive/base/node/Multiway_Node.hpp index 5105b5e..7058712 100644 --- a/Kernel/src/renderive/base/node/Multiway_Node.hpp +++ b/Kernel/src/renderive/base/node/Multiway_Node.hpp @@ -8,44 +8,81 @@ template class Multiway_Node { public: explicit Multiway_Node(Owner* owner = nullptr, std::pmr::memory_resource& memory_resource = *std::pmr::get_default_resource()) noexcept - : owner(owner), children(&memory_resource) {} + : owner_(owner), children_(&memory_resource) {} Multiway_Node(const Multiway_Node&) = delete; Multiway_Node& operator=(const Multiway_Node&) = delete; Multiway_Node(Multiway_Node&&) = delete; Multiway_Node& operator=(Multiway_Node&&) = delete; ~Multiway_Node() { detach(); - for (Multiway_Node* child : children) { - child->parent = nullptr; + for (Multiway_Node* child : children_) { + child->parent_ = nullptr; } } void append_child(Multiway_Node& child) { - insert_child(children.size(), child); + insert_child(children_.size(), child); } void insert_child(std::size_t index, Multiway_Node& child) { + if (index > children_.size()) { + throw std::out_of_range("multiway node child index"); + } if (&child == this || child.is_ancestor_of(*this)) { throw std::invalid_argument("multiway node cycle"); } + if (child.parent_ == this) { + const auto current = static_cast(std::find(children_.begin(), children_.end(), &child) - children_.begin()); + if (current < index) { + --index; + } + if (current == index) { + return; + } + } child.detach(); - child.parent = this; - children.insert(children.begin() + static_cast(index), &child); + child.parent_ = this; + children_.insert(children_.begin() + static_cast(index), &child); } void detach() noexcept { - if (!parent) { + if (!parent_) { return; } - std::erase(parent->children, this); - parent = nullptr; + std::erase(parent_->children_, this); + parent_ = nullptr; } bool is_ancestor_of(const Multiway_Node& node) const noexcept { - for (const Multiway_Node* current = node.parent; current; current = current->parent) { + for (const Multiway_Node* current = node.parent_; current; current = current->parent_) { if (current == this) { return true; } } return false; } - Owner* owner{}; - Multiway_Node* parent{}; - std::pmr::vector children; + Owner* owner() noexcept { + return owner_; + } + const Owner* owner() const noexcept { + return owner_; + } + Multiway_Node* parent() noexcept { + return parent_; + } + const Multiway_Node* parent() const noexcept { + return parent_; + } + std::size_t child_count() const noexcept { + return children_.size(); + } + bool children_empty() const noexcept { + return children_.empty(); + } + Multiway_Node& child(std::size_t index) { + return *children_.at(index); + } + const Multiway_Node& child(std::size_t index) const { + return *children_.at(index); + } +private: + Owner* owner_{}; + Multiway_Node* parent_{}; + std::pmr::vector children_; }; diff --git a/Kernel/src/renderive/base/node/concept/Multiway_Node.hpp b/Kernel/src/renderive/base/node/concept/Multiway_Node.hpp index 5dc090b..d32ece6 100644 --- a/Kernel/src/renderive/base/node/concept/Multiway_Node.hpp +++ b/Kernel/src/renderive/base/node/concept/Multiway_Node.hpp @@ -1,11 +1,14 @@ #pragma once #include +#include #include "renderive/base/node/Multiway_Node.hpp" template -concept Multiway_Node_Type = std::same_as> && requires(Node& node, Node& child) { - { node.owner } -> std::convertible_to; - { node.parent } -> std::convertible_to; - node.children; +concept Multiway_Node_Type = std::same_as> && requires(Node& node, const Node& const_node, Node& child) { + { node.owner() } -> std::same_as; + { const_node.owner() } -> std::same_as; + { node.parent() } -> std::same_as; + { const_node.parent() } -> std::same_as; + { const_node.child_count() } -> std::same_as; { node.append_child(child) } -> std::same_as; { child.detach() } -> std::same_as; }; diff --git a/Kernel/src/renderive/base/observer/Observer_State.hpp b/Kernel/src/renderive/base/observer/Observer_State.hpp index b573f5f..41cf73f 100644 --- a/Kernel/src/renderive/base/observer/Observer_State.hpp +++ b/Kernel/src/renderive/base/observer/Observer_State.hpp @@ -8,7 +8,7 @@ #include "concept/Observer.hpp" #include "concept/Time_Source.hpp" #include "time_source/Steady_Time_Source.hpp" -template +template class Observer_State { public: static constexpr bool enabled = Observer::enabled; @@ -24,14 +24,12 @@ public: return static_cast(time_source_.now_ns()); } template - requires requires(Observer& observer, Target& target) { - { observer.bind(target) } -> std::same_as; + requires Observer::enabled && requires(Observer& observer, Target& target) { + observer.bind(target); } - void bind(Target& target) { - if constexpr (Observer::enabled) { - std::lock_guard lock(mutex_); - observer_.bind(target); - } + decltype(auto) bind(Target& target) { + std::lock_guard lock(mutex_); + return observer_.bind(target); } template requires Struct_Observer diff --git a/Kernel/src/renderive/compat/taskflow/taskflow.hpp b/Kernel/src/renderive/compat/taskflow/taskflow.hpp new file mode 100644 index 0000000..8890ad8 --- /dev/null +++ b/Kernel/src/renderive/compat/taskflow/taskflow.hpp @@ -0,0 +1,129 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#define TF_VERSION 400100 +namespace tf { +class Taskflow; +class Task { +public: + Task() = default; + Task& name(std::string name); + void precede(Task task) const; +private: + friend class Taskflow; + Task(Taskflow& owner, std::size_t index) noexcept : owner_(&owner), index_(index) {} + Taskflow* owner_{}; + std::size_t index_{}; +}; +class Taskflow { +public: + Taskflow() = default; + Taskflow(const Taskflow&) = delete; + Taskflow& operator=(const Taskflow&) = delete; + Taskflow(Taskflow&&) = default; + Taskflow& operator=(Taskflow&&) = default; + template + Task emplace(Work&& work) { + nodes_.push_back({std::function(std::forward(work)), nullptr, {}, {}}); + return Task(*this, nodes_.size() - 1); + } + Task composed_of(Taskflow& module) { + nodes_.push_back({{}, &module, {}, {}}); + return Task(*this, nodes_.size() - 1); + } +private: + friend class Task; + friend class Executor; + struct Node { + std::function work; + Taskflow* module{}; + std::string name; + std::vector successors; + }; + void precede(std::size_t from, std::size_t to) { + if (from >= nodes_.size() || to >= nodes_.size()) { + throw std::out_of_range("taskflow compatibility task index"); + } + nodes_[from].successors.push_back(to); + } + void set_name(std::size_t index, std::string name) { + nodes_.at(index).name = std::move(name); + } + void run() { + std::vector indegrees(nodes_.size()); + for (const Node& node : nodes_) { + for (std::size_t successor : node.successors) { + if (successor >= nodes_.size()) { + throw std::out_of_range("taskflow compatibility successor index"); + } + ++indegrees[successor]; + } + } + std::deque ready; + for (std::size_t index = 0; index < indegrees.size(); ++index) { + if (indegrees[index] == 0) { + ready.push_back(index); + } + } + std::size_t completed{}; + while (!ready.empty()) { + const std::size_t index = ready.front(); + ready.pop_front(); + Node& node = nodes_[index]; + if (node.module) { + node.module->run(); + } else if (node.work) { + node.work(); + } + ++completed; + for (std::size_t successor : node.successors) { + if (--indegrees[successor] == 0) { + ready.push_back(successor); + } + } + } + if (completed != nodes_.size()) { + throw std::runtime_error("taskflow compatibility graph contains a cycle"); + } + } + std::vector nodes_; +}; +inline Task& Task::name(std::string name) { + owner_->set_name(index_, std::move(name)); + return *this; +} +inline void Task::precede(Task task) const { + if (!owner_ || owner_ != task.owner_) { + throw std::invalid_argument("taskflow compatibility tasks belong to different graphs"); + } + owner_->precede(index_, task.index_); +} +class Future { +public: + explicit Future(std::exception_ptr exception) noexcept : exception_(std::move(exception)) {} + void get() { + if (exception_) { + std::rethrow_exception(exception_); + } + } +private: + std::exception_ptr exception_; +}; +class Executor { +public: + Future run(Taskflow& graph) { + try { + graph.run(); + return Future(nullptr); + } catch (...) { + return Future(std::current_exception()); + } + } +}; +} diff --git a/Kernel/src/renderive/frame_control/base/Frame_Control_Strategy_Base.hpp b/Kernel/src/renderive/frame_control/base/Frame_Control_Strategy_Base.hpp index e000152..83e0c72 100644 --- a/Kernel/src/renderive/frame_control/base/Frame_Control_Strategy_Base.hpp +++ b/Kernel/src/renderive/frame_control/base/Frame_Control_Strategy_Base.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -24,6 +25,16 @@ public: static constexpr double invalid_frequency_hz() noexcept { return std::numeric_limits::quiet_NaN(); } + static std::uint64_t frequency_interval_ns(double frequency_hz) noexcept { + if (!std::isfinite(frequency_hz) || frequency_hz <= 0.0) { + return 0; + } + const double interval_ns = 1'000'000'000.0 / frequency_hz; + if (!std::isfinite(interval_ns) || interval_ns >= static_cast(std::numeric_limits::max())) { + return std::numeric_limits::max(); + } + return static_cast(interval_ns); + } protected: explicit Frame_Control_Strategy_Base(double frequency_hz = invalid_frequency_hz(), std::uint64_t next_refresh_interval_ns = 0) : states_{{frequency_hz, next_refresh_interval_ns, 0, 0}, {frequency_hz, next_refresh_interval_ns, 0, 0}}, render_state_(&states_[0]), cache_state_(&states_[1]) {} diff --git a/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.hpp b/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.hpp index 4bcb693..2d094c5 100644 --- a/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.hpp @@ -2,11 +2,9 @@ #include #include #include -#include #include #include #include -#include #include "renderive/base/Concepts.hpp" #include "renderive/base/memory/Memory_Resource.hpp" #include "renderive/base/observer/Observer.hpp" @@ -89,6 +87,7 @@ public: const Frame* get() const; private: Flow_Refresh_Strategy* strategy_{}; + Flow_Refresh_Strategy* previous_render_strategy_{}; Memory_Resource_Unique_Ptr frame_; std::unique_lock lease_lock_; }; @@ -106,6 +105,7 @@ public: private: std::uint64_t now_ns() const noexcept; void observe(const Observation& observation) noexcept; + inline static thread_local Flow_Refresh_Strategy* rendering_strategy_{}; std::pmr::memory_resource* const memory_resource_; Observer observer_; std::pmr::deque> frames_; diff --git a/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.inl b/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.inl index 4a925ce..4a9ecb8 100644 --- a/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.inl +++ b/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.inl @@ -60,7 +60,13 @@ auto Flow_Refresh_Strategy::Painter_Lease::get() c } template Flow_Refresh_Strategy::Render_Lease::Render_Lease(Flow_Refresh_Strategy& strategy) - : strategy_(&strategy), lease_lock_(strategy.render_mutex_) { + : strategy_(&strategy) { + if (rendering_strategy_ == &strategy) { + strategy_ = nullptr; + return; + } + lease_lock_ = std::unique_lock(strategy.render_mutex_); + previous_render_strategy_ = std::exchange(rendering_strategy_, &strategy); Observation observation; bool has_frame{}; { @@ -78,18 +84,20 @@ Flow_Refresh_Strategy::Render_Lease::Render_Lease( has_frame = true; } else { ++strategy.state_.empty_acquire_count; + strategy.state_.pending_frame_count = 0; strategy.update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); observation = {Observation_Event::queue_empty, {}, strategy.state_, strategy.last_real_time_data_update_}; } } - strategy.observe(observation); if (!has_frame) { lease_lock_.unlock(); + rendering_strategy_ = previous_render_strategy_; } + strategy.observe(observation); } template Flow_Refresh_Strategy::Render_Lease::Render_Lease(Render_Lease&& other) noexcept - : strategy_(std::exchange(other.strategy_, nullptr)), frame_(std::move(other.frame_)), lease_lock_(std::move(other.lease_lock_)) {} + : strategy_(std::exchange(other.strategy_, nullptr)), previous_render_strategy_(other.previous_render_strategy_), frame_(std::move(other.frame_)), lease_lock_(std::move(other.lease_lock_)) {} template Flow_Refresh_Strategy::Render_Lease::~Render_Lease() { if (!strategy_ || !frame_) { @@ -101,10 +109,12 @@ Flow_Refresh_Strategy::Render_Lease::~Render_Lease { std::lock_guard lock(strategy_->state_mutex_); ++strategy_->state_.rendered_frame_count; + strategy_->state_.pending_frame_count = strategy_->frames_.size(); strategy_->update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); observation = {Observation_Event::rendered, frame_->statistics, strategy_->state_, strategy_->last_real_time_data_update_}; } lease_lock_.unlock(); + rendering_strategy_ = previous_render_strategy_; strategy_->observe(observation); } template @@ -173,7 +183,9 @@ std::size_t Flow_Refresh_Strategy::pending_frame_c template auto Flow_Refresh_Strategy::state() const -> State { std::lock_guard lock(state_mutex_); - return state_; + State result = state_; + result.pending_frame_count = frames_.size(); + return result; } template void Flow_Refresh_Strategy::on_real_time_data_update(const Real_Time_Data_Observation& observation) { diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp index 755f6ad..8161bcc 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -150,7 +151,7 @@ public: bool discard_pending_frame_before(std::uint64_t real_time_data_update_sequence); bool discard_stale_latest_data_frame() override; private: - static std::uint64_t frequency_interval_ns(double frequency_hz) noexcept; + static double checked_frequency_hz(double frequency_hz); std::uint64_t now_ns() const noexcept; void update_state(const Frame& frame); void observe(const Observation& observation) noexcept; diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl index 47aa213..69e9504 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl @@ -1,5 +1,6 @@ #pragma once #include +#include template Low_Latency_Strategy::Painter_Lease::Painter_Lease(Low_Latency_Strategy& strategy) : strategy_(&strategy), request_time_ns_(strategy.now_ns()), lease_lock_(strategy.painter_mutex_) { @@ -203,7 +204,7 @@ auto Low_Latency_Strategy::Render_Lease::get() con } template Low_Latency_Strategy::Low_Latency_Strategy(Observer observer, Configuration configuration) - : Frame_Control_Strategy_Base(configuration.frequency_hz, frequency_interval_ns(configuration.frequency_hz)), observer_(std::move(observer)), configuration_(configuration), paint_(&frames_[0]), cache_(&frames_[1]), render_(&frames_[2]) { + : Frame_Control_Strategy_Base(checked_frequency_hz(configuration.frequency_hz), frequency_interval_ns(configuration.frequency_hz)), observer_(std::move(observer)), configuration_(configuration), paint_(&frames_[0]), cache_(&frames_[1]), render_(&frames_[2]) { state_.frequency_hz = configuration_.frequency_hz; state_.target_interval_ns = frequency_interval_ns(configuration_.frequency_hz); state_.next_refresh_interval_ns = state_.target_interval_ns; @@ -218,6 +219,7 @@ auto Low_Latency_Strategy::acquire_renderer() -> R } template void Low_Latency_Strategy::set_frequency_hz(double frequency_hz) { + frequency_hz = checked_frequency_hz(frequency_hz); std::lock_guard lock(state_mutex_); configuration_.frequency_hz = frequency_hz; } @@ -303,11 +305,11 @@ bool Low_Latency_Strategy::discard_stale_latest_da return discarded; } template -std::uint64_t Low_Latency_Strategy::frequency_interval_ns(double frequency_hz) noexcept { - if (frequency_hz <= 0.0) { - return 0; +double Low_Latency_Strategy::checked_frequency_hz(double frequency_hz) { + if (!std::isfinite(frequency_hz)) { + throw std::invalid_argument("frequency_hz must be finite"); } - return static_cast(1'000'000'000.0 / frequency_hz); + return frequency_hz; } template std::uint64_t Low_Latency_Strategy::now_ns() const noexcept { diff --git a/Kernel/src/renderive/real_time_data/Attach_Real_Time_Data.hpp b/Kernel/src/renderive/real_time_data/Attach_Real_Time_Data.hpp index a0fe07f..1a1c82e 100644 --- a/Kernel/src/renderive/real_time_data/Attach_Real_Time_Data.hpp +++ b/Kernel/src/renderive/real_time_data/Attach_Real_Time_Data.hpp @@ -1,7 +1,10 @@ #pragma once -#include +#include +#include +#include #include #include +#include #include #include #include @@ -11,11 +14,11 @@ #include "renderive/scene/base/Scene_Render_Context.hpp" template struct With_Real_Time_Data { - explicit With_Real_Time_Data(Data&... data) : data(&data...) {} - std::tuple data; + explicit With_Real_Time_Data(std::shared_ptr... data) : data(std::move(data)...) {} + std::tuple...> data; }; template -With_Real_Time_Data(Data&...) -> With_Real_Time_Data; +With_Real_Time_Data(std::shared_ptr...) -> With_Real_Time_Data; struct Real_Time_Data_Attachment_State { std::size_t source_count{}; std::uint64_t revision_sum{}; @@ -30,33 +33,30 @@ class Attach_Real_Time_Data : public Renderable_Type { public: template explicit Attach_Real_Time_Data(With_Real_Time_Data attachment, Args&&... args) - : Renderable_Type(std::forward(args)...), data_(attachment.data) { - std::apply([self = this](auto*... data) { - (self->bind_data(*data), ...); - }, data_); + : Renderable_Type(std::forward(args)...), data_(std::move(attachment.data)) { + bind_data(std::index_sequence_for{}); } template requires ((std::same_as || ...)) Data_Type& real_time_data() { - return *std::get(data_); + return *std::get>(data_); } template requires ((std::same_as || ...)) const Data_Type& real_time_data() const { - return *std::get(data_); + return *std::get>(data_); } std::size_t discard_real_time_data() { - if (real_time_data_discard_mode != Real_Time_Data_Discard_Mode::retain_frame_interval || !this->scene || !this->scene->frame_control_strategy) { + if (real_time_data_discard_mode.load(std::memory_order_acquire) != Real_Time_Data_Discard_Mode::retain_frame_interval) { return 0; } - const double frequency_hz = this->scene->frame_control_strategy->frequency_hz(); - if (!std::isfinite(frequency_hz) || frequency_hz <= 0.0) { + const std::uint64_t interval_ns = Frame_Control_Strategy_Base::frequency_interval_ns(this->scene().frame_control_strategy().frequency_hz()); + if (interval_ns == 0) { return 0; } - const std::uint64_t interval_ns = static_cast(1'000'000'000.0 / frequency_hz); std::size_t discarded{}; - std::apply([interval_ns, &discarded](auto*... data) { - ([interval_ns, &discarded, data] { + std::apply([interval_ns, &discarded](const auto&... data) { + ([interval_ns, &discarded, &data] { const auto state = data->update_state(); const std::uint64_t threshold_ns = state.update_time_ns > interval_ns ? state.update_time_ns - interval_ns : 0; discarded += data->discard_before_time_ns(threshold_ns); @@ -66,8 +66,8 @@ public: } Real_Time_Data_Attachment_State real_time_data_attachment_state() const { Real_Time_Data_Attachment_State state{sizeof...(Data), 0, 0}; - std::apply([&state](const auto*... data) { - ([&state, data] { + std::apply([&state](const auto&... data) { + ([&state, &data] { const auto update = data->update_state(); state.revision_sum += update.revision; state.total_update_count += update.total_update_count; @@ -79,17 +79,28 @@ public: discard_real_time_data(); Renderable_Type::render(context); } - Real_Time_Data_Discard_Mode real_time_data_discard_mode{Real_Time_Data_Discard_Mode::retain_all}; + std::atomic real_time_data_discard_mode{Real_Time_Data_Discard_Mode::retain_all}; private: - template + template + void bind_data(std::index_sequence) { + (bind_data(*std::get(data_)), ...); + } + template void bind_data(Data_Type& data) { if constexpr (requires { data.bind(*this); }) { - data.bind(*this); + using Binding = decltype(data.bind(*this)); + if constexpr (std::is_void_v) { + data.bind(*this); + } else { + static_assert(std::same_as, std::shared_ptr>); + bindings_[Index] = data.bind(*this); + } } } static_assert(sizeof...(Data) > 0); static_assert((std::is_class_v && ...)); - std::tuple data_; + std::tuple...> data_; + std::array, sizeof...(Data)> bindings_; }; diff --git a/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp b/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp index 993a97b..1df5ac5 100644 --- a/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp +++ b/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp @@ -1,27 +1,130 @@ #pragma once +#include +#include #include +#include +#include +#include +#include #include "Observation.hpp" #include "renderive/renderable/base/Renderable_Base.hpp" #include "renderive/scene/base/Scene_Base.hpp" class Frame_Strategy_Real_Time_Data_Observer { public: static constexpr bool enabled = true; - Frame_Strategy_Real_Time_Data_Observer() = default; - explicit Frame_Strategy_Real_Time_Data_Observer(Renderable_Base& renderable) : renderable_(&renderable) {} - void bind(Renderable_Base& renderable) { - renderable_ = &renderable; + Frame_Strategy_Real_Time_Data_Observer() : state_(std::make_shared()) {} + explicit Frame_Strategy_Real_Time_Data_Observer(Renderable_Base& renderable) : Frame_Strategy_Real_Time_Data_Observer() { + direct_binding_ = bind(renderable); + } + std::shared_ptr bind(Renderable_Base& renderable) { + const auto target = renderable.real_time_data_state_; + std::shared_ptr entry; + { + std::lock_guard lock(state_->mutex); + for (const auto& value : *state_->entries) { + if (value->active.load(std::memory_order_acquire) && value->target == target) { + ++value->binding_count; + entry = value; + break; + } + } + if (!entry) { + entry = std::make_shared(target); + auto entries = std::make_shared(); + entries->reserve(state_->entries->size() + 1); + for (const auto& value : *state_->entries) { + if (value->active.load(std::memory_order_acquire)) { + entries->push_back(value); + } + } + entries->push_back(entry); + state_->entries = std::move(entries); + } + } + return std::make_shared(state_, std::move(entry)); } void observe(const Real_Time_Data_Observation& observation) noexcept { - if (!renderable_ || !renderable_->scene || !renderable_->scene->frame_control_strategy) { - return; + std::shared_ptr entries; + { + std::lock_guard lock(state_->mutex); + entries = state_->entries; } - Frame_Control_Strategy_Base* strategy = renderable_->scene->frame_control_strategy; - strategy->on_real_time_data_update(observation); - const double frequency_hz = strategy->frequency_hz(); - if (observation.state.retention == Real_Time_Data_Retention::latest && renderable_->discard_stale_frame_on_latest_data_update && std::isfinite(frequency_hz) && frequency_hz > 0.0) { - strategy->discard_stale_latest_data_frame(); + for (std::size_t index = 0; index < entries->size(); ++index) { + const auto& entry = (*entries)[index]; + if (!entry->active.load(std::memory_order_acquire) || !entry->target->renderable_alive.load(std::memory_order_acquire)) { + continue; + } + const auto& lifetime = entry->target->scene_lifetime; + bool first_for_scene = true; + for (std::size_t previous = 0; previous < index; ++previous) { + const auto& previous_entry = (*entries)[previous]; + if (previous_entry->active.load(std::memory_order_acquire) && previous_entry->target->renderable_alive.load(std::memory_order_acquire) && previous_entry->target->scene_lifetime.get() == lifetime.get()) { + first_for_scene = false; + break; + } + } + if (!first_for_scene) { + continue; + } + bool discard_stale_frame{}; + if (observation.state.retention == Real_Time_Data_Retention::latest) { + for (const auto& value : *entries) { + if (value->active.load(std::memory_order_acquire) && value->target->renderable_alive.load(std::memory_order_acquire) && value->target->scene_lifetime.get() == lifetime.get() && value->target->discard_stale_frame_on_latest_data_update.load(std::memory_order_acquire)) { + discard_stale_frame = true; + break; + } + } + } + auto lease = lifetime->acquire(); + if (!lease) { + continue; + } + auto& strategy = lease.scene().frame_control_strategy(); + strategy.on_real_time_data_update(observation); + const double frequency_hz = strategy.frequency_hz(); + if (discard_stale_frame && std::isfinite(frequency_hz) && frequency_hz > 0.0) { + strategy.discard_stale_latest_data_frame(); + } } } private: - Renderable_Base* renderable_{}; + struct Entry { + explicit Entry(std::shared_ptr target) : target(std::move(target)) {} + std::shared_ptr target; + std::size_t binding_count{1}; + std::atomic active{true}; + }; + using Entry_List = std::vector>; + struct State { + std::mutex mutex; + std::shared_ptr entries{std::make_shared()}; + }; + class Binding { + public: + Binding(std::shared_ptr state, std::shared_ptr entry) : state_(std::move(state)), entry_(std::move(entry)) {} + ~Binding() { + std::lock_guard lock(state_->mutex); + if (entry_->binding_count > 1) { + --entry_->binding_count; + return; + } + entry_->binding_count = 0; + entry_->active.store(false, std::memory_order_release); + try { + auto entries = std::make_shared(); + entries->reserve(state_->entries->size()); + for (const auto& value : *state_->entries) { + if (value->active.load(std::memory_order_acquire)) { + entries->push_back(value); + } + } + state_->entries = std::move(entries); + } catch (...) {} + } + private: + std::shared_ptr state_; + std::shared_ptr entry_; + }; + std::shared_ptr state_; + std::shared_ptr direct_binding_; }; diff --git a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp index 2c0c090..3e7d968 100644 --- a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp +++ b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp @@ -33,10 +33,10 @@ public: std::size_t discard_before_time_ns(std::uint64_t time_ns) override; template requires requires(Observer& observer, Target& target) { - { observer.bind(target) } -> std::same_as; + observer.bind(target); } - void bind(Target& target) { - observer_.bind(target); + decltype(auto) bind(Target& target) { + return observer_.bind(target); } private: static Container make_container(std::pmr::memory_resource& memory_resource); diff --git a/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.hpp b/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.hpp index 4e09bb4..582b1e3 100644 --- a/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.hpp +++ b/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.hpp @@ -27,10 +27,10 @@ public: std::size_t discard_before_time_ns(std::uint64_t time_ns) override; template requires requires(Observer& observer, Target& target) { - { observer.bind(target) } -> std::same_as; + observer.bind(target); } - void bind(Target& target) { - observer_.bind(target); + decltype(auto) bind(Target& target) { + return observer_.bind(target); } private: mutable Mutex mutex_; diff --git a/Kernel/src/renderive/renderable/Renderable_Task_Graph.cpp b/Kernel/src/renderive/renderable/Renderable_Task_Graph.cpp index 671756f..dfa62ba 100644 --- a/Kernel/src/renderive/renderable/Renderable_Task_Graph.cpp +++ b/Kernel/src/renderive/renderable/Renderable_Task_Graph.cpp @@ -5,16 +5,19 @@ Renderable_Task_Graph::Renderable_Task_Graph(std::pmr::memory_resource& memory_r : memory_resource_(&memory_resource), nodes_(&memory_resource) {} Renderable_Task_Graph::Task Renderable_Task_Graph::emplace(Task_Function function, std::string name) { nodes_.emplace_back(std::move(function), std::move(name), *memory_resource_); - return {nodes_.size() - 1}; + return {this, generation_, nodes_.size() - 1}; } void Renderable_Task_Graph::precede(Task from, Task to) { - if (from.index >= nodes_.size() || to.index >= nodes_.size()) { - throw std::out_of_range("renderable task index"); + validate_task(from); + validate_task(to); + if (reaches(to.index, from.index)) { + throw std::invalid_argument("renderable task graph cycle"); } nodes_[from.index].successors.push_back(to.index); } void Renderable_Task_Graph::clear() { nodes_.clear(); + ++generation_; } bool Renderable_Task_Graph::empty() const noexcept { return nodes_.empty(); @@ -22,3 +25,39 @@ bool Renderable_Task_Graph::empty() const noexcept { const std::pmr::vector& Renderable_Task_Graph::nodes() const noexcept { return nodes_; } +void Renderable_Task_Graph::validate_task(Task task) const { + if (task.graph != this) { + throw std::invalid_argument("renderable tasks belong to different graphs"); + } + if (task.generation != generation_) { + throw std::invalid_argument("renderable task is stale"); + } + if (task.index >= nodes_.size()) { + throw std::out_of_range("renderable task index"); + } +} +bool Renderable_Task_Graph::reaches(std::size_t from, std::size_t target) const { + if (from == target) { + return true; + } + std::pmr::vector stack(memory_resource_); + std::pmr::vector visited(nodes_.size(), false, memory_resource_); + stack.push_back(from); + while (!stack.empty()) { + const std::size_t index = stack.back(); + stack.pop_back(); + if (visited[index]) { + continue; + } + visited[index] = true; + for (std::size_t successor : nodes_[index].successors) { + if (successor == target) { + return true; + } + if (!visited[successor]) { + stack.push_back(successor); + } + } + } + return false; +} diff --git a/Kernel/src/renderive/renderable/Renderable_Task_Graph.hpp b/Kernel/src/renderive/renderable/Renderable_Task_Graph.hpp index 850c67c..78b113d 100644 --- a/Kernel/src/renderive/renderable/Renderable_Task_Graph.hpp +++ b/Kernel/src/renderive/renderable/Renderable_Task_Graph.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -9,6 +10,8 @@ class Renderable_Task_Graph { public: using Task_Function = std::function; struct Task { + const Renderable_Task_Graph* graph{}; + std::uint64_t generation{}; std::size_t index{}; friend bool operator==(Task, Task) = default; }; @@ -26,6 +29,9 @@ public: bool empty() const noexcept; const std::pmr::vector& nodes() const noexcept; private: + void validate_task(Task task) const; + bool reaches(std::size_t from, std::size_t target) const; std::pmr::memory_resource* const memory_resource_; std::pmr::vector nodes_; + std::uint64_t generation_{1}; }; diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base.cpp b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp index 847b9ce..e934d98 100644 --- a/Kernel/src/renderive/renderable/base/Renderable_Base.cpp +++ b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp @@ -1,8 +1,11 @@ #include "Renderable_Base.hpp" +#include #include "renderive/scene/base/Scene_Base.hpp" -Renderable_Base::Renderable_Base(Scene_Base& scene, Renderable_Configuration configuration) noexcept - : scene(&scene), layer_node(this, scene.memory_resource()), dependency_node(this, scene.memory_resource()), configuration(configuration), memory_resource_(&scene.memory_resource()), task_graph_(*memory_resource_) {} -Renderable_Base::~Renderable_Base() = default; +Renderable_Base::Renderable_Base(Scene_Base& scene, Renderable_Configuration configuration) + : memory_domain_(scene.memory_domain_), real_time_data_state_(std::allocate_shared(Scene_Memory_Allocator{memory_domain_}, scene.scene_lifetime_)), discard_stale_frame_on_latest_data_update(real_time_data_state_->discard_stale_frame_on_latest_data_update), layer_node_(this, memory_domain_->resource()), dependency_node_(this, memory_domain_->resource()), configuration_(configuration) {} +Renderable_Base::~Renderable_Base() { + real_time_data_state_->renderable_alive.store(false, std::memory_order_release); +} void Renderable_Base::render(const Scene_Render_Context&) {} void Renderable_Base::invalidate_cache() noexcept { cache_revision_.fetch_add(1, std::memory_order_release); @@ -14,22 +17,38 @@ std::uint64_t Renderable_Base::rendered_cache_revision() const noexcept { return rendered_cache_revision_.load(std::memory_order_acquire); } bool Renderable_Base::cache_valid() const noexcept { - return configuration.cache_enabled && rendered_cache_revision() == cache_revision(); + return configuration().cache_enabled && rendered_cache_revision() == cache_revision(); } void Renderable_Base::rebuild_task_graph() noexcept { - task_graph_ready_.store(false, std::memory_order_release); + { + std::lock_guard lock(task_graph_mutex_); + ++task_graph_revision_; + } invalidate_cache(); } -const Renderable_Task_Graph& Renderable_Base::task_graph() { - if (!task_graph_ready_.load(std::memory_order_acquire)) { - task_graph_.clear(); - build_task_graph(task_graph_); - task_graph_ready_.store(true, std::memory_order_release); +std::shared_ptr Renderable_Base::task_graph() { + std::lock_guard lock(task_graph_mutex_); + if (built_task_graph_revision_ != task_graph_revision_) { + auto graph = std::allocate_shared(Scene_Memory_Allocator{memory_domain_}, memory_domain_->resource()); + build_task_graph(*graph); + task_graph_ = std::move(graph); + built_task_graph_revision_ = task_graph_revision_; } return task_graph_; } std::pmr::memory_resource& Renderable_Base::memory_resource() const noexcept { - return *memory_resource_; + return memory_domain_->resource(); +} +Scene_Base& Renderable_Base::scene() const { + auto lease = real_time_data_state_->scene_lifetime->acquire(); + if (!lease) { + throw std::logic_error("renderable scene is no longer alive"); + } + return lease.scene(); +} +Renderable_Configuration Renderable_Base::configuration() const noexcept { + std::lock_guard lock(configuration_mutex_); + return configuration_; } void Renderable_Base::build_task_graph(Renderable_Task_Graph& graph) { graph.emplace([this](const Scene_Render_Context& context) { @@ -37,8 +56,13 @@ void Renderable_Base::build_task_graph(Renderable_Task_Graph& graph) { }, "render"); } bool Renderable_Base::requires_render() const noexcept { - return !configuration.cache_enabled || !cache_valid(); + const auto config = configuration(); + return !config.cache_enabled || rendered_cache_revision() != cache_revision(); } -void Renderable_Base::mark_rendered() noexcept { - rendered_cache_revision_.store(cache_revision(), std::memory_order_release); +void Renderable_Base::mark_rendered(std::uint64_t revision) noexcept { + rendered_cache_revision_.store(revision, std::memory_order_release); +} +void Renderable_Base::set_configuration(Renderable_Configuration configuration) noexcept { + std::lock_guard lock(configuration_mutex_); + configuration_ = configuration; } diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base.hpp b/Kernel/src/renderive/renderable/base/Renderable_Base.hpp index 49061cd..3230016 100644 --- a/Kernel/src/renderive/renderable/base/Renderable_Base.hpp +++ b/Kernel/src/renderive/renderable/base/Renderable_Base.hpp @@ -1,9 +1,15 @@ #pragma once #include #include +#include #include +#include +#include +#include "renderive/base/memory/Memory_Resource.hpp" #include "renderive/base/node/Multiway_Node.hpp" #include "renderive/renderable/Renderable_Task_Graph.hpp" +#include "renderive/scene/base/Scene_Lifetime.hpp" +class Frame_Strategy_Real_Time_Data_Observer; class Scene_Base; struct Renderable_Layer_Node_Tag {}; struct Renderable_Dependency_Node_Tag {}; @@ -11,10 +17,19 @@ struct Renderable_Configuration { bool cache_enabled{false}; }; class Renderable_Base { +private: + struct Real_Time_Data_State { + explicit Real_Time_Data_State(std::shared_ptr scene_lifetime) : scene_lifetime(std::move(scene_lifetime)) {} + std::shared_ptr scene_lifetime; + std::atomic renderable_alive{true}; + std::atomic discard_stale_frame_on_latest_data_update{}; + }; + std::shared_ptr memory_domain_; + std::shared_ptr real_time_data_state_; public: using Layer_Node = Multiway_Node; using Dependency_Node = Multiway_Node; - explicit Renderable_Base(Scene_Base& scene, Renderable_Configuration configuration = {}) noexcept; + explicit Renderable_Base(Scene_Base& scene, Renderable_Configuration configuration = {}); virtual ~Renderable_Base(); virtual void render(const Scene_Render_Context& context); void invalidate_cache() noexcept; @@ -22,22 +37,27 @@ public: std::uint64_t rendered_cache_revision() const noexcept; bool cache_valid() const noexcept; void rebuild_task_graph() noexcept; - const Renderable_Task_Graph& task_graph(); + std::shared_ptr task_graph(); std::pmr::memory_resource& memory_resource() const noexcept; - Scene_Base* scene; - Layer_Node layer_node; - Dependency_Node dependency_node; - Renderable_Configuration configuration; - bool discard_stale_frame_on_latest_data_update{}; + Scene_Base& scene() const; + Renderable_Configuration configuration() const noexcept; + std::atomic& discard_stale_frame_on_latest_data_update; protected: virtual void build_task_graph(Renderable_Task_Graph& graph); private: + friend class Frame_Strategy_Real_Time_Data_Observer; friend class Scene_Base; bool requires_render() const noexcept; - void mark_rendered() noexcept; - std::pmr::memory_resource* const memory_resource_; - Renderable_Task_Graph task_graph_; + void mark_rendered(std::uint64_t revision) noexcept; + void set_configuration(Renderable_Configuration configuration) noexcept; + Layer_Node layer_node_; + Dependency_Node dependency_node_; + mutable std::mutex configuration_mutex_; + Renderable_Configuration configuration_; + std::mutex task_graph_mutex_; + std::shared_ptr task_graph_; + std::uint64_t task_graph_revision_{1}; + std::uint64_t built_task_graph_revision_{}; std::atomic cache_revision_{1}; std::atomic rendered_cache_revision_{}; - std::atomic task_graph_ready_{}; }; diff --git a/Kernel/src/renderive/renderable/concept/Renderable.hpp b/Kernel/src/renderive/renderable/concept/Renderable.hpp index 613a611..427fbc4 100644 --- a/Kernel/src/renderive/renderable/concept/Renderable.hpp +++ b/Kernel/src/renderive/renderable/concept/Renderable.hpp @@ -2,11 +2,9 @@ #include #include "renderive/renderable/base/Renderable_Base.hpp" template -concept Renderable = std::derived_from && requires(That& renderable, const Scene_Render_Context& context) { - { renderable.scene } -> std::convertible_to; - { renderable.layer_node } -> std::same_as; - { renderable.dependency_node } -> std::same_as; - { renderable.configuration } -> std::same_as; +concept Renderable = std::derived_from && requires(That& renderable, const That& const_renderable, const Scene_Render_Context& context) { + { const_renderable.scene() } -> std::same_as; + { const_renderable.configuration() } -> std::same_as; { renderable.render(context) } -> std::same_as; { renderable.invalidate_cache() } -> std::same_as; }; diff --git a/Kernel/src/renderive/scene/Scene2D_Context.hpp b/Kernel/src/renderive/scene/Scene2D_Context.hpp index 249f125..36d39c5 100644 --- a/Kernel/src/renderive/scene/Scene2D_Context.hpp +++ b/Kernel/src/renderive/scene/Scene2D_Context.hpp @@ -37,9 +37,7 @@ public: template requires Scene2D_Frame_Control_Constructible explicit Scene2D_Context(std::pmr::memory_resource& memory_resource, Args&&... args) - : Scene_State_Strategy(State{}, memory_resource), frame_control(make_frame_control(this->memory_resource(), std::forward(args)...)), display_root(nullptr, this->memory_resource()), dependency_root(nullptr, this->memory_resource()), final_color_cache(make_cache(this->memory_resource())), color_caches_(&this->memory_resource()) { - this->frame_control_strategy = &frame_control; - } + : Scene_State_Strategy(State{}, memory_resource), frame_control(make_frame_control(this->memory_resource(), std::forward(args)...)), final_color_cache_(make_cache(this->memory_resource())), display_root_(nullptr, this->memory_resource()), dependency_root_(nullptr, this->memory_resource()), color_caches_(&this->memory_resource()) {} template requires Scene2D_Frame_Control_Constructible Scene2D_Context(With_Observer state_observer, With_Observer scene_observer, Args&&... args) @@ -47,35 +45,54 @@ public: template requires Scene2D_Frame_Control_Constructible Scene2D_Context(std::pmr::memory_resource& memory_resource, With_Observer state_observer, With_Observer scene_observer, Args&&... args) - : Scene_State_Strategy(State{}, std::move(state_observer), memory_resource), frame_control(make_frame_control(this->memory_resource(), std::forward(args)...)), display_root(nullptr, this->memory_resource()), dependency_root(nullptr, this->memory_resource()), final_color_cache(make_cache(this->memory_resource())), scene_observer_(std::move(scene_observer.observer)), color_caches_(&this->memory_resource()) { - this->frame_control_strategy = &frame_control; - } + : Scene_State_Strategy(State{}, std::move(state_observer), memory_resource), frame_control(make_frame_control(this->memory_resource(), std::forward(args)...)), final_color_cache_(make_cache(this->memory_resource())), display_root_(nullptr, this->memory_resource()), dependency_root_(nullptr, this->memory_resource()), scene_observer_(std::move(scene_observer.observer)), color_caches_(&this->memory_resource()) {} ~Scene2D_Context() override { - this->wait_for_render(); - this->frame_control_strategy = nullptr; + this->shutdown(); } Frame_Control frame_control; - Renderable_Base::Layer_Node display_root; - Renderable_Base::Dependency_Node dependency_root; - Cache final_color_cache; + template + void with_final_color_cache(Function&& function) { + if (this->is_render_worker_thread()) { + function(static_cast(final_color_cache_)); + return; + } + auto lock = this->lock_render_idle(); + function(static_cast(final_color_cache_)); + } protected: + Frame_Control_Strategy_Base& frame_control_strategy_impl() noexcept override { + return frame_control; + } + const Frame_Control_Strategy_Base& frame_control_strategy_impl() const noexcept override { + return frame_control; + } + Renderable_Base::Layer_Node* display_root_node() noexcept override { + return &display_root_; + } + Renderable_Base::Dependency_Node* dependency_root_node() noexcept override { + return &dependency_root_; + } void on_renderable_attached(Renderable_Base& renderable) override { - if (!renderable.layer_node.parent) { - display_root.append_child(renderable.layer_node); + auto& display_node = Scene_Base::layer_node(renderable); + auto& dependency_node = Scene_Base::dependency_node(renderable); + if (!display_node.parent()) { + display_root_.append_child(display_node); } - if (!renderable.dependency_node.parent) { - dependency_root.append_child(renderable.dependency_node); + if (!dependency_node.parent()) { + dependency_root_.append_child(dependency_node); + } + if (color_caches_.try_emplace(&renderable, make_cache_pointer(this->memory_resource())).second) { + renderable.invalidate_cache(); } - color_caches_.try_emplace(&renderable, make_cache_pointer(this->memory_resource())); } void on_renderable_detached(Renderable_Base& renderable) override { - promote_children(renderable.layer_node); - promote_children(renderable.dependency_node); + promote_children(Scene_Base::layer_node(renderable)); + promote_children(Scene_Base::dependency_node(renderable)); color_caches_.erase(&renderable); } void prepare_render_task(Render_Task& task, const Renderable_List& renderables) override { - task.render_order = tree_order(dependency_root, renderables); - task.display_order = tree_order(display_root, renderables); + task.render_order = tree_order(dependency_root_, renderables); + task.display_order = tree_order(display_root_, renderables); } Color_Cache* prepare_renderable_cache(Renderable_Base& renderable, bool clear) override { Cache* cache = color_caches_.at(&renderable).get(); @@ -85,9 +102,9 @@ protected: return cache; } void generate_final_color_cache(const Scene_Render_Context&, const Renderable_List& renderables) override { - final_color_cache.clear(); + final_color_cache_.clear(); for (const Renderable& renderable : renderables) { - final_color_cache.composite(*color_caches_.at(renderable.get())); + final_color_cache_.composite(*color_caches_.at(renderable.get())); } } std::uint64_t acquire_scene_state() override { @@ -125,13 +142,19 @@ private: } template void promote_children(Node& node) { - if (!node.parent) { + if (!node.parent()) { return; } - Node* parent = node.parent; - const auto iterator = std::find(parent->children.begin(), parent->children.end(), &node); - std::size_t index = static_cast(iterator - parent->children.begin()); - std::pmr::vector children(node.children.begin(), node.children.end(), &this->memory_resource()); + Node* parent = node.parent(); + std::size_t index{}; + while (&parent->child(index) != &node) { + ++index; + } + std::pmr::vector children(&this->memory_resource()); + children.reserve(node.child_count()); + for (std::size_t child_index = 0; child_index < node.child_count(); ++child_index) { + children.push_back(&node.child(child_index)); + } node.detach(); for (Node* child : children) { parent->insert_child(index++, *child); @@ -139,14 +162,14 @@ private: } template static void append_tree_order(Node& node, const Attached& attached, Visited& visited, Renderable_List& order) { - if (node.owner) { - auto iterator = attached.find(node.owner); - if (iterator != attached.end() && visited.insert(node.owner).second) { + if (node.owner()) { + auto iterator = attached.find(node.owner()); + if (iterator != attached.end() && visited.insert(node.owner()).second) { order.push_back(iterator->second); } } - for (Node* child : node.children) { - append_tree_order(*child, attached, visited, order); + for (std::size_t index = 0; index < node.child_count(); ++index) { + append_tree_order(node.child(index), attached, visited, order); } } template @@ -168,6 +191,9 @@ private: } return order; } + Cache final_color_cache_; + Renderable_Base::Layer_Node display_root_; + Renderable_Base::Dependency_Node dependency_root_; Scene_Observer scene_observer_; std::pmr::unordered_map color_caches_; }; diff --git a/Kernel/src/renderive/scene/Scene3D_Context.hpp b/Kernel/src/renderive/scene/Scene3D_Context.hpp index a878a09..fc75de9 100644 --- a/Kernel/src/renderive/scene/Scene3D_Context.hpp +++ b/Kernel/src/renderive/scene/Scene3D_Context.hpp @@ -27,9 +27,7 @@ public: template requires Scene3D_Frame_Control_Constructible explicit Scene3D_Context(std::pmr::memory_resource& memory_resource, Args&&... args) - : Scene_State_Strategy(State{}, memory_resource), frame_control(make_frame_control(this->memory_resource(), std::forward(args)...)) { - this->frame_control_strategy = &frame_control; - } + : Scene_State_Strategy(State{}, memory_resource), frame_control(make_frame_control(this->memory_resource(), std::forward(args)...)) {} template requires Scene3D_Frame_Control_Constructible Scene3D_Context(With_Observer state_observer, With_Observer scene_observer, Args&&... args) @@ -37,15 +35,18 @@ public: template requires Scene3D_Frame_Control_Constructible Scene3D_Context(std::pmr::memory_resource& memory_resource, With_Observer state_observer, With_Observer scene_observer, Args&&... args) - : Scene_State_Strategy(State{}, std::move(state_observer), memory_resource), frame_control(make_frame_control(this->memory_resource(), std::forward(args)...)), scene_observer_(std::move(scene_observer.observer)) { - this->frame_control_strategy = &frame_control; - } + : Scene_State_Strategy(State{}, std::move(state_observer), memory_resource), frame_control(make_frame_control(this->memory_resource(), std::forward(args)...)), scene_observer_(std::move(scene_observer.observer)) {} ~Scene3D_Context() override { - this->wait_for_render(); - this->frame_control_strategy = nullptr; + this->shutdown(); } Frame_Control frame_control; protected: + Frame_Control_Strategy_Base& frame_control_strategy_impl() noexcept override { + return frame_control; + } + const Frame_Control_Strategy_Base& frame_control_strategy_impl() const noexcept override { + return frame_control; + } std::uint64_t acquire_scene_state() override { return this->Scene_State_Strategy::acquire_render_state(); } diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index 3cf02ff..9b0a87a 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -8,39 +8,25 @@ #include #include "renderive/renderable/color/Color_Cache.hpp" static_assert(TF_VERSION == 400100, "Renderive requires Taskflow 4.1.0"); +class Scene_Base::Execution_Context { +public: + static tf::Executor& executor() { + static tf::Executor executor; + return executor; + } +}; Scene_Base::Scene_Base() : Scene_Base(*std::pmr::get_default_resource()) {} Scene_Base::Scene_Base(std::pmr::memory_resource& upstream_memory_resource) - : memory_domain_(std::allocate_shared(std::pmr::polymorphic_allocator(&upstream_memory_resource), upstream_memory_resource)), renderable_states_{Renderable_List(&memory_domain_->resource()), Renderable_List(&memory_domain_->resource())}, render_renderables_(&renderable_states_[0]), cache_renderables_(&renderable_states_[1]), task_(memory_domain_->resource()) { + : scene_lifetime_(std::make_shared(*this)), memory_domain_(std::allocate_shared(std::pmr::polymorphic_allocator(&upstream_memory_resource), upstream_memory_resource)), renderable_states_{Renderable_List(&memory_domain_->resource()), Renderable_List(&memory_domain_->resource())}, render_renderables_(&renderable_states_[0]), cache_renderables_(&renderable_states_[1]), task_(memory_domain_->resource()) { worker_ = std::thread([this] { render_loop(); }); } Scene_Base::~Scene_Base() { - try { - wait_for_render(); - } catch (...) {} - { - std::lock_guard lock(task_mutex_); - stop_ = true; - } - task_ready_.notify_one(); - if (worker_.joinable()) { - worker_.join(); - } + shutdown(); } void Scene_Base::render() { - std::unique_lock task_lock(task_mutex_); - render_completed_.wait(task_lock, [this] { - return !task_pending_ && !rendering_; - }); - if (render_exception_) { - std::exception_ptr exception = std::exchange(render_exception_, {}); - task_lock.unlock(); - std::rethrow_exception(exception); - } - if (!frame_control_strategy) { - throw std::logic_error("frame control strategy is not bound"); - } + auto task_lock = lock_render_idle(); Render_Task task(memory_resource()); { std::lock_guard renderable_lock(renderable_mutex_); @@ -48,10 +34,13 @@ void Scene_Base::render() { *cache_renderables_ = *render_renderables_; prepare_render_task(task, *render_renderables_); } - frame_control_strategy->swap(); - task.frame_control_state = frame_control_strategy->frame_control_state(); + auto& strategy = frame_control_strategy(); + strategy.swap(); + task.frame_control_state = strategy.frame_control_state(); task.scene_state_revision = acquire_scene_state(); task.render_sequence = ++render_sequence_; + task.completion = std::make_shared(); + current_completion_ = task.completion; observe_scene({Observation_Event::render_submitted, observer_now_ns(), task.render_sequence, task.scene_state_revision, task.render_order.size()}); task_ = std::move(task); task_pending_ = true; @@ -59,46 +48,158 @@ void Scene_Base::render() { task_ready_.notify_one(); } void Scene_Base::wait_for_render() { - std::unique_lock lock(task_mutex_); - render_completed_.wait(lock, [this] { - return !task_pending_ && !rendering_; + if (is_render_worker_thread()) { + return; + } + std::unique_lock lock(task_mutex_); + const auto completion = current_completion_; + if (!completion) { + return; + } + render_completed_.wait(lock, [&completion] { + return completion->completed; }); - if (render_exception_) { - std::exception_ptr exception = std::exchange(render_exception_, {}); - lock.unlock(); + const auto exception = completion->exception; + lock.unlock(); + if (exception) { std::rethrow_exception(exception); } } void Scene_Base::attach_renderable(Renderable renderable) { - wait_for_render(); - if (renderable->scene != this) { - throw std::invalid_argument("renderable belongs to another scene"); + if (!renderable) { + throw std::invalid_argument("renderable is null"); } + auto task_lock = lock_render_idle(); + validate_renderable_scene(*renderable); std::lock_guard lock(renderable_mutex_); - if (std::find(cache_renderables_->begin(), cache_renderables_->end(), renderable) != cache_renderables_->end()) { + if (is_renderable_attached_locked(*renderable)) { return; } cache_renderables_->push_back(renderable); on_renderable_attached(*renderable); } -void Scene_Base::detach_renderable(const Renderable_Base& renderable) { - wait_for_render(); +void Scene_Base::detach_renderable(Renderable_Base& renderable) { + auto task_lock = lock_render_idle(); + validate_renderable_scene(renderable); std::lock_guard lock(renderable_mutex_); - on_renderable_detached(const_cast(renderable)); - std::erase_if(*cache_renderables_, [&renderable](const Renderable& value) { + if (!is_renderable_attached_locked(renderable)) { + return; + } + on_renderable_detached(renderable); + const auto detached = [&renderable](const Renderable& value) { return value.get() == &renderable; - }); + }; + std::erase_if(*cache_renderables_, detached); + std::erase_if(*render_renderables_, detached); +} +void Scene_Base::set_display_parent(Renderable_Base& renderable, Renderable_Base* parent) { + auto task_lock = lock_render_idle(); + validate_renderable_scene(renderable); + if (parent) { + validate_renderable_scene(*parent); + } + std::lock_guard lock(renderable_mutex_); + validate_renderable_attached_locked(renderable); + if (parent) { + validate_renderable_attached_locked(*parent); + } + auto& node = layer_node(renderable); + auto* target = parent ? &layer_node(*parent) : display_root_node(); + if (node.parent() == target) { + return; + } + if (target) { + target->append_child(node); + } else { + node.detach(); + } +} +void Scene_Base::set_dependency_parent(Renderable_Base& renderable, Renderable_Base* parent) { + auto task_lock = lock_render_idle(); + validate_renderable_scene(renderable); + if (parent) { + validate_renderable_scene(*parent); + } + std::lock_guard lock(renderable_mutex_); + validate_renderable_attached_locked(renderable); + if (parent) { + validate_renderable_attached_locked(*parent); + } + auto& node = dependency_node(renderable); + auto* target = parent ? &dependency_node(*parent) : dependency_root_node(); + if (node.parent() == target) { + return; + } + if (target) { + target->append_child(node); + } else { + node.detach(); + } + renderable.invalidate_cache(); +} +void Scene_Base::set_renderable_configuration(Renderable_Base& renderable, Renderable_Configuration configuration) { + auto task_lock = lock_render_idle(); + validate_renderable_scene(renderable); + std::lock_guard lock(renderable_mutex_); + renderable.set_configuration(configuration); } std::size_t Scene_Base::renderable_count() const { std::lock_guard lock(renderable_mutex_); return cache_renderables_->size(); } +Scene_Base::Topology_Snapshot Scene_Base::topology_snapshot() const { + std::lock_guard lock(renderable_mutex_); + Topology_Snapshot snapshot; + std::unordered_map renderables; + renderables.reserve(cache_renderables_->size()); + snapshot.renderables.reserve(cache_renderables_->size()); + snapshot.display.reserve(cache_renderables_->size()); + snapshot.dependency.reserve(cache_renderables_->size()); + for (const Renderable& renderable : *cache_renderables_) { + Const_Renderable value = renderable; + renderables.emplace(renderable.get(), value); + snapshot.renderables.push_back(std::move(value)); + } + for (const Renderable& renderable : *cache_renderables_) { + const auto* display_parent_node = renderable->layer_node_.parent(); + const auto* dependency_parent_node = renderable->dependency_node_.parent(); + const auto* display_parent = display_parent_node ? display_parent_node->owner() : nullptr; + const auto* dependency_parent = dependency_parent_node ? dependency_parent_node->owner() : nullptr; + snapshot.display.push_back({renderables.at(renderable.get()), display_parent ? renderables.at(display_parent) : Const_Renderable{}}); + snapshot.dependency.push_back({renderables.at(renderable.get()), dependency_parent ? renderables.at(dependency_parent) : Const_Renderable{}}); + } + return snapshot; +} std::pmr::memory_resource& Scene_Base::memory_resource() const noexcept { return memory_domain_->resource(); } std::pmr::memory_resource& Scene_Base::upstream_memory_resource() const noexcept { return memory_domain_->upstream(); } +Frame_Control_Strategy_Base& Scene_Base::frame_control_strategy() { + return frame_control_strategy_impl(); +} +const Frame_Control_Strategy_Base& Scene_Base::frame_control_strategy() const { + return frame_control_strategy_impl(); +} +Frame_Control_Strategy_Base& Scene_Base::frame_control_strategy_impl() { + throw std::logic_error("frame control strategy is not available"); +} +const Frame_Control_Strategy_Base& Scene_Base::frame_control_strategy_impl() const { + throw std::logic_error("frame control strategy is not available"); +} +Renderable_Base::Layer_Node& Scene_Base::layer_node(Renderable_Base& renderable) noexcept { + return renderable.layer_node_; +} +Renderable_Base::Dependency_Node& Scene_Base::dependency_node(Renderable_Base& renderable) noexcept { + return renderable.dependency_node_; +} +Renderable_Base::Layer_Node* Scene_Base::display_root_node() noexcept { + return nullptr; +} +Renderable_Base::Dependency_Node* Scene_Base::dependency_root_node() noexcept { + return nullptr; +} void Scene_Base::on_renderable_attached(Renderable_Base&) {} void Scene_Base::on_renderable_detached(Renderable_Base&) {} void Scene_Base::prepare_render_task(Render_Task& task, const Renderable_List& renderables) { @@ -116,11 +217,35 @@ void Scene_Base::observe_scene(const Observation&) noexcept {} std::uint64_t Scene_Base::observer_now_ns() const noexcept { return 0; } +std::unique_lock Scene_Base::lock_render_idle() { + std::unique_lock lock(task_mutex_); + render_completed_.wait(lock, [this] { + return !task_pending_ && !rendering_; + }); + return lock; +} +bool Scene_Base::is_render_worker_thread() const noexcept { + return worker_.joinable() && std::this_thread::get_id() == worker_.get_id(); +} +void Scene_Base::shutdown() noexcept { + { + std::unique_lock lock(task_mutex_); + render_completed_.wait(lock, [this] { + return !task_pending_ && !rendering_; + }); + stop_ = true; + } + task_ready_.notify_one(); + if (worker_.joinable()) { + worker_.join(); + } + scene_lifetime_->invalidate(); +} void Scene_Base::render_loop() { for (;;) { Render_Task task(memory_resource()); { - std::unique_lock lock(task_mutex_); + std::unique_lock lock(task_mutex_); task_ready_.wait(lock, [this] { return stop_ || task_pending_; }); @@ -132,18 +257,18 @@ void Scene_Base::render_loop() { rendering_ = true; } observe_scene({Observation_Event::render_started, observer_now_ns(), task.render_sequence, task.scene_state_revision, task.render_order.size()}); + std::exception_ptr exception; try { execute_taskflow(task); observe_scene({Observation_Event::render_completed, observer_now_ns(), task.render_sequence, task.scene_state_revision, task.render_order.size()}); } catch (...) { - { - std::lock_guard lock(task_mutex_); - render_exception_ = std::current_exception(); - } + exception = std::current_exception(); observe_scene({Observation_Event::render_failed, observer_now_ns(), task.render_sequence, task.scene_state_revision, task.render_order.size()}); } { - std::lock_guard lock(task_mutex_); + std::lock_guard lock(task_mutex_); + task.completion->exception = exception; + task.completion->completed = true; rendering_ = false; } render_completed_.notify_all(); @@ -156,7 +281,6 @@ void Scene_Base::execute_taskflow(Render_Task& task) { bool render{}; }; std::pmr::monotonic_buffer_resource scratch_resource(&memory_resource()); - tf::Executor executor; tf::Taskflow scene_graph; std::pmr::unordered_map indices(&scratch_resource); indices.reserve(task.render_order.size()); @@ -165,11 +289,23 @@ void Scene_Base::execute_taskflow(Render_Task& task) { indices.emplace(task.render_order[index].get(), index); render_flags[index] = task.render_order[index]->requires_render(); } + std::pmr::vector dirty_queue(&scratch_resource); + dirty_queue.reserve(task.render_order.size()); for (std::size_t index = 0; index < task.render_order.size(); ++index) { - Renderable_Base* parent = task.render_order[index]->dependency_node.parent ? task.render_order[index]->dependency_node.parent->owner : nullptr; - auto parent_iterator = indices.find(parent); - if (parent_iterator != indices.end() && render_flags[parent_iterator->second]) { - render_flags[index] = true; + if (render_flags[index]) { + dirty_queue.push_back(index); + } + } + for (std::size_t queue_index = 0; queue_index < dirty_queue.size(); ++queue_index) { + auto& node = dependency_node(*task.render_order[dirty_queue[queue_index]]); + for (std::size_t child_index = 0; child_index < node.child_count(); ++child_index) { + Renderable_Base* child = node.child(child_index).owner(); + auto child_iterator = indices.find(child); + if (child_iterator == indices.end() || render_flags[child_iterator->second]) { + continue; + } + render_flags[child_iterator->second] = true; + dirty_queue.push_back(child_iterator->second); } } std::pmr::vector modules(&scratch_resource); @@ -179,9 +315,11 @@ void Scene_Base::execute_taskflow(Render_Task& task) { Module& module = modules[index]; module.render = render_flags[index]; if (module.render) { + const std::uint64_t render_revision = renderable.cache_revision(); Color_Cache* color_cache = prepare_renderable_cache(renderable, true); const Scene_Render_Context context{this, task.frame_control_state, task.scene_state_revision, task.render_sequence, &renderable, color_cache}; - const auto& nodes = renderable.task_graph().nodes(); + const auto task_graph = renderable.task_graph(); + const auto& nodes = task_graph->nodes(); std::pmr::vector internal_tasks(&scratch_resource); internal_tasks.reserve(nodes.size()); for (const auto& node : nodes) { @@ -194,16 +332,14 @@ void Scene_Base::execute_taskflow(Render_Task& task) { internal_tasks[node_index].precede(internal_tasks.at(successor)); } } - tf::Task mark_rendered = module.graph.emplace([&renderable] { - renderable.mark_rendered(); + tf::Task mark_rendered = module.graph.emplace([&renderable, render_revision] { + renderable.mark_rendered(render_revision); }).name("cache_publish"); if (internal_tasks.empty()) { module.graph.emplace([] {}).precede(mark_rendered); } else { - std::pmr::vector has_successor(nodes.size(), false, &scratch_resource); for (std::size_t node_index = 0; node_index < nodes.size(); ++node_index) { - has_successor[node_index] = !nodes[node_index].successors.empty(); - if (!has_successor[node_index]) { + if (nodes[node_index].successors.empty()) { internal_tasks[node_index].precede(mark_rendered); } } @@ -214,7 +350,8 @@ void Scene_Base::execute_taskflow(Render_Task& task) { module.module_task = scene_graph.composed_of(module.graph).name("renderable"); } for (std::size_t index = 0; index < task.render_order.size(); ++index) { - Renderable_Base* parent = task.render_order[index]->dependency_node.parent ? task.render_order[index]->dependency_node.parent->owner : nullptr; + auto& node = dependency_node(*task.render_order[index]); + Renderable_Base* parent = node.parent() ? node.parent()->owner() : nullptr; auto parent_iterator = indices.find(parent); if (parent_iterator != indices.end()) { modules[parent_iterator->second].module_task.precede(modules[index].module_task); @@ -231,5 +368,20 @@ void Scene_Base::execute_taskflow(Render_Task& task) { module.module_task.precede(final_task); } } - executor.run(scene_graph).get(); + Execution_Context::executor().run(scene_graph).get(); +} +void Scene_Base::validate_renderable_scene(const Renderable_Base& renderable) const { + if (renderable.real_time_data_state_->scene_lifetime.get() != scene_lifetime_.get()) { + throw std::invalid_argument("renderable belongs to another scene"); + } +} +bool Scene_Base::is_renderable_attached_locked(const Renderable_Base& renderable) const { + return std::find_if(cache_renderables_->begin(), cache_renderables_->end(), [&renderable](const Renderable& value) { + return value.get() == &renderable; + }) != cache_renderables_->end(); +} +void Scene_Base::validate_renderable_attached_locked(const Renderable_Base& renderable) const { + if (!is_renderable_attached_locked(renderable)) { + throw std::invalid_argument("renderable is not attached to this scene"); + } } diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp index b788054..7e0a9d5 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -8,14 +8,17 @@ #include #include #include -#include #include +#include #include "renderive/base/memory/Memory_Resource.hpp" #include "renderive/frame_control/base/Frame_Control_Strategy_Base.hpp" #include "renderive/renderable/base/Renderable_Base.hpp" +#include "Scene_Lifetime.hpp" #include "Scene_Render_Context.hpp" class Color_Cache; class Scene_Base { +private: + struct Render_Completion; public: using Renderable = std::shared_ptr; using Renderable_List = std::pmr::vector; @@ -32,6 +35,16 @@ public: std::uint64_t scene_state_revision{}; std::size_t renderable_count{}; }; + using Const_Renderable = std::shared_ptr; + struct Topology_Relationship { + Const_Renderable child; + Const_Renderable parent; + }; + struct Topology_Snapshot { + std::vector renderables; + std::vector display; + std::vector dependency; + }; Scene_Base(); explicit Scene_Base(std::pmr::memory_resource& upstream_memory_resource); Scene_Base(const Scene_Base&) = delete; @@ -42,16 +55,21 @@ public: void render(); void wait_for_render(); void attach_renderable(Renderable renderable); - void detach_renderable(const Renderable_Base& renderable); + void detach_renderable(Renderable_Base& renderable); + void set_display_parent(Renderable_Base& renderable, Renderable_Base* parent); + void set_dependency_parent(Renderable_Base& renderable, Renderable_Base* parent); + void set_renderable_configuration(Renderable_Base& renderable, Renderable_Configuration configuration); std::size_t renderable_count() const; + Topology_Snapshot topology_snapshot() const; std::pmr::memory_resource& memory_resource() const noexcept; std::pmr::memory_resource& upstream_memory_resource() const noexcept; + Frame_Control_Strategy_Base& frame_control_strategy(); + const Frame_Control_Strategy_Base& frame_control_strategy() const; template requires std::derived_from && std::constructible_from std::shared_ptr make_renderable(Args&&... args) { return std::allocate_shared(Scene_Memory_Allocator{memory_domain_}, *this, std::forward(args)...); } - Frame_Control_Strategy_Base* frame_control_strategy{}; protected: struct Render_Task { explicit Render_Task(std::pmr::memory_resource& memory_resource) : render_order(&memory_resource), display_order(&memory_resource) {} @@ -60,7 +78,14 @@ protected: std::uint64_t render_sequence{}; Renderable_List render_order; Renderable_List display_order; + std::shared_ptr completion; }; + static Renderable_Base::Layer_Node& layer_node(Renderable_Base& renderable) noexcept; + static Renderable_Base::Dependency_Node& dependency_node(Renderable_Base& renderable) noexcept; + virtual Frame_Control_Strategy_Base& frame_control_strategy_impl(); + virtual const Frame_Control_Strategy_Base& frame_control_strategy_impl() const; + virtual Renderable_Base::Layer_Node* display_root_node() noexcept; + virtual Renderable_Base::Dependency_Node* dependency_root_node() noexcept; virtual void on_renderable_attached(Renderable_Base& renderable); virtual void on_renderable_detached(Renderable_Base& renderable); virtual void prepare_render_task(Render_Task& task, const Renderable_List& renderables); @@ -69,20 +94,33 @@ protected: virtual std::uint64_t acquire_scene_state(); virtual void observe_scene(const Observation& observation) noexcept; virtual std::uint64_t observer_now_ns() const noexcept; + std::unique_lock lock_render_idle(); + bool is_render_worker_thread() const noexcept; + void shutdown() noexcept; private: + friend class Renderable_Base; + class Execution_Context; + struct Render_Completion { + std::exception_ptr exception; + bool completed{}; + }; void render_loop(); void execute_taskflow(Render_Task& task); + void validate_renderable_scene(const Renderable_Base& renderable) const; + bool is_renderable_attached_locked(const Renderable_Base& renderable) const; + void validate_renderable_attached_locked(const Renderable_Base& renderable) const; + std::shared_ptr scene_lifetime_; std::shared_ptr memory_domain_; std::array renderable_states_; Renderable_List* render_renderables_; Renderable_List* cache_renderables_; mutable std::mutex renderable_mutex_; - std::mutex task_mutex_; - std::condition_variable task_ready_; - std::condition_variable render_completed_; + std::recursive_mutex task_mutex_; + std::condition_variable_any task_ready_; + std::condition_variable_any render_completed_; std::thread worker_; Render_Task task_; - std::exception_ptr render_exception_; + std::shared_ptr current_completion_; std::uint64_t render_sequence_{}; bool task_pending_{}; bool rendering_{}; diff --git a/Kernel/src/renderive/scene/base/Scene_Lifetime.hpp b/Kernel/src/renderive/scene/base/Scene_Lifetime.hpp new file mode 100644 index 0000000..b655ec8 --- /dev/null +++ b/Kernel/src/renderive/scene/base/Scene_Lifetime.hpp @@ -0,0 +1,35 @@ +#pragma once +#include +#include +class Scene_Base; +class Scene_Lifetime { +public: + class Lease { + public: + Lease(Lease&&) noexcept = default; + Lease& operator=(Lease&&) noexcept = default; + explicit operator bool() const noexcept { + return scene_ != nullptr; + } + Scene_Base& scene() const noexcept { + return *scene_; + } + private: + friend class Scene_Lifetime; + Lease(std::unique_lock lock, Scene_Base* scene) noexcept : lock_(std::move(lock)), scene_(scene) {} + std::unique_lock lock_; + Scene_Base* scene_{}; + }; + explicit Scene_Lifetime(Scene_Base& scene) noexcept : scene_(&scene) {} + Lease acquire() const noexcept { + std::unique_lock lock(mutex_); + return Lease(std::move(lock), scene_); + } + void invalidate() noexcept { + std::lock_guard lock(mutex_); + scene_ = nullptr; + } +private: + mutable std::mutex mutex_; + Scene_Base* scene_{}; +}; diff --git a/Kernel/src/renderive/scene/concept/Scene.hpp b/Kernel/src/renderive/scene/concept/Scene.hpp index 35e958c..c781588 100644 --- a/Kernel/src/renderive/scene/concept/Scene.hpp +++ b/Kernel/src/renderive/scene/concept/Scene.hpp @@ -2,8 +2,9 @@ #include #include "renderive/scene/base/Scene_Base.hpp" template -concept Scene = std::derived_from && requires(That& scene) { - { scene.frame_control_strategy } -> std::convertible_to; +concept Scene = std::derived_from && requires(That& scene, const That& const_scene) { + { scene.frame_control_strategy() } -> std::same_as; + { const_scene.frame_control_strategy() } -> std::same_as; { scene.render() } -> std::same_as; { scene.wait_for_render() } -> std::same_as; }; diff --git a/Kernel/src/renderive/state/Concepts.hpp b/Kernel/src/renderive/state/Concepts.hpp index d321aed..d4ee287 100644 --- a/Kernel/src/renderive/state/Concepts.hpp +++ b/Kernel/src/renderive/state/Concepts.hpp @@ -14,7 +14,7 @@ concept Double_State_Strategy_Type = std::derived_from std::same_as; { const_strategy.state_revision() } -> std::same_as; - { const_strategy.render_use_state() } -> std::same_as; + { const_strategy.render_use_state() } -> std::same_as; }; template concept Triple_State_Strategy_Type = Double_State_Strategy_Type && requires(That& strategy) { diff --git a/Kernel/src/renderive/state/Double_State_Strategy.hpp b/Kernel/src/renderive/state/Double_State_Strategy.hpp index aafb622..d2d800c 100644 --- a/Kernel/src/renderive/state/Double_State_Strategy.hpp +++ b/Kernel/src/renderive/state/Double_State_Strategy.hpp @@ -73,7 +73,7 @@ struct Double_State_Strategy : That, State_Strategy_Base { std::lock_guard lock(mtx); return publish_count; } - const State& render_use_state() const { + State render_use_state() const { std::lock_guard lock(mtx); return *render_state; } diff --git a/Kernel/src/renderive/state/Triple_State_Strategy.hpp b/Kernel/src/renderive/state/Triple_State_Strategy.hpp index 6e5161f..ba96ff5 100644 --- a/Kernel/src/renderive/state/Triple_State_Strategy.hpp +++ b/Kernel/src/renderive/state/Triple_State_Strategy.hpp @@ -76,6 +76,7 @@ struct Triple_State_Strategy : That, State_Strategy_Base { std::uint64_t acquire_render_state() { Observation observation; bool acquired{}; + std::uint64_t revision{}; { std::lock_guard lock(mtx); if (render_revision != published_revision) { @@ -84,17 +85,18 @@ struct Triple_State_Strategy : That, State_Strategy_Base { acquired = true; observation = {Observation_Event::render_acquired, observer.now_ns(), cache_update_count, publish_count, render_revision, *render_state}; } + revision = render_revision; } if (acquired) { observer.observe(observation); } - return render_revision; + return revision; } std::uint64_t state_revision() const override { std::lock_guard lock(mtx); return publish_count; } - const State& render_use_state() const { + State render_use_state() const { std::lock_guard lock(mtx); return *render_state; } diff --git a/Kernel/tests/compat/taskflow/taskflow.hpp b/Kernel/tests/compat/taskflow/taskflow.hpp deleted file mode 100644 index 8a3b157..0000000 --- a/Kernel/tests/compat/taskflow/taskflow.hpp +++ /dev/null @@ -1,125 +0,0 @@ -#pragma once -#define TF_VERSION 400100 -#define TF_MAJOR_VERSION TF_VERSION/100000 -#define TF_MINOR_VERSION TF_VERSION/100%1000 -#define TF_PATCH_VERSION TF_VERSION%100 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -namespace tf { -class Taskflow; -class Task { -public: - Task() = default; - Task& name(std::string name); - template - Task& precede(Tasks... tasks) { - (precede_one(tasks), ...); - return *this; - } - template - Task& succeed(Tasks... tasks) { - (tasks.precede(*this), ...); - return *this; - } -private: - friend class Taskflow; - Task(Taskflow* graph, std::size_t index) : graph_(graph), index_(index) {} - void precede_one(Task task); - Taskflow* graph_{}; - std::size_t index_{}; -}; -class Taskflow { -public: - struct Node { - std::function work; - Taskflow* module{}; - std::string name; - std::vector successors; - }; - template - Task emplace(Callable&& callable) { - nodes_.push_back({std::function(std::forward(callable)), nullptr, {}, {}}); - return Task(this, nodes_.size() - 1); - } - Task composed_of(Taskflow& module) { - nodes_.push_back({{}, &module, {}, {}}); - return Task(this, nodes_.size() - 1); - } -private: - friend class Task; - friend class Executor; - void run() { - std::vector indegrees(nodes_.size()); - for (const Node& node : nodes_) { - for (std::size_t successor : node.successors) { - ++indegrees.at(successor); - } - } - std::queue ready; - for (std::size_t index = 0; index < indegrees.size(); ++index) { - if (indegrees[index] == 0) { - ready.push(index); - } - } - std::size_t completed{}; - while (!ready.empty()) { - std::size_t index = ready.front(); - ready.pop(); - Node& node = nodes_[index]; - if (node.module) { - node.module->run(); - } else if (node.work) { - node.work(); - } - ++completed; - for (std::size_t successor : node.successors) { - if (--indegrees.at(successor) == 0) { - ready.push(successor); - } - } - } - if (completed != nodes_.size()) { - throw std::logic_error("taskflow cycle"); - } - } - std::vector nodes_; -}; -inline Task& Task::name(std::string name) { - graph_->nodes_.at(index_).name = std::move(name); - return *this; -} -inline void Task::precede_one(Task task) { - if (graph_ != task.graph_) { - throw std::invalid_argument("taskflow task owner"); - } - graph_->nodes_.at(index_).successors.push_back(task.index_); -} -class Future { -public: - explicit Future(std::future future) : future_(std::move(future)) {} - void wait() { - future_.wait(); - } - void get() { - future_.get(); - } -private: - std::future future_; -}; -class Executor { -public: - Future run(Taskflow& graph) { - return Future(std::async(std::launch::async, [&graph] { - graph.run(); - })); - } -}; -} diff --git a/Kernel/tests/main.cpp b/Kernel/tests/main.cpp index 7bb10c5..1edad91 100644 --- a/Kernel/tests/main.cpp +++ b/Kernel/tests/main.cpp @@ -1,75 +1,5 @@ -#include -#include -#include -#include -#include -#ifdef RENDERIVE_WITH_GTEST #include -#endif -#include "../src/renderive/base/property/Property.hpp" -#include "../src/renderive/renderable/Renderable.hpp" -#include "../src/renderive/scene/Scene.hpp" -#include "../src/renderive/state/State_Strategy.hpp" -struct Radio_State { - struct Even_Validator { - void operator()(const int& value) const { - if (value % 2 != 0) { - throw std::invalid_argument("buffer_count must be even"); - } - } - }; - Range_Value port{8080}; - Validated_Value buffer_count{2}; - int low_watermark{}; - int high_watermark{}; -}; -struct Radio_Base { - Radio_Base(int id, std::string name) : id(id), name(std::move(name)) {} - static void validate_state(const Radio_State& state) { - if (state.low_watermark > state.high_watermark) { - throw std::invalid_argument("low_watermark must not exceed high_watermark"); - } - } - int id; - std::string name; -}; -struct Demo_Renderable : Renderable_Base { - explicit Demo_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = true}) {} - void render(const Scene_Render_Context&) override { - ++render_count; - } - std::atomic render_count{}; -}; -using Radio_Strategy = Double_State_Strategy; -using Radio = Attach_State_Builder; -int run_main_flow() { - auto radio = Radio::Builder{} - .set < &Radio_State::port > (9000) - .set<&Radio_State::buffer_count>(8) - .configure([](Radio_State& state) { - state.low_watermark = 16; - state.high_watermark = 64; - }) - .build(1, "primary"); - radio->set < &Radio_State::port > (9100); - radio->publish(); - Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); - scene.render(); - scene.wait_for_render(); - const auto& state = radio->render_use_state(); - std::cout << radio->id << ' ' << radio->name << ' ' << state.port.get() << ' ' << state.buffer_count.get() << ' ' << renderable->render_count.load() << ' ' << scene.frame_control.frequency_hz() << '\n'; - return 0; -} -int main([[maybe_unused]] int argc, [[maybe_unused]] char** argv) { -#ifdef RENDERIVE_WITH_GTEST - if (argc > 1 && std::string(argv[1]) == "--gtest") { - --argc; - ++argv; - testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); - } -#endif - return run_main_flow(); +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/Kernel/tests/renderive/base/node/Multiway_Node_Test.cpp b/Kernel/tests/renderive/base/node/Multiway_Node_Test.cpp index d9da298..31af006 100644 --- a/Kernel/tests/renderive/base/node/Multiway_Node_Test.cpp +++ b/Kernel/tests/renderive/base/node/Multiway_Node_Test.cpp @@ -1,11 +1,13 @@ #include #include +#include #include "renderive/base/node/Node.hpp" struct Multiway_Node_Test_Owner {}; struct Multiway_Node_Test_Tag {}; using Multiway_Node_Test_Type = Multiway_Node; static_assert(Multiway_Node_Type); static_assert(!std::same_as, Multiway_Node_Test_Type>); +static_assert(std::same_as().child(0)), const Multiway_Node_Test_Type&>); TEST(multiway_node_test, reparents_child_and_preserves_order) { Multiway_Node_Test_Owner first_owner; Multiway_Node_Test_Owner second_owner; @@ -17,12 +19,12 @@ TEST(multiway_node_test, reparents_child_and_preserves_order) { root.append_child(first); root.append_child(second); first.append_child(third); - EXPECT_EQ(root.children.size(), 2); - EXPECT_EQ(first.children.front(), &third); - EXPECT_EQ(third.parent, &first); + EXPECT_EQ(root.child_count(), 2); + EXPECT_EQ(&first.child(0), &third); + EXPECT_EQ(third.parent(), &first); second.append_child(third); - EXPECT_TRUE(first.children.empty()); - EXPECT_EQ(third.parent, &second); + EXPECT_TRUE(first.children_empty()); + EXPECT_EQ(third.parent(), &second); } TEST(multiway_node_test, rejects_cycles) { Multiway_Node_Test_Type root; @@ -30,3 +32,22 @@ TEST(multiway_node_test, rejects_cycles) { root.append_child(child); EXPECT_THROW(child.append_child(root), std::invalid_argument); } +TEST(multiway_node_test, reorders_existing_child_without_invalid_iterator) { + Multiway_Node_Test_Type root; + Multiway_Node_Test_Type first; + Multiway_Node_Test_Type second; + root.append_child(first); + root.append_child(second); + root.append_child(first); + EXPECT_EQ(root.child_count(), 2); + EXPECT_EQ(&root.child(0), &second); + EXPECT_EQ(&root.child(1), &first); + root.insert_child(0, first); + EXPECT_EQ(&root.child(0), &first); + EXPECT_EQ(&root.child(1), &second); +} +TEST(multiway_node_test, rejects_child_index_past_end) { + Multiway_Node_Test_Type root; + Multiway_Node_Test_Type child; + EXPECT_THROW(root.insert_child(1, child), std::out_of_range); +} diff --git a/Kernel/tests/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy_Test.cpp index 42d4df7..5ebeb67 100644 --- a/Kernel/tests/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy_Test.cpp @@ -1,4 +1,11 @@ #include +#include +#include +#include +#include +#include +#include +#include #include "renderive/frame_control/Frame_Control.hpp" struct Flow_Refresh_Test_Frame { int value{}; @@ -26,3 +33,150 @@ TEST(flow_refresh_strategy_test, returns_empty_lease_when_queue_is_empty) { EXPECT_FALSE(frame); EXPECT_EQ(strategy.state().empty_acquire_count, 1); } +TEST(flow_refresh_strategy_test, accepts_multiple_concurrent_producers_without_losing_frames) { + Flow_Refresh_Test_Strategy strategy; + constexpr int producer_count = 4; + constexpr int frames_per_producer = 100; + std::vector producers; + for (int producer = 0; producer < producer_count; ++producer) { + producers.emplace_back([&strategy, producer] { + for (int index = 0; index < frames_per_producer; ++index) { + auto frame = strategy.acquire_painter(); + frame->value = producer * 1000 + index; + } + }); + } + for (auto& producer : producers) { + producer.join(); + } + EXPECT_EQ(strategy.pending_frame_count(), producer_count * frames_per_producer); + std::vector values; + while (auto frame = strategy.acquire_renderer()) { + values.push_back(frame->value); + } + std::sort(values.begin(), values.end()); + EXPECT_EQ(values.size(), producer_count * frames_per_producer); + for (int producer = 0; producer < producer_count; ++producer) { + for (int index = 0; index < frames_per_producer; ++index) { + EXPECT_TRUE(std::binary_search(values.begin(), values.end(), producer * 1000 + index)); + } + } +} +TEST(flow_refresh_strategy_test, concurrent_consumer_does_not_underflow_pending_count) { + Flow_Refresh_Test_Strategy strategy; + constexpr int producer_count = 4; + constexpr int frames_per_producer = 200; + constexpr int frame_count = producer_count * frames_per_producer; + std::atomic producers_done{}; + std::atomic consumed{}; + std::vector values; + values.reserve(frame_count); + std::thread consumer([&] { + while (consumed.load(std::memory_order_acquire) != frame_count) { + auto frame = strategy.acquire_renderer(); + if (!frame) { + if (producers_done.load(std::memory_order_acquire) == producer_count && strategy.pending_frame_count() == 0) { + break; + } + std::this_thread::yield(); + continue; + } + values.push_back(frame->value); + consumed.fetch_add(1, std::memory_order_release); + } + }); + std::vector producers; + for (int producer = 0; producer < producer_count; ++producer) { + producers.emplace_back([&strategy, &producers_done, producer] { + for (int index = 0; index < frames_per_producer; ++index) { + auto frame = strategy.acquire_painter(); + frame->value = producer * 1000 + index; + } + producers_done.fetch_add(1, std::memory_order_release); + }); + } + for (auto& producer : producers) { + producer.join(); + } + consumer.join(); + EXPECT_EQ(consumed.load(std::memory_order_acquire), frame_count); + EXPECT_EQ(strategy.pending_frame_count(), 0); + std::sort(values.begin(), values.end()); + EXPECT_EQ(values.size(), frame_count); + for (int producer = 0; producer < producer_count; ++producer) { + for (int index = 0; index < frames_per_producer; ++index) { + EXPECT_TRUE(std::binary_search(values.begin(), values.end(), producer * 1000 + index)); + } + } +} +struct Flow_Refresh_Reentrant_Observer_Data { + std::function callback; +}; +struct Flow_Refresh_Reentrant_Observer { + static constexpr bool enabled = true; + std::shared_ptr data{std::make_shared()}; + template + void observe(const Observation& observation) noexcept { + if (data->callback) { + data->callback(static_cast(observation.event)); + } + } +}; +using Flow_Refresh_Reentrant_Observer_State = Observer_State; +using Flow_Refresh_Reentrant_Strategy = Flow_Refresh_Strategy; +TEST(flow_refresh_strategy_test, observer_can_reenter_observer_state_without_deadlock) { + Flow_Refresh_Reentrant_Observer recorder; + auto data = recorder.data; + Flow_Refresh_Reentrant_Strategy strategy{Flow_Refresh_Reentrant_Observer_State(recorder)}; + std::atomic reentered{}; + data->callback = [&](int event) { + if (event != static_cast(Flow_Refresh_Reentrant_Strategy::Observation_Event::enqueued) || reentered.exchange(true)) { + return; + } + strategy.on_real_time_data_update({Real_Time_Data_Observation_Event::updated, {nullptr, Real_Time_Data_Retention::latest, 1, 1, 1, 1}}); + }; + { + auto frame = strategy.acquire_painter(); + frame->value = 1; + } + EXPECT_TRUE(reentered.load()); + EXPECT_EQ(strategy.state().real_time_data_update_sequence, 1); +} +struct Flow_Refresh_Render_Reentrant_Observer_Data { + std::function callback; +}; +struct Flow_Refresh_Render_Reentrant_Observer { + static constexpr bool enabled = true; + std::shared_ptr data{std::make_shared()}; + template + void observe(const Observation& observation) noexcept { + if (static_cast(observation.event) == 1 && data->callback) { + data->callback(); + } + } +}; +using Flow_Refresh_Render_Reentrant_Observer_State = Observer_State; +using Flow_Refresh_Render_Reentrant_Strategy = Flow_Refresh_Strategy; +TEST(flow_refresh_strategy_test, render_observer_reentry_does_not_deadlock_render_mutex) { + Flow_Refresh_Render_Reentrant_Observer recorder; + auto data = recorder.data; + Flow_Refresh_Render_Reentrant_Strategy strategy{Flow_Refresh_Render_Reentrant_Observer_State(recorder)}; + for (int value = 1; value <= 2; ++value) { + auto frame = strategy.acquire_painter(); + frame->value = value; + } + std::atomic nested_result{true}; + data->callback = [&] { + auto nested = strategy.acquire_renderer(); + nested_result.store(static_cast(nested), std::memory_order_release); + }; + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, 1); + } + EXPECT_FALSE(nested_result.load(std::memory_order_acquire)); + auto second = strategy.acquire_renderer(); + ASSERT_TRUE(second); + EXPECT_EQ(second->value, 2); +} diff --git a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp index 7587ebe..63f8ef9 100644 --- a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include "Low_Latency_Strategy_Test_Types.hpp" TEST(low_latency_strategy_test, publishes_and_renders_latest_frame) { Low_Latency_Test_Time_Source time_source; @@ -98,3 +100,27 @@ TEST(low_latency_strategy_test, detects_render_limited_state) { } EXPECT_EQ(strategy.state().limit_state, Low_Latency_Test_Strategy::Limit_State::render_limited); } +TEST(low_latency_strategy_test, rejects_non_finite_frequency_configuration) { + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + EXPECT_THROW((Low_Latency_Test_Strategy(Low_Latency_Test_Observer_State(observer, time_source), {std::numeric_limits::quiet_NaN()})), std::invalid_argument); + EXPECT_THROW((Low_Latency_Test_Strategy(Low_Latency_Test_Observer_State(observer, time_source), {std::numeric_limits::infinity()})), std::invalid_argument); + auto strategy = make_low_latency_test_strategy(time_source, observer); + EXPECT_THROW(strategy.set_frequency_hz(std::numeric_limits::quiet_NaN()), std::invalid_argument); + EXPECT_THROW(strategy.set_frequency_hz(std::numeric_limits::infinity()), std::invalid_argument); +} +TEST(low_latency_strategy_test, clamps_extremely_small_frequency_interval_without_overflow) { + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + auto strategy = make_low_latency_test_strategy(time_source, observer); + strategy.set_frequency_hz(1e-300); + { + auto frame = strategy.acquire_painter(); + frame->value = 1; + } + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + } + EXPECT_EQ(strategy.state().target_interval_ns, std::numeric_limits::max()); +} diff --git a/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp b/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp index 5d9667b..5e2e0c7 100644 --- a/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp +++ b/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "renderive/real_time_data/Real_Time_Data.hpp" #include "renderive/renderable/Renderable.hpp" @@ -45,15 +46,15 @@ TEST(history_real_time_data_test, retains_all_values_until_explicit_discard) { } TEST(real_time_data_attachment_test, binds_updates_to_renderable_frame_strategy) { Scene2D_Context<> scene; - Real_Time_Data_Test_Latest latest; - Real_Time_Data_Test_History history; + auto latest = std::make_shared(); + auto history = std::make_shared(); auto renderable = std::make_shared(With_Real_Time_Data(latest, history), scene); renderable->discard_stale_frame_on_latest_data_update = true; { auto frame = scene.frame_control.acquire_painter(); ASSERT_TRUE(frame); } - latest.update(1); + latest->update(1); EXPECT_EQ(scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); auto frame = scene.frame_control.acquire_renderer(); EXPECT_FALSE(frame); @@ -61,16 +62,130 @@ TEST(real_time_data_attachment_test, binds_updates_to_renderable_frame_strategy) TEST(real_time_data_attachment_test, discards_history_older_than_one_frame_interval) { using Strategy = Low_Latency_Strategy; Scene2D_Context scene(Observer_State<>{}, Strategy::Configuration{100'000'000.0}); - Real_Time_Data_Test_Latest latest; - Real_Time_Data_Test_History history; + auto latest = std::make_shared(); + auto history = std::make_shared(); auto renderable = std::make_shared(With_Real_Time_Data(latest, history), scene); renderable->real_time_data_discard_mode = Real_Time_Data_Discard_Mode::retain_frame_interval; - history.update(1); - history.update(2); - history.update(3); + history->update(1); + history->update(2); + history->update(3); scene.attach_renderable(renderable); scene.render(); scene.wait_for_render(); - EXPECT_EQ(history.snapshot(), (std::vector{2, 3})); + EXPECT_EQ(history->snapshot(), (std::vector{2, 3})); EXPECT_EQ(renderable->render_count.load(), 1); } +using Real_Time_Data_Test_Latest_Attachment = Attach_Real_Time_Data; +TEST(real_time_data_attachment_test, supports_multiple_renderables_and_unbinds_destroyed_renderable) { + Scene2D_Context<> first_scene; + Scene2D_Context<> second_scene; + auto latest = std::make_shared(); + auto first = std::make_shared(With_Real_Time_Data(latest), first_scene); + auto second = std::make_shared(With_Real_Time_Data(latest), second_scene); + first->discard_stale_frame_on_latest_data_update = true; + second->discard_stale_frame_on_latest_data_update = true; + { + auto frame = first_scene.frame_control.acquire_painter(); + ASSERT_TRUE(frame); + } + { + auto frame = second_scene.frame_control.acquire_painter(); + ASSERT_TRUE(frame); + } + latest->update(1); + EXPECT_EQ(first_scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); + EXPECT_EQ(second_scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); + first.reset(); + { + auto frame = second_scene.frame_control.acquire_painter(); + ASSERT_TRUE(frame); + } + latest->update(2); + EXPECT_EQ(first_scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); + EXPECT_EQ(second_scene.frame_control.counter_statistics().manually_discarded_frame_count, 2); +} +TEST(real_time_data_attachment_test, unbind_is_synchronized_with_concurrent_updates) { + Scene2D_Context<> scene; + auto latest = std::make_shared(); + auto renderable = std::make_shared(With_Real_Time_Data(latest), scene); + std::atomic running{true}; + std::atomic updated{}; + std::thread updater([&] { + int value{}; + while (running.load(std::memory_order_acquire)) { + latest->update(++value); + updated.store(true, std::memory_order_release); + } + }); + while (!updated.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + renderable.reset(); + running.store(false, std::memory_order_release); + updater.join(); + EXPECT_TRUE(latest->revision() > 0); +} +TEST(real_time_data_attachment_test, runtime_discard_configuration_is_safe_during_updates_and_rendering) { + Scene2D_Context<> scene; + auto latest = std::make_shared(); + auto history = std::make_shared(); + auto renderable = std::make_shared(With_Real_Time_Data(latest, history), scene); + scene.attach_renderable(renderable); + std::thread updater([&] { + for (int value = 0; value < 1000; ++value) { + latest->update(value); + } + }); + std::thread configurator([&] { + for (int index = 0; index < 1000; ++index) { + renderable->discard_stale_frame_on_latest_data_update = index % 2 != 0; + renderable->real_time_data_discard_mode = index % 2 == 0 ? Real_Time_Data_Discard_Mode::retain_all : Real_Time_Data_Discard_Mode::retain_frame_interval; + } + }); + for (int index = 0; index < 20; ++index) { + scene.render(); + scene.wait_for_render(); + } + updater.join(); + configurator.join(); + EXPECT_EQ(latest->revision(), 1000); +} +TEST(real_time_data_attachment_test, ignores_updates_after_bound_scene_is_destroyed) { + auto latest = std::make_shared(); + std::shared_ptr renderable; + { + Scene2D_Context<> scene; + renderable = std::make_shared(With_Real_Time_Data(latest), scene); + latest->update(1); + EXPECT_EQ(scene.frame_control.state().real_time_data_update_sequence, 1); + } + EXPECT_NO_THROW(latest->update(2)); + EXPECT_THROW(renderable->scene(), std::logic_error); + renderable.reset(); +} +TEST(real_time_data_attachment_test, notifies_each_scene_once_for_one_source_update) { + Scene2D_Context<> scene; + auto latest = std::make_shared(); + auto first = std::make_shared(With_Real_Time_Data(latest), scene); + auto second = std::make_shared(With_Real_Time_Data(latest), scene); + first->discard_stale_frame_on_latest_data_update = true; + second->discard_stale_frame_on_latest_data_update = true; + { + auto frame = scene.frame_control.acquire_painter(); + ASSERT_TRUE(frame); + } + latest->update(1); + EXPECT_EQ(scene.frame_control.state().real_time_data_update_sequence, 1); + EXPECT_EQ(scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); +} +TEST(real_time_data_attachment_test, attachment_owns_real_time_data_sources) { + Scene2D_Context<> scene; + auto latest = std::make_shared(); + std::weak_ptr weak = latest; + auto renderable = std::make_shared(With_Real_Time_Data(latest), scene); + latest.reset(); + EXPECT_FALSE(weak.expired()); + renderable->real_time_data().update(1); + renderable.reset(); + EXPECT_TRUE(weak.expired()); +} diff --git a/Kernel/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp b/Kernel/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp index 58f8308..93c3af9 100644 --- a/Kernel/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp +++ b/Kernel/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include "renderive/renderable/Renderable.hpp" @@ -25,14 +26,83 @@ TEST(renderable_task_graph_test, combines_internal_and_external_dependencies) { auto dependent = std::make_shared(scene, order, 10); scene.attach_renderable(dependency); scene.attach_renderable(dependent); - dependency->dependency_node.append_child(dependent->dependency_node); + scene.set_dependency_parent(*dependent, dependency.get()); scene.render(); scene.wait_for_render(); EXPECT_EQ(order, (std::vector{1, 2, 11, 12})); } - -#include -#include +struct Renderable_Task_Graph_Branch_Test_Renderable : Renderable_Base { + Renderable_Task_Graph_Branch_Test_Renderable(Scene_Base& scene, std::atomic& state, std::atomic& invalid) + : Renderable_Base(scene, {.cache_enabled = false}), state(&state), invalid(&invalid) {} + void build_task_graph(Renderable_Task_Graph& graph) override { + auto first = graph.emplace([this](const Scene_Render_Context&) { + state->fetch_or(1u, std::memory_order_release); + }, "first"); + auto second = graph.emplace([this](const Scene_Render_Context&) { + state->fetch_or(2u, std::memory_order_release); + }, "second"); + auto join = graph.emplace([this](const Scene_Render_Context&) { + if ((state->load(std::memory_order_acquire) & 3u) != 3u) { + invalid->store(true, std::memory_order_release); + } + state->fetch_or(4u, std::memory_order_release); + }, "join"); + graph.precede(first, join); + graph.precede(second, join); + } + std::atomic* state; + std::atomic* invalid; +}; +TEST(renderable_task_graph_test, preserves_internal_fan_in_dependencies) { + Scene2D_Context<> scene; + std::atomic state{}; + std::atomic invalid{}; + auto renderable = std::make_shared(scene, state, invalid); + scene.attach_renderable(renderable); + scene.render(); + scene.wait_for_render(); + EXPECT_FALSE(invalid.load(std::memory_order_acquire)); + EXPECT_EQ(state.load(std::memory_order_acquire), 7u); +} +struct Renderable_Task_Graph_Parent_Test_Renderable : Renderable_Base { + Renderable_Task_Graph_Parent_Test_Renderable(Scene_Base& scene, std::atomic& state) + : Renderable_Base(scene, {.cache_enabled = false}), state(&state) {} + void build_task_graph(Renderable_Task_Graph& graph) override { + graph.emplace([this](const Scene_Render_Context&) { + state->fetch_or(1u, std::memory_order_release); + }, "parent_a"); + graph.emplace([this](const Scene_Render_Context&) { + state->fetch_or(2u, std::memory_order_release); + }, "parent_b"); + } + std::atomic* state; +}; +struct Renderable_Task_Graph_Child_Test_Renderable : Renderable_Base { + Renderable_Task_Graph_Child_Test_Renderable(Scene_Base& scene, std::atomic& state, std::atomic& invalid) + : Renderable_Base(scene, {.cache_enabled = false}), state(&state), invalid(&invalid) {} + void render(const Scene_Render_Context&) override { + if ((state->load(std::memory_order_acquire) & 3u) != 3u) { + invalid->store(true, std::memory_order_release); + } + } + std::atomic* state; + std::atomic* invalid; +}; +TEST(renderable_task_graph_test, waits_for_all_parent_terminal_tasks_before_dependent_module) { + Scene2D_Context<> scene; + std::atomic state{}; + std::atomic invalid{}; + auto parent = std::make_shared(scene, state); + auto child = std::make_shared(scene, state, invalid); + scene.attach_renderable(parent); + scene.attach_renderable(child); + scene.set_dependency_parent(*child, parent.get()); + scene.render(); + scene.wait_for_render(); + EXPECT_FALSE(invalid.load(std::memory_order_acquire)); + EXPECT_EQ(state.load(std::memory_order_acquire), 3u); +} +#ifndef RENDERIVE_TASKFLOW_MOCK #include #include struct Renderable_Task_Graph_Concurrency_Test_Renderable : Renderable_Base { @@ -52,7 +122,7 @@ struct Renderable_Task_Graph_Concurrency_Test_Renderable : Renderable_Base { std::atomic* active; std::atomic* maximum; }; -TEST(renderable_task_graph_concurrency_test, DISABLED_runs_independent_internal_tasks_concurrently) { +TEST(renderable_task_graph_concurrency_test, runs_independent_internal_tasks_concurrently) { Scene2D_Context<> scene; std::atomic active{}; std::atomic maximum{}; @@ -62,3 +132,27 @@ TEST(renderable_task_graph_concurrency_test, DISABLED_runs_independent_internal_ scene.wait_for_render(); EXPECT_GE(maximum.load(std::memory_order_acquire), 2); } +#endif +TEST(renderable_task_graph_test, rejects_tasks_from_different_graphs) { + Renderable_Task_Graph first; + Renderable_Task_Graph second; + auto first_task = first.emplace([](const Scene_Render_Context&) {}); + auto second_task = second.emplace([](const Scene_Render_Context&) {}); + EXPECT_THROW(first.precede(first_task, second_task), std::invalid_argument); +} +TEST(renderable_task_graph_test, rejects_stale_task_handles_after_clear) { + Renderable_Task_Graph graph; + auto stale = graph.emplace([](const Scene_Render_Context&) {}); + graph.clear(); + auto current = graph.emplace([](const Scene_Render_Context&) {}); + EXPECT_THROW(graph.precede(stale, current), std::invalid_argument); +} +TEST(renderable_task_graph_test, rejects_cycles_when_dependency_is_added) { + Renderable_Task_Graph graph; + auto first = graph.emplace([](const Scene_Render_Context&) {}); + auto second = graph.emplace([](const Scene_Render_Context&) {}); + auto third = graph.emplace([](const Scene_Render_Context&) {}); + graph.precede(first, second); + graph.precede(second, third); + EXPECT_THROW(graph.precede(third, first), std::invalid_argument); +} diff --git a/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp b/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp index 7442f8a..ff45d92 100644 --- a/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp +++ b/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp @@ -1,6 +1,9 @@ #include #include +#include #include +#include +#include #include "renderive/renderable/Renderable.hpp" #include "renderive/scene/Scene.hpp" struct Renderable_Base_Test_Renderable : Renderable_Base { @@ -10,15 +13,15 @@ struct Renderable_Base_Test_Renderable : Renderable_Base { } int render_count{}; }; -TEST(renderable_base_test, exposes_scene_and_strategy_members_directly) { +TEST(renderable_base_test, exposes_scene_and_strategy_through_accessors) { Scene2D_Context<> low_latency_scene; Scene2D_Context> manual_scene; Renderable_Base_Test_Renderable low_latency_renderable(low_latency_scene, true); Renderable_Base_Test_Renderable manual_renderable(manual_scene, true); - EXPECT_EQ(low_latency_renderable.scene, &low_latency_scene); - EXPECT_EQ(low_latency_renderable.scene->frame_control_strategy, &low_latency_scene.frame_control); - EXPECT_DOUBLE_EQ(low_latency_renderable.scene->frame_control_strategy->frequency_hz(), 60.0); - EXPECT_TRUE(std::isnan(manual_renderable.scene->frame_control_strategy->frequency_hz())); + EXPECT_EQ(&low_latency_renderable.scene(), &low_latency_scene); + EXPECT_EQ(&low_latency_renderable.scene().frame_control_strategy(), &low_latency_scene.frame_control); + EXPECT_DOUBLE_EQ(low_latency_renderable.scene().frame_control_strategy().frequency_hz(), 60.0); + EXPECT_TRUE(std::isnan(manual_renderable.scene().frame_control_strategy().frequency_hz())); } TEST(renderable_base_test, controls_cache_with_configuration) { Scene2D_Context<> scene; @@ -38,3 +41,149 @@ TEST(renderable_base_test, controls_cache_with_configuration) { EXPECT_EQ(cached->render_count, 2); EXPECT_EQ(uncached->render_count, 3); } +TEST(renderable_base_test, changes_configuration_through_scene) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene, false); + scene.attach_renderable(renderable); + scene.render(); + scene.wait_for_render(); + scene.set_renderable_configuration(*renderable, {.cache_enabled = true}); + scene.render(); + scene.wait_for_render(); + EXPECT_TRUE(renderable->configuration().cache_enabled); + EXPECT_EQ(renderable->render_count, 1); +} +struct Renderable_Base_Cache_Invalidation_Test_Renderable : Renderable_Base { + explicit Renderable_Base_Cache_Invalidation_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = true}) {} + void render(const Scene_Render_Context&) override { + const int count = render_count.fetch_add(1, std::memory_order_acq_rel) + 1; + if (count != 1) { + return; + } + std::unique_lock lock(mutex); + started = true; + condition.notify_all(); + condition.wait(lock, [this] { + return released; + }); + } + void wait_started() { + std::unique_lock lock(mutex); + condition.wait(lock, [this] { + return started; + }); + } + void release() { + std::lock_guard lock(mutex); + released = true; + condition.notify_all(); + } + std::atomic render_count{}; + std::mutex mutex; + std::condition_variable condition; + bool started{}; + bool released{}; +}; +TEST(renderable_base_test, preserves_cache_invalidation_that_happens_during_render) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene); + scene.attach_renderable(renderable); + scene.render(); + renderable->wait_started(); + renderable->invalidate_cache(); + renderable->release(); + scene.wait_for_render(); + EXPECT_FALSE(renderable->cache_valid()); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(renderable->render_count.load(std::memory_order_acquire), 2); + EXPECT_TRUE(renderable->cache_valid()); +} +struct Renderable_Base_Task_Graph_Rebuild_Test_Renderable : Renderable_Base { + explicit Renderable_Base_Task_Graph_Rebuild_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene) {} + void build_task_graph(Renderable_Task_Graph& graph) override { + const int count = build_count.fetch_add(1, std::memory_order_acq_rel) + 1; + if (count == 1) { + std::unique_lock lock(mutex); + build_started = true; + condition.notify_all(); + condition.wait(lock, [this] { + return release_build; + }); + } + graph.emplace([](const Scene_Render_Context&) {}, "graph_rebuild_test"); + } + void wait_build_started() { + std::unique_lock lock(mutex); + condition.wait(lock, [this] { + return build_started; + }); + } + void release() { + std::lock_guard lock(mutex); + release_build = true; + condition.notify_all(); + } + std::atomic build_count{}; + std::mutex mutex; + std::condition_variable condition; + bool build_started{}; + bool release_build{}; +}; +TEST(renderable_base_test, preserves_rebuild_request_that_arrives_during_task_graph_build) { + Scene2D_Context<> scene; + Renderable_Base_Task_Graph_Rebuild_Test_Renderable renderable(scene); + std::shared_ptr first_graph; + std::thread builder([&] { + first_graph = renderable.task_graph(); + }); + renderable.wait_build_started(); + std::thread rebuilder([&] { + renderable.rebuild_task_graph(); + }); + renderable.release(); + builder.join(); + rebuilder.join(); + const auto second_graph = renderable.task_graph(); + EXPECT_EQ(renderable.build_count.load(std::memory_order_acquire), 2); + EXPECT_NE(first_graph.get(), second_graph.get()); +} +TEST(renderable_base_test, task_graph_snapshots_remain_valid_during_concurrent_rebuilds) { + Scene2D_Context<> scene; + Renderable_Base_Task_Graph_Rebuild_Test_Renderable renderable(scene); + renderable.release(); + std::atomic running{true}; + std::thread rebuilder([&] { + for (int index = 0; index < 500; ++index) { + renderable.rebuild_task_graph(); + } + running.store(false, std::memory_order_release); + }); + while (running.load(std::memory_order_acquire)) { + const auto graph = renderable.task_graph(); + ASSERT_TRUE(graph); + EXPECT_EQ(graph->nodes().size(), 1); + } + rebuilder.join(); + const auto graph = renderable.task_graph(); + ASSERT_TRUE(graph); + EXPECT_EQ(graph->nodes().size(), 1); +} +TEST(renderable_base_test, configuration_snapshot_is_safe_during_scene_updates) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene, false); + scene.attach_renderable(renderable); + std::atomic running{true}; + std::thread writer([&] { + for (int index = 0; index < 1000; ++index) { + scene.set_renderable_configuration(*renderable, {.cache_enabled = index % 2 != 0}); + } + running.store(false, std::memory_order_release); + }); + while (running.load(std::memory_order_acquire)) { + const auto configuration = renderable->configuration(); + static_cast(configuration.cache_enabled); + } + writer.join(); + EXPECT_TRUE(renderable->configuration().cache_enabled); +} diff --git a/Kernel/tests/renderive/renderable/concept/Renderable_Test.cpp b/Kernel/tests/renderive/renderable/concept/Renderable_Test.cpp index aaa5603..428b9b7 100644 --- a/Kernel/tests/renderive/renderable/concept/Renderable_Test.cpp +++ b/Kernel/tests/renderive/renderable/concept/Renderable_Test.cpp @@ -8,11 +8,9 @@ struct Renderable_Concept_Test : Renderable_Base { }; static_assert(Renderable); static_assert(!std::same_as); -TEST(renderable_concept_test, exposes_tree_nodes_scene_and_configuration) { +TEST(renderable_concept_test, exposes_scene_and_configuration_state) { Scene2D_Context<> scene; Renderable_Concept_Test renderable(scene); - EXPECT_EQ(renderable.scene, &scene); - EXPECT_EQ(renderable.layer_node.owner, &renderable); - EXPECT_EQ(renderable.dependency_node.owner, &renderable); - EXPECT_FALSE(renderable.configuration.cache_enabled); + EXPECT_EQ(&renderable.scene(), &scene); + EXPECT_FALSE(renderable.configuration().cache_enabled); } diff --git a/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp b/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp index 8b78930..d8555c3 100644 --- a/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp @@ -2,8 +2,10 @@ #include #include #include +#include #include "renderive/renderable/Renderable.hpp" #include "renderive/scene/Scene.hpp" +static_assert(std::same_as&>().topology_snapshot()), Scene_Base::Topology_Snapshot>); struct Scene2D_Context_Test_Renderable : Renderable_Base { explicit Scene2D_Context_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} void render(const Scene_Render_Context& context) override { @@ -32,5 +34,98 @@ TEST(scene2d_context_test, swaps_renderable_cache_and_renders_in_background) { scene.wait_for_render(); EXPECT_EQ(first->render_count.load(), 1); EXPECT_EQ(second->render_count.load(), 1); - EXPECT_EQ(scene.frame_control_strategy->frame_control_state().publish_revision, 2); + EXPECT_EQ(scene.frame_control_strategy().frame_control_state().publish_revision, 2); +} +struct Scene2D_Final_Cache_Blocking_Renderable : Renderable_Base { + explicit Scene2D_Final_Cache_Blocking_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} + void render(const Scene_Render_Context&) override { + while (!release.load(std::memory_order_acquire)) { + started.store(true, std::memory_order_release); + std::this_thread::yield(); + } + } + std::atomic started{}; + std::atomic release{}; +}; +TEST(scene2d_context_test, final_color_cache_access_waits_for_render_completion) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene); + scene.attach_renderable(renderable); + scene.render(); + while (!renderable->started.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + std::atomic access_started{}; + std::atomic cache_accessed{}; + std::thread reader([&] { + access_started.store(true, std::memory_order_release); + scene.with_final_color_cache([&](const Recording_Color_Cache&) { + cache_accessed.store(true, std::memory_order_release); + }); + }); + while (!access_started.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + EXPECT_FALSE(cache_accessed.load(std::memory_order_acquire)); + renderable->release.store(true, std::memory_order_release); + reader.join(); + EXPECT_TRUE(cache_accessed.load(std::memory_order_acquire)); +} +struct Scene2D_Cached_Renderable : Renderable_Base { + Scene2D_Cached_Renderable(Scene_Base& scene, std::uint64_t value) : Renderable_Base(scene, {.cache_enabled = true}), value(value) {} + void render(const Scene_Render_Context& context) override { + ++render_count; + dynamic_cast(*context.color_cache).append(value); + } + std::uint64_t value; + int render_count{}; +}; +TEST(scene2d_context_test, reattached_cached_renderable_rebuilds_removed_color_cache) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene, 7); + scene.attach_renderable(renderable); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(renderable->render_count, 1); + scene.detach_renderable(*renderable); + scene.attach_renderable(renderable); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(renderable->render_count, 2); + scene.with_final_color_cache([](const Recording_Color_Cache& cache) { + EXPECT_EQ(cache.values.size(), 1); + EXPECT_EQ(cache.values.at(0), 7); + }); +} +TEST(scene2d_context_test, dependency_reparent_invalidates_cached_child) { + Scene2D_Context<> scene; + auto first_parent = std::make_shared(scene, 1); + auto second_parent = std::make_shared(scene, 2); + auto child = std::make_shared(scene, 3); + scene.attach_renderable(first_parent); + scene.attach_renderable(second_parent); + scene.attach_renderable(child); + scene.set_dependency_parent(*child, first_parent.get()); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(first_parent->render_count, 1); + EXPECT_EQ(second_parent->render_count, 1); + EXPECT_EQ(child->render_count, 1); + scene.set_dependency_parent(*child, second_parent.get()); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(first_parent->render_count, 1); + EXPECT_EQ(second_parent->render_count, 1); + EXPECT_EQ(child->render_count, 2); +} +TEST(scene2d_context_test, final_color_cache_callback_can_reenter_scene_control_api) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene, 9); + scene.attach_renderable(renderable); + scene.render(); + scene.wait_for_render(); + scene.with_final_color_cache([&](const Recording_Color_Cache&) { + scene.set_renderable_configuration(*renderable, {.cache_enabled = false}); + }); + EXPECT_FALSE(renderable->configuration().cache_enabled); } diff --git a/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp b/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp index 332797c..2abd9c5 100644 --- a/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp @@ -24,12 +24,16 @@ TEST(scene2d_render_order_test, separates_dependency_order_from_display_order) { auto axis = std::make_shared(scene, state, 1); scene.attach_renderable(spectrum); scene.attach_renderable(axis); - axis->dependency_node.append_child(spectrum->dependency_node); - spectrum->layer_node.append_child(axis->layer_node); + scene.set_dependency_parent(*spectrum, axis.get()); + scene.set_display_parent(*axis, spectrum.get()); scene.render(); scene.wait_for_render(); EXPECT_EQ(state.render_order, (std::vector{1, 2})); - EXPECT_EQ(scene.final_color_cache.values.size(), 2); - EXPECT_EQ(scene.final_color_cache.values.at(0), 2); - EXPECT_EQ(scene.final_color_cache.values.at(1), 1); + std::vector final_values; + scene.with_final_color_cache([&final_values](const Recording_Color_Cache& cache) { + final_values.assign(cache.values.begin(), cache.values.end()); + }); + EXPECT_EQ(final_values.size(), 2); + EXPECT_EQ(final_values.at(0), 2); + EXPECT_EQ(final_values.at(1), 1); } diff --git a/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp b/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp index 60215fe..a08e3cf 100644 --- a/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp @@ -21,3 +21,32 @@ TEST(scene3d_context_test, uses_scene_3d_base_contract) { EXPECT_EQ(renderable->render_count.load(), 1); EXPECT_TRUE(renderable->scene_is_3d); } +struct Scene3D_Dependency_Cache_Test_Renderable : Renderable_Base { + explicit Scene3D_Dependency_Cache_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = true}) {} + void render(const Scene_Render_Context&) override { + ++render_count; + } + int render_count{}; +}; +TEST(scene3d_context_test, propagates_cache_invalidation_through_unsorted_dependency_chain) { + Scene3D_Context<> scene; + auto leaf = std::make_shared(scene); + auto middle = std::make_shared(scene); + auto root = std::make_shared(scene); + scene.attach_renderable(leaf); + scene.attach_renderable(middle); + scene.attach_renderable(root); + scene.set_dependency_parent(*leaf, middle.get()); + scene.set_dependency_parent(*middle, root.get()); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(root->render_count, 1); + EXPECT_EQ(middle->render_count, 1); + EXPECT_EQ(leaf->render_count, 1); + root->invalidate_cache(); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(root->render_count, 2); + EXPECT_EQ(middle->render_count, 2); + EXPECT_EQ(leaf->render_count, 2); +} diff --git a/Kernel/tests/renderive/scene/Scene_Memory_Resource_Test.cpp b/Kernel/tests/renderive/scene/Scene_Memory_Resource_Test.cpp index f4df722..3c4b91a 100644 --- a/Kernel/tests/renderive/scene/Scene_Memory_Resource_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene_Memory_Resource_Test.cpp @@ -96,3 +96,17 @@ TEST(scene_memory_resource_test, supports_independent_resource_aware_components) EXPECT_EQ(unique_product->value, 8); EXPECT_GT(memory_resource.allocation_count(), 0); } +TEST(scene_memory_resource_test, keeps_independent_renderable_memory_domain_alive_until_renderable_release) { + Scene_Memory_Resource_Test_Resource upstream; + std::shared_ptr renderable; + { + Scene2D_Context<> scene(upstream); + renderable = std::make_shared(scene); + renderable->task_graph(); + } + auto& memory_resource = renderable->memory_resource(); + void* storage = memory_resource.allocate(32, alignof(std::max_align_t)); + memory_resource.deallocate(storage, 32, alignof(std::max_align_t)); + renderable.reset(); + EXPECT_GT(upstream.deallocation_count(), 0); +} diff --git a/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp b/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp index 6a56b59..62b0789 100644 --- a/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp @@ -1,5 +1,7 @@ #include +#include #include +#include #include #include #include "renderive/scene/Scene.hpp" @@ -34,3 +36,32 @@ TEST(scene_state_observer_test, scene_uses_triple_state_and_observes_lifecycle) EXPECT_EQ(scene.Scene_State_Strategy::render_use_state().value, 9); EXPECT_EQ(recorder.data->events, (std::vector{Scene_Base::Observation_Event::render_submitted, Scene_Base::Observation_Event::render_started, Scene_Base::Observation_Event::render_completed})); } +struct Scene_Reentrant_Observer_Data { + std::function callback; +}; +struct Scene_Reentrant_Observer { + static constexpr bool enabled = true; + std::shared_ptr data{std::make_shared()}; + void observe(const Scene_Base::Observation& observation) noexcept { + if (data->callback) { + data->callback(observation.event); + } + } +}; +using Scene_Reentrant_Observer_State = Observer_State; +TEST(scene_state_observer_test, render_worker_observer_can_wait_for_current_render_without_deadlock) { + Scene_Reentrant_Observer recorder; + auto data = recorder.data; + using Scene = Scene2D_Context, Recording_Color_Cache, Scene_State_Observer_Test_State, Scene_State_Observer_Test_State_Observer, Scene_Reentrant_Observer_State>; + Scene scene(With_Observer(Scene_State_Observer_Test_State_Observer{}), With_Observer(Scene_Reentrant_Observer_State(recorder, Scene_State_Observer_Test_Time_Source{}))); + std::atomic reentered{}; + data->callback = [&](Scene_Base::Observation_Event event) { + if (event == Scene_Base::Observation_Event::render_started) { + scene.wait_for_render(); + reentered.store(true, std::memory_order_release); + } + }; + scene.render(); + scene.wait_for_render(); + EXPECT_TRUE(reentered.load(std::memory_order_acquire)); +} diff --git a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp index ff83ad5..9fc60b5 100644 --- a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp +++ b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp @@ -1,5 +1,10 @@ #include +#include +#include #include +#include +#include +#include #include "renderive/renderable/Renderable.hpp" #include "renderive/scene/Scene.hpp" struct Scene_Base_Test_Renderable : Renderable_Base { @@ -21,3 +26,192 @@ TEST(scene_base_test, publishes_renderable_collection_before_background_render) EXPECT_EQ(renderable->render_sequence, 1); EXPECT_EQ(scene.renderable_count(), 1); } +struct Scene_Base_Throwing_Renderable : Renderable_Base { + explicit Scene_Base_Throwing_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} + void render(const Scene_Render_Context&) override { + throw std::runtime_error("render failed"); + } +}; +TEST(scene_base_test, wait_for_render_propagates_background_render_failure) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene); + scene.attach_renderable(renderable); + scene.render(); + EXPECT_THROW(scene.wait_for_render(), std::runtime_error); +} +TEST(scene_base_test, scene_destruction_does_not_throw_after_background_render_failure) { + { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene); + scene.attach_renderable(renderable); + scene.render(); + } + EXPECT_TRUE(true); +} +struct Scene_Base_Blocking_Renderable : Renderable_Base { + explicit Scene_Base_Blocking_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} + void render(const Scene_Render_Context&) override { + std::unique_lock lock(mutex); + rendering = true; + condition.notify_all(); + condition.wait(lock, [this] { + return released; + }); + } + void wait_rendering() { + std::unique_lock lock(mutex); + condition.wait(lock, [this] { + return rendering; + }); + } + void release() { + std::lock_guard lock(mutex); + released = true; + condition.notify_all(); + } + std::mutex mutex; + std::condition_variable condition; + bool rendering{}; + bool released{}; +}; +TEST(scene_base_test, scene_mutation_waits_until_background_render_is_idle) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene); + scene.attach_renderable(renderable); + scene.render(); + renderable->wait_rendering(); + std::atomic mutation_finished{}; + std::thread mutation([&] { + scene.set_renderable_configuration(*renderable, {.cache_enabled = true}); + mutation_finished.store(true, std::memory_order_release); + }); + std::this_thread::yield(); + EXPECT_FALSE(mutation_finished.load(std::memory_order_acquire)); + renderable->release(); + mutation.join(); + EXPECT_TRUE(mutation_finished.load(std::memory_order_acquire)); + EXPECT_TRUE(renderable->configuration().cache_enabled); +} +TEST(scene_base_test, concurrent_render_and_topology_updates_remain_serialized) { + Scene2D_Context<> scene; + auto parent = std::make_shared(scene); + auto child = std::make_shared(scene); + scene.attach_renderable(parent); + scene.attach_renderable(child); + std::thread renderer([&] { + for (int index = 0; index < 100; ++index) { + scene.render(); + scene.wait_for_render(); + } + }); + std::thread mutator([&] { + for (int index = 0; index < 100; ++index) { + Renderable_Base* value = index % 2 == 0 ? parent.get() : nullptr; + scene.set_dependency_parent(*child, value); + scene.set_display_parent(*child, value); + scene.set_renderable_configuration(*child, {.cache_enabled = index % 2 == 0}); + } + }); + renderer.join(); + mutator.join(); + EXPECT_EQ(scene.renderable_count(), 2); +} +TEST(scene_base_test, detach_releases_renderable_from_both_scene_buffers) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene); + std::weak_ptr weak = renderable; + scene.attach_renderable(renderable); + scene.render(); + scene.wait_for_render(); + scene.detach_renderable(*renderable); + renderable.reset(); + EXPECT_TRUE(weak.expired()); + EXPECT_EQ(scene.renderable_count(), 0); +} +TEST(scene_base_test, attach_rejects_null_renderable) { + Scene2D_Context<> scene; + EXPECT_THROW(scene.attach_renderable({}), std::invalid_argument); +} +TEST(scene_base_test, topology_rejects_unattached_renderables) { + Scene2D_Context<> scene; + auto parent = std::make_shared(scene); + auto child = std::make_shared(scene); + scene.attach_renderable(child); + EXPECT_THROW(scene.set_dependency_parent(*child, parent.get()), std::invalid_argument); + EXPECT_THROW(scene.set_display_parent(*child, parent.get()), std::invalid_argument); + scene.attach_renderable(parent); + auto detached_child = std::make_shared(scene); + EXPECT_THROW(scene.set_dependency_parent(*detached_child, parent.get()), std::invalid_argument); +} +TEST(scene_base_test, setting_same_topology_parent_is_a_noop) { + Scene2D_Context<> scene; + auto parent = std::make_shared(scene); + auto child = std::make_shared(scene); + scene.attach_renderable(parent); + scene.attach_renderable(child); + scene.set_dependency_parent(*child, parent.get()); + const auto revision = child->cache_revision(); + EXPECT_NO_THROW(scene.set_dependency_parent(*child, parent.get())); + EXPECT_EQ(child->cache_revision(), revision); + const auto topology = scene.topology_snapshot(); + EXPECT_EQ(topology.dependency.size(), 2); + bool found{}; + for (const auto& relation : topology.dependency) { + if (relation.child.get() == child.get()) { + EXPECT_EQ(relation.parent.get(), parent.get()); + found = true; + } + } + EXPECT_TRUE(found); +} +TEST(scene_base_test, topology_snapshot_retains_renderable_lifetime) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene); + std::weak_ptr weak = renderable; + scene.attach_renderable(renderable); + const auto snapshot = scene.topology_snapshot(); + scene.detach_renderable(*renderable); + renderable.reset(); + EXPECT_FALSE(weak.expired()); + EXPECT_EQ(snapshot.renderables.size(), 1); + EXPECT_EQ(snapshot.renderables.front().get(), weak.lock().get()); +} +TEST(scene_base_test, topology_snapshot_is_safe_during_topology_updates) { + Scene2D_Context<> scene; + auto parent = std::make_shared(scene); + auto child = std::make_shared(scene); + scene.attach_renderable(parent); + scene.attach_renderable(child); + std::thread writer([&] { + for (int index = 0; index < 500; ++index) { + scene.set_dependency_parent(*child, index % 2 == 0 ? parent.get() : nullptr); + scene.set_display_parent(*child, index % 2 == 0 ? parent.get() : nullptr); + } + }); + for (int index = 0; index < 500; ++index) { + const auto snapshot = scene.topology_snapshot(); + EXPECT_EQ(snapshot.renderables.size(), 2); + EXPECT_EQ(snapshot.display.size(), 2); + EXPECT_EQ(snapshot.dependency.size(), 2); + } + writer.join(); +} +TEST(scene_base_test, all_waiters_receive_the_same_render_failure) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene); + scene.attach_renderable(renderable); + scene.render(); + std::atomic failures{}; + auto waiter = [&] { + try { + scene.wait_for_render(); + } catch (const std::runtime_error&) { + failures.fetch_add(1, std::memory_order_relaxed); + } + }; + std::thread first(waiter); + std::thread second(waiter); + first.join(); + second.join(); + EXPECT_EQ(failures.load(), 2); +} diff --git a/Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp b/Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp index 71787fc..c69ea18 100644 --- a/Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp +++ b/Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp @@ -19,6 +19,14 @@ TEST(double_state_strategy_test, keeps_cache_separate_until_publish) { product->publish(); EXPECT_EQ(product->render_use_state().percent.get(), 60); } +TEST(double_state_strategy_test, returns_render_state_snapshot) { + State_Plain_Strategy strategy(State_Plain_Value{.count = 1, .name = "snapshot"}); + const auto snapshot = strategy.render_use_state(); + strategy.set<&State_Plain_Value::count>(2); + strategy.publish(); + EXPECT_EQ(snapshot.count, 1); + EXPECT_EQ(strategy.render_use_state().count, 2); +} TEST(double_state_strategy_test, keeps_cached_value_when_runtime_validation_fails) { auto product = State_Checked_Product::Builder{}.set<&State_Checked_Value::percent>(20).set<&State_Checked_Value::batch_size>(4).build(1, "state"); EXPECT_THROW((product->set<&State_Checked_Value::percent>(200)), std::out_of_range); diff --git a/Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp b/Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp index bac90a1..cf63bcc 100644 --- a/Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp +++ b/Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp @@ -1,6 +1,8 @@ #include +#include #include #include +#include #include "renderive/state/State_Strategy.hpp" struct Triple_State_Test_Base {}; struct Triple_State_Test_State { @@ -14,6 +16,15 @@ TEST(triple_state_strategy_test, publishes_then_acquires_render_state) { EXPECT_EQ(strategy.acquire_render_state(), 1); EXPECT_EQ(strategy.render_use_state().value, 7); } +TEST(triple_state_strategy_test, returns_render_state_snapshot) { + Triple_State_Strategy strategy; + const auto snapshot = strategy.render_use_state(); + strategy.set<&Triple_State_Test_State::value>(7); + strategy.publish(); + strategy.acquire_render_state(); + EXPECT_EQ(snapshot.value, 0); + EXPECT_EQ(strategy.render_use_state().value, 7); +} struct Multi_State_A_Base {}; struct Multi_State_B_Base {}; struct Multi_State_A { @@ -34,3 +45,24 @@ TEST(double_state_strategy_test, supports_named_base_access_in_multiple_inherita EXPECT_EQ(product.Multi_State_A_Strategy::render_use_state().value, 3); EXPECT_EQ(product.Multi_State_B_Strategy::render_use_state().value, 8); } +TEST(triple_state_strategy_test, concurrently_acquires_render_revision_without_data_race) { + Triple_State_Strategy strategy; + std::atomic finished{}; + auto acquire = [&] { + while (!finished.load(std::memory_order_acquire)) { + strategy.acquire_render_state(); + } + strategy.acquire_render_state(); + }; + std::thread first(acquire); + std::thread second(acquire); + for (int value = 1; value <= 1000; ++value) { + strategy.set<&Triple_State_Test_State::value>(value); + strategy.publish(); + } + finished.store(true, std::memory_order_release); + first.join(); + second.join(); + EXPECT_EQ(strategy.acquire_render_state(), 1000); + EXPECT_EQ(strategy.render_use_state().value, 1000); +}