diff --git a/Core/architecture/Render_Data.h b/Core/architecture/Render_Data.h index 2113553..930d9b6 100644 --- a/Core/architecture/Render_Data.h +++ b/Core/architecture/Render_Data.h @@ -28,6 +28,9 @@ class Renderable_Runtime; struct Input_Data { virtual ~Input_Data() = default; virtual void clear() {} + virtual void consume() { + clear(); + } }; struct Latest_Input_Data : Input_Data { @@ -165,7 +168,7 @@ struct Render_Input_Frame_Handle { virtual ~Render_Input_Frame_Handle() = default; virtual void consume_input(Input_Data* input) { if (input) - input->clear(); + input->consume(); } }; @@ -293,6 +296,7 @@ public: virtual Render_State_Read_Guard acquire_edit_state_read() = 0; virtual Render_State_Write_Guard acquire_edit_state() = 0; virtual Render_Input_Write_Guard acquire_edit_input(bool notify_render) = 0; + virtual Render_Input_Frame_Guard acquire_input_read(Renderable_Input_Role role) = 0; virtual Renderable_Frame_View capture_frame_view() = 0; virtual Render_State* state(Renderable_State_Role role) = 0; virtual Input_Data* input_data(Renderable_Input_Role role) = 0; @@ -310,6 +314,7 @@ struct Render_Data { virtual Render_State_Read_Guard acquire_edit_state_read() = 0; virtual Render_State_Write_Guard acquire_edit_state() = 0; virtual Render_Input_Write_Guard acquire_edit_input(bool notify_render) = 0; + virtual Render_Input_Frame_Guard acquire_input_read(Renderable_Input_Role role) = 0; virtual Renderable_Frame_View capture_frame_view() = 0; virtual Render_State* state(Renderable_State_Role role) = 0; virtual Render_State* render_state() = 0; @@ -352,6 +357,9 @@ struct Base_Render_Data : Render_Data { Render_Input_Write_Guard acquire_edit_input(bool) override { return {}; } + Render_Input_Frame_Guard acquire_input_read(Renderable_Input_Role) override { + return {}; + } Render_State* state(Renderable_State_Role) override { return nullptr; } diff --git a/Core/architecture/Render_Lease.h b/Core/architecture/Render_Lease.h index b33781e..a3a4b3d 100644 --- a/Core/architecture/Render_Lease.h +++ b/Core/architecture/Render_Lease.h @@ -188,4 +188,25 @@ struct Render_Input_Lease : Immovable { Input* input{}; }; +template + requires std::derived_from +struct Render_Input_Read_Lease : Immovable { + explicit Render_Input_Read_Lease(Data* data, Renderable_Input_Role role = Renderable_Input_Role::Render) { + guard = data->acquire_input_read(role); + input = reinterpret_cast(guard.input); + } + ~Render_Input_Read_Lease() = default; + const Input* operator->() const { + return input; + } + const Input& operator*() const { + return *input; + } + explicit operator bool() const { + return input && guard; + } + Render_Input_Frame_Guard guard; + Input* input{}; +}; + } // namespace renderive diff --git a/Core/architecture/Strategy_Render_Data.cpp b/Core/architecture/Strategy_Render_Data.cpp index 53b6554..08c6034 100644 --- a/Core/architecture/Strategy_Render_Data.cpp +++ b/Core/architecture/Strategy_Render_Data.cpp @@ -45,6 +45,10 @@ Render_Input_Write_Guard Strategy_Render_Data::acquire_edit_input(bool notify_re return runtime ? runtime->acquire_edit_input(notify_render) : Render_Input_Write_Guard{}; } +Render_Input_Frame_Guard Strategy_Render_Data::acquire_input_read(Renderable_Input_Role role) { + return runtime ? runtime->acquire_input_read(role) : Render_Input_Frame_Guard{}; +} + Renderable_Frame_View Strategy_Render_Data::capture_frame_view() { return runtime ? runtime->capture_frame_view() : Renderable_Frame_View{}; } diff --git a/Core/architecture/Strategy_Render_Data.h b/Core/architecture/Strategy_Render_Data.h index 92d2b25..31ddb9d 100644 --- a/Core/architecture/Strategy_Render_Data.h +++ b/Core/architecture/Strategy_Render_Data.h @@ -23,6 +23,7 @@ struct LIB_DECL Strategy_Render_Data : Render_Data { Render_State_Read_Guard acquire_edit_state_read() override; Render_State_Write_Guard acquire_edit_state() override; Render_Input_Write_Guard acquire_edit_input(bool notify_render) override; + Render_Input_Frame_Guard acquire_input_read(Renderable_Input_Role role) override; Renderable_Frame_View capture_frame_view() override; Render_State* state(Renderable_State_Role role) override; Input_Data* input_data(Renderable_Input_Role role) override; diff --git a/Core/flow/explicit/Explicit_Renderable_Runtime.cpp b/Core/flow/explicit/Explicit_Renderable_Runtime.cpp index 4a68071..124330d 100644 --- a/Core/flow/explicit/Explicit_Renderable_Runtime.cpp +++ b/Core/flow/explicit/Explicit_Renderable_Runtime.cpp @@ -92,7 +92,7 @@ struct Explicit_Input_Frame_Handle final : Render_Input_Frame_Handle { runtime->committed_update_states.push_back(std::move(runtime->input.completion_state)); } if (input) - input->clear(); + input->consume(); runtime->input.pending = false; } Explicit_Renderable_Runtime_Data* runtime{}; @@ -192,6 +192,13 @@ Render_Input_Write_Guard Explicit_Renderable_Runtime::acquire_edit_input(bool no return Render_Input_Write_Guard(d->input.data, std::make_unique(d.get(), std::move(lock), notify_render)); } +Render_Input_Frame_Guard Explicit_Renderable_Runtime::acquire_input_read(Renderable_Input_Role) { + std::unique_lock lock(d->input_mutex, std::try_to_lock); + if (!lock || !d->input.data) + return {}; + return Render_Input_Frame_Guard(d->input.data, std::make_unique(d.get(), std::move(lock))); +} + Renderable_Frame_View Explicit_Renderable_Runtime::capture_frame_view() { std::unique_lock state_lock(d->state_mutex); if (!d->state) diff --git a/Core/flow/explicit/Explicit_Renderable_Runtime.h b/Core/flow/explicit/Explicit_Renderable_Runtime.h index c147ad2..e87c6e2 100644 --- a/Core/flow/explicit/Explicit_Renderable_Runtime.h +++ b/Core/flow/explicit/Explicit_Renderable_Runtime.h @@ -21,6 +21,7 @@ public: Render_State_Read_Guard acquire_edit_state_read() override; Render_State_Write_Guard acquire_edit_state() override; Render_Input_Write_Guard acquire_edit_input(bool notify_render) override; + Render_Input_Frame_Guard acquire_input_read(Renderable_Input_Role role) override; Renderable_Frame_View capture_frame_view() override; Render_State* state(Renderable_State_Role role) override; Input_Data* input_data(Renderable_Input_Role role) override; diff --git a/Core/flow/low_latency/Low_Latency_Renderable_Runtime.h b/Core/flow/low_latency/Low_Latency_Renderable_Runtime.h index da9415a..c029187 100644 --- a/Core/flow/low_latency/Low_Latency_Renderable_Runtime.h +++ b/Core/flow/low_latency/Low_Latency_Renderable_Runtime.h @@ -21,6 +21,7 @@ public: Render_State_Read_Guard acquire_edit_state_read() override; Render_State_Write_Guard acquire_edit_state() override; Render_Input_Write_Guard acquire_edit_input(bool notify_render) override; + Render_Input_Frame_Guard acquire_input_read(Renderable_Input_Role role) override; Renderable_Frame_View capture_frame_view() override; Render_State* state(Renderable_State_Role role) override; Input_Data* input_data(Renderable_Input_Role role) override; diff --git a/Core/flow/low_latency/Render_Data.cpp b/Core/flow/low_latency/Render_Data.cpp index 15a240b..8210320 100644 --- a/Core/flow/low_latency/Render_Data.cpp +++ b/Core/flow/low_latency/Render_Data.cpp @@ -240,7 +240,7 @@ struct Low_Latency_Renderable_Runtime_Data { committed_update_states.push_back(std::move(slot.completion_state)); } if (slot.data) - slot.data->clear(); + slot.data->consume(); slot.pending = false; } }; @@ -379,6 +379,18 @@ Render_Input_Write_Guard Low_Latency_Renderable_Runtime::acquire_edit_input(bool return Render_Input_Write_Guard(input, std::make_unique(d.get(), record.lease, notify_render)); } +Render_Input_Frame_Guard Low_Latency_Renderable_Runtime::acquire_input_read(Renderable_Input_Role role) { + Triple_Buffer_Lease lease = d->try_mark_input_role(input_role_index(role)); + if (!lease) + return {}; + Input_Data* input = d->input_data_by_index(lease.index); + if (!input) { + d->input_control.unmark_use(lease); + return {}; + } + return Render_Input_Frame_Guard(input, std::make_unique(d.get(), lease)); +} + Renderable_Frame_View Low_Latency_Renderable_Runtime::capture_frame_view() { d->sync_state_pipeline(); Triple_Buffer_Lease state_lease = d->try_mark_state_role(State_Render); diff --git a/Core/plottable/Spectrum.cpp b/Core/plottable/Spectrum.cpp index e62b97e..0879bd7 100644 --- a/Core/plottable/Spectrum.cpp +++ b/Core/plottable/Spectrum.cpp @@ -237,14 +237,14 @@ void Spectrum_Private::update_frequency_point_size(int frequency_point_size, Ran frequency[i] = cur; } } -void Spectrum_Private::set_frequency_data(std::pmr::vector& line_data, const std::pmr::vector* max_data, const std::pmr::vector* min_data, const Spectrum_Render_State* s) { +void Spectrum_Private::set_frequency_data(const std::pmr::vector& line_data, const std::pmr::vector* max_data, const std::pmr::vector* min_data, const Spectrum_Render_State* s) { if (s->frequency_point_size <= 0) return; if (line_data.size() != s->frequency_point_size) return; if ((max_data && max_data->size() != s->frequency_point_size) || (min_data && min_data->size() != s->frequency_point_size)) return; - cur_powers.swap(line_data); + cur_powers.assign(line_data.begin(), line_data.end()); if (!has_power_data) { max_powers.assign(cur_powers.begin(), cur_powers.end()); min_powers.assign(cur_powers.begin(), cur_powers.end()); @@ -449,36 +449,44 @@ void Spectrum::update_samples(std::span line_data) { return; Spectrum_Private* pd = d(); int frequency_point_size{}; + Range frequency_range{}; + Line_Interpolation_Mode interpolation_mode{}; bool use_max_line{}; bool use_min_line{}; { Render_State_Lease state(pd); frequency_point_size = state->frequency_point_size; + frequency_range = state->frequency_range; + interpolation_mode = state->interpolation_mode; use_max_line = state->max_hold_visible; use_min_line = state->min_hold_visible; } if (line_data.size() != frequency_point_size) return; Render_Input_Lease input(pd); - input->push(line_data, use_max_line, use_min_line); + input->push(line_data, frequency_range, interpolation_mode, use_max_line, use_min_line); } void Spectrum::update_samples(std::pmr::vector&& line_data) { if (!ok()) return; Spectrum_Private* pd = d(); int frequency_point_size{}; + Range frequency_range{}; + Line_Interpolation_Mode interpolation_mode{}; bool use_max_line{}; bool use_min_line{}; { Render_State_Lease state(pd); frequency_point_size = state->frequency_point_size; + frequency_range = state->frequency_range; + interpolation_mode = state->interpolation_mode; use_max_line = state->max_hold_visible; use_min_line = state->min_hold_visible; } if (line_data.size() != frequency_point_size) return; Render_Input_Lease input(pd); - input->push(std::move(line_data), use_max_line, use_min_line); + input->push(std::move(line_data), frequency_range, interpolation_mode, use_max_line, use_min_line); } Update_Ticket Spectrum::submit_data(std::span line_data) { auto update_state = make_update_state(); @@ -488,11 +496,15 @@ Update_Ticket Spectrum::submit_data(std::span line_data) { } Spectrum_Private* pd = d(); int frequency_point_size{}; + Range frequency_range{}; + Line_Interpolation_Mode interpolation_mode{}; bool use_max_line{}; bool use_min_line{}; { Render_State_Lease state(pd); frequency_point_size = state->frequency_point_size; + frequency_range = state->frequency_range; + interpolation_mode = state->interpolation_mode; use_max_line = state->max_hold_visible; use_min_line = state->min_hold_visible; } @@ -502,7 +514,7 @@ Update_Ticket Spectrum::submit_data(std::span line_data) { } { Render_Input_Lease input(pd); - input->push(line_data, use_max_line, use_min_line); + input->push(line_data, frequency_range, interpolation_mode, use_max_line, use_min_line); input.set_completion(update_state); } update_state->complete_until(Update_Stage::Input_Released); @@ -516,11 +528,15 @@ Update_Ticket Spectrum::submit_data(std::pmr::vector&& line_data) { } Spectrum_Private* pd = d(); int frequency_point_size{}; + Range frequency_range{}; + Line_Interpolation_Mode interpolation_mode{}; bool use_max_line{}; bool use_min_line{}; { Render_State_Lease state(pd); frequency_point_size = state->frequency_point_size; + frequency_range = state->frequency_range; + interpolation_mode = state->interpolation_mode; use_max_line = state->max_hold_visible; use_min_line = state->min_hold_visible; } @@ -530,7 +546,7 @@ Update_Ticket Spectrum::submit_data(std::pmr::vector&& line_data) { } { Render_Input_Lease input(pd); - input->push(std::move(line_data), use_max_line, use_min_line); + input->push(std::move(line_data), frequency_range, interpolation_mode, use_max_line, use_min_line); input.set_completion(update_state); } update_state->complete_until(Update_Stage::Input_Released); @@ -605,6 +621,21 @@ void Spectrum::clear_marker_selection() { edit->un_select(); } double Spectrum::power_at(double frequency, bool& ok) { - return d()->get_y(frequency, ok, interpolation_mode()); + Spectrum_Private* pd = d(); + Render_Input_Read_Lease input(pd); + if (!input || !input->has_data) { + ok = false; + return -1.0; + } + const auto& powers = input->cur_data; + if (powers.size() < 2 || input->frequency_range.length() == 0.0 || !input->frequency_range.contains(frequency)) { + ok = false; + return -1.0; + } + double index = coordinate_to_source_index(input->frequency_range, powers.size(), frequency); + ok = true; + return interpolate_sample_value(index, powers.size(), input->interpolation_mode, [&powers](int i) { + return powers.at(i); + }); } } // namespace renderive diff --git a/Core/plottable/Spectrum_p.h b/Core/plottable/Spectrum_p.h index bfe35fc..a7848ff 100644 --- a/Core/plottable/Spectrum_p.h +++ b/Core/plottable/Spectrum_p.h @@ -32,10 +32,20 @@ struct Spectrum_Input_Data : Latest_Input_Data { std::pmr::vector cur_data{memory_resource(Memory_Domain::Spectrum)}; std::pmr::vector max_data{memory_resource(Memory_Domain::Spectrum)}; std::pmr::vector min_data{memory_resource(Memory_Domain::Spectrum)}; + Range frequency_range{}; + Line_Interpolation_Mode interpolation_mode = Line_Interpolation_Mode::Nearest_Sample; bool has_data = false; bool has_max_data = false; bool has_min_data = false; - void push(std::span data, bool use_max, bool use_min) { + void push(std::span data, Range range, Line_Interpolation_Mode mode, bool use_max, bool use_min) { + if (has_data && frequency_range != range) { + max_data.clear(); + min_data.clear(); + has_max_data = false; + has_min_data = false; + } + frequency_range = range; + interpolation_mode = mode; cur_data.assign(data.begin(), data.end()); has_data = true; if (use_max) { @@ -64,7 +74,15 @@ struct Spectrum_Input_Data : Latest_Input_Data { } mark_latest_pushed(); } - void push(std::pmr::vector&& data, bool use_max, bool use_min) { + void push(std::pmr::vector&& data, Range range, Line_Interpolation_Mode mode, bool use_max, bool use_min) { + if (has_data && frequency_range != range) { + max_data.clear(); + min_data.clear(); + has_max_data = false; + has_min_data = false; + } + frequency_range = range; + interpolation_mode = mode; cur_data = std::move(data); has_data = true; if (use_max) { @@ -94,11 +112,18 @@ struct Spectrum_Input_Data : Latest_Input_Data { mark_latest_pushed(); } template - void read_all(Handler handler) { - if (!has_data) + void read_all(Handler handler) const { + if (!pending || !has_data) return; handler(cur_data, has_max_data ? &max_data : nullptr, has_min_data ? &min_data : nullptr); } + void consume() override { + max_data.clear(); + min_data.clear(); + has_max_data = false; + has_min_data = false; + clear_latest(); + } void clear() override { cur_data.clear(); max_data.clear(); @@ -176,7 +201,7 @@ struct Spectrum_Private : Typed_Render_Data prepared_marker_items{memory_resource(Memory_Domain::Other)}; std::pmr::vector prepared_label_items{memory_resource(Memory_Domain::Other)}; void update_frequency_point_size(int frequency_point_size, Range frequency_range); - void set_frequency_data(std::pmr::vector& line_data, const std::pmr::vector* max_data, const std::pmr::vector* min_data, const Spectrum_Render_State* s); + void set_frequency_data(const std::pmr::vector& line_data, const std::pmr::vector* max_data, const std::pmr::vector* min_data, const Spectrum_Render_State* s); void update_marker_items(const Spectrum_Render_State* s, const Render_Frame_Snapshot& snapshot); [[nodiscard]] double get_y(double x, bool& ok, Line_Interpolation_Mode mode); void draw_power_curve(Canvas& canvas, const Render_Frame_Snapshot& snapshot, Spectrum_Render_State* s, const std::pmr::vector& powers, const Pen& pen, const Brush& brush); @@ -192,7 +217,7 @@ struct Spectrum_Private : Typed_Render_Datafrequency_point_size || cache_frequency_range != s->frequency_range) { update_frequency_point_size(s->frequency_point_size, s->frequency_range); } - input->read_all([this, s](std::pmr::vector& data, const std::pmr::vector* max_data, const std::pmr::vector* min_data) { + input->read_all([this, s](const std::pmr::vector& data, const std::pmr::vector* max_data, const std::pmr::vector* min_data) { set_frequency_data(data, max_data, min_data, s); }); if (has_power_data) { @@ -209,7 +234,7 @@ struct Spectrum_Private : Typed_Render_Datainterpolation_mode); + const double power = q()->power_at(frequency_val, ok); if (!ok) return false; return std::abs(mapping.value.coord_to_pixel(power) - mapping.value_pixel(pos)) < 4.0; diff --git a/Kernel/CMakeLists.txt b/Kernel/CMakeLists.txt new file mode 100644 index 0000000..57e2253 --- /dev/null +++ b/Kernel/CMakeLists.txt @@ -0,0 +1,73 @@ +cmake_minimum_required(VERSION 3.20) +project(Renderive_Scene_Taskflow LANGUAGES CXX) +set(RENDERIVE_TASKFLOW_ROOT "" CACHE PATH "Taskflow 4.1.0 root") +find_path(RENDERIVE_TASKFLOW_INCLUDE_DIR + NAMES taskflow/taskflow.hpp + PATHS + "${RENDERIVE_TASKFLOW_ROOT}" + "${RENDERIVE_TASKFLOW_ROOT}/include" + "${CMAKE_CURRENT_LIST_DIR}/third_party/taskflow" + "${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") +endif() +add_library(renderive_scene STATIC + "${CMAKE_CURRENT_LIST_DIR}/src/renderive/renderable/Renderable_Task_Graph.cpp" + "${CMAKE_CURRENT_LIST_DIR}/src/renderive/renderable/base/Renderable_Base.cpp" + "${CMAKE_CURRENT_LIST_DIR}/src/renderive/scene/base/Scene_Base.cpp" +) +target_include_directories(renderive_scene + PUBLIC "$" + PRIVATE "${RENDERIVE_TASKFLOW_INCLUDE_DIR}" +) +target_compile_features(renderive_scene PUBLIC cxx_std_20) +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/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 "${CMAKE_CURRENT_LIST_DIR}/src/main.cpp") +target_link_libraries(renderive_scene_app PRIVATE renderive_scene) +include(CTest) +if(BUILD_TESTING) + find_package(GTest CONFIG QUIET) + if(NOT GTest_FOUND) + add_library(renderive_gtest_compat INTERFACE) + target_include_directories(renderive_gtest_compat INTERFACE "${CMAKE_CURRENT_LIST_DIR}/tests/compat") + 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) +endif() diff --git a/Kernel/readme.md b/Kernel/readme.md new file mode 100644 index 0000000..fbfdd43 --- /dev/null +++ b/Kernel/readme.md @@ -0,0 +1,100 @@ +# Renderive Scene 与 Taskflow 任务图 + +## Scene 类型层次 + +Scene 不再使用维度枚举。运行时类型关系为: + +```text +Scene_Base +├── Scene_2D_Base +└── Scene_3D_Base +``` + +`Scene2D_Context` 通过 `Triple_State_Strategy` 继承 `Scene_2D_Base`,`Scene3D_Context` 通过 `Triple_State_Strategy` 继承 `Scene_3D_Base`。调用方通过类型系统、概念或 `dynamic_cast` 区分 2D 与 3D Scene。 + +## Scene 三缓冲状态 + +`Scene2D_Context` 与 `Scene3D_Context` 都公开类型别名 `Scene_State_Strategy`。状态访问使用基类名限定,避免多继承时成员歧义: + +```cpp +scene.Scene_State_Strategy::set<&Scene_State::value>(value); +scene.Scene_State_Strategy::publish(); +scene.Scene_State_Strategy::render_use_state(); +``` + +三缓冲分别承担正在渲染、已发布待获取、前台写入三种职责。`render()` 提交任务前调用 `acquire_render_state()`,后台帧获得稳定的状态版本。 + +## Scene 观察者 + +Scene 状态观察者记录状态缓存修改、发布和渲染获取事件。Scene 生命周期观察者记录: + +- `render_submitted` +- `render_started` +- `render_completed` +- `render_failed` + +观察器和时间源沿用 `Observer_State`。 + +## 单一 Renderable + +Renderable 不再拆分为 Scene 持有缓存和独立持有缓存两类。所有对象统一继承 `Renderable_Base`: + +```cpp +struct Spectrum : Renderable_Base { + explicit Spectrum(Scene_Base& scene) + : Renderable_Base(scene, {.cache_enabled = true}) {} +}; +``` + +`Renderable_Configuration::cache_enabled` 只控制渲染结果是否跨帧复用。默认关闭,以保持每次 Scene 提交都重新渲染的旧语义。Scene2D 仍为每个 Renderable 维护颜色缓存,以支持渲染依赖顺序和显示合成顺序相互独立。 + +缓存开启后,首次渲染或调用 `invalidate_cache()` 后执行任务图;缓存有效时跳过内部任务图,直接参与最终颜色合成。缓存关闭时每帧执行。 + +## Renderable 内部任务图 + +每个 Renderable 通过 `build_task_graph(Renderable_Task_Graph&)` 定义自己的 DAG。公开接口只暴露 Renderive 的任务句柄,不暴露 `tf::Taskflow`、`tf::Task` 或任何 Taskflow 头文件: + +```cpp +void build_task_graph(Renderable_Task_Graph& graph) override { + auto prepare = graph.emplace([this](const Scene_Render_Context&) { + prepare_data(); + }, "prepare"); + auto rasterize = graph.emplace([this](const Scene_Render_Context&) { + rasterize_cache(); + }, "rasterize"); + graph.precede(prepare, rasterize); +} +``` + +未覆盖 `build_task_graph` 时,基类自动创建一个调用虚函数 `render()` 的单节点图。 + +## 外部依赖与总图组合 + +`dependency_node` 表达 Renderable 之间的外部依赖,父节点先于子节点。每次后台渲染会: + +1. 为每个 Renderable 构造或复用其内部图描述。 +2. 将内部图转换为一个 Taskflow module task。 +3. 按 `dependency_node` 连接 module task。 +4. 将所有 module task 连接到最终颜色缓存生成任务。 +5. 交给 `tf::Executor` 执行并等待完成。 + +Taskflow 只在 `Scene_Base.cpp` 中包含。`renderive_scene` 对 Taskflow include 路径使用 `PRIVATE`,外部头文件不会受到 Taskflow 宏、类型或包含链污染。 + +## 显示树与依赖树 + +`display_root` 只决定最终颜色缓存合成顺序。`dependency_root` 只决定 Renderable module task 的执行依赖。两棵树继续保持独立,轴可以先计算但最后合成。 + +## 主程序入口 + +同一个 `main()` 支持两种模式: + +```text +renderive_scene_app +renderive_scene_app --gtest +``` + +不带参数执行主流程;传入 `--gtest` 后初始化 GTest 并运行测试。其他 GTest 参数可以放在 `--gtest` 后面。 + +## Taskflow 查找 + +CMake 通过 `RENDERIVE_TASKFLOW_ROOT` 或系统 include 路径查找 `taskflow/taskflow.hpp`。Taskflow 不存在时,仅测试和沙盒构建使用 `tests/compat/taskflow/taskflow.hpp` 验证图生成与依赖语义;正式集成时把 `RENDERIVE_TASKFLOW_ROOT` 指向 Taskflow 4.1.0 根目录或 include 目录。 diff --git a/Kernel/src/Double_State_Strategy.hpp b/Kernel/src/Double_State_Strategy.hpp deleted file mode 100644 index 3cad56d..0000000 --- a/Kernel/src/Double_State_Strategy.hpp +++ /dev/null @@ -1,127 +0,0 @@ -#pragma once -#include "global.hpp" -template -struct State_Strategy_Builder { - using State = typename Strategy::State; - State temp{}; - template Value_Type> - State_Strategy_Builder& set(Value_Type&& value) { - temp.*Member = std::forward(value); - return *this; - } - std::shared_ptr build() const { - return std::make_shared(temp); - } -}; -template -struct Double_State_Strategy : That { - using State = typename That::State; - using Self = Double_State_Strategy; - using Builder = State_Strategy_Builder; - Double_State_Strategy() : render_state(&states[0]), cache_state(&states[1]) {} - explicit Double_State_Strategy(const State& state) : - states{state, state}, - render_state(&states[0]), - cache_state(&states[1]) {} - template Value_Type> - Self& set(Value_Type&& value) { - std::lock_guard lock(mtx); - cache_state->*Member = std::forward(value); - ++cache_ver; - return *this; - } - template - auto get() const { - std::lock_guard lock(mtx); - return cache_state->*Member; - } - void publish() { - std::lock_guard lock(mtx); - std::swap(render_state, cache_state); - *cache_state = *render_state; - render_ver.store(cache_ver, std::memory_order_release); - } - const State& render_use_state() const { - std::lock_guard lock(mtx); - return *render_state; - } - std::uint64_t render_version() const { - return render_ver.load(std::memory_order_acquire); - } -private: - State states[2]{}; - State* render_state; - State* cache_state; - mutable std::mutex mtx; - std::uint64_t cache_ver{}; - std::atomic render_ver{}; -}; -// SPSC -template -struct Triple_State_Strategy : That { - using State = typename That::State; - using Self = Triple_State_Strategy; - using Builder = State_Strategy_Builder; - static_assert(std::atomic::is_always_lock_free); - Triple_State_Strategy() = default; - explicit Triple_State_Strategy(const State& state) : states{state, state, state} {} - template Value_Type> - Self& set(Value_Type&& value) { - states[write_index].*Member = std::forward(value); - ++cache_ver; - return *this; - } - template - auto get() const { - return states[write_index].*Member; - } - void publish() { - const std::size_t published_index = write_index; - const std::uint64_t previous = middle_state.exchange( - pack(published_index, cache_ver), - std::memory_order_acq_rel); - write_index = unpack_index(previous); - states[write_index] = states[published_index]; - } - const State& render_use_state() { - acquire_render_state(); - return states[render_index]; - } - std::uint64_t render_version() const { - return render_ver; - } -private: - bool acquire_render_state() { - std::uint64_t expected = middle_state.load(std::memory_order_acquire); - while (unpack_version(expected) != render_ver) { - const std::uint64_t desired = pack(render_index, render_ver); - if (middle_state.compare_exchange_weak( - expected, - desired, - std::memory_order_acq_rel, - std::memory_order_acquire)) { - render_index = unpack_index(expected); - render_ver = unpack_version(expected); - return true; - } - } - return false; - } - static constexpr std::uint64_t index_bits = 2; - static constexpr std::uint64_t index_mask = 0x3; - static constexpr std::uint64_t pack(std::size_t index, std::uint64_t version) { - return (version << index_bits) | index; - } - static constexpr std::size_t unpack_index(std::uint64_t value) { - return static_cast(value & index_mask); - } - static constexpr std::uint64_t unpack_version(std::uint64_t value) { - return value >> index_bits; - } - State states[3]{}; - std::size_t write_index{}; - std::size_t render_index{1}; - std::atomic middle_state{pack(2, 0)}; - std::uint64_t cache_ver{}; - std::uint64_t render_ver{}; -}; diff --git a/Kernel/src/global.hpp b/Kernel/src/global.hpp deleted file mode 100644 index 15f7cb2..0000000 --- a/Kernel/src/global.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -template -concept Has_State_Type = requires { typename That::State; } && std::default_initializable; -template -concept State_Member_Assignable = std::is_member_object_pointer_v && -requires(State_Type& state, Value_Type&& value) { - state.*Member = std::forward(value); -}; diff --git a/Kernel/src/main.cpp b/Kernel/src/main.cpp index cf39259..02a844b 100644 --- a/Kernel/src/main.cpp +++ b/Kernel/src/main.cpp @@ -1,59 +1,75 @@ +#include #include -#include "global.hpp" -struct 抽象帧策略 {}; -struct 抽象状态策略 {}; -struct 上下文 { - std::shared_ptr<抽象帧策略> 帧策略; - std::shared_ptr<抽象状态策略> 状态策略; -}; -template -struct Attach_Builder { - struct Builder { - using State = That::State; - State temp; - std::shared_ptr build() { - auto ret = std::make_shared(); - ret.render_state = temp; - return temp; - } - template Value_Type> - Builder& set(Value_Type&& value) { - temp.*Member = std::forward(value); - return *this; +#include +#include +#include +#ifdef RENDERIVE_WITH_GTEST +#include +#endif +#include "renderive/base/property/Property.hpp" +#include "renderive/renderable/Renderable.hpp" +#include "renderive/scene/Scene.hpp" +#include "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{}; }; -template -struct Double_State_Strategy { - using State = That::State; - State* render_state; - State* cache_state; - template Value_Type> - void set(Value_Type&& value) { - cache_state->*Member = std::forward(value); - return *this; - } - template Value_Type> - const Value_Type& get() const { - return cache_state->*Member; - } -protected: - std::atomic cache_ver; - void swap() { - std::swap(render_state, cache_state); +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; }; -template -struct Double_Cache {}; -struct 可渲染 { - virtual ~可渲染() = default; - std::shared_ptr<上下文> 上下文; - std::shared_ptr<抽象状态策略> 状态策略; - struct Private {}; - struct State {}; +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{}; }; -struct 频谱图 : 可渲染 {}; -int main() { - std::cout << "Hello World!" << std::endl; +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(); +} diff --git a/Kernel/src/renderive/base/Atomic_Mutex.hpp b/Kernel/src/renderive/base/Atomic_Mutex.hpp new file mode 100644 index 0000000..eee20ce --- /dev/null +++ b/Kernel/src/renderive/base/Atomic_Mutex.hpp @@ -0,0 +1,30 @@ +#pragma once +#include +#include +struct Atomic_Wait_Mutex { + void lock() noexcept { + while (locked.test_and_set(std::memory_order_acquire)) { + locked.wait(true, std::memory_order_relaxed); + } + } + void unlock() noexcept { + locked.clear(std::memory_order_release); + locked.notify_one(); + } +private: + std::atomic_flag locked = ATOMIC_FLAG_INIT; +}; +struct Atomic_Spin_Mutex { + void lock() noexcept { + while (locked.test_and_set(std::memory_order_acquire)) { + while (locked.test(std::memory_order_relaxed)) { + std::this_thread::yield(); + } + } + } + void unlock() noexcept { + locked.clear(std::memory_order_release); + } +private: + std::atomic_flag locked = ATOMIC_FLAG_INIT; +}; diff --git a/Kernel/src/renderive/base/Concepts.hpp b/Kernel/src/renderive/base/Concepts.hpp new file mode 100644 index 0000000..7a5bf38 --- /dev/null +++ b/Kernel/src/renderive/base/Concepts.hpp @@ -0,0 +1,12 @@ +#pragma once +#include +#include +template +concept Class_Type = std::is_class_v; +template +concept Derivable_Type = Class_Type && !std::is_final_v; +template +concept Mutex_Type = std::default_initializable && requires(That& mutex) { + { mutex.lock() } -> std::same_as; + { mutex.unlock() } -> std::same_as; +}; diff --git a/Kernel/src/renderive/base/node/Multiway_Node.hpp b/Kernel/src/renderive/base/node/Multiway_Node.hpp new file mode 100644 index 0000000..5151000 --- /dev/null +++ b/Kernel/src/renderive/base/node/Multiway_Node.hpp @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include +#include +template +class Multiway_Node { +public: + explicit Multiway_Node(Owner* owner = nullptr) noexcept : owner(owner) {} + 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; + } + } + void append_child(Multiway_Node& child) { + insert_child(children.size(), child); + } + void insert_child(std::size_t index, Multiway_Node& child) { + if (&child == this || child.is_ancestor_of(*this)) { + throw std::invalid_argument("multiway node cycle"); + } + child.detach(); + child.parent = this; + children.insert(children.begin() + static_cast(index), &child); + } + void detach() noexcept { + if (!parent) { + return; + } + 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) { + if (current == this) { + return true; + } + } + return false; + } + Owner* owner{}; + Multiway_Node* parent{}; + std::vector children; +}; diff --git a/Kernel/src/renderive/base/node/Node.hpp b/Kernel/src/renderive/base/node/Node.hpp new file mode 100644 index 0000000..b48ee91 --- /dev/null +++ b/Kernel/src/renderive/base/node/Node.hpp @@ -0,0 +1,3 @@ +#pragma once +#include "Multiway_Node.hpp" +#include "concept/Multiway_Node.hpp" diff --git a/Kernel/src/renderive/base/node/concept/Multiway_Node.hpp b/Kernel/src/renderive/base/node/concept/Multiway_Node.hpp new file mode 100644 index 0000000..5dc090b --- /dev/null +++ b/Kernel/src/renderive/base/node/concept/Multiway_Node.hpp @@ -0,0 +1,11 @@ +#pragma once +#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; + { node.append_child(child) } -> std::same_as; + { child.detach() } -> std::same_as; +}; diff --git a/Kernel/src/renderive/base/observer/Concepts.hpp b/Kernel/src/renderive/base/observer/Concepts.hpp new file mode 100644 index 0000000..96546ac --- /dev/null +++ b/Kernel/src/renderive/base/observer/Concepts.hpp @@ -0,0 +1,3 @@ +#pragma once +#include "concept/Observer.hpp" +#include "concept/Time_Source.hpp" diff --git a/Kernel/src/renderive/base/observer/Disabled_Observer.hpp b/Kernel/src/renderive/base/observer/Disabled_Observer.hpp new file mode 100644 index 0000000..7007c44 --- /dev/null +++ b/Kernel/src/renderive/base/observer/Disabled_Observer.hpp @@ -0,0 +1,6 @@ +#pragma once +struct Disabled_Observer { + static constexpr bool enabled = false; + template + void observe(const Observation&) noexcept {} +}; diff --git a/Kernel/src/renderive/base/observer/Observer.hpp b/Kernel/src/renderive/base/observer/Observer.hpp new file mode 100644 index 0000000..dfe1595 --- /dev/null +++ b/Kernel/src/renderive/base/observer/Observer.hpp @@ -0,0 +1,5 @@ +#pragma once +#include "Concepts.hpp" +#include "Disabled_Observer.hpp" +#include "Observer_State.hpp" +#include "time_source/Steady_Time_Source.hpp" diff --git a/Kernel/src/renderive/base/observer/Observer_State.hpp b/Kernel/src/renderive/base/observer/Observer_State.hpp new file mode 100644 index 0000000..b573f5f --- /dev/null +++ b/Kernel/src/renderive/base/observer/Observer_State.hpp @@ -0,0 +1,55 @@ +#pragma once +#include +#include +#include +#include +#include "renderive/base/Concepts.hpp" +#include "Disabled_Observer.hpp" +#include "concept/Observer.hpp" +#include "concept/Time_Source.hpp" +#include "time_source/Steady_Time_Source.hpp" +template +class Observer_State { +public: + static constexpr bool enabled = Observer::enabled; + Observer_State() = default; + explicit Observer_State(Observer observer) : observer_(std::move(observer)) {} + Observer_State(Observer observer, Time_Source time_source) : observer_(std::move(observer)), time_source_(std::move(time_source)) {} + Observer_State(const Observer_State&) = delete; + Observer_State& operator=(const Observer_State&) = delete; + Observer_State(Observer_State&& other) noexcept requires std::move_constructible && std::move_constructible + : observer_(std::move(other.observer_)), time_source_(std::move(other.time_source_)) {} + Observer_State& operator=(Observer_State&&) = delete; + std::uint64_t now_ns() const noexcept { + return static_cast(time_source_.now_ns()); + } + template + requires requires(Observer& observer, Target& target) { + { observer.bind(target) } -> std::same_as; + } + void bind(Target& target) { + if constexpr (Observer::enabled) { + std::lock_guard lock(mutex_); + observer_.bind(target); + } + } + template + requires Struct_Observer + void observe(const Observation& observation) noexcept { + if constexpr (Observer::enabled) { + std::lock_guard lock(mutex_); + observer_.observe(observation); + } + } +private: + struct Disabled_Mutex {}; + [[no_unique_address]] Observer observer_; + [[no_unique_address]] Time_Source time_source_; + [[no_unique_address]] std::conditional_t mutex_; +}; +template +struct With_Observer { + Observer observer; +}; +template +With_Observer(Observer) -> With_Observer; diff --git a/Kernel/src/renderive/base/observer/concept/Observer.hpp b/Kernel/src/renderive/base/observer/concept/Observer.hpp new file mode 100644 index 0000000..312c1dc --- /dev/null +++ b/Kernel/src/renderive/base/observer/concept/Observer.hpp @@ -0,0 +1,20 @@ +#pragma once +#include +#include +#include +template +concept Observation_Struct = std::is_class_v; +template +concept Observer_Enable_Flag = requires { + { That::enabled } -> std::convertible_to; + typename std::bool_constant(That::enabled)>; +}; +template +concept Struct_Observer = Observation_Struct && Observer_Enable_Flag && requires(That& observer, const Observation& observation) { + { observer.observe(observation) } noexcept -> std::same_as; +}; +template +concept Timed_Struct_Observer = Struct_Observer && requires(That& observer, const That& const_observer, const Observation& observation) { + { const_observer.now_ns() } noexcept -> std::convertible_to; + { observer.observe(observation) } noexcept -> std::same_as; +}; diff --git a/Kernel/src/renderive/base/observer/concept/Time_Source.hpp b/Kernel/src/renderive/base/observer/concept/Time_Source.hpp new file mode 100644 index 0000000..028a90d --- /dev/null +++ b/Kernel/src/renderive/base/observer/concept/Time_Source.hpp @@ -0,0 +1,7 @@ +#pragma once +#include +#include +template +concept Observer_Time_Source = requires(const That& source) { + { source.now_ns() } noexcept -> std::convertible_to; +}; diff --git a/Kernel/src/renderive/base/observer/time_source/Steady_Time_Source.hpp b/Kernel/src/renderive/base/observer/time_source/Steady_Time_Source.hpp new file mode 100644 index 0000000..4738c5e --- /dev/null +++ b/Kernel/src/renderive/base/observer/time_source/Steady_Time_Source.hpp @@ -0,0 +1,8 @@ +#pragma once +#include +#include +struct Steady_Time_Source { + std::uint64_t now_ns() const noexcept { + return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); + } +}; diff --git a/Kernel/src/renderive/base/property/Attach_Builder.hpp b/Kernel/src/renderive/base/property/Attach_Builder.hpp new file mode 100644 index 0000000..9c96ceb --- /dev/null +++ b/Kernel/src/renderive/base/property/Attach_Builder.hpp @@ -0,0 +1,9 @@ +#pragma once +#include "Property_Builder.hpp" +template class Builder_Type = Property_Builder, class... Builder_Args> +struct Attach_Builder : That { + using Self = Attach_Builder; + using Properties = Properties_Type; + using Builder = Builder_Type; + using That::That; +}; diff --git a/Kernel/src/renderive/base/property/Concepts.hpp b/Kernel/src/renderive/base/property/Concepts.hpp new file mode 100644 index 0000000..b943f19 --- /dev/null +++ b/Kernel/src/renderive/base/property/Concepts.hpp @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#include +#include "renderive/base/Concepts.hpp" +template +concept Property_Set = std::copyable; +template +concept Property_Product = Class_Type; +template +concept Value_Validator = std::default_initializable && requires(const Validator& validator, const Value& value) { + { validator(value) } -> std::same_as; +}; +template +concept Validated_Property_Value = requires(const That& value) { + typename That::Value; + { value.get() } -> std::same_as; +}; +template +concept Property_Member_Assignable = std::is_member_object_pointer_v && requires(Properties& properties, Value&& value) { + properties.*Member = std::forward(value); +}; +template +concept Property_Validator = std::movable && requires(const Validator& validator, const Properties& properties) { + { validator(properties) } -> std::same_as; +}; +template +concept Property_Configurator = std::invocable; +template +concept Property_Constructible = std::constructible_from; +template +concept Validated_Value_Input = !std::same_as, Owner> && std::constructible_from; diff --git a/Kernel/src/renderive/base/property/Property.hpp b/Kernel/src/renderive/base/property/Property.hpp new file mode 100644 index 0000000..45c9c3c --- /dev/null +++ b/Kernel/src/renderive/base/property/Property.hpp @@ -0,0 +1,6 @@ +#pragma once +#include "Attach_Builder.hpp" +#include "Concepts.hpp" +#include "Property_Builder.hpp" +#include "Validated_Value.hpp" +#include "Validators.hpp" diff --git a/Kernel/src/renderive/base/property/Property_Builder.hpp b/Kernel/src/renderive/base/property/Property_Builder.hpp new file mode 100644 index 0000000..392c0f3 --- /dev/null +++ b/Kernel/src/renderive/base/property/Property_Builder.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "Concepts.hpp" +#include "Validators.hpp" +#include +#include +#include +template Validator_Type = No_Property_Validator> +class Property_Builder { +public: + using Product = Product_Type; + using Properties = Properties_Type; + using Validator = Validator_Type; + using Self = Property_Builder; + Property_Builder() requires std::default_initializable && std::default_initializable : properties{}, validator{} {} + explicit Property_Builder(Properties properties) requires std::default_initializable : properties(std::move(properties)), validator{} {} + Property_Builder(Properties properties, Validator validator) : properties(std::move(properties)), validator(std::move(validator)) {} + template Value> + Self& set(Value&& value) { + properties.*Member = std::forward(value); + return *this; + } + template Configure> + Self& configure(Configure&& configure) { + std::invoke(std::forward(configure), properties); + return *this; + } + const Properties& properties_value() const noexcept { + return properties; + } + template + requires Property_Constructible + std::shared_ptr build(Args&&... args) const { + validator(properties); + return std::make_shared(properties, std::forward(args)...); + } + template + requires Property_Constructible + std::unique_ptr build_unique(Args&&... args) const { + validator(properties); + return std::make_unique(properties, std::forward(args)...); + } +private: + Properties properties; + [[no_unique_address]] Validator validator; +}; diff --git a/Kernel/src/renderive/base/property/Validated_Value.hpp b/Kernel/src/renderive/base/property/Validated_Value.hpp new file mode 100644 index 0000000..b7d5d8c --- /dev/null +++ b/Kernel/src/renderive/base/property/Validated_Value.hpp @@ -0,0 +1,38 @@ +#pragma once +#include "Concepts.hpp" +#include "Validators.hpp" +#include +#include +template Validator> +class Validated_Value { +public: + using Value = Value_Type; + Validated_Value() = delete; + template Input> + Validated_Value(Input&& input) : value(make_validated(std::forward(input))) {} + Validated_Value(const Validated_Value&) = default; + Validated_Value(Validated_Value&&) noexcept(std::is_nothrow_move_constructible_v) = default; + Validated_Value& operator=(const Validated_Value&) = default; + Validated_Value& operator=(Validated_Value&&) noexcept(std::is_nothrow_move_assignable_v) = default; + template Input> + Validated_Value& operator=(Input&& input) { + value = make_validated(std::forward(input)); + return *this; + } + const Value_Type& get() const noexcept { + return value; + } + operator const Value_Type&() const noexcept { + return value; + } +private: + template Input> + static Value_Type make_validated(Input&& input) { + Value_Type result(std::forward(input)); + Validator{}(result); + return result; + } + Value_Type value; +}; +template +using Range_Value = Validated_Value>; diff --git a/Kernel/src/renderive/base/property/Validators.hpp b/Kernel/src/renderive/base/property/Validators.hpp new file mode 100644 index 0000000..8b0d9f2 --- /dev/null +++ b/Kernel/src/renderive/base/property/Validators.hpp @@ -0,0 +1,16 @@ +#pragma once +#include "Concepts.hpp" +#include +template +struct No_Property_Validator { + void operator()(const Properties&) const noexcept {} +}; +template +struct Range_Validator { + static_assert(!(Max < Min)); + void operator()(const Value& value) const { + if (value < Min || Max < value) { + throw std::out_of_range("property value is outside the declared range"); + } + } +}; diff --git a/Kernel/src/renderive/frame_control/Concepts.hpp b/Kernel/src/renderive/frame_control/Concepts.hpp new file mode 100644 index 0000000..cb999f4 --- /dev/null +++ b/Kernel/src/renderive/frame_control/Concepts.hpp @@ -0,0 +1,8 @@ +#pragma once +#include "concept/Flow_Refresh_Strategy.hpp" +#include "concept/Frame_Control_Strategy_Base.hpp" +#include "concept/Frame_Control_Strategy.hpp" +#include "concept/Frame_Lease.hpp" +#include "concept/Frame_Refresh_Strategy.hpp" +#include "concept/Manual_Refresh_Strategy.hpp" +#include "concept/Real_Time_Data_Aware_Strategy.hpp" diff --git a/Kernel/src/renderive/frame_control/Frame_Control.hpp b/Kernel/src/renderive/frame_control/Frame_Control.hpp new file mode 100644 index 0000000..6bcae1a --- /dev/null +++ b/Kernel/src/renderive/frame_control/Frame_Control.hpp @@ -0,0 +1,7 @@ +#pragma once +#include "renderive/base/observer/Observer.hpp" +#include "base/Frame_Control_Strategy_Base.hpp" +#include "Concepts.hpp" +#include "strategy/flow/Flow_Refresh_Strategy.hpp" +#include "strategy/low_latency/Low_Latency_Strategy.hpp" +#include "strategy/manual/Manual_Refresh_Strategy.hpp" 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 new file mode 100644 index 0000000..e000152 --- /dev/null +++ b/Kernel/src/renderive/frame_control/base/Frame_Control_Strategy_Base.hpp @@ -0,0 +1,51 @@ +#pragma once +#include +#include +#include +#include +struct Real_Time_Data_Observation; +class Frame_Control_Strategy_Base { +public: + struct State { + double frequency_hz{invalid_frequency_hz()}; + std::uint64_t next_refresh_interval_ns{}; + std::uint64_t update_revision{}; + std::uint64_t publish_revision{}; + }; + virtual ~Frame_Control_Strategy_Base() = default; + virtual void swap() = 0; + virtual double frequency_hz() const noexcept = 0; + virtual std::uint64_t next_refresh_interval_ns() const noexcept = 0; + virtual State frame_control_state() const = 0; + virtual void on_real_time_data_update(const Real_Time_Data_Observation& observation) = 0; + virtual bool discard_stale_latest_data_frame() { + return false; + } + static constexpr double invalid_frequency_hz() noexcept { + return std::numeric_limits::quiet_NaN(); + } +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]) {} + void update_frame_control_state(double frequency_hz, std::uint64_t next_refresh_interval_ns) { + std::lock_guard lock(state_mutex_); + cache_state_->frequency_hz = frequency_hz; + cache_state_->next_refresh_interval_ns = next_refresh_interval_ns; + ++cache_state_->update_revision; + } + void swap_frame_control_state() { + std::lock_guard lock(state_mutex_); + std::swap(render_state_, cache_state_); + ++render_state_->publish_revision; + *cache_state_ = *render_state_; + } + State render_frame_control_state() const { + std::lock_guard lock(state_mutex_); + return *render_state_; + } +private: + State states_[2]; + State* render_state_; + State* cache_state_; + mutable std::mutex state_mutex_; +}; diff --git a/Kernel/src/renderive/frame_control/concept/Flow_Refresh_Strategy.hpp b/Kernel/src/renderive/frame_control/concept/Flow_Refresh_Strategy.hpp new file mode 100644 index 0000000..97945ce --- /dev/null +++ b/Kernel/src/renderive/frame_control/concept/Flow_Refresh_Strategy.hpp @@ -0,0 +1,8 @@ +#pragma once +#include +#include +#include "Frame_Control_Strategy.hpp" +template +concept Flow_Frame_Refresh_Strategy = Frame_Control_Strategy && requires(const That& strategy) { + { strategy.pending_frame_count() } -> std::same_as; +}; diff --git a/Kernel/src/renderive/frame_control/concept/Frame_Control_Strategy.hpp b/Kernel/src/renderive/frame_control/concept/Frame_Control_Strategy.hpp new file mode 100644 index 0000000..d2948b3 --- /dev/null +++ b/Kernel/src/renderive/frame_control/concept/Frame_Control_Strategy.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include "Frame_Control_Strategy_Base.hpp" +#include "Frame_Lease.hpp" +template +concept Frame_Lease_Strategy = Runtime_Frame_Control_Strategy && requires(That& strategy) { + typename That::Frame; + typename That::Painter_Lease; + typename That::Render_Lease; + requires Painter_Frame_Lease; + requires Render_Frame_Lease; + { strategy.acquire_painter() } -> std::same_as; + { strategy.acquire_renderer() } -> std::same_as; +}; +template +concept Frame_Control_Strategy = Frame_Lease_Strategy; +template +concept Frame_Control_Strategy_For = Frame_Control_Strategy && std::derived_from; diff --git a/Kernel/src/renderive/frame_control/concept/Frame_Control_Strategy_Base.hpp b/Kernel/src/renderive/frame_control/concept/Frame_Control_Strategy_Base.hpp new file mode 100644 index 0000000..a01ca38 --- /dev/null +++ b/Kernel/src/renderive/frame_control/concept/Frame_Control_Strategy_Base.hpp @@ -0,0 +1,11 @@ +#pragma once +#include +#include +#include "renderive/frame_control/base/Frame_Control_Strategy_Base.hpp" +template +concept Runtime_Frame_Control_Strategy = std::derived_from && requires(That& strategy, const That& const_strategy) { + { strategy.swap() } -> std::same_as; + { const_strategy.frequency_hz() } noexcept -> std::same_as; + { const_strategy.next_refresh_interval_ns() } noexcept -> std::same_as; + { const_strategy.frame_control_state() } -> std::same_as; +}; diff --git a/Kernel/src/renderive/frame_control/concept/Frame_Lease.hpp b/Kernel/src/renderive/frame_control/concept/Frame_Lease.hpp new file mode 100644 index 0000000..0129d73 --- /dev/null +++ b/Kernel/src/renderive/frame_control/concept/Frame_Lease.hpp @@ -0,0 +1,16 @@ +#pragma once +#include +template +concept Frame_Lease = std::move_constructible && !std::copy_constructible && requires(That& lease, const That& const_lease) { + { static_cast(const_lease) } -> std::same_as; + { lease.get() } -> std::same_as; + { const_lease.get() } -> std::same_as; + { lease.operator->() } -> std::same_as; + { const_lease.operator->() } -> std::same_as; + { *lease } -> std::same_as; + { *const_lease } -> std::same_as; +}; +template +concept Painter_Frame_Lease = Frame_Lease; +template +concept Render_Frame_Lease = Frame_Lease; diff --git a/Kernel/src/renderive/frame_control/concept/Frame_Refresh_Strategy.hpp b/Kernel/src/renderive/frame_control/concept/Frame_Refresh_Strategy.hpp new file mode 100644 index 0000000..f7449d9 --- /dev/null +++ b/Kernel/src/renderive/frame_control/concept/Frame_Refresh_Strategy.hpp @@ -0,0 +1,12 @@ +#pragma once +#include +#include +#include "Frame_Control_Strategy.hpp" +template +concept Frame_Refresh_Strategy = Frame_Control_Strategy && requires(That& strategy, const That& const_strategy, double frequency_hz) { + typename That::State; + { strategy.set_frequency_hz(frequency_hz) } -> std::same_as; + { const_strategy.frequency_hz() } -> std::same_as; + { const_strategy.state() } -> std::same_as; + { const_strategy.next_refresh_interval_ns() } -> std::same_as; +}; diff --git a/Kernel/src/renderive/frame_control/concept/Manual_Refresh_Strategy.hpp b/Kernel/src/renderive/frame_control/concept/Manual_Refresh_Strategy.hpp new file mode 100644 index 0000000..8b06ae8 --- /dev/null +++ b/Kernel/src/renderive/frame_control/concept/Manual_Refresh_Strategy.hpp @@ -0,0 +1,7 @@ +#pragma once +#include +#include "Frame_Control_Strategy.hpp" +template +concept Manual_Frame_Refresh_Strategy = Frame_Control_Strategy && requires(That& strategy) { + { strategy.refresh() } -> std::same_as; +}; diff --git a/Kernel/src/renderive/frame_control/concept/Real_Time_Data_Aware_Strategy.hpp b/Kernel/src/renderive/frame_control/concept/Real_Time_Data_Aware_Strategy.hpp new file mode 100644 index 0000000..3f21959 --- /dev/null +++ b/Kernel/src/renderive/frame_control/concept/Real_Time_Data_Aware_Strategy.hpp @@ -0,0 +1,8 @@ +#pragma once +#include +#include "Frame_Control_Strategy.hpp" +#include "renderive/real_time_data/Observation.hpp" +template +concept Real_Time_Data_Aware_Frame_Strategy = Frame_Control_Strategy && requires(That& strategy, const Real_Time_Data_Observation& observation) { + { strategy.on_real_time_data_update(observation) } -> std::same_as; +}; 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 new file mode 100644 index 0000000..c61257e --- /dev/null +++ b/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.hpp @@ -0,0 +1,114 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "renderive/base/Concepts.hpp" +#include "renderive/base/observer/Observer.hpp" +#include "renderive/frame_control/base/Frame_Control_Strategy_Base.hpp" +#include "renderive/real_time_data/Observation.hpp" +template > +class Flow_Refresh_Strategy : public Frame_Control_Strategy_Base { +public: + enum class Observation_Event { + enqueued, + dequeued, + queue_empty, + rendered, + real_time_data_updated + }; + struct Statistics { + std::uint64_t sequence{}; + std::uint64_t real_time_data_update_sequence{}; + std::uint64_t paint_begin_time_ns{}; + std::uint64_t paint_end_time_ns{}; + std::uint64_t paint_duration_ns{}; + std::uint64_t queue_enter_time_ns{}; + std::uint64_t queue_leave_time_ns{}; + std::uint64_t queue_wait_ns{}; + std::uint64_t render_begin_time_ns{}; + std::uint64_t render_end_time_ns{}; + std::uint64_t render_duration_ns{}; + }; + struct State { + std::uint64_t enqueued_frame_count{}; + std::uint64_t dequeued_frame_count{}; + std::uint64_t rendered_frame_count{}; + std::uint64_t empty_acquire_count{}; + std::uint64_t real_time_data_update_sequence{}; + std::size_t pending_frame_count{}; + }; + struct Observation { + Observation_Event event{}; + Statistics statistics; + State state; + Real_Time_Data_Update_State real_time_data; + }; + struct Frame : Scene_Frame { + Statistics statistics; + }; + static_assert(Timed_Struct_Observer); + class Painter_Lease { + public: + explicit Painter_Lease(Flow_Refresh_Strategy& strategy); + Painter_Lease(const Painter_Lease&) = delete; + Painter_Lease& operator=(const Painter_Lease&) = delete; + Painter_Lease(Painter_Lease&& other) noexcept; + Painter_Lease& operator=(Painter_Lease&&) = delete; + ~Painter_Lease(); + explicit operator bool() const; + Frame* operator->(); + const Frame* operator->() const; + Frame& operator*(); + const Frame& operator*() const; + Frame* get(); + const Frame* get() const; + private: + Flow_Refresh_Strategy* strategy_{}; + std::unique_ptr frame_; + }; + class Render_Lease { + public: + explicit Render_Lease(Flow_Refresh_Strategy& strategy); + Render_Lease(const Render_Lease&) = delete; + Render_Lease& operator=(const Render_Lease&) = delete; + Render_Lease(Render_Lease&& other) noexcept; + Render_Lease& operator=(Render_Lease&&) = delete; + ~Render_Lease(); + explicit operator bool() const; + Frame* operator->(); + const Frame* operator->() const; + Frame& operator*(); + const Frame& operator*() const; + Frame* get(); + const Frame* get() const; + private: + Flow_Refresh_Strategy* strategy_{}; + std::unique_ptr frame_; + std::unique_lock lease_lock_; + }; + explicit Flow_Refresh_Strategy(Observer observer = {}); + Painter_Lease acquire_painter(); + Render_Lease acquire_renderer(); + void swap() override; + double frequency_hz() const noexcept override; + std::uint64_t next_refresh_interval_ns() const noexcept override; + Frame_Control_Strategy_Base::State frame_control_state() const override; + std::size_t pending_frame_count() const; + State state() const; + void on_real_time_data_update(const Real_Time_Data_Observation& observation) override; +private: + std::uint64_t now_ns() const noexcept; + void observe(const Observation& observation) noexcept; + Observer observer_; + std::deque> frames_; + std::uint64_t next_sequence_{}; + std::uint64_t real_time_data_update_sequence_{}; + Real_Time_Data_Update_State last_real_time_data_update_; + State state_; + mutable Mutex state_mutex_; + Mutex render_mutex_; +}; +#include "Flow_Refresh_Strategy.inl" 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 new file mode 100644 index 0000000..4780313 --- /dev/null +++ b/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.inl @@ -0,0 +1,194 @@ +#pragma once +template +Flow_Refresh_Strategy::Painter_Lease::Painter_Lease(Flow_Refresh_Strategy& strategy) + : strategy_(&strategy), frame_(std::make_unique()) { + std::lock_guard lock(strategy.state_mutex_); + frame_->statistics.sequence = ++strategy.next_sequence_; + frame_->statistics.real_time_data_update_sequence = strategy.real_time_data_update_sequence_; + frame_->statistics.paint_begin_time_ns = strategy.now_ns(); +} +template +Flow_Refresh_Strategy::Painter_Lease::Painter_Lease(Painter_Lease&& other) noexcept + : strategy_(std::exchange(other.strategy_, nullptr)), frame_(std::move(other.frame_)) {} +template +Flow_Refresh_Strategy::Painter_Lease::~Painter_Lease() { + if (!strategy_) { + return; + } + frame_->statistics.paint_end_time_ns = strategy_->now_ns(); + frame_->statistics.paint_duration_ns = frame_->statistics.paint_end_time_ns - frame_->statistics.paint_begin_time_ns; + frame_->statistics.queue_enter_time_ns = strategy_->now_ns(); + Observation observation; + { + std::lock_guard lock(strategy_->state_mutex_); + const Statistics statistics = frame_->statistics; + strategy_->frames_.push_back(std::move(frame_)); + ++strategy_->state_.enqueued_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::enqueued, statistics, strategy_->state_, strategy_->last_real_time_data_update_}; + } + strategy_->observe(observation); +} +template +Flow_Refresh_Strategy::Painter_Lease::operator bool() const { + return frame_ != nullptr; +} +template +auto Flow_Refresh_Strategy::Painter_Lease::operator->() -> Frame* { + return frame_.get(); +} +template +auto Flow_Refresh_Strategy::Painter_Lease::operator->() const -> const Frame* { + return frame_.get(); +} +template +auto Flow_Refresh_Strategy::Painter_Lease::operator*() -> Frame& { + return *frame_; +} +template +auto Flow_Refresh_Strategy::Painter_Lease::operator*() const -> const Frame& { + return *frame_; +} +template +auto Flow_Refresh_Strategy::Painter_Lease::get() -> Frame* { + return frame_.get(); +} +template +auto Flow_Refresh_Strategy::Painter_Lease::get() const -> const Frame* { + return frame_.get(); +} +template +Flow_Refresh_Strategy::Render_Lease::Render_Lease(Flow_Refresh_Strategy& strategy) + : strategy_(&strategy), lease_lock_(strategy.render_mutex_) { + Observation observation; + bool has_frame{}; + { + std::lock_guard lock(strategy.state_mutex_); + if (!strategy.frames_.empty()) { + frame_ = std::move(strategy.frames_.front()); + strategy.frames_.pop_front(); + frame_->statistics.queue_leave_time_ns = strategy.now_ns(); + frame_->statistics.queue_wait_ns = frame_->statistics.queue_leave_time_ns - frame_->statistics.queue_enter_time_ns; + frame_->statistics.render_begin_time_ns = strategy.now_ns(); + ++strategy.state_.dequeued_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::dequeued, frame_->statistics, strategy.state_, strategy.last_real_time_data_update_}; + has_frame = true; + } else { + ++strategy.state_.empty_acquire_count; + 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(); + } +} +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_)) {} +template +Flow_Refresh_Strategy::Render_Lease::~Render_Lease() { + if (!strategy_ || !frame_) { + return; + } + frame_->statistics.render_end_time_ns = strategy_->now_ns(); + frame_->statistics.render_duration_ns = frame_->statistics.render_end_time_ns - frame_->statistics.render_begin_time_ns; + Observation observation; + { + std::lock_guard lock(strategy_->state_mutex_); + ++strategy_->state_.rendered_frame_count; + 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(); + strategy_->observe(observation); +} +template +Flow_Refresh_Strategy::Render_Lease::operator bool() const { + return frame_ != nullptr; +} +template +auto Flow_Refresh_Strategy::Render_Lease::operator->() -> Frame* { + return frame_.get(); +} +template +auto Flow_Refresh_Strategy::Render_Lease::operator->() const -> const Frame* { + return frame_.get(); +} +template +auto Flow_Refresh_Strategy::Render_Lease::operator*() -> Frame& { + return *frame_; +} +template +auto Flow_Refresh_Strategy::Render_Lease::operator*() const -> const Frame& { + return *frame_; +} +template +auto Flow_Refresh_Strategy::Render_Lease::get() -> Frame* { + return frame_.get(); +} +template +auto Flow_Refresh_Strategy::Render_Lease::get() const -> const Frame* { + return frame_.get(); +} +template +Flow_Refresh_Strategy::Flow_Refresh_Strategy(Observer observer) : Frame_Control_Strategy_Base(), observer_(std::move(observer)) {} +template +auto Flow_Refresh_Strategy::acquire_painter() -> Painter_Lease { + return Painter_Lease(*this); +} +template +auto Flow_Refresh_Strategy::acquire_renderer() -> Render_Lease { + return Render_Lease(*this); +} +template +void Flow_Refresh_Strategy::swap() { + swap_frame_control_state(); +} +template +double Flow_Refresh_Strategy::frequency_hz() const noexcept { + return render_frame_control_state().frequency_hz; +} +template +std::uint64_t Flow_Refresh_Strategy::next_refresh_interval_ns() const noexcept { + return render_frame_control_state().next_refresh_interval_ns; +} +template +auto Flow_Refresh_Strategy::frame_control_state() const -> Frame_Control_Strategy_Base::State { + return render_frame_control_state(); +} +template +std::size_t Flow_Refresh_Strategy::pending_frame_count() const { + std::lock_guard lock(state_mutex_); + return frames_.size(); +} +template +auto Flow_Refresh_Strategy::state() const -> State { + std::lock_guard lock(state_mutex_); + return state_; +} +template +void Flow_Refresh_Strategy::on_real_time_data_update(const Real_Time_Data_Observation& observation) { + Observation strategy_observation; + { + std::lock_guard lock(state_mutex_); + ++real_time_data_update_sequence_; + state_.real_time_data_update_sequence = real_time_data_update_sequence_; + last_real_time_data_update_ = observation.state; + update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); + strategy_observation = {Observation_Event::real_time_data_updated, {}, state_, last_real_time_data_update_}; + } + observe(strategy_observation); +} +template +std::uint64_t Flow_Refresh_Strategy::now_ns() const noexcept { + return observer_.now_ns(); +} +template +void Flow_Refresh_Strategy::observe(const Observation& observation) noexcept { + observer_.observe(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 new file mode 100644 index 0000000..755f6ad --- /dev/null +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp @@ -0,0 +1,175 @@ +#pragma once +#include +#include +#include +#include "renderive/base/Concepts.hpp" +#include "renderive/base/observer/Observer.hpp" +#include "renderive/frame_control/base/Frame_Control_Strategy_Base.hpp" +#include "renderive/real_time_data/Observation.hpp" +template > +class Low_Latency_Strategy : public Frame_Control_Strategy_Base { +public: + enum class Observation_Event { + published, + abandoned, + manually_discarded, + swap_failed, + rendered, + lifecycle_completed, + real_time_data_updated + }; + enum class Limit_State { + frequency_limited, + paint_limited, + render_limited + }; + struct Configuration { + double frequency_hz{60.0}; + }; + struct Timing_Statistics { + std::uint64_t paint_request_time_ns{}; + std::uint64_t paint_lease_acquired_time_ns{}; + std::uint64_t paint_begin_time_ns{}; + std::uint64_t paint_end_time_ns{}; + std::uint64_t publish_time_ns{}; + std::uint64_t render_request_time_ns{}; + std::uint64_t render_lease_acquired_time_ns{}; + std::uint64_t render_begin_time_ns{}; + std::uint64_t render_end_time_ns{}; + std::uint64_t paint_lease_wait_ns{}; + std::uint64_t paint_state_wait_ns{}; + std::uint64_t paint_duration_ns{}; + std::uint64_t publish_state_wait_ns{}; + std::uint64_t ready_wait_ns{}; + std::uint64_t frame_age_at_render_ns{}; + std::uint64_t render_lease_wait_ns{}; + std::uint64_t render_state_wait_ns{}; + std::uint64_t render_duration_ns{}; + std::uint64_t render_finish_state_wait_ns{}; + std::uint64_t end_to_end_ns{}; + }; + struct Counter_Statistics { + std::uint64_t published_frame_count{}; + std::uint64_t render_pass_count{}; + std::uint64_t abandoned_frame_count{}; + std::uint64_t manually_discarded_frame_count{}; + std::uint64_t swap_failure_count{}; + }; + struct Statistics { + std::uint64_t sequence{}; + std::uint64_t frame_render_count{}; + std::uint64_t real_time_data_update_sequence{}; + Timing_Statistics timing; + Counter_Statistics counters; + }; + struct State { + double frequency_hz{60.0}; + Limit_State limit_state{Limit_State::frequency_limited}; + std::uint64_t target_interval_ns{}; + std::uint64_t paint_duration_ns{}; + std::uint64_t render_duration_ns{}; + std::uint64_t bottleneck_duration_ns{}; + std::uint64_t end_to_end_ns{}; + std::uint64_t next_refresh_interval_ns{}; + std::uint64_t completed_lifecycle_count{}; + std::uint64_t frame_sequence{}; + std::uint64_t real_time_data_update_sequence{}; + std::uint64_t completed_frame_real_time_data_update_sequence{}; + }; + struct Observation { + Observation_Event event{}; + Statistics statistics; + State state; + Real_Time_Data_Update_State real_time_data; + }; + struct Frame : Scene_Frame { + Statistics statistics; + }; + static_assert(Timed_Struct_Observer); + class Painter_Lease { + public: + explicit Painter_Lease(Low_Latency_Strategy& strategy); + Painter_Lease(const Painter_Lease&) = delete; + Painter_Lease& operator=(const Painter_Lease&) = delete; + Painter_Lease(Painter_Lease&& other) noexcept; + Painter_Lease& operator=(Painter_Lease&&) = delete; + ~Painter_Lease(); + explicit operator bool() const; + Frame* operator->(); + const Frame* operator->() const; + Frame& operator*(); + const Frame& operator*() const; + Frame* get(); + const Frame* get() const; + private: + Low_Latency_Strategy* strategy_{}; + Frame* frame_{}; + std::uint64_t request_time_ns_{}; + std::unique_lock lease_lock_; + }; + class Render_Lease { + public: + explicit Render_Lease(Low_Latency_Strategy& strategy); + Render_Lease(const Render_Lease&) = delete; + Render_Lease& operator=(const Render_Lease&) = delete; + Render_Lease(Render_Lease&& other) noexcept; + Render_Lease& operator=(Render_Lease&&) = delete; + ~Render_Lease(); + explicit operator bool() const; + Frame* operator->(); + const Frame* operator->() const; + Frame& operator*(); + const Frame& operator*() const; + Frame* get(); + const Frame* get() const; + private: + Low_Latency_Strategy* strategy_{}; + Frame* frame_{}; + std::uint64_t request_time_ns_{}; + bool acquired_new_frame_{}; + bool swap_failed_{}; + Observation swap_failed_observation_; + std::unique_lock lease_lock_; + }; + explicit Low_Latency_Strategy(Observer observer = {}, Configuration configuration = {}); + Low_Latency_Strategy(const Low_Latency_Strategy&) = delete; + Low_Latency_Strategy& operator=(const Low_Latency_Strategy&) = delete; + Low_Latency_Strategy(Low_Latency_Strategy&&) = delete; + Low_Latency_Strategy& operator=(Low_Latency_Strategy&&) = delete; + Painter_Lease acquire_painter(); + Render_Lease acquire_renderer(); + void set_frequency_hz(double frequency_hz); + void swap() override; + double frequency_hz() const noexcept override; + std::uint64_t next_refresh_interval_ns() const noexcept override; + Frame_Control_Strategy_Base::State frame_control_state() const override; + State state() const; + Counter_Statistics counter_statistics() const; + void on_real_time_data_update(const Real_Time_Data_Observation& observation) override; + bool discard_pending_frame(); + 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; + std::uint64_t now_ns() const noexcept; + void update_state(const Frame& frame); + void observe(const Observation& observation) noexcept; + bool discard_pending_frame_locked(Observation& observation); + Frame frames_[3]; + Observer observer_; + Configuration configuration_; + State state_; + Frame* paint_; + Frame* cache_; + Frame* render_; + std::uint64_t next_sequence_{}; + std::uint64_t real_time_data_update_sequence_{}; + Real_Time_Data_Update_State last_real_time_data_update_; + Counter_Statistics counters_; + bool cache_ready_{}; + bool render_ready_{}; + mutable Mutex state_mutex_; + Mutex painter_mutex_; + Mutex render_mutex_; +}; +#include "Low_Latency_Strategy.inl" 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 new file mode 100644 index 0000000..47aa213 --- /dev/null +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl @@ -0,0 +1,351 @@ +#pragma once +#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_) { + const std::uint64_t lease_acquired_time_ns = strategy.now_ns(); + const std::uint64_t state_request_time_ns = strategy.now_ns(); + { + std::lock_guard lock(strategy.state_mutex_); + const std::uint64_t state_acquired_time_ns = strategy.now_ns(); + frame_ = strategy.paint_; + frame_->statistics = {}; + frame_->statistics.sequence = ++strategy.next_sequence_; + frame_->statistics.real_time_data_update_sequence = strategy.real_time_data_update_sequence_; + frame_->statistics.timing.paint_request_time_ns = request_time_ns_; + frame_->statistics.timing.paint_lease_acquired_time_ns = lease_acquired_time_ns; + frame_->statistics.timing.paint_lease_wait_ns = lease_acquired_time_ns - request_time_ns_; + frame_->statistics.timing.paint_state_wait_ns = state_acquired_time_ns - state_request_time_ns; + frame_->statistics.counters = strategy.counters_; + } + frame_->statistics.timing.paint_begin_time_ns = strategy.now_ns(); +} +template +Low_Latency_Strategy::Painter_Lease::Painter_Lease(Painter_Lease&& other) noexcept + : strategy_(std::exchange(other.strategy_, nullptr)), frame_(std::exchange(other.frame_, nullptr)), request_time_ns_(other.request_time_ns_), lease_lock_(std::move(other.lease_lock_)) {} +template +Low_Latency_Strategy::Painter_Lease::~Painter_Lease() { + if (!strategy_) { + return; + } + frame_->statistics.timing.paint_end_time_ns = strategy_->now_ns(); + frame_->statistics.timing.paint_duration_ns = frame_->statistics.timing.paint_end_time_ns - frame_->statistics.timing.paint_begin_time_ns; + Observation abandoned_observation; + Observation published_observation; + bool abandoned{}; + const std::uint64_t state_request_time_ns = strategy_->now_ns(); + { + std::lock_guard lock(strategy_->state_mutex_); + const std::uint64_t state_acquired_time_ns = strategy_->now_ns(); + frame_->statistics.timing.publish_state_wait_ns = state_acquired_time_ns - state_request_time_ns; + if (strategy_->cache_ready_) { + ++strategy_->counters_.abandoned_frame_count; + strategy_->cache_->statistics.counters = strategy_->counters_; + abandoned_observation = {Observation_Event::abandoned, strategy_->cache_->statistics, strategy_->state_, strategy_->last_real_time_data_update_}; + abandoned = true; + } + std::swap(strategy_->paint_, strategy_->cache_); + strategy_->cache_ready_ = true; + ++strategy_->counters_.published_frame_count; + frame_->statistics.timing.publish_time_ns = strategy_->now_ns(); + frame_->statistics.counters = strategy_->counters_; + published_observation = {Observation_Event::published, frame_->statistics, strategy_->state_, strategy_->last_real_time_data_update_}; + } + lease_lock_.unlock(); + if (abandoned) { + strategy_->observe(abandoned_observation); + } + strategy_->observe(published_observation); +} +template +Low_Latency_Strategy::Painter_Lease::operator bool() const { + return frame_ != nullptr; +} +template +auto Low_Latency_Strategy::Painter_Lease::operator->() -> Frame* { + return frame_; +} +template +auto Low_Latency_Strategy::Painter_Lease::operator->() const -> const Frame* { + return frame_; +} +template +auto Low_Latency_Strategy::Painter_Lease::operator*() -> Frame& { + return *frame_; +} +template +auto Low_Latency_Strategy::Painter_Lease::operator*() const -> const Frame& { + return *frame_; +} +template +auto Low_Latency_Strategy::Painter_Lease::get() -> Frame* { + return frame_; +} +template +auto Low_Latency_Strategy::Painter_Lease::get() const -> const Frame* { + return frame_; +} +template +Low_Latency_Strategy::Render_Lease::Render_Lease(Low_Latency_Strategy& strategy) + : strategy_(&strategy), request_time_ns_(strategy.now_ns()), lease_lock_(strategy.render_mutex_) { + const std::uint64_t lease_acquired_time_ns = strategy.now_ns(); + const std::uint64_t state_request_time_ns = strategy.now_ns(); + { + std::lock_guard lock(strategy.state_mutex_); + const std::uint64_t state_acquired_time_ns = strategy.now_ns(); + if (strategy.cache_ready_) { + std::swap(strategy.cache_, strategy.render_); + strategy.cache_ready_ = false; + strategy.render_ready_ = true; + acquired_new_frame_ = true; + } else { + ++strategy.counters_.swap_failure_count; + swap_failed_ = true; + } + frame_ = strategy.render_ready_ ? strategy.render_ : nullptr; + if (frame_) { + ++frame_->statistics.frame_render_count; + frame_->statistics.timing.render_request_time_ns = request_time_ns_; + frame_->statistics.timing.render_lease_acquired_time_ns = lease_acquired_time_ns; + frame_->statistics.timing.render_lease_wait_ns = lease_acquired_time_ns - request_time_ns_; + frame_->statistics.timing.render_state_wait_ns = state_acquired_time_ns - state_request_time_ns; + frame_->statistics.counters = strategy.counters_; + } + if (swap_failed_) { + swap_failed_observation_.event = Observation_Event::swap_failed; + swap_failed_observation_.state = strategy.state_; + swap_failed_observation_.real_time_data = strategy.last_real_time_data_update_; + if (frame_) { + swap_failed_observation_.statistics = frame_->statistics; + } else { + swap_failed_observation_.statistics.counters = strategy.counters_; + } + } + } + if (frame_) { + frame_->statistics.timing.render_begin_time_ns = strategy.now_ns(); + frame_->statistics.timing.frame_age_at_render_ns = frame_->statistics.timing.render_begin_time_ns - frame_->statistics.timing.publish_time_ns; + if (acquired_new_frame_) { + frame_->statistics.timing.ready_wait_ns = frame_->statistics.timing.frame_age_at_render_ns; + } + } +} +template +Low_Latency_Strategy::Render_Lease::Render_Lease(Render_Lease&& other) noexcept + : strategy_(std::exchange(other.strategy_, nullptr)), frame_(std::exchange(other.frame_, nullptr)), request_time_ns_(other.request_time_ns_), acquired_new_frame_(other.acquired_new_frame_), swap_failed_(other.swap_failed_), swap_failed_observation_(other.swap_failed_observation_), lease_lock_(std::move(other.lease_lock_)) {} +template +Low_Latency_Strategy::Render_Lease::~Render_Lease() { + if (!strategy_) { + return; + } + Observation rendered_observation; + Observation lifecycle_observation; + bool rendered{}; + bool lifecycle_completed{}; + if (frame_) { + frame_->statistics.timing.render_end_time_ns = strategy_->now_ns(); + frame_->statistics.timing.render_duration_ns = frame_->statistics.timing.render_end_time_ns - frame_->statistics.timing.render_begin_time_ns; + frame_->statistics.timing.end_to_end_ns = frame_->statistics.timing.render_end_time_ns - frame_->statistics.timing.paint_request_time_ns; + const std::uint64_t state_request_time_ns = strategy_->now_ns(); + { + std::lock_guard lock(strategy_->state_mutex_); + const std::uint64_t state_acquired_time_ns = strategy_->now_ns(); + frame_->statistics.timing.render_finish_state_wait_ns = state_acquired_time_ns - state_request_time_ns; + ++strategy_->counters_.render_pass_count; + frame_->statistics.counters = strategy_->counters_; + if (acquired_new_frame_) { + strategy_->update_state(*frame_); + lifecycle_observation = {Observation_Event::lifecycle_completed, frame_->statistics, strategy_->state_, strategy_->last_real_time_data_update_}; + lifecycle_completed = true; + } + rendered_observation = {Observation_Event::rendered, frame_->statistics, strategy_->state_, strategy_->last_real_time_data_update_}; + rendered = true; + } + } + lease_lock_.unlock(); + if (swap_failed_) { + strategy_->observe(swap_failed_observation_); + } + if (rendered) { + strategy_->observe(rendered_observation); + } + if (lifecycle_completed) { + strategy_->observe(lifecycle_observation); + } +} +template +Low_Latency_Strategy::Render_Lease::operator bool() const { + return frame_ != nullptr; +} +template +auto Low_Latency_Strategy::Render_Lease::operator->() -> Frame* { + return frame_; +} +template +auto Low_Latency_Strategy::Render_Lease::operator->() const -> const Frame* { + return frame_; +} +template +auto Low_Latency_Strategy::Render_Lease::operator*() -> Frame& { + return *frame_; +} +template +auto Low_Latency_Strategy::Render_Lease::operator*() const -> const Frame& { + return *frame_; +} +template +auto Low_Latency_Strategy::Render_Lease::get() -> Frame* { + return frame_; +} +template +auto Low_Latency_Strategy::Render_Lease::get() const -> const Frame* { + return frame_; +} +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]) { + 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; +} +template +auto Low_Latency_Strategy::acquire_painter() -> Painter_Lease { + return Painter_Lease(*this); +} +template +auto Low_Latency_Strategy::acquire_renderer() -> Render_Lease { + return Render_Lease(*this); +} +template +void Low_Latency_Strategy::set_frequency_hz(double frequency_hz) { + std::lock_guard lock(state_mutex_); + configuration_.frequency_hz = frequency_hz; +} +template +void Low_Latency_Strategy::swap() { + swap_frame_control_state(); +} +template +double Low_Latency_Strategy::frequency_hz() const noexcept { + return render_frame_control_state().frequency_hz; +} +template +std::uint64_t Low_Latency_Strategy::next_refresh_interval_ns() const noexcept { + return render_frame_control_state().next_refresh_interval_ns; +} +template +auto Low_Latency_Strategy::frame_control_state() const -> Frame_Control_Strategy_Base::State { + return render_frame_control_state(); +} +template +auto Low_Latency_Strategy::state() const -> State { + std::lock_guard lock(state_mutex_); + return state_; +} +template +auto Low_Latency_Strategy::counter_statistics() const -> Counter_Statistics { + std::lock_guard lock(state_mutex_); + return counters_; +} +template +void Low_Latency_Strategy::on_real_time_data_update(const Real_Time_Data_Observation& observation) { + Observation strategy_observation; + { + std::lock_guard lock(state_mutex_); + ++real_time_data_update_sequence_; + last_real_time_data_update_ = observation.state; + state_.real_time_data_update_sequence = real_time_data_update_sequence_; + strategy_observation = {Observation_Event::real_time_data_updated, {}, state_, last_real_time_data_update_}; + } + observe(strategy_observation); +} +template +bool Low_Latency_Strategy::discard_pending_frame() { + Observation observation; + bool discarded{}; + { + std::lock_guard lock(state_mutex_); + discarded = discard_pending_frame_locked(observation); + } + if (discarded) { + observe(observation); + } + return discarded; +} +template +bool Low_Latency_Strategy::discard_pending_frame_before(std::uint64_t real_time_data_update_sequence) { + Observation observation; + bool discarded{}; + { + std::lock_guard lock(state_mutex_); + if (cache_ready_ && cache_->statistics.real_time_data_update_sequence < real_time_data_update_sequence) { + discarded = discard_pending_frame_locked(observation); + } + } + if (discarded) { + observe(observation); + } + return discarded; +} +template +bool Low_Latency_Strategy::discard_stale_latest_data_frame() { + Observation observation; + bool discarded{}; + { + std::lock_guard lock(state_mutex_); + if (last_real_time_data_update_.retention == Real_Time_Data_Retention::latest && cache_ready_ && cache_->statistics.real_time_data_update_sequence < real_time_data_update_sequence_) { + discarded = discard_pending_frame_locked(observation); + } + } + if (discarded) { + observe(observation); + } + return discarded; +} +template +std::uint64_t Low_Latency_Strategy::frequency_interval_ns(double frequency_hz) noexcept { + if (frequency_hz <= 0.0) { + return 0; + } + return static_cast(1'000'000'000.0 / frequency_hz); +} +template +std::uint64_t Low_Latency_Strategy::now_ns() const noexcept { + return observer_.now_ns(); +} +template +void Low_Latency_Strategy::update_state(const Frame& frame) { + state_.frequency_hz = configuration_.frequency_hz; + state_.target_interval_ns = frequency_interval_ns(configuration_.frequency_hz); + state_.paint_duration_ns = frame.statistics.timing.paint_duration_ns; + state_.render_duration_ns = frame.statistics.timing.render_duration_ns; + state_.bottleneck_duration_ns = std::max(state_.paint_duration_ns, state_.render_duration_ns); + state_.end_to_end_ns = frame.statistics.timing.end_to_end_ns; + state_.next_refresh_interval_ns = std::max(state_.target_interval_ns, state_.bottleneck_duration_ns); + if (state_.target_interval_ns != 0 && state_.bottleneck_duration_ns <= state_.target_interval_ns) { + state_.limit_state = Limit_State::frequency_limited; + } else if (state_.paint_duration_ns >= state_.render_duration_ns) { + state_.limit_state = Limit_State::paint_limited; + } else { + state_.limit_state = Limit_State::render_limited; + } + ++state_.completed_lifecycle_count; + state_.frame_sequence = frame.statistics.sequence; + state_.completed_frame_real_time_data_update_sequence = frame.statistics.real_time_data_update_sequence; + update_frame_control_state(state_.frequency_hz, state_.next_refresh_interval_ns); +} +template +void Low_Latency_Strategy::observe(const Observation& observation) noexcept { + observer_.observe(observation); +} +template +bool Low_Latency_Strategy::discard_pending_frame_locked(Observation& observation) { + if (!cache_ready_) { + return false; + } + cache_ready_ = false; + ++counters_.manually_discarded_frame_count; + cache_->statistics.counters = counters_; + observation = {Observation_Event::manually_discarded, cache_->statistics, state_, last_real_time_data_update_}; + return true; +} diff --git a/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.hpp b/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.hpp new file mode 100644 index 0000000..06dd67d --- /dev/null +++ b/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.hpp @@ -0,0 +1,118 @@ +#pragma once +#include +#include +#include +#include "renderive/base/Concepts.hpp" +#include "renderive/base/observer/Observer.hpp" +#include "renderive/frame_control/base/Frame_Control_Strategy_Base.hpp" +#include "renderive/real_time_data/Observation.hpp" +template > +class Manual_Refresh_Strategy : public Frame_Control_Strategy_Base { +public: + enum class Observation_Event { + prepared, + prepared_replaced, + refresh_succeeded, + refresh_failed, + rendered, + real_time_data_updated + }; + struct Statistics { + std::uint64_t sequence{}; + std::uint64_t real_time_data_update_sequence{}; + std::uint64_t paint_begin_time_ns{}; + std::uint64_t paint_end_time_ns{}; + std::uint64_t paint_duration_ns{}; + std::uint64_t render_begin_time_ns{}; + std::uint64_t render_end_time_ns{}; + std::uint64_t render_duration_ns{}; + }; + struct State { + std::uint64_t prepared_frame_count{}; + std::uint64_t replaced_prepared_frame_count{}; + std::uint64_t successful_refresh_count{}; + std::uint64_t failed_refresh_count{}; + std::uint64_t render_count{}; + std::uint64_t real_time_data_update_sequence{}; + std::uint64_t render_frame_sequence{}; + bool pending_frame{}; + bool render_frame{}; + }; + struct Observation { + Observation_Event event{}; + Statistics statistics; + State state; + Real_Time_Data_Update_State real_time_data; + }; + struct Frame : Scene_Frame { + Statistics statistics; + }; + static_assert(Timed_Struct_Observer); + class Painter_Lease { + public: + explicit Painter_Lease(Manual_Refresh_Strategy& strategy); + Painter_Lease(const Painter_Lease&) = delete; + Painter_Lease& operator=(const Painter_Lease&) = delete; + Painter_Lease(Painter_Lease&& other) noexcept; + Painter_Lease& operator=(Painter_Lease&&) = delete; + ~Painter_Lease(); + explicit operator bool() const; + Frame* operator->(); + const Frame* operator->() const; + Frame& operator*(); + const Frame& operator*() const; + Frame* get(); + const Frame* get() const; + private: + Manual_Refresh_Strategy* strategy_{}; + Frame* frame_{}; + std::unique_lock lease_lock_; + }; + class Render_Lease { + public: + explicit Render_Lease(Manual_Refresh_Strategy& strategy); + Render_Lease(const Render_Lease&) = delete; + Render_Lease& operator=(const Render_Lease&) = delete; + Render_Lease(Render_Lease&& other) noexcept; + Render_Lease& operator=(Render_Lease&&) = delete; + ~Render_Lease(); + explicit operator bool() const; + Frame* operator->(); + const Frame* operator->() const; + Frame& operator*(); + const Frame& operator*() const; + Frame* get(); + const Frame* get() const; + private: + Manual_Refresh_Strategy* strategy_{}; + Frame* frame_{}; + std::unique_lock lease_lock_; + }; + explicit Manual_Refresh_Strategy(Observer observer = {}); + Painter_Lease acquire_painter(); + Render_Lease acquire_renderer(); + void swap() override; + double frequency_hz() const noexcept override; + std::uint64_t next_refresh_interval_ns() const noexcept override; + Frame_Control_Strategy_Base::State frame_control_state() const override; + bool refresh(); + bool discard_pending_frame(); + State state() const; + void on_real_time_data_update(const Real_Time_Data_Observation& observation) override; +private: + std::uint64_t now_ns() const noexcept; + void observe(const Observation& observation) noexcept; + Frame frames_[3]; + Observer observer_; + Frame* paint_; + Frame* pending_; + Frame* render_; + std::uint64_t next_sequence_{}; + std::uint64_t real_time_data_update_sequence_{}; + Real_Time_Data_Update_State last_real_time_data_update_; + State state_; + mutable Mutex state_mutex_; + Mutex painter_mutex_; + Mutex render_mutex_; +}; +#include "Manual_Refresh_Strategy.inl" diff --git a/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.inl b/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.inl new file mode 100644 index 0000000..bb897b0 --- /dev/null +++ b/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.inl @@ -0,0 +1,216 @@ +#pragma once +template +Manual_Refresh_Strategy::Painter_Lease::Painter_Lease(Manual_Refresh_Strategy& strategy) + : strategy_(&strategy), lease_lock_(strategy.painter_mutex_) { + std::lock_guard lock(strategy.state_mutex_); + frame_ = strategy.paint_; + frame_->statistics = {}; + frame_->statistics.sequence = ++strategy.next_sequence_; + frame_->statistics.real_time_data_update_sequence = strategy.real_time_data_update_sequence_; + frame_->statistics.paint_begin_time_ns = strategy.now_ns(); +} +template +Manual_Refresh_Strategy::Painter_Lease::Painter_Lease(Painter_Lease&& other) noexcept + : strategy_(std::exchange(other.strategy_, nullptr)), frame_(std::exchange(other.frame_, nullptr)), lease_lock_(std::move(other.lease_lock_)) {} +template +Manual_Refresh_Strategy::Painter_Lease::~Painter_Lease() { + if (!strategy_) { + return; + } + frame_->statistics.paint_end_time_ns = strategy_->now_ns(); + frame_->statistics.paint_duration_ns = frame_->statistics.paint_end_time_ns - frame_->statistics.paint_begin_time_ns; + Observation replaced_observation; + Observation prepared_observation; + bool replaced{}; + { + std::lock_guard lock(strategy_->state_mutex_); + if (strategy_->state_.pending_frame) { + ++strategy_->state_.replaced_prepared_frame_count; + replaced_observation = {Observation_Event::prepared_replaced, strategy_->pending_->statistics, strategy_->state_, strategy_->last_real_time_data_update_}; + replaced = true; + } + std::swap(strategy_->paint_, strategy_->pending_); + strategy_->state_.pending_frame = true; + ++strategy_->state_.prepared_frame_count; + strategy_->update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); + prepared_observation = {Observation_Event::prepared, frame_->statistics, strategy_->state_, strategy_->last_real_time_data_update_}; + } + lease_lock_.unlock(); + if (replaced) { + strategy_->observe(replaced_observation); + } + strategy_->observe(prepared_observation); +} +template +Manual_Refresh_Strategy::Painter_Lease::operator bool() const { + return frame_ != nullptr; +} +template +auto Manual_Refresh_Strategy::Painter_Lease::operator->() -> Frame* { + return frame_; +} +template +auto Manual_Refresh_Strategy::Painter_Lease::operator->() const -> const Frame* { + return frame_; +} +template +auto Manual_Refresh_Strategy::Painter_Lease::operator*() -> Frame& { + return *frame_; +} +template +auto Manual_Refresh_Strategy::Painter_Lease::operator*() const -> const Frame& { + return *frame_; +} +template +auto Manual_Refresh_Strategy::Painter_Lease::get() -> Frame* { + return frame_; +} +template +auto Manual_Refresh_Strategy::Painter_Lease::get() const -> const Frame* { + return frame_; +} +template +Manual_Refresh_Strategy::Render_Lease::Render_Lease(Manual_Refresh_Strategy& strategy) + : strategy_(&strategy), lease_lock_(strategy.render_mutex_) { + std::lock_guard lock(strategy.state_mutex_); + frame_ = strategy.state_.render_frame ? strategy.render_ : nullptr; + if (frame_) { + frame_->statistics.render_begin_time_ns = strategy.now_ns(); + } +} +template +Manual_Refresh_Strategy::Render_Lease::Render_Lease(Render_Lease&& other) noexcept + : strategy_(std::exchange(other.strategy_, nullptr)), frame_(std::exchange(other.frame_, nullptr)), lease_lock_(std::move(other.lease_lock_)) {} +template +Manual_Refresh_Strategy::Render_Lease::~Render_Lease() { + if (!strategy_ || !frame_) { + return; + } + frame_->statistics.render_end_time_ns = strategy_->now_ns(); + frame_->statistics.render_duration_ns = frame_->statistics.render_end_time_ns - frame_->statistics.render_begin_time_ns; + Observation observation; + { + std::lock_guard lock(strategy_->state_mutex_); + ++strategy_->state_.render_count; + 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(); + strategy_->observe(observation); +} +template +Manual_Refresh_Strategy::Render_Lease::operator bool() const { + return frame_ != nullptr; +} +template +auto Manual_Refresh_Strategy::Render_Lease::operator->() -> Frame* { + return frame_; +} +template +auto Manual_Refresh_Strategy::Render_Lease::operator->() const -> const Frame* { + return frame_; +} +template +auto Manual_Refresh_Strategy::Render_Lease::operator*() -> Frame& { + return *frame_; +} +template +auto Manual_Refresh_Strategy::Render_Lease::operator*() const -> const Frame& { + return *frame_; +} +template +auto Manual_Refresh_Strategy::Render_Lease::get() -> Frame* { + return frame_; +} +template +auto Manual_Refresh_Strategy::Render_Lease::get() const -> const Frame* { + return frame_; +} +template +Manual_Refresh_Strategy::Manual_Refresh_Strategy(Observer observer) + : Frame_Control_Strategy_Base(), observer_(std::move(observer)), paint_(&frames_[0]), pending_(&frames_[1]), render_(&frames_[2]) {} +template +auto Manual_Refresh_Strategy::acquire_painter() -> Painter_Lease { + return Painter_Lease(*this); +} +template +auto Manual_Refresh_Strategy::acquire_renderer() -> Render_Lease { + return Render_Lease(*this); +} +template +void Manual_Refresh_Strategy::swap() { + swap_frame_control_state(); +} +template +double Manual_Refresh_Strategy::frequency_hz() const noexcept { + return render_frame_control_state().frequency_hz; +} +template +std::uint64_t Manual_Refresh_Strategy::next_refresh_interval_ns() const noexcept { + return render_frame_control_state().next_refresh_interval_ns; +} +template +auto Manual_Refresh_Strategy::frame_control_state() const -> Frame_Control_Strategy_Base::State { + return render_frame_control_state(); +} +template +bool Manual_Refresh_Strategy::refresh() { + std::lock_guard render_lock(render_mutex_); + Observation observation; + bool refreshed{}; + { + std::lock_guard lock(state_mutex_); + if (state_.pending_frame) { + std::swap(pending_, render_); + state_.pending_frame = false; + state_.render_frame = true; + state_.render_frame_sequence = render_->statistics.sequence; + ++state_.successful_refresh_count; + update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); + observation = {Observation_Event::refresh_succeeded, render_->statistics, state_, last_real_time_data_update_}; + refreshed = true; + } else { + ++state_.failed_refresh_count; + update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); + observation = {Observation_Event::refresh_failed, {}, state_, last_real_time_data_update_}; + } + } + observe(observation); + return refreshed; +} +template +bool Manual_Refresh_Strategy::discard_pending_frame() { + std::lock_guard lock(state_mutex_); + if (!state_.pending_frame) { + return false; + } + state_.pending_frame = false; + update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); + return true; +} +template +auto Manual_Refresh_Strategy::state() const -> State { + std::lock_guard lock(state_mutex_); + return state_; +} +template +void Manual_Refresh_Strategy::on_real_time_data_update(const Real_Time_Data_Observation& observation) { + Observation strategy_observation; + { + std::lock_guard lock(state_mutex_); + ++real_time_data_update_sequence_; + state_.real_time_data_update_sequence = real_time_data_update_sequence_; + last_real_time_data_update_ = observation.state; + update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); + strategy_observation = {Observation_Event::real_time_data_updated, {}, state_, last_real_time_data_update_}; + } + observe(strategy_observation); +} +template +std::uint64_t Manual_Refresh_Strategy::now_ns() const noexcept { + return observer_.now_ns(); +} +template +void Manual_Refresh_Strategy::observe(const Observation& observation) noexcept { + observer_.observe(observation); +} 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 new file mode 100644 index 0000000..a0fe07f --- /dev/null +++ b/Kernel/src/renderive/real_time_data/Attach_Real_Time_Data.hpp @@ -0,0 +1,95 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "concept/Real_Time_Data.hpp" +#include "renderive/renderable/concept/Renderable.hpp" +#include "renderive/scene/base/Scene_Base.hpp" +#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; +}; +template +With_Real_Time_Data(Data&...) -> With_Real_Time_Data; +struct Real_Time_Data_Attachment_State { + std::size_t source_count{}; + std::uint64_t revision_sum{}; + std::uint64_t total_update_count{}; +}; +enum class Real_Time_Data_Discard_Mode { + retain_all, + retain_frame_interval +}; +template +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_); + } + template + requires ((std::same_as || ...)) + Data_Type& real_time_data() { + return *std::get(data_); + } + template + requires ((std::same_as || ...)) + const Data_Type& real_time_data() const { + 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) { + return 0; + } + const double frequency_hz = this->scene->frame_control_strategy->frequency_hz(); + if (!std::isfinite(frequency_hz) || frequency_hz <= 0.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] { + 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); + }(), ...); + }, data_); + return discarded; + } + 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] { + const auto update = data->update_state(); + state.revision_sum += update.revision; + state.total_update_count += update.total_update_count; + }(), ...); + }, data_); + return state; + } + void render(const Scene_Render_Context& context) override { + 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}; +private: + template + void bind_data(Data_Type& data) { + if constexpr (requires { + data.bind(*this); + }) { + data.bind(*this); + } + } + static_assert(sizeof...(Data) > 0); + static_assert((std::is_class_v && ...)); + std::tuple data_; +}; diff --git a/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp b/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp new file mode 100644 index 0000000..993a97b --- /dev/null +++ b/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp @@ -0,0 +1,27 @@ +#pragma once +#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; + } + void observe(const Real_Time_Data_Observation& observation) noexcept { + if (!renderable_ || !renderable_->scene || !renderable_->scene->frame_control_strategy) { + return; + } + 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(); + } + } +private: + Renderable_Base* renderable_{}; +}; 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 new file mode 100644 index 0000000..5698b81 --- /dev/null +++ b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp @@ -0,0 +1,48 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "renderive/base/Concepts.hpp" +#include "renderive/base/observer/Observer.hpp" +#include "Observation.hpp" +#include "base/Real_Time_Data_Base.hpp" +#include "concept/Real_Time_Data.hpp" +template , Mutex_Type Mutex = std::mutex, class Observer = Observer_State<>> +class History_Real_Time_Data : public Real_Time_Data_Base { +public: + using Value = Value_Type; + using Container_Type = Container; + static_assert(Real_Time_Data_Value); + static_assert(Real_Time_Data_History_Container); + static_assert(Timed_Struct_Observer); + explicit History_Real_Time_Data(Observer observer = {}); + History_Real_Time_Data(const History_Real_Time_Data&) = delete; + History_Real_Time_Data& operator=(const History_Real_Time_Data&) = delete; + void update(Value value); + void clear(); + Container snapshot() const; + std::size_t size() const; + std::uint64_t revision() const; + Real_Time_Data_Retention retention() const noexcept override; + Real_Time_Data_Update_State update_state() const override; + 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; + } + void bind(Target& target) { + observer_.bind(target); + } +private: + mutable Mutex mutex_; + Observer observer_; + Container values_; + std::vector update_times_; + std::uint64_t revision_{}; + std::uint64_t total_update_count_{}; + std::uint64_t last_update_time_ns_{}; +}; +#include "History_Real_Time_Data.inl" diff --git a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl new file mode 100644 index 0000000..d321f0a --- /dev/null +++ b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl @@ -0,0 +1,75 @@ +#pragma once +template +History_Real_Time_Data::History_Real_Time_Data(Observer observer) : observer_(std::move(observer)) {} +template +void History_Real_Time_Data::update(Value value) { + Real_Time_Data_Observation observation; + { + std::lock_guard lock(mutex_); + values_.push_back(std::move(value)); + ++revision_; + ++total_update_count_; + last_update_time_ns_ = observer_.now_ns(); + update_times_.push_back(last_update_time_ns_); + observation = {Real_Time_Data_Observation_Event::updated, {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, values_.size()}}; + } + observer_.observe(observation); +} +template +void History_Real_Time_Data::clear() { + Real_Time_Data_Observation observation; + { + std::lock_guard lock(mutex_); + values_.clear(); + update_times_.clear(); + ++revision_; + last_update_time_ns_ = observer_.now_ns(); + observation = {Real_Time_Data_Observation_Event::cleared, {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, 0}}; + } + observer_.observe(observation); +} +template +auto History_Real_Time_Data::snapshot() const -> Container { + std::lock_guard lock(mutex_); + return values_; +} +template +std::size_t History_Real_Time_Data::size() const { + std::lock_guard lock(mutex_); + return values_.size(); +} +template +std::uint64_t History_Real_Time_Data::revision() const { + std::lock_guard lock(mutex_); + return revision_; +} +template +Real_Time_Data_Update_State History_Real_Time_Data::update_state() const { + std::lock_guard lock(mutex_); + return {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, values_.size()}; +} + +template +Real_Time_Data_Retention History_Real_Time_Data::retention() const noexcept { + return Real_Time_Data_Retention::history; +} +template +std::size_t History_Real_Time_Data::discard_before_time_ns(std::uint64_t time_ns) { + Real_Time_Data_Observation observation; + std::size_t discarded{}; + { + std::lock_guard lock(mutex_); + while (discarded < update_times_.size() && update_times_[discarded] < time_ns) { + ++discarded; + } + if (discarded == 0) { + return 0; + } + values_.erase(values_.begin(), std::next(values_.begin(), static_cast(discarded))); + update_times_.erase(update_times_.begin(), update_times_.begin() + static_cast(discarded)); + ++revision_; + observation = {Real_Time_Data_Observation_Event::discarded, {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, values_.size()}}; + } + observer_.observe(observation); + return discarded; +} 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 new file mode 100644 index 0000000..4e09bb4 --- /dev/null +++ b/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.hpp @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include +#include +#include +#include "renderive/base/Concepts.hpp" +#include "renderive/base/observer/Observer.hpp" +#include "Observation.hpp" +#include "base/Real_Time_Data_Base.hpp" +#include "concept/Real_Time_Data.hpp" +template > +class Latest_Real_Time_Data : public Real_Time_Data_Base { +public: + using Value = Value_Type; + static_assert(Real_Time_Data_Value); + static_assert(Timed_Struct_Observer); + explicit Latest_Real_Time_Data(Observer observer = {}); + Latest_Real_Time_Data(const Latest_Real_Time_Data&) = delete; + Latest_Real_Time_Data& operator=(const Latest_Real_Time_Data&) = delete; + void update(Value value); + std::optional snapshot() const; + bool has_value() const; + std::uint64_t revision() const; + Real_Time_Data_Retention retention() const noexcept override; + Real_Time_Data_Update_State update_state() const override; + 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; + } + void bind(Target& target) { + observer_.bind(target); + } +private: + mutable Mutex mutex_; + Observer observer_; + std::optional value_; + std::uint64_t revision_{}; + std::uint64_t total_update_count_{}; + std::uint64_t last_update_time_ns_{}; +}; +#include "Latest_Real_Time_Data.inl" diff --git a/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.inl b/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.inl new file mode 100644 index 0000000..7046b30 --- /dev/null +++ b/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.inl @@ -0,0 +1,45 @@ +#pragma once +template +Latest_Real_Time_Data::Latest_Real_Time_Data(Observer observer) : observer_(std::move(observer)) {} +template +void Latest_Real_Time_Data::update(Value value) { + Real_Time_Data_Observation observation; + { + std::lock_guard lock(mutex_); + value_ = std::move(value); + ++revision_; + ++total_update_count_; + last_update_time_ns_ = observer_.now_ns(); + observation = {Real_Time_Data_Observation_Event::updated, {this, Real_Time_Data_Retention::latest, revision_, last_update_time_ns_, total_update_count_, 1}}; + } + observer_.observe(observation); +} +template +auto Latest_Real_Time_Data::snapshot() const -> std::optional { + std::lock_guard lock(mutex_); + return value_; +} +template +bool Latest_Real_Time_Data::has_value() const { + std::lock_guard lock(mutex_); + return value_.has_value(); +} +template +std::uint64_t Latest_Real_Time_Data::revision() const { + std::lock_guard lock(mutex_); + return revision_; +} +template +Real_Time_Data_Update_State Latest_Real_Time_Data::update_state() const { + std::lock_guard lock(mutex_); + return {this, Real_Time_Data_Retention::latest, revision_, last_update_time_ns_, total_update_count_, value_ ? 1U : 0U}; +} + +template +Real_Time_Data_Retention Latest_Real_Time_Data::retention() const noexcept { + return Real_Time_Data_Retention::latest; +} +template +std::size_t Latest_Real_Time_Data::discard_before_time_ns(std::uint64_t) { + return 0; +} diff --git a/Kernel/src/renderive/real_time_data/Observation.hpp b/Kernel/src/renderive/real_time_data/Observation.hpp new file mode 100644 index 0000000..481c56d --- /dev/null +++ b/Kernel/src/renderive/real_time_data/Observation.hpp @@ -0,0 +1,24 @@ +#pragma once +#include +#include +enum class Real_Time_Data_Retention { + latest, + history +}; +enum class Real_Time_Data_Observation_Event { + updated, + cleared, + discarded +}; +struct Real_Time_Data_Update_State { + const void* source{}; + Real_Time_Data_Retention retention{Real_Time_Data_Retention::latest}; + std::uint64_t revision{}; + std::uint64_t update_time_ns{}; + std::uint64_t total_update_count{}; + std::size_t retained_value_count{}; +}; +struct Real_Time_Data_Observation { + Real_Time_Data_Observation_Event event{}; + Real_Time_Data_Update_State state; +}; diff --git a/Kernel/src/renderive/real_time_data/Real_Time_Data.hpp b/Kernel/src/renderive/real_time_data/Real_Time_Data.hpp new file mode 100644 index 0000000..a8e2e65 --- /dev/null +++ b/Kernel/src/renderive/real_time_data/Real_Time_Data.hpp @@ -0,0 +1,8 @@ +#pragma once +#include "base/Real_Time_Data_Base.hpp" +#include "Attach_Real_Time_Data.hpp" +#include "Frame_Strategy_Observer.hpp" +#include "History_Real_Time_Data.hpp" +#include "Latest_Real_Time_Data.hpp" +#include "Observation.hpp" +#include "concept/Real_Time_Data.hpp" diff --git a/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.hpp b/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.hpp new file mode 100644 index 0000000..f0cba2f --- /dev/null +++ b/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.hpp @@ -0,0 +1,11 @@ +#pragma once +#include +#include +#include "renderive/real_time_data/Observation.hpp" +class Real_Time_Data_Base { +public: + virtual ~Real_Time_Data_Base() = default; + virtual Real_Time_Data_Retention retention() const noexcept = 0; + virtual Real_Time_Data_Update_State update_state() const = 0; + virtual std::size_t discard_before_time_ns(std::uint64_t time_ns) = 0; +}; diff --git a/Kernel/src/renderive/real_time_data/concept/Real_Time_Data.hpp b/Kernel/src/renderive/real_time_data/concept/Real_Time_Data.hpp new file mode 100644 index 0000000..66de4ce --- /dev/null +++ b/Kernel/src/renderive/real_time_data/concept/Real_Time_Data.hpp @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include +#include +#include +#include "renderive/real_time_data/Observation.hpp" +#include "renderive/real_time_data/base/Real_Time_Data_Base.hpp" +template +concept Real_Time_Data_Value = std::copy_constructible && std::movable; +template +concept Real_Time_Data_History_Container = std::copy_constructible && requires(Container& container, Value value) { + { container.push_back(std::move(value)) } -> std::same_as; + { container.clear() } -> std::same_as; + { container.size() } -> std::convertible_to; + { container.begin() }; + { container.end() }; + { container.erase(container.begin(), container.end()) }; +}; +template +concept Real_Time_Data = std::derived_from && requires(That& data, const That& const_data, typename That::Value value) { + typename That::Value; + requires Real_Time_Data_Value; + { data.update(std::move(value)) } -> std::same_as; + { const_data.revision() } -> std::same_as; + { const_data.update_state() } -> std::same_as; +}; +template +concept Latest_Real_Time_Data_Source = Real_Time_Data && requires(const That& data) { + { data.snapshot() } -> std::same_as>; +}; +template +concept History_Real_Time_Data_Source = Real_Time_Data && requires(const That& data) { + typename That::Container_Type; + { data.snapshot() } -> std::same_as; + { data.size() } -> std::convertible_to; +}; diff --git a/Kernel/src/renderive/renderable/Renderable.hpp b/Kernel/src/renderive/renderable/Renderable.hpp new file mode 100644 index 0000000..b8f0e24 --- /dev/null +++ b/Kernel/src/renderive/renderable/Renderable.hpp @@ -0,0 +1,5 @@ +#pragma once +#include "Renderable_Task_Graph.hpp" +#include "base/Renderable_Base.hpp" +#include "color/Concepts.hpp" +#include "concept/Renderable.hpp" diff --git a/Kernel/src/renderive/renderable/Renderable_Task_Graph.cpp b/Kernel/src/renderive/renderable/Renderable_Task_Graph.cpp new file mode 100644 index 0000000..f55a5ce --- /dev/null +++ b/Kernel/src/renderive/renderable/Renderable_Task_Graph.cpp @@ -0,0 +1,22 @@ +#include "Renderable_Task_Graph.hpp" +#include +#include +Renderable_Task_Graph::Task Renderable_Task_Graph::emplace(Task_Function function, std::string name) { + nodes_.push_back({std::move(function), std::move(name), {}}); + return {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"); + } + nodes_[from.index].successors.push_back(to.index); +} +void Renderable_Task_Graph::clear() { + nodes_.clear(); +} +bool Renderable_Task_Graph::empty() const noexcept { + return nodes_.empty(); +} +const std::vector& Renderable_Task_Graph::nodes() const noexcept { + return nodes_; +} diff --git a/Kernel/src/renderive/renderable/Renderable_Task_Graph.hpp b/Kernel/src/renderive/renderable/Renderable_Task_Graph.hpp new file mode 100644 index 0000000..ce97397 --- /dev/null +++ b/Kernel/src/renderive/renderable/Renderable_Task_Graph.hpp @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include +#include +#include "renderive/scene/base/Scene_Render_Context.hpp" +class Renderable_Task_Graph { +public: + using Task_Function = std::function; + struct Task { + std::size_t index{}; + friend bool operator==(Task, Task) = default; + }; + struct Node { + Task_Function function; + std::string name; + std::vector successors; + }; + Task emplace(Task_Function function, std::string name = {}); + void precede(Task from, Task to); + void clear(); + bool empty() const noexcept; + const std::vector& nodes() const noexcept; +private: + std::vector nodes_; +}; diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base.cpp b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp new file mode 100644 index 0000000..ac2f517 --- /dev/null +++ b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp @@ -0,0 +1,41 @@ +#include "Renderable_Base.hpp" +#include "renderive/scene/base/Scene_Base.hpp" +Renderable_Base::Renderable_Base(Scene_Base& scene, Renderable_Configuration configuration) noexcept + : scene(&scene), layer_node(this), dependency_node(this), configuration(configuration) {} +Renderable_Base::~Renderable_Base() = default; +void Renderable_Base::render(const Scene_Render_Context&) {} +void Renderable_Base::invalidate_cache() noexcept { + cache_revision_.fetch_add(1, std::memory_order_release); +} +std::uint64_t Renderable_Base::cache_revision() const noexcept { + return cache_revision_.load(std::memory_order_acquire); +} +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(); +} +void Renderable_Base::rebuild_task_graph() noexcept { + task_graph_ready_.store(false, std::memory_order_release); + 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); + } + return task_graph_; +} +void Renderable_Base::build_task_graph(Renderable_Task_Graph& graph) { + graph.emplace([this](const Scene_Render_Context& context) { + render(context); + }, "render"); +} +bool Renderable_Base::requires_render() const noexcept { + return !configuration.cache_enabled || !cache_valid(); +} +void Renderable_Base::mark_rendered() noexcept { + rendered_cache_revision_.store(cache_revision(), std::memory_order_release); +} diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base.hpp b/Kernel/src/renderive/renderable/base/Renderable_Base.hpp new file mode 100644 index 0000000..d03d49d --- /dev/null +++ b/Kernel/src/renderive/renderable/base/Renderable_Base.hpp @@ -0,0 +1,40 @@ +#pragma once +#include +#include +#include "renderive/base/node/Multiway_Node.hpp" +#include "renderive/renderable/Renderable_Task_Graph.hpp" +class Scene_Base; +struct Renderable_Layer_Node_Tag {}; +struct Renderable_Dependency_Node_Tag {}; +struct Renderable_Configuration { + bool cache_enabled{false}; +}; +class Renderable_Base { +public: + using Layer_Node = Multiway_Node; + using Dependency_Node = Multiway_Node; + explicit Renderable_Base(Scene_Base& scene, Renderable_Configuration configuration = {}) noexcept; + virtual ~Renderable_Base(); + virtual void render(const Scene_Render_Context& context); + void invalidate_cache() noexcept; + std::uint64_t cache_revision() const noexcept; + std::uint64_t rendered_cache_revision() const noexcept; + bool cache_valid() const noexcept; + void rebuild_task_graph() noexcept; + const Renderable_Task_Graph& task_graph(); + Scene_Base* scene; + Layer_Node layer_node; + Dependency_Node dependency_node; + Renderable_Configuration configuration; + bool discard_stale_frame_on_latest_data_update{}; +protected: + virtual void build_task_graph(Renderable_Task_Graph& graph); +private: + friend class Scene_Base; + bool requires_render() const noexcept; + void mark_rendered() noexcept; + Renderable_Task_Graph task_graph_; + std::atomic cache_revision_{1}; + std::atomic rendered_cache_revision_{}; + std::atomic task_graph_ready_{}; +}; diff --git a/Kernel/src/renderive/renderable/color/Color_Cache.hpp b/Kernel/src/renderive/renderable/color/Color_Cache.hpp new file mode 100644 index 0000000..f517576 --- /dev/null +++ b/Kernel/src/renderive/renderable/color/Color_Cache.hpp @@ -0,0 +1,27 @@ +#pragma once +#include +#include +class Color_Cache { +public: + virtual ~Color_Cache() = default; + virtual void clear() = 0; + virtual void composite(const Color_Cache& source) = 0; +}; +class Recording_Color_Cache : public Color_Cache { +public: + void clear() override { + values.clear(); + ++revision; + } + void composite(const Color_Cache& source) override { + const auto& cache = dynamic_cast(source); + values.insert(values.end(), cache.values.begin(), cache.values.end()); + ++revision; + } + void append(std::uint64_t value) { + values.push_back(value); + ++revision; + } + std::vector values; + std::uint64_t revision{}; +}; diff --git a/Kernel/src/renderive/renderable/color/Concepts.hpp b/Kernel/src/renderive/renderable/color/Concepts.hpp new file mode 100644 index 0000000..8bf6807 --- /dev/null +++ b/Kernel/src/renderive/renderable/color/Concepts.hpp @@ -0,0 +1,8 @@ +#pragma once +#include +#include "Color_Cache.hpp" +template +concept Color_Cache_Type = std::derived_from && std::default_initializable && requires(Cache& cache, const Cache& source) { + { cache.clear() } -> std::same_as; + { cache.composite(source) } -> std::same_as; +}; diff --git a/Kernel/src/renderive/renderable/concept/Renderable.hpp b/Kernel/src/renderive/renderable/concept/Renderable.hpp new file mode 100644 index 0000000..613a611 --- /dev/null +++ b/Kernel/src/renderive/renderable/concept/Renderable.hpp @@ -0,0 +1,12 @@ +#pragma once +#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; + { renderable.render(context) } -> std::same_as; + { renderable.invalidate_cache() } -> std::same_as; +}; diff --git a/Kernel/src/renderive/scene/Scene.hpp b/Kernel/src/renderive/scene/Scene.hpp new file mode 100644 index 0000000..4aceaed --- /dev/null +++ b/Kernel/src/renderive/scene/Scene.hpp @@ -0,0 +1,6 @@ +#pragma once +#include "Scene2D_Context.hpp" +#include "Scene3D_Context.hpp" +#include "base/Scene_Base.hpp" +#include "base/Scene_Render_Context.hpp" +#include "concept/Scene.hpp" diff --git a/Kernel/src/renderive/scene/Scene2D_Context.hpp b/Kernel/src/renderive/scene/Scene2D_Context.hpp new file mode 100644 index 0000000..ea43438 --- /dev/null +++ b/Kernel/src/renderive/scene/Scene2D_Context.hpp @@ -0,0 +1,136 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "renderive/base/observer/Observer.hpp" +#include "renderive/frame_control/Frame_Control.hpp" +#include "renderive/renderable/color/Concepts.hpp" +#include "renderive/state/Triple_State_Strategy.hpp" +#include "base/Scene_Base.hpp" +struct Scene2D_Frame_Data {}; +struct Scene2D_State { + std::uint64_t revision{}; +}; +template , Color_Cache_Type Cache = Recording_Color_Cache, class State = Scene2D_State, class State_Observer = Observer_State<>, class Scene_Observer = Observer_State<>> +requires Frame_Control_Strategy_For && State_Value +class Scene2D_Context : public Triple_State_Strategy { +public: + using Scene_State_Strategy = Triple_State_Strategy; + using Render_Task = Scene_Base::Render_Task; + using Renderable = Scene_Base::Renderable; + using Renderable_List = Scene_Base::Renderable_List; + using Frame_Control = Strategy; + using Frame = typename Frame_Control::Frame; + using Painter_Lease = typename Frame_Control::Painter_Lease; + using Render_Lease = typename Frame_Control::Render_Lease; + template + requires std::constructible_from + explicit Scene2D_Context(Args&&... args) : Scene_State_Strategy(), frame_control(std::forward(args)...), display_root(nullptr), dependency_root(nullptr) { + this->frame_control_strategy = &frame_control; + } + template + requires std::constructible_from + Scene2D_Context(With_Observer state_observer, With_Observer scene_observer, Args&&... args) + : Scene_State_Strategy(std::move(state_observer)), frame_control(std::forward(args)...), display_root(nullptr), dependency_root(nullptr), scene_observer_(std::move(scene_observer.observer)) { + this->frame_control_strategy = &frame_control; + } + ~Scene2D_Context() override { + this->wait_for_render(); + this->frame_control_strategy = nullptr; + } + Frame_Control frame_control; + Renderable_Base::Layer_Node display_root; + Renderable_Base::Dependency_Node dependency_root; + Cache final_color_cache; +protected: + void on_renderable_attached(Renderable_Base& renderable) override { + if (!renderable.layer_node.parent) { + display_root.append_child(renderable.layer_node); + } + if (!renderable.dependency_node.parent) { + dependency_root.append_child(renderable.dependency_node); + } + color_caches_.try_emplace(&renderable, std::make_unique()); + } + void on_renderable_detached(Renderable_Base& renderable) override { + promote_children(renderable.layer_node); + promote_children(renderable.dependency_node); + 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); + } + Color_Cache* prepare_renderable_cache(Renderable_Base& renderable, bool clear) override { + Cache* cache = color_caches_.at(&renderable).get(); + if (clear) { + cache->clear(); + } + return cache; + } + void generate_final_color_cache(const Scene_Render_Context&, const Renderable_List& renderables) override { + final_color_cache.clear(); + for (const Renderable& renderable : renderables) { + final_color_cache.composite(*color_caches_.at(renderable.get())); + } + } + std::uint64_t acquire_scene_state() override { + return this->Scene_State_Strategy::acquire_render_state(); + } + void observe_scene(const Scene_Base::Observation& observation) noexcept override { + scene_observer_.observe(observation); + } + std::uint64_t observer_now_ns() const noexcept override { + return scene_observer_.now_ns(); + } +private: + template + static void promote_children(Node& node) { + 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()); + const auto children = node.children; + node.detach(); + for (Node* child : children) { + parent->insert_child(index++, *child); + } + } + template + static void append_tree_order(Node& node, const std::unordered_map& attached, std::unordered_set& visited, Renderable_List& order) { + 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); + } + } + template + static Renderable_List tree_order(Node& root, const Renderable_List& renderables) { + std::unordered_map attached; + attached.reserve(renderables.size()); + for (const Renderable& renderable : renderables) { + attached.emplace(renderable.get(), renderable); + } + std::unordered_set visited; + Renderable_List order; + order.reserve(renderables.size()); + append_tree_order(root, attached, visited, order); + for (const Renderable& renderable : renderables) { + if (visited.insert(renderable.get()).second) { + order.push_back(renderable); + } + } + return order; + } + Scene_Observer scene_observer_; + std::unordered_map> color_caches_; +}; diff --git a/Kernel/src/renderive/scene/Scene3D_Context.hpp b/Kernel/src/renderive/scene/Scene3D_Context.hpp new file mode 100644 index 0000000..1ae2201 --- /dev/null +++ b/Kernel/src/renderive/scene/Scene3D_Context.hpp @@ -0,0 +1,48 @@ +#pragma once +#include +#include "renderive/base/observer/Observer.hpp" +#include "renderive/frame_control/Frame_Control.hpp" +#include "renderive/state/Triple_State_Strategy.hpp" +#include "base/Scene_Base.hpp" +struct Scene3D_Frame_Data {}; +struct Scene3D_State { + std::uint64_t revision{}; +}; +template , class State = Scene3D_State, class State_Observer = Observer_State<>, class Scene_Observer = Observer_State<>> +requires Frame_Control_Strategy_For && State_Value +class Scene3D_Context : public Triple_State_Strategy { +public: + using Scene_State_Strategy = Triple_State_Strategy; + using Frame_Control = Strategy; + using Frame = typename Frame_Control::Frame; + using Painter_Lease = typename Frame_Control::Painter_Lease; + using Render_Lease = typename Frame_Control::Render_Lease; + template + requires std::constructible_from + explicit Scene3D_Context(Args&&... args) : Scene_State_Strategy(), frame_control(std::forward(args)...) { + this->frame_control_strategy = &frame_control; + } + template + requires std::constructible_from + Scene3D_Context(With_Observer state_observer, With_Observer scene_observer, Args&&... args) + : Scene_State_Strategy(std::move(state_observer)), frame_control(std::forward(args)...), scene_observer_(std::move(scene_observer.observer)) { + this->frame_control_strategy = &frame_control; + } + ~Scene3D_Context() override { + this->wait_for_render(); + this->frame_control_strategy = nullptr; + } + Frame_Control frame_control; +protected: + std::uint64_t acquire_scene_state() override { + return this->Scene_State_Strategy::acquire_render_state(); + } + void observe_scene(const Scene_Base::Observation& observation) noexcept override { + scene_observer_.observe(observation); + } + std::uint64_t observer_now_ns() const noexcept override { + return scene_observer_.now_ns(); + } +private: + Scene_Observer scene_observer_; +}; diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp new file mode 100644 index 0000000..96bd052 --- /dev/null +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -0,0 +1,223 @@ +#include "Scene_Base.hpp" +#include +#include +#include +#include +#include +#include "renderive/renderable/color/Color_Cache.hpp" +static_assert(TF_VERSION == 400100, "Renderive requires Taskflow 4.1.0"); +Scene_Base::Scene_Base() : render_renderables_(&renderable_states_[0]), cache_renderables_(&renderable_states_[1]) { + 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(); + } +} +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"); + } + Render_Task task; + { + std::lock_guard renderable_lock(renderable_mutex_); + std::swap(render_renderables_, cache_renderables_); + *cache_renderables_ = *render_renderables_; + prepare_render_task(task, *render_renderables_); + } + frame_control_strategy->swap(); + task.frame_control_state = frame_control_strategy->frame_control_state(); + task.scene_state_revision = acquire_scene_state(); + task.render_sequence = ++render_sequence_; + 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; + task_lock.unlock(); + 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 (render_exception_) { + std::exception_ptr exception = std::exchange(render_exception_, {}); + lock.unlock(); + 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"); + } + std::lock_guard lock(renderable_mutex_); + if (std::find(cache_renderables_->begin(), cache_renderables_->end(), renderable) != cache_renderables_->end()) { + return; + } + cache_renderables_->push_back(renderable); + on_renderable_attached(*renderable); +} +void Scene_Base::detach_renderable(const Renderable_Base& renderable) { + wait_for_render(); + std::lock_guard lock(renderable_mutex_); + on_renderable_detached(const_cast(renderable)); + std::erase_if(*cache_renderables_, [&renderable](const Renderable& value) { + return value.get() == &renderable; + }); +} +std::size_t Scene_Base::renderable_count() const { + std::lock_guard lock(renderable_mutex_); + return cache_renderables_->size(); +} +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) { + task.render_order = renderables; + task.display_order = renderables; +} +Color_Cache* Scene_Base::prepare_renderable_cache(Renderable_Base&, bool) { + return nullptr; +} +void Scene_Base::generate_final_color_cache(const Scene_Render_Context&, const Renderable_List&) {} +std::uint64_t Scene_Base::acquire_scene_state() { + return 0; +} +void Scene_Base::observe_scene(const Observation&) noexcept {} +std::uint64_t Scene_Base::observer_now_ns() const noexcept { + return 0; +} +void Scene_Base::render_loop() { + for (;;) { + Render_Task task; + { + std::unique_lock lock(task_mutex_); + task_ready_.wait(lock, [this] { + return stop_ || task_pending_; + }); + if (stop_) { + return; + } + task = std::move(task_); + task_pending_ = false; + rendering_ = true; + } + observe_scene({Observation_Event::render_started, observer_now_ns(), task.render_sequence, task.scene_state_revision, task.render_order.size()}); + 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(); + } + 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_); + rendering_ = false; + } + render_completed_.notify_all(); + } +} +void Scene_Base::execute_taskflow(Render_Task& task) { + struct Module { + tf::Taskflow graph; + tf::Task module_task; + bool render{}; + }; + tf::Executor executor; + tf::Taskflow scene_graph; + std::unordered_map indices; + indices.reserve(task.render_order.size()); + std::vector render_flags(task.render_order.size()); + for (std::size_t index = 0; index < task.render_order.size(); ++index) { + indices.emplace(task.render_order[index].get(), index); + render_flags[index] = task.render_order[index]->requires_render(); + } + 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; + } + } + std::vector modules(task.render_order.size()); + for (std::size_t index = 0; index < task.render_order.size(); ++index) { + Renderable_Base& renderable = *task.render_order[index]; + Module& module = modules[index]; + module.render = render_flags[index]; + if (module.render) { + 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(); + std::vector internal_tasks; + internal_tasks.reserve(nodes.size()); + for (const auto& node : nodes) { + internal_tasks.push_back(module.graph.emplace([function = node.function, context] { + function(context); + }).name(node.name)); + } + for (std::size_t node_index = 0; node_index < nodes.size(); ++node_index) { + for (std::size_t successor : nodes[node_index].successors) { + internal_tasks[node_index].precede(internal_tasks.at(successor)); + } + } + tf::Task mark_rendered = module.graph.emplace([&renderable] { + renderable.mark_rendered(); + }).name("cache_publish"); + if (internal_tasks.empty()) { + module.graph.emplace([] {}).precede(mark_rendered); + } else { + std::vector has_successor(nodes.size()); + 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]) { + internal_tasks[node_index].precede(mark_rendered); + } + } + } + } else { + module.graph.emplace([] {}).name("cache_hit"); + } + 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 parent_iterator = indices.find(parent); + if (parent_iterator != indices.end()) { + modules[parent_iterator->second].module_task.precede(modules[index].module_task); + } + } + const Scene_Render_Context final_context{this, task.frame_control_state, task.scene_state_revision, task.render_sequence, nullptr, nullptr}; + tf::Task final_task = scene_graph.emplace([this, final_context, &task] { + generate_final_color_cache(final_context, task.display_order); + }).name("final_color_cache"); + if (modules.empty()) { + scene_graph.emplace([] {}).precede(final_task); + } else { + for (Module& module : modules) { + module.module_task.precede(final_task); + } + } + executor.run(scene_graph).get(); +} diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp new file mode 100644 index 0000000..afd9b63 --- /dev/null +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -0,0 +1,77 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include "renderive/frame_control/base/Frame_Control_Strategy_Base.hpp" +#include "renderive/renderable/base/Renderable_Base.hpp" +#include "Scene_Render_Context.hpp" +class Color_Cache; +class Scene_Base { +public: + using Renderable = std::shared_ptr; + using Renderable_List = std::vector; + enum class Observation_Event { + render_submitted, + render_started, + render_completed, + render_failed + }; + struct Observation { + Observation_Event event{}; + std::uint64_t time_ns{}; + std::uint64_t render_sequence{}; + std::uint64_t scene_state_revision{}; + std::size_t renderable_count{}; + }; + Scene_Base(); + Scene_Base(const Scene_Base&) = delete; + Scene_Base& operator=(const Scene_Base&) = delete; + Scene_Base(Scene_Base&&) = delete; + Scene_Base& operator=(Scene_Base&&) = delete; + virtual ~Scene_Base(); + void render(); + void wait_for_render(); + void attach_renderable(Renderable renderable); + void detach_renderable(const Renderable_Base& renderable); + std::size_t renderable_count() const; + Frame_Control_Strategy_Base* frame_control_strategy{}; +protected: + struct Render_Task { + Frame_Control_Strategy_Base::State frame_control_state; + std::uint64_t scene_state_revision{}; + std::uint64_t render_sequence{}; + Renderable_List render_order; + Renderable_List display_order; + }; + 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); + virtual Color_Cache* prepare_renderable_cache(Renderable_Base& renderable, bool clear); + virtual void generate_final_color_cache(const Scene_Render_Context& context, const Renderable_List& renderables); + virtual std::uint64_t acquire_scene_state(); + virtual void observe_scene(const Observation& observation) noexcept; + virtual std::uint64_t observer_now_ns() const noexcept; +private: + void render_loop(); + void execute_taskflow(Render_Task& task); + Renderable_List renderable_states_[2]; + 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::thread worker_; + Render_Task task_; + std::exception_ptr render_exception_; + std::uint64_t render_sequence_{}; + bool task_pending_{}; + bool rendering_{}; + bool stop_{}; +}; +class Scene_2D_Base : public Scene_Base {}; +class Scene_3D_Base : public Scene_Base {}; diff --git a/Kernel/src/renderive/scene/base/Scene_Render_Context.hpp b/Kernel/src/renderive/scene/base/Scene_Render_Context.hpp new file mode 100644 index 0000000..8f3b897 --- /dev/null +++ b/Kernel/src/renderive/scene/base/Scene_Render_Context.hpp @@ -0,0 +1,14 @@ +#pragma once +#include +#include "renderive/frame_control/base/Frame_Control_Strategy_Base.hpp" +class Color_Cache; +class Renderable_Base; +class Scene_Base; +struct Scene_Render_Context { + Scene_Base* scene{}; + Frame_Control_Strategy_Base::State frame_control_state; + std::uint64_t scene_state_revision{}; + std::uint64_t render_sequence{}; + Renderable_Base* renderable{}; + Color_Cache* color_cache{}; +}; diff --git a/Kernel/src/renderive/scene/concept/Scene.hpp b/Kernel/src/renderive/scene/concept/Scene.hpp new file mode 100644 index 0000000..35e958c --- /dev/null +++ b/Kernel/src/renderive/scene/concept/Scene.hpp @@ -0,0 +1,13 @@ +#pragma once +#include +#include "renderive/scene/base/Scene_Base.hpp" +template +concept Scene = std::derived_from && requires(That& scene) { + { scene.frame_control_strategy } -> std::convertible_to; + { scene.render() } -> std::same_as; + { scene.wait_for_render() } -> std::same_as; +}; +template +concept Scene_2D = Scene && std::derived_from; +template +concept Scene_3D = Scene && std::derived_from; diff --git a/Kernel/src/renderive/state/Attach_State_Builder.hpp b/Kernel/src/renderive/state/Attach_State_Builder.hpp new file mode 100644 index 0000000..d7278e1 --- /dev/null +++ b/Kernel/src/renderive/state/Attach_State_Builder.hpp @@ -0,0 +1,5 @@ +#pragma once +#include "../base/property/Attach_Builder.hpp" +#include "Optional_State_Validator.hpp" +template class Builder_Type = Property_Builder, class... Builder_Args> +using Attach_State_Builder = Attach_Builder, Builder_Args...>; diff --git a/Kernel/src/renderive/state/Concepts.hpp b/Kernel/src/renderive/state/Concepts.hpp new file mode 100644 index 0000000..d321aed --- /dev/null +++ b/Kernel/src/renderive/state/Concepts.hpp @@ -0,0 +1,22 @@ +#pragma once +#include +#include "renderive/base/property/Concepts.hpp" +#include "base/State_Strategy_Base.hpp" +template +concept State_Value = Property_Set; +template +concept Has_State_Validator = requires(const State& state) { + { Product::validate_state(state) } -> std::same_as; +}; +template +concept Double_State_Strategy_Type = std::derived_from && requires(That& strategy, const That& const_strategy) { + typename That::State; + typename That::Observation; + { strategy.publish() } -> std::same_as; + { const_strategy.state_revision() } -> std::same_as; + { const_strategy.render_use_state() } -> std::same_as; +}; +template +concept Triple_State_Strategy_Type = Double_State_Strategy_Type && requires(That& strategy) { + { strategy.acquire_render_state() } -> std::same_as; +}; diff --git a/Kernel/src/renderive/state/Double_State_Strategy.hpp b/Kernel/src/renderive/state/Double_State_Strategy.hpp new file mode 100644 index 0000000..aafb622 --- /dev/null +++ b/Kernel/src/renderive/state/Double_State_Strategy.hpp @@ -0,0 +1,88 @@ +#pragma once +#include +#include +#include +#include +#include "renderive/base/Atomic_Mutex.hpp" +#include "renderive/base/Concepts.hpp" +#include "renderive/base/observer/Observer.hpp" +#include "renderive/base/property/Concepts.hpp" +#include "Concepts.hpp" +#include "base/State_Strategy_Base.hpp" +template > +struct Double_State_Strategy : That, State_Strategy_Base { + using Self = Double_State_Strategy; + using State = State_Type; + enum class Observation_Event { + cache_updated, + published + }; + struct Observation { + Observation_Event event{}; + std::uint64_t time_ns{}; + std::uint64_t cache_update_count{}; + std::uint64_t publish_count{}; + State state; + }; + static_assert(Timed_Struct_Observer); + Double_State_Strategy() requires std::default_initializable && std::default_initializable : That(), states{}, render_state(&states[0]), cache_state(&states[1]) {} + explicit Double_State_Strategy(With_Observer option) requires std::default_initializable && std::default_initializable + : That(), observer(std::move(option.observer)), states{}, render_state(&states[0]), cache_state(&states[1]) {} + template + requires std::constructible_from + explicit Double_State_Strategy(const State& state, Args&&... args) + : That(std::forward(args)...), states{state, state}, render_state(&states[0]), cache_state(&states[1]) {} + template + requires std::constructible_from + Double_State_Strategy(const State& state, With_Observer option, Args&&... args) + : That(std::forward(args)...), observer(std::move(option.observer)), states{state, state}, render_state(&states[0]), cache_state(&states[1]) {} + template Value> + Self& set(Value&& value) { + Observation observation; + { + std::lock_guard lock(mtx); + cache_state->*Member = std::forward(value); + ++cache_update_count; + observation = {Observation_Event::cache_updated, observer.now_ns(), cache_update_count, publish_count, *cache_state}; + } + observer.observe(observation); + return *this; + } + template + auto get() const { + std::lock_guard lock(mtx); + const auto& value = cache_state->*Member; + if constexpr (Validated_Property_Value>) { + return value.get(); + } else { + return value; + } + } + void publish() override { + Observation observation; + { + std::lock_guard lock(mtx); + std::swap(render_state, cache_state); + *cache_state = *render_state; + ++publish_count; + observation = {Observation_Event::published, observer.now_ns(), cache_update_count, publish_count, *render_state}; + } + observer.observe(observation); + } + std::uint64_t state_revision() const override { + std::lock_guard lock(mtx); + return publish_count; + } + const State& render_use_state() const { + std::lock_guard lock(mtx); + return *render_state; + } +private: + Observer observer; + State states[2]; + State* render_state; + State* cache_state; + std::uint64_t cache_update_count{}; + std::uint64_t publish_count{}; + mutable Mutex mtx; +}; diff --git a/Kernel/src/renderive/state/Optional_State_Validator.hpp b/Kernel/src/renderive/state/Optional_State_Validator.hpp new file mode 100644 index 0000000..c245a6e --- /dev/null +++ b/Kernel/src/renderive/state/Optional_State_Validator.hpp @@ -0,0 +1,10 @@ +#pragma once +#include "Concepts.hpp" +template +struct Optional_State_Validator { + void operator()(const State& state) const { + if constexpr (Has_State_Validator) { + Product::validate_state(state); + } + } +}; diff --git a/Kernel/src/renderive/state/State_Strategy.hpp b/Kernel/src/renderive/state/State_Strategy.hpp new file mode 100644 index 0000000..ea7ca96 --- /dev/null +++ b/Kernel/src/renderive/state/State_Strategy.hpp @@ -0,0 +1,7 @@ +#pragma once +#include "Attach_State_Builder.hpp" +#include "Concepts.hpp" +#include "Double_State_Strategy.hpp" +#include "Optional_State_Validator.hpp" +#include "Triple_State_Strategy.hpp" +#include "base/State_Strategy_Base.hpp" diff --git a/Kernel/src/renderive/state/Triple_State_Strategy.hpp b/Kernel/src/renderive/state/Triple_State_Strategy.hpp new file mode 100644 index 0000000..6e5161f --- /dev/null +++ b/Kernel/src/renderive/state/Triple_State_Strategy.hpp @@ -0,0 +1,112 @@ +#pragma once +#include +#include +#include +#include +#include "renderive/base/Atomic_Mutex.hpp" +#include "renderive/base/Concepts.hpp" +#include "renderive/base/observer/Observer.hpp" +#include "renderive/base/property/Concepts.hpp" +#include "Concepts.hpp" +#include "base/State_Strategy_Base.hpp" +template > +struct Triple_State_Strategy : That, State_Strategy_Base { + using Self = Triple_State_Strategy; + using State = State_Type; + enum class Observation_Event { + cache_updated, + published, + render_acquired + }; + struct Observation { + Observation_Event event{}; + std::uint64_t time_ns{}; + std::uint64_t cache_update_count{}; + std::uint64_t publish_count{}; + std::uint64_t render_revision{}; + State state; + }; + static_assert(Timed_Struct_Observer); + Triple_State_Strategy() requires std::default_initializable && std::default_initializable + : That(), states{}, render_state(&states[0]), published_state(&states[1]), cache_state(&states[2]) {} + explicit Triple_State_Strategy(With_Observer option) requires std::default_initializable && std::default_initializable + : That(), observer(std::move(option.observer)), states{}, render_state(&states[0]), published_state(&states[1]), cache_state(&states[2]) {} + template + requires std::constructible_from + explicit Triple_State_Strategy(const State& state, Args&&... args) + : That(std::forward(args)...), states{state, state, state}, render_state(&states[0]), published_state(&states[1]), cache_state(&states[2]) {} + template + requires std::constructible_from + Triple_State_Strategy(const State& state, With_Observer option, Args&&... args) + : That(std::forward(args)...), observer(std::move(option.observer)), states{state, state, state}, render_state(&states[0]), published_state(&states[1]), cache_state(&states[2]) {} + template Value> + Self& set(Value&& value) { + Observation observation; + { + std::lock_guard lock(mtx); + cache_state->*Member = std::forward(value); + ++cache_update_count; + observation = {Observation_Event::cache_updated, observer.now_ns(), cache_update_count, publish_count, render_revision, *cache_state}; + } + observer.observe(observation); + return *this; + } + template + auto get() const { + std::lock_guard lock(mtx); + const auto& value = cache_state->*Member; + if constexpr (Validated_Property_Value>) { + return value.get(); + } else { + return value; + } + } + void publish() override { + Observation observation; + { + std::lock_guard lock(mtx); + std::swap(published_state, cache_state); + *cache_state = *published_state; + ++publish_count; + published_revision = publish_count; + observation = {Observation_Event::published, observer.now_ns(), cache_update_count, publish_count, render_revision, *published_state}; + } + observer.observe(observation); + } + std::uint64_t acquire_render_state() { + Observation observation; + bool acquired{}; + { + std::lock_guard lock(mtx); + if (render_revision != published_revision) { + std::swap(render_state, published_state); + render_revision = published_revision; + acquired = true; + observation = {Observation_Event::render_acquired, observer.now_ns(), cache_update_count, publish_count, render_revision, *render_state}; + } + } + if (acquired) { + observer.observe(observation); + } + return render_revision; + } + std::uint64_t state_revision() const override { + std::lock_guard lock(mtx); + return publish_count; + } + const State& render_use_state() const { + std::lock_guard lock(mtx); + return *render_state; + } +private: + Observer observer; + State states[3]; + State* render_state; + State* published_state; + State* cache_state; + std::uint64_t cache_update_count{}; + std::uint64_t publish_count{}; + std::uint64_t published_revision{}; + std::uint64_t render_revision{}; + mutable Mutex mtx; +}; diff --git a/Kernel/src/renderive/state/base/State_Strategy_Base.hpp b/Kernel/src/renderive/state/base/State_Strategy_Base.hpp new file mode 100644 index 0000000..7b72f16 --- /dev/null +++ b/Kernel/src/renderive/state/base/State_Strategy_Base.hpp @@ -0,0 +1,8 @@ +#pragma once +#include +class State_Strategy_Base { +public: + virtual ~State_Strategy_Base() = default; + virtual void publish() = 0; + virtual std::uint64_t state_revision() const = 0; +}; diff --git a/Kernel/tests/compat/gtest/gtest.h b/Kernel/tests/compat/gtest/gtest.h new file mode 100644 index 0000000..70e15fa --- /dev/null +++ b/Kernel/tests/compat/gtest/gtest.h @@ -0,0 +1,75 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +namespace testing { +struct Test_Info { + std::string suite; + std::string name; + std::function function; +}; +inline std::vector& registry() { + static std::vector tests; + return tests; +} +inline int& current_failures() { + static thread_local int failures{}; + return failures; +} +class Fatal_Failure {}; +inline bool register_test(const char* suite, const char* name, std::function function) { + registry().push_back({suite, name, std::move(function)}); + return true; +} +inline void fail(const char* file, int line, const char* expression, bool fatal) { + ++current_failures(); + std::cerr << file << ':' << line << ": failure: " << expression << '\n'; + if (fatal) { + throw Fatal_Failure{}; + } +} +inline void InitGoogleTest(int*, char**) {} +inline int Run_All_Tests() { + int failed_tests{}; + for (const Test_Info& test : registry()) { + if (test.suite.starts_with("DISABLED_") || test.name.starts_with("DISABLED_")) { + std::cout << "[ DISABLED ] " << test.suite << '.' << test.name << '\n'; + continue; + } + current_failures() = 0; + try { + test.function(); + } catch (const Fatal_Failure&) { + } catch (const std::exception& exception) { + fail(__FILE__, __LINE__, exception.what(), false); + } catch (...) { + fail(__FILE__, __LINE__, "unknown exception", false); + } + if (current_failures() != 0) { + ++failed_tests; + std::cerr << "[ FAILED ] " << test.suite << '.' << test.name << '\n'; + } else { + std::cout << "[ OK ] " << test.suite << '.' << test.name << '\n'; + } + } + std::cout << registry().size() << " tests, " << failed_tests << " failed\n"; + return failed_tests == 0 ? 0 : 1; +} +} +#define TEST(suite, name) static void suite##_##name##_test(); static const bool suite##_##name##_registered = ::testing::register_test(#suite, #name, suite##_##name##_test); static void suite##_##name##_test() +#define EXPECT_TRUE(value) do { if (!(value)) ::testing::fail(__FILE__, __LINE__, "EXPECT_TRUE(" #value ")", false); } while (false) +#define EXPECT_FALSE(value) do { if ((value)) ::testing::fail(__FILE__, __LINE__, "EXPECT_FALSE(" #value ")", false); } while (false) +#define ASSERT_TRUE(value) do { if (!(value)) ::testing::fail(__FILE__, __LINE__, "ASSERT_TRUE(" #value ")", true); } while (false) +#define EXPECT_EQ(left, right) do { if (!((left) == (right))) ::testing::fail(__FILE__, __LINE__, "EXPECT_EQ(" #left ", " #right ")", false); } while (false) +#define EXPECT_NE(left, right) do { if (!((left) != (right))) ::testing::fail(__FILE__, __LINE__, "EXPECT_NE(" #left ", " #right ")", false); } while (false) +#define EXPECT_GT(left, right) do { if (!((left) > (right))) ::testing::fail(__FILE__, __LINE__, "EXPECT_GT(" #left ", " #right ")", false); } while (false) +#define EXPECT_GE(left, right) do { if (!((left) >= (right))) ::testing::fail(__FILE__, __LINE__, "EXPECT_GE(" #left ", " #right ")", false); } while (false) +#define EXPECT_DOUBLE_EQ(left, right) do { double gtest_left = static_cast(left); double gtest_right = static_cast(right); if (!(std::fabs(gtest_left - gtest_right) <= 1e-12 || (std::isnan(gtest_left) && std::isnan(gtest_right)))) ::testing::fail(__FILE__, __LINE__, "EXPECT_DOUBLE_EQ(" #left ", " #right ")", false); } while (false) +#define EXPECT_THROW(statement, exception_type) do { bool gtest_thrown = false; try { statement; } catch (const exception_type&) { gtest_thrown = true; } catch (...) {} if (!gtest_thrown) ::testing::fail(__FILE__, __LINE__, "EXPECT_THROW(" #statement ", " #exception_type ")", false); } while (false) +#define EXPECT_NO_THROW(statement) do { bool gtest_thrown = false; try { statement; } catch (...) { gtest_thrown = true; } if (gtest_thrown) ::testing::fail(__FILE__, __LINE__, "EXPECT_NO_THROW(" #statement ")", false); } while (false) +#define SUCCEED() do {} while (false) +#define RUN_ALL_TESTS() ::testing::Run_All_Tests() diff --git a/Kernel/tests/compat/taskflow/taskflow.hpp b/Kernel/tests/compat/taskflow/taskflow.hpp new file mode 100644 index 0000000..8a3b157 --- /dev/null +++ b/Kernel/tests/compat/taskflow/taskflow.hpp @@ -0,0 +1,125 @@ +#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/renderive/base/Concepts_Test.cpp b/Kernel/tests/renderive/base/Concepts_Test.cpp new file mode 100644 index 0000000..b3765f5 --- /dev/null +++ b/Kernel/tests/renderive/base/Concepts_Test.cpp @@ -0,0 +1,11 @@ +#include +#include "renderive/base/Atomic_Mutex.hpp" +#include "renderive/base/Concepts.hpp" +struct Base_Plain_Type {}; +static_assert(Class_Type); +static_assert(Derivable_Type); +static_assert(Mutex_Type); +static_assert(Mutex_Type); +TEST(base_concepts_test, concepts_compile) { + SUCCEED(); +} diff --git a/Kernel/tests/renderive/base/node/Multiway_Node_Test.cpp b/Kernel/tests/renderive/base/node/Multiway_Node_Test.cpp new file mode 100644 index 0000000..d9da298 --- /dev/null +++ b/Kernel/tests/renderive/base/node/Multiway_Node_Test.cpp @@ -0,0 +1,32 @@ +#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>); +TEST(multiway_node_test, reparents_child_and_preserves_order) { + Multiway_Node_Test_Owner first_owner; + Multiway_Node_Test_Owner second_owner; + Multiway_Node_Test_Owner third_owner; + Multiway_Node_Test_Type root; + Multiway_Node_Test_Type first(&first_owner); + Multiway_Node_Test_Type second(&second_owner); + Multiway_Node_Test_Type third(&third_owner); + 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); + second.append_child(third); + EXPECT_TRUE(first.children.empty()); + EXPECT_EQ(third.parent, &second); +} +TEST(multiway_node_test, rejects_cycles) { + Multiway_Node_Test_Type root; + Multiway_Node_Test_Type child; + root.append_child(child); + EXPECT_THROW(child.append_child(root), std::invalid_argument); +} diff --git a/Kernel/tests/renderive/base/observer/Observer_Test.cpp b/Kernel/tests/renderive/base/observer/Observer_Test.cpp new file mode 100644 index 0000000..7729987 --- /dev/null +++ b/Kernel/tests/renderive/base/observer/Observer_Test.cpp @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include "renderive/base/observer/Observer.hpp" +struct Observer_Test_Observation { + int value{}; +}; +struct Observer_Test_Time_Source { + std::shared_ptr> time{std::make_shared>(10)}; + std::uint64_t now_ns() const noexcept { + return time->fetch_add(5, std::memory_order_relaxed); + } +}; +struct Observer_Test_Recorder { + static constexpr bool enabled = true; + std::shared_ptr value{std::make_shared()}; + void observe(const Observer_Test_Observation& observation) noexcept { + *value = observation.value; + } +}; +using Observer_Test_State = Observer_State; +static_assert(Observation_Struct); +static_assert(Observer_Time_Source); +static_assert(Struct_Observer); +static_assert(Timed_Struct_Observer); +TEST(observer_state_test, forwards_observation_structure) { + Observer_Test_Recorder recorder; + Observer_Test_State observer(recorder, Observer_Test_Time_Source{}); + observer.observe(Observer_Test_Observation{7}); + EXPECT_EQ(*recorder.value, 7); +} +TEST(observer_state_test, owns_time_source) { + Observer_Test_State observer; + EXPECT_EQ(observer.now_ns(), 10); + EXPECT_EQ(observer.now_ns(), 15); +} +TEST(disabled_observer_test, accepts_any_observation_structure) { + Observer_State<> observer; + EXPECT_NO_THROW(observer.observe(Observer_Test_Observation{7})); + EXPECT_GT(observer.now_ns(), 0); +} diff --git a/Kernel/tests/renderive/base/observer/time_source/Steady_Time_Source_Test.cpp b/Kernel/tests/renderive/base/observer/time_source/Steady_Time_Source_Test.cpp new file mode 100644 index 0000000..ae23965 --- /dev/null +++ b/Kernel/tests/renderive/base/observer/time_source/Steady_Time_Source_Test.cpp @@ -0,0 +1,9 @@ +#include +#include "renderive/base/observer/Observer.hpp" +static_assert(Observer_Time_Source); +TEST(steady_time_source_test, advances_monotonically) { + Steady_Time_Source source; + const auto first = source.now_ns(); + const auto second = source.now_ns(); + EXPECT_GE(second, first); +} diff --git a/Kernel/tests/renderive/base/property/Concepts_Test.cpp b/Kernel/tests/renderive/base/property/Concepts_Test.cpp new file mode 100644 index 0000000..8625047 --- /dev/null +++ b/Kernel/tests/renderive/base/property/Concepts_Test.cpp @@ -0,0 +1,11 @@ +#include +#include "Property_Test_Types.hpp" +static_assert(Property_Set); +static_assert(Property_Product); +static_assert(Value_Validator); +static_assert(Validated_Property_Value>); +static_assert(Property_Validator); +static_assert(Property_Constructible); +TEST(property_concepts_test, concepts_compile) { + SUCCEED(); +} diff --git a/Kernel/tests/renderive/base/property/Property_Builder_Test.cpp b/Kernel/tests/renderive/base/property/Property_Builder_Test.cpp new file mode 100644 index 0000000..4b9da50 --- /dev/null +++ b/Kernel/tests/renderive/base/property/Property_Builder_Test.cpp @@ -0,0 +1,24 @@ +#include +#include +#include "Property_Test_Types.hpp" +TEST(property_builder_test, builds_independent_product) { + auto product = Property_Direct_Builder{Property_Direct_Properties{.value = 42}}.build(7); + EXPECT_EQ(product->value, 42); + EXPECT_EQ(product->id, 7); +} +TEST(property_builder_test, configures_properties) { + auto product = Property_Direct_Builder{}.set<&Property_Direct_Properties::value>(10).configure([](Property_Direct_Properties& properties) { + properties.value += 5; + }).build(1); + EXPECT_EQ(product->value, 15); +} +TEST(property_builder_test, supports_external_validator) { + auto builder = Property_External_Product::Builder{}.set<&Property_External_Properties::minimum>(10).set<&Property_External_Properties::maximum>(5); + EXPECT_THROW(builder.build(), std::invalid_argument); +} +TEST(property_builder_test, builds_unique_product) { + auto product = Property_Direct_Builder{Property_Direct_Properties{.value = 12}}.build_unique(3); + static_assert(std::same_as>); + EXPECT_EQ(product->value, 12); + EXPECT_EQ(product->id, 3); +} diff --git a/Kernel/tests/renderive/base/property/Property_Test_Types.hpp b/Kernel/tests/renderive/base/property/Property_Test_Types.hpp new file mode 100644 index 0000000..a4cd632 --- /dev/null +++ b/Kernel/tests/renderive/base/property/Property_Test_Types.hpp @@ -0,0 +1,35 @@ +#pragma once +#include +#include "renderive/base/property/Property.hpp" +struct Property_External_Properties { + int minimum{}; + int maximum{}; +}; +struct Property_External_Validator { + void operator()(const Property_External_Properties& properties) const { + if (properties.minimum > properties.maximum) { + throw std::invalid_argument("external minimum must not exceed maximum"); + } + } +}; +struct Property_External_Product_Base { + explicit Property_External_Product_Base(const Property_External_Properties& properties) : properties(properties) {} + Property_External_Properties properties; +}; +using Property_External_Product = Attach_Builder; +struct Property_Direct_Properties { + int value{}; +}; +struct Property_Direct_Product { + Property_Direct_Product(const Property_Direct_Properties& properties, int id) : value(properties.value), id(id) {} + int value; + int id; +}; +using Property_Direct_Builder = Property_Builder; +struct Property_Checked_Value_Validator { + void operator()(const int& value) const { + if (value % 2 != 0) { + throw std::invalid_argument("value must be even"); + } + } +}; diff --git a/Kernel/tests/renderive/base/property/Validated_Value_Test.cpp b/Kernel/tests/renderive/base/property/Validated_Value_Test.cpp new file mode 100644 index 0000000..5cd0063 --- /dev/null +++ b/Kernel/tests/renderive/base/property/Validated_Value_Test.cpp @@ -0,0 +1,23 @@ +#include +#include "Property_Test_Types.hpp" +TEST(validated_value_test, accepts_range_boundaries) { + Range_Value minimum{0}; + Range_Value maximum{10}; + EXPECT_EQ(minimum.get(), 0); + EXPECT_EQ(maximum.get(), 10); +} +TEST(validated_value_test, rejects_values_outside_range) { + EXPECT_THROW((Range_Value{-1}), std::out_of_range); + EXPECT_THROW((Range_Value{11}), std::out_of_range); +} +TEST(validated_value_test, keeps_old_value_when_validation_fails) { + Range_Value value{5}; + EXPECT_THROW(value = 20, std::out_of_range); + EXPECT_EQ(value.get(), 5); +} +TEST(validated_value_test, supports_custom_validator) { + Validated_Value value{4}; + EXPECT_NO_THROW(value = 6); + EXPECT_THROW(value = 7, std::invalid_argument); + EXPECT_EQ(value.get(), 6); +} diff --git a/Kernel/tests/renderive/frame_control/base/Frame_Control_Strategy_Base_Test.cpp b/Kernel/tests/renderive/frame_control/base/Frame_Control_Strategy_Base_Test.cpp new file mode 100644 index 0000000..649999a --- /dev/null +++ b/Kernel/tests/renderive/frame_control/base/Frame_Control_Strategy_Base_Test.cpp @@ -0,0 +1,32 @@ +#include +#include +#include "renderive/frame_control/Frame_Control.hpp" +struct Frame_Control_Base_Test_Frame {}; +using Frame_Control_Base_Test_Strategy = Low_Latency_Strategy; +TEST(frame_control_strategy_base_test, publishes_cached_state_only_on_swap) { + Frame_Control_Base_Test_Strategy strategy; + strategy.set_frequency_hz(120.0); + { + auto frame = strategy.acquire_painter(); + ASSERT_TRUE(frame); + } + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + } + EXPECT_DOUBLE_EQ(strategy.state().frequency_hz, 120.0); + EXPECT_DOUBLE_EQ(strategy.frequency_hz(), 60.0); + strategy.swap(); + EXPECT_DOUBLE_EQ(strategy.frequency_hz(), 120.0); + EXPECT_EQ(strategy.next_refresh_interval_ns(), strategy.state().next_refresh_interval_ns); +} +TEST(frame_control_strategy_base_test, manual_and_flow_publish_invalid_frequency) { + Manual_Refresh_Strategy manual; + Flow_Refresh_Strategy flow; + manual.swap(); + flow.swap(); + EXPECT_TRUE(std::isnan(manual.frequency_hz())); + EXPECT_TRUE(std::isnan(flow.frequency_hz())); + EXPECT_EQ(manual.next_refresh_interval_ns(), 0); + EXPECT_EQ(flow.next_refresh_interval_ns(), 0); +} diff --git a/Kernel/tests/renderive/frame_control/concept/Concepts_Test.cpp b/Kernel/tests/renderive/frame_control/concept/Concepts_Test.cpp new file mode 100644 index 0000000..34a92fd --- /dev/null +++ b/Kernel/tests/renderive/frame_control/concept/Concepts_Test.cpp @@ -0,0 +1,20 @@ +#include +#include "../strategy/low_latency/Low_Latency_Strategy_Test_Types.hpp" +struct Frame_Control_Concept_Frame {}; +using Frame_Control_Concept_Manual = Manual_Refresh_Strategy; +using Frame_Control_Concept_Flow = Flow_Refresh_Strategy; +static_assert(Timed_Struct_Observer); +static_assert(Painter_Frame_Lease); +static_assert(Render_Frame_Lease); +static_assert(Runtime_Frame_Control_Strategy); +static_assert(Runtime_Frame_Control_Strategy); +static_assert(Runtime_Frame_Control_Strategy); +static_assert(Frame_Lease_Strategy); +static_assert(Frame_Control_Strategy_For); +static_assert(Frame_Refresh_Strategy); +static_assert(Real_Time_Data_Aware_Frame_Strategy); +static_assert(Manual_Frame_Refresh_Strategy); +static_assert(Flow_Frame_Refresh_Strategy); +TEST(frame_control_concepts_test, concepts_compile) { + SUCCEED(); +} 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 new file mode 100644 index 0000000..42d4df7 --- /dev/null +++ b/Kernel/tests/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy_Test.cpp @@ -0,0 +1,28 @@ +#include +#include "renderive/frame_control/Frame_Control.hpp" +struct Flow_Refresh_Test_Frame { + int value{}; +}; +using Flow_Refresh_Test_Strategy = Flow_Refresh_Strategy; +static_assert(Flow_Frame_Refresh_Strategy); +TEST(flow_refresh_strategy_test, preserves_all_frames_in_fifo_order) { + Flow_Refresh_Test_Strategy strategy; + for (int value = 1; value <= 3; ++value) { + auto frame = strategy.acquire_painter(); + frame->value = value; + } + EXPECT_EQ(strategy.pending_frame_count(), 3); + for (int value = 1; value <= 3; ++value) { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, value); + } + EXPECT_EQ(strategy.pending_frame_count(), 0); + EXPECT_EQ(strategy.state().rendered_frame_count, 3); +} +TEST(flow_refresh_strategy_test, returns_empty_lease_when_queue_is_empty) { + Flow_Refresh_Test_Strategy strategy; + auto frame = strategy.acquire_renderer(); + EXPECT_FALSE(frame); + EXPECT_EQ(strategy.state().empty_acquire_count, 1); +} 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 new file mode 100644 index 0000000..7587ebe --- /dev/null +++ b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp @@ -0,0 +1,100 @@ +#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; + Low_Latency_Test_Observer observer; + auto strategy = make_low_latency_test_strategy(time_source, observer); + { + auto frame = strategy.acquire_painter(); + frame->value = 7; + } + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, 7); + } + EXPECT_EQ(strategy.state().completed_lifecycle_count, 1); +} +TEST(low_latency_strategy_test, abandons_unconsumed_cached_frame) { + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + auto strategy = make_low_latency_test_strategy(time_source, observer); + { + auto frame = strategy.acquire_painter(); + frame->value = 1; + } + { + auto frame = strategy.acquire_painter(); + frame->value = 2; + } + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, 2); + } + EXPECT_EQ(strategy.counter_statistics().abandoned_frame_count, 1); +} +TEST(low_latency_strategy_test, manually_discards_stale_latest_data_frame) { + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + auto strategy = make_low_latency_test_strategy(time_source, observer); + strategy.on_real_time_data_update({Real_Time_Data_Observation_Event::updated, {nullptr, Real_Time_Data_Retention::latest, 1, 10, 1, 1}}); + { + auto frame = strategy.acquire_painter(); + frame->value = 1; + } + strategy.on_real_time_data_update({Real_Time_Data_Observation_Event::updated, {nullptr, Real_Time_Data_Retention::latest, 2, 20, 2, 1}}); + EXPECT_TRUE(strategy.discard_stale_latest_data_frame()); + EXPECT_EQ(strategy.counter_statistics().manually_discarded_frame_count, 1); + auto frame = strategy.acquire_renderer(); + EXPECT_FALSE(frame); +} +TEST(low_latency_strategy_test, does_not_discard_current_data_frame) { + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + auto strategy = make_low_latency_test_strategy(time_source, observer); + strategy.on_real_time_data_update({Real_Time_Data_Observation_Event::updated, {nullptr, Real_Time_Data_Retention::latest, 1, 10, 1, 1}}); + { + auto frame = strategy.acquire_painter(); + frame->value = 9; + } + EXPECT_FALSE(strategy.discard_stale_latest_data_frame()); + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, 9); +} +TEST(low_latency_strategy_test, updates_frequency_after_completed_lifecycle) { + 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(100.0); + { + auto frame = strategy.acquire_painter(); + frame->value = 3; + } + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + } + EXPECT_DOUBLE_EQ(strategy.state().frequency_hz, 100.0); + EXPECT_DOUBLE_EQ(strategy.frequency_hz(), 60.0); + strategy.swap(); + EXPECT_DOUBLE_EQ(strategy.frequency_hz(), 100.0); + EXPECT_EQ(strategy.next_refresh_interval_ns(), 10'000'000); +} +TEST(low_latency_strategy_test, detects_render_limited_state) { + 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(1'000'000.0); + { + auto frame = strategy.acquire_painter(); + frame->value = 5; + } + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + time_source.advance_ns(5'000); + } + EXPECT_EQ(strategy.state().limit_state, Low_Latency_Test_Strategy::Limit_State::render_limited); +} diff --git a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test_Types.hpp b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test_Types.hpp new file mode 100644 index 0000000..2a9ad12 --- /dev/null +++ b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test_Types.hpp @@ -0,0 +1,77 @@ +#pragma once +#include +#include +#include +#include +#include +#include "renderive/frame_control/Frame_Control.hpp" +#include "renderive/real_time_data/Real_Time_Data.hpp" +struct Low_Latency_Test_Frame { + int value{}; +}; +struct Low_Latency_Test_Time_Source { + std::shared_ptr> time_ns{std::make_shared>(100)}; + std::uint64_t now_ns() const noexcept { + return time_ns->fetch_add(10, std::memory_order_relaxed); + } + void advance_ns(std::uint64_t duration_ns) const noexcept { + time_ns->fetch_add(duration_ns, std::memory_order_relaxed); + } +}; +struct Low_Latency_Recorded_Observation { + int event{}; + std::uint64_t sequence{}; + std::uint64_t frame_render_count{}; + std::uint64_t real_time_data_update_sequence{}; + std::uint64_t paint_duration_ns{}; + std::uint64_t render_duration_ns{}; + std::uint64_t published_frame_count{}; + std::uint64_t render_pass_count{}; + std::uint64_t abandoned_frame_count{}; + std::uint64_t manually_discarded_frame_count{}; + std::uint64_t swap_failure_count{}; + double frequency_hz{}; + int limit_state{}; + std::uint64_t next_refresh_interval_ns{}; + std::uint64_t completed_lifecycle_count{}; +}; +struct Low_Latency_Observer_Data { + std::mutex mutex; + std::vector observations; +}; +struct Low_Latency_Test_Observer { + static constexpr bool enabled = true; + std::shared_ptr data{std::make_shared()}; + template + void observe(const Observation& observation) noexcept { + const auto& statistics = observation.statistics; + const auto& counters = statistics.counters; + std::lock_guard lock(data->mutex); + data->observations.push_back({ + static_cast(observation.event), + statistics.sequence, + statistics.frame_render_count, + statistics.real_time_data_update_sequence, + statistics.timing.paint_duration_ns, + statistics.timing.render_duration_ns, + counters.published_frame_count, + counters.render_pass_count, + counters.abandoned_frame_count, + counters.manually_discarded_frame_count, + counters.swap_failure_count, + observation.state.frequency_hz, + static_cast(observation.state.limit_state), + observation.state.next_refresh_interval_ns, + observation.state.completed_lifecycle_count + }); + } +}; +using Low_Latency_Test_Observer_State = Observer_State; +using Low_Latency_Test_Strategy = Low_Latency_Strategy; +inline Low_Latency_Test_Strategy make_low_latency_test_strategy(const Low_Latency_Test_Time_Source& time_source, const Low_Latency_Test_Observer& observer) { + return Low_Latency_Test_Strategy(Low_Latency_Test_Observer_State(observer, time_source)); +} +inline std::vector low_latency_observations(const std::shared_ptr& data) { + std::lock_guard lock(data->mutex); + return data->observations; +} diff --git a/Kernel/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp new file mode 100644 index 0000000..d6c1f25 --- /dev/null +++ b/Kernel/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp @@ -0,0 +1,46 @@ +#include +#include "renderive/frame_control/Frame_Control.hpp" +struct Manual_Refresh_Test_Frame { + int value{}; +}; +using Manual_Refresh_Test_Strategy = Manual_Refresh_Strategy; +static_assert(Manual_Frame_Refresh_Strategy); +TEST(manual_refresh_strategy_test, publishes_only_after_manual_refresh) { + Manual_Refresh_Test_Strategy strategy; + { + auto frame = strategy.acquire_painter(); + ASSERT_TRUE(frame); + frame->value = 7; + } + { + auto frame = strategy.acquire_renderer(); + EXPECT_FALSE(frame); + } + EXPECT_TRUE(strategy.refresh()); + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, 7); + } +} +TEST(manual_refresh_strategy_test, keeps_latest_prepared_frame) { + Manual_Refresh_Test_Strategy strategy; + { + auto frame = strategy.acquire_painter(); + frame->value = 1; + } + { + auto frame = strategy.acquire_painter(); + frame->value = 2; + } + EXPECT_EQ(strategy.state().replaced_prepared_frame_count, 1); + ASSERT_TRUE(strategy.refresh()); + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, 2); +} +TEST(manual_refresh_strategy_test, reports_failed_refresh_without_pending_frame) { + Manual_Refresh_Test_Strategy strategy; + EXPECT_FALSE(strategy.refresh()); + EXPECT_EQ(strategy.state().failed_refresh_count, 1); +} 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 new file mode 100644 index 0000000..5d9667b --- /dev/null +++ b/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include +#include +#include "renderive/real_time_data/Real_Time_Data.hpp" +#include "renderive/renderable/Renderable.hpp" +#include "renderive/scene/Scene.hpp" +struct Real_Time_Data_Test_Time_Source { + std::shared_ptr> time_ns{std::make_shared>()}; + std::uint64_t now_ns() const noexcept { + return time_ns->fetch_add(10, std::memory_order_relaxed); + } +}; +struct Real_Time_Data_Test_Renderable : Renderable_Base { + explicit Real_Time_Data_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene) {} + void render(const Scene_Render_Context&) override { + ++render_count; + } + std::atomic render_count{}; +}; +using Real_Time_Data_Test_Bridge = Observer_State; +using Real_Time_Data_Test_Latest = Latest_Real_Time_Data; +using Real_Time_Data_Test_History = History_Real_Time_Data, std::mutex, Real_Time_Data_Test_Bridge>; +using Real_Time_Data_Test_Attachment = Attach_Real_Time_Data; +TEST(latest_real_time_data_test, retains_only_latest_value) { + Latest_Real_Time_Data data; + data.update(1); + data.update(2); + ASSERT_TRUE(data.snapshot()); + EXPECT_EQ(*data.snapshot(), 2); + EXPECT_EQ(data.update_state().retained_value_count, 1); + EXPECT_EQ(data.revision(), 2); +} +TEST(history_real_time_data_test, retains_all_values_until_explicit_discard) { + using Observer = Observer_State; + History_Real_Time_Data, std::mutex, Observer> data; + data.update(1); + data.update(2); + data.update(3); + EXPECT_EQ(data.snapshot(), (std::vector{1, 2, 3})); + EXPECT_EQ(data.discard_before_time_ns(15), 2); + EXPECT_EQ(data.snapshot(), (std::vector{3})); +} +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 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); + EXPECT_EQ(scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); + auto frame = scene.frame_control.acquire_renderer(); + EXPECT_FALSE(frame); +} +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 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); + scene.attach_renderable(renderable); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(history.snapshot(), (std::vector{2, 3})); + EXPECT_EQ(renderable->render_count.load(), 1); +} diff --git a/Kernel/tests/renderive/real_time_data/base/Real_Time_Data_Base_Test.cpp b/Kernel/tests/renderive/real_time_data/base/Real_Time_Data_Base_Test.cpp new file mode 100644 index 0000000..685e743 --- /dev/null +++ b/Kernel/tests/renderive/real_time_data/base/Real_Time_Data_Base_Test.cpp @@ -0,0 +1,12 @@ +#include +#include "renderive/real_time_data/Real_Time_Data.hpp" +static_assert(std::derived_from, Real_Time_Data_Base>); +static_assert(std::derived_from, Real_Time_Data_Base>); +TEST(real_time_data_base_test, exposes_runtime_retention) { + Latest_Real_Time_Data latest; + History_Real_Time_Data history; + Real_Time_Data_Base* latest_base = &latest; + Real_Time_Data_Base* history_base = &history; + EXPECT_EQ(latest_base->retention(), Real_Time_Data_Retention::latest); + EXPECT_EQ(history_base->retention(), Real_Time_Data_Retention::history); +} diff --git a/Kernel/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp b/Kernel/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp new file mode 100644 index 0000000..58f8308 --- /dev/null +++ b/Kernel/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include "renderive/renderable/Renderable.hpp" +#include "renderive/scene/Scene.hpp" +struct Renderable_Task_Graph_Test_Renderable : Renderable_Base { + Renderable_Task_Graph_Test_Renderable(Scene_Base& scene, std::vector& order, int base) + : Renderable_Base(scene, {.cache_enabled = false}), order(&order), base(base) {} + void build_task_graph(Renderable_Task_Graph& graph) override { + auto first = graph.emplace([this](const Scene_Render_Context&) { + order->push_back(base + 1); + }, "first"); + auto second = graph.emplace([this](const Scene_Render_Context&) { + order->push_back(base + 2); + }, "second"); + graph.precede(first, second); + } + std::vector* order; + int base; +}; +TEST(renderable_task_graph_test, combines_internal_and_external_dependencies) { + Scene2D_Context<> scene; + std::vector order; + auto dependency = std::make_shared(scene, order, 0); + 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.render(); + scene.wait_for_render(); + EXPECT_EQ(order, (std::vector{1, 2, 11, 12})); +} + +#include +#include +#include +#include +struct Renderable_Task_Graph_Concurrency_Test_Renderable : Renderable_Base { + Renderable_Task_Graph_Concurrency_Test_Renderable(Scene_Base& scene, std::atomic& active, std::atomic& maximum) + : Renderable_Base(scene, {.cache_enabled = false}), active(&active), maximum(&maximum) {} + void build_task_graph(Renderable_Task_Graph& graph) override { + auto work = [this](const Scene_Render_Context&) { + const int value = active->fetch_add(1, std::memory_order_acq_rel) + 1; + int observed = maximum->load(std::memory_order_acquire); + while (observed < value && !maximum->compare_exchange_weak(observed, value, std::memory_order_acq_rel)) {} + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + active->fetch_sub(1, std::memory_order_acq_rel); + }; + graph.emplace(work, "parallel_a"); + graph.emplace(work, "parallel_b"); + } + std::atomic* active; + std::atomic* maximum; +}; +TEST(renderable_task_graph_concurrency_test, DISABLED_runs_independent_internal_tasks_concurrently) { + Scene2D_Context<> scene; + std::atomic active{}; + std::atomic maximum{}; + auto renderable = std::make_shared(scene, active, maximum); + scene.attach_renderable(renderable); + scene.render(); + scene.wait_for_render(); + EXPECT_GE(maximum.load(std::memory_order_acquire), 2); +} diff --git a/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp b/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp new file mode 100644 index 0000000..7442f8a --- /dev/null +++ b/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp @@ -0,0 +1,40 @@ +#include +#include +#include +#include "renderive/renderable/Renderable.hpp" +#include "renderive/scene/Scene.hpp" +struct Renderable_Base_Test_Renderable : Renderable_Base { + Renderable_Base_Test_Renderable(Scene_Base& scene, bool cache_enabled) : Renderable_Base(scene, {.cache_enabled = cache_enabled}) {} + void render(const Scene_Render_Context&) override { + ++render_count; + } + int render_count{}; +}; +TEST(renderable_base_test, exposes_scene_and_strategy_members_directly) { + 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())); +} +TEST(renderable_base_test, controls_cache_with_configuration) { + Scene2D_Context<> scene; + auto cached = std::make_shared(scene, true); + auto uncached = std::make_shared(scene, false); + scene.attach_renderable(cached); + scene.attach_renderable(uncached); + scene.render(); + scene.wait_for_render(); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(cached->render_count, 1); + EXPECT_EQ(uncached->render_count, 2); + cached->invalidate_cache(); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(cached->render_count, 2); + EXPECT_EQ(uncached->render_count, 3); +} diff --git a/Kernel/tests/renderive/renderable/color/Color_Cache_Test.cpp b/Kernel/tests/renderive/renderable/color/Color_Cache_Test.cpp new file mode 100644 index 0000000..41c8c23 --- /dev/null +++ b/Kernel/tests/renderive/renderable/color/Color_Cache_Test.cpp @@ -0,0 +1,13 @@ +#include +#include "renderive/renderable/Renderable.hpp" +static_assert(Color_Cache_Type); +TEST(color_cache_test, composites_source_values_in_order) { + Recording_Color_Cache first; + Recording_Color_Cache second; + Recording_Color_Cache result; + first.append(1); + second.append(2); + result.composite(first); + result.composite(second); + EXPECT_EQ(result.values, (std::vector{1, 2})); +} diff --git a/Kernel/tests/renderive/renderable/concept/Renderable_Test.cpp b/Kernel/tests/renderive/renderable/concept/Renderable_Test.cpp new file mode 100644 index 0000000..aaa5603 --- /dev/null +++ b/Kernel/tests/renderive/renderable/concept/Renderable_Test.cpp @@ -0,0 +1,18 @@ +#include +#include +#include "renderive/renderable/Renderable.hpp" +#include "renderive/scene/Scene.hpp" +struct Renderable_Concept_Test : Renderable_Base { + explicit Renderable_Concept_Test(Scene_Base& scene) : Renderable_Base(scene) {} + void render(const Scene_Render_Context&) override {} +}; +static_assert(Renderable); +static_assert(!std::same_as); +TEST(renderable_concept_test, exposes_tree_nodes_scene_and_configuration) { + 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); +} diff --git a/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp b/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp new file mode 100644 index 0000000..8b78930 --- /dev/null +++ b/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp @@ -0,0 +1,36 @@ +#include +#include +#include +#include +#include "renderive/renderable/Renderable.hpp" +#include "renderive/scene/Scene.hpp" +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 { + ++render_count; + scene_is_2d = dynamic_cast(context.scene) != nullptr; + render_thread = std::this_thread::get_id(); + } + std::atomic render_count{}; + bool scene_is_2d{}; + std::thread::id render_thread; +}; +TEST(scene2d_context_test, swaps_renderable_cache_and_renders_in_background) { + Scene2D_Context<> scene; + const auto caller_thread = std::this_thread::get_id(); + auto first = std::make_shared(scene); + auto second = std::make_shared(scene); + scene.attach_renderable(first); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(first->render_count.load(), 1); + EXPECT_TRUE(first->scene_is_2d); + EXPECT_NE(first->render_thread, caller_thread); + scene.detach_renderable(*first); + scene.attach_renderable(second); + scene.render(); + 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); +} diff --git a/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp b/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp new file mode 100644 index 0000000..77cf0c1 --- /dev/null +++ b/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp @@ -0,0 +1,33 @@ +#include +#include +#include +#include +#include "renderive/renderable/Renderable.hpp" +#include "renderive/scene/Scene.hpp" +struct Scene2D_Render_Order_State { + std::vector render_order; +}; +struct Scene2D_Render_Order_Renderable : Renderable_Base { + Scene2D_Render_Order_Renderable(Scene_Base& scene, Scene2D_Render_Order_State& state, std::uint64_t id) + : Renderable_Base(scene, {.cache_enabled = false}), state(&state), id(id) {} + void render(const Scene_Render_Context& context) override { + state->render_order.push_back(id); + dynamic_cast(*context.color_cache).append(id); + } + Scene2D_Render_Order_State* state; + std::uint64_t id; +}; +TEST(scene2d_render_order_test, separates_dependency_order_from_display_order) { + Scene2D_Context<> scene; + Scene2D_Render_Order_State state; + auto spectrum = std::make_shared(scene, state, 2); + 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.render(); + scene.wait_for_render(); + EXPECT_EQ(state.render_order, (std::vector{1, 2})); + EXPECT_EQ(scene.final_color_cache.values, (std::vector{2, 1})); +} diff --git a/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp b/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp new file mode 100644 index 0000000..60215fe --- /dev/null +++ b/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp @@ -0,0 +1,23 @@ +#include +#include +#include +#include "renderive/renderable/Renderable.hpp" +#include "renderive/scene/Scene.hpp" +struct Scene3D_Context_Test_Renderable : Renderable_Base { + explicit Scene3D_Context_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene) {} + void render(const Scene_Render_Context& context) override { + scene_is_3d = dynamic_cast(context.scene) != nullptr; + ++render_count; + } + std::atomic render_count{}; + bool scene_is_3d{}; +}; +TEST(scene3d_context_test, uses_scene_3d_base_contract) { + Scene3D_Context<> scene; + auto renderable = std::make_shared(scene); + scene.attach_renderable(renderable); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(renderable->render_count.load(), 1); + EXPECT_TRUE(renderable->scene_is_3d); +} diff --git a/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp b/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp new file mode 100644 index 0000000..6a56b59 --- /dev/null +++ b/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp @@ -0,0 +1,36 @@ +#include +#include +#include +#include +#include "renderive/scene/Scene.hpp" +struct Scene_State_Observer_Test_State { + int value{}; +}; +struct Scene_State_Observer_Test_Time_Source { + std::uint64_t now_ns() const noexcept { + return 100; + } +}; +struct Scene_State_Observer_Test_Data { + std::vector events; +}; +struct Scene_State_Observer_Test_Recorder { + static constexpr bool enabled = true; + std::shared_ptr data{std::make_shared()}; + void observe(const Scene_Base::Observation& observation) noexcept { + data->events.push_back(observation.event); + } +}; +using Scene_State_Observer_Test_Scene_Observer = Observer_State; +using Scene_State_Observer_Test_State_Observer = Observer_State<>; +TEST(scene_state_observer_test, scene_uses_triple_state_and_observes_lifecycle) { + Scene_State_Observer_Test_Recorder recorder; + using Scene = Scene2D_Context, Recording_Color_Cache, Scene_State_Observer_Test_State, Scene_State_Observer_Test_State_Observer, Scene_State_Observer_Test_Scene_Observer>; + Scene scene(With_Observer(Scene_State_Observer_Test_State_Observer{}), With_Observer(Scene_State_Observer_Test_Scene_Observer(recorder, Scene_State_Observer_Test_Time_Source{}))); + scene.Scene_State_Strategy::set<&Scene_State_Observer_Test_State::value>(9); + scene.Scene_State_Strategy::publish(); + scene.render(); + scene.wait_for_render(); + 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})); +} diff --git a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp new file mode 100644 index 0000000..ff83ad5 --- /dev/null +++ b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp @@ -0,0 +1,23 @@ +#include +#include +#include "renderive/renderable/Renderable.hpp" +#include "renderive/scene/Scene.hpp" +struct Scene_Base_Test_Renderable : Renderable_Base { + explicit Scene_Base_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene) {} + void render(const Scene_Render_Context& context) override { + render_sequence = context.render_sequence; + ++render_count; + } + std::uint64_t render_sequence{}; + int render_count{}; +}; +TEST(scene_base_test, publishes_renderable_collection_before_background_render) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene); + scene.attach_renderable(renderable); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(renderable->render_count, 1); + EXPECT_EQ(renderable->render_sequence, 1); + EXPECT_EQ(scene.renderable_count(), 1); +} diff --git a/Kernel/tests/renderive/scene/concept/Scene_Test.cpp b/Kernel/tests/renderive/scene/concept/Scene_Test.cpp new file mode 100644 index 0000000..18780c7 --- /dev/null +++ b/Kernel/tests/renderive/scene/concept/Scene_Test.cpp @@ -0,0 +1,10 @@ +#include +#include "renderive/scene/Scene.hpp" +static_assert(Scene>); +static_assert(Scene>); +static_assert(Scene_2D>); +static_assert(Scene_3D>); +static_assert(!Scene_3D>); +TEST(scene_concept_test, accepts_2d_and_3d_contexts) { + SUCCEED(); +} diff --git a/Kernel/tests/renderive/state/Concepts_Test.cpp b/Kernel/tests/renderive/state/Concepts_Test.cpp new file mode 100644 index 0000000..6f58fb5 --- /dev/null +++ b/Kernel/tests/renderive/state/Concepts_Test.cpp @@ -0,0 +1,10 @@ +#include +#include "State_Test_Types.hpp" +static_assert(State_Value); +static_assert(std::derived_from); +static_assert(Double_State_Strategy_Type); +static_assert(!Has_State_Validator); +static_assert(Has_State_Validator); +TEST(state_concepts_test, concepts_compile) { + SUCCEED(); +} diff --git a/Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp b/Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp new file mode 100644 index 0000000..71787fc --- /dev/null +++ b/Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#include "State_Test_Types.hpp" +TEST(double_state_strategy_test, builds_without_state_validator) { + auto product = State_Plain_Product::Builder{}.set<&State_Plain_Value::count>(10).set<&State_Plain_Value::name>("alpha").build(); + EXPECT_EQ(product->get<&State_Plain_Value::count>(), 10); + EXPECT_EQ(product->get<&State_Plain_Value::name>(), "alpha"); +} +TEST(double_state_strategy_test, calls_optional_state_validator_when_present) { + auto builder = State_Checked_Product::Builder{}.set<&State_Checked_Value::minimum>(10).set<&State_Checked_Value::maximum>(5); + EXPECT_THROW(builder.build(1, "invalid"), std::invalid_argument); +} +TEST(double_state_strategy_test, keeps_cache_separate_until_publish) { + auto product = State_Checked_Product::Builder{}.set<&State_Checked_Value::percent>(20).set<&State_Checked_Value::batch_size>(4).build(1, "state"); + product->set<&State_Checked_Value::percent>(60); + EXPECT_EQ(product->get<&State_Checked_Value::percent>(), 60); + EXPECT_EQ(product->render_use_state().percent.get(), 20); + product->publish(); + EXPECT_EQ(product->render_use_state().percent.get(), 60); +} +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); + EXPECT_EQ(product->get<&State_Checked_Value::percent>(), 20); +} +TEST(double_state_strategy_test, supports_atomic_wait_mutex) { + auto product = State_Wait_Product::Builder{State_Plain_Value{.count = 1, .name = "wait"}}.build(); + product->set<&State_Plain_Value::count>(2); + product->publish(); + EXPECT_EQ(product->render_use_state().count, 2); +} +TEST(double_state_strategy_test, passes_additional_builder_template_arguments) { + static_assert(std::same_as); + static_assert(std::same_as); + auto product = State_Tagged_Product::Builder{State_Plain_Value{.count = 3, .name = "tagged"}}.build(); + EXPECT_EQ(product->get<&State_Plain_Value::count>(), 3); +} +struct Double_State_Observer_Test_Time_Source { + std::uint64_t value{100}; + std::uint64_t now_ns() const noexcept { + return value; + } +}; +struct Double_State_Observer_Test_Data { + std::uint64_t event_count{}; + std::uint64_t publish_count{}; + int value{}; +}; +struct Double_State_Observer_Test_Recorder { + static constexpr bool enabled = true; + std::shared_ptr data{std::make_shared()}; + template + void observe(const Observation& observation) noexcept { + ++data->event_count; + data->publish_count = observation.publish_count; + data->value = observation.state.count; + } +}; +TEST(double_state_strategy_test, notifies_observer_for_cache_update_and_publish) { + using Observer = Observer_State; + using Strategy = Double_State_Strategy; + Double_State_Observer_Test_Recorder recorder; + Strategy strategy(State_Plain_Value{}, With_Observer(Observer(recorder, Double_State_Observer_Test_Time_Source{}))); + strategy.set<&State_Plain_Value::count>(8); + strategy.publish(); + EXPECT_EQ(recorder.data->event_count, 2); + EXPECT_EQ(recorder.data->publish_count, 1); + EXPECT_EQ(recorder.data->value, 8); +} diff --git a/Kernel/tests/renderive/state/State_Test_Types.hpp b/Kernel/tests/renderive/state/State_Test_Types.hpp new file mode 100644 index 0000000..4d73168 --- /dev/null +++ b/Kernel/tests/renderive/state/State_Test_Types.hpp @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include "renderive/base/property/Property.hpp" +#include "renderive/state/State_Strategy.hpp" +struct State_Plain_Value { + int count{}; + std::string name; +}; +struct State_Plain_Base {}; +using State_Plain_Strategy = Double_State_Strategy; +using State_Plain_Product = Attach_State_Builder; +struct State_Checked_Value { + struct Even_Validator { + void operator()(const int& value) const { + if (value % 2 != 0) { + throw std::invalid_argument("value must be even"); + } + } + }; + Range_Value percent{0}; + Validated_Value batch_size{0}; + int minimum{}; + int maximum{}; +}; +struct State_Checked_Base { + State_Checked_Base(int id, std::string name) : id(id), name(std::move(name)) {} + static void validate_state(const State_Checked_Value& state) { + if (state.minimum > state.maximum) { + throw std::invalid_argument("minimum must not exceed maximum"); + } + } + int id; + std::string name; +}; +using State_Checked_Strategy = Double_State_Strategy; +using State_Checked_Product = Attach_State_Builder; +using State_Wait_Product = Attach_State_Builder, State_Plain_Value>; +struct State_Builder_Tag {}; +struct State_Builder_Mode {}; +template Validator, class Tag_Type, class Mode_Type> +class State_Tagged_Builder : public Property_Builder { +public: + using Base = Property_Builder; + using Tag = Tag_Type; + using Mode = Mode_Type; + using Base::Base; +}; +using State_Tagged_Product = Attach_State_Builder; diff --git a/Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp b/Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp new file mode 100644 index 0000000..bac90a1 --- /dev/null +++ b/Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp @@ -0,0 +1,36 @@ +#include +#include +#include +#include "renderive/state/State_Strategy.hpp" +struct Triple_State_Test_Base {}; +struct Triple_State_Test_State { + int value{}; +}; +TEST(triple_state_strategy_test, publishes_then_acquires_render_state) { + Triple_State_Strategy strategy; + strategy.set<&Triple_State_Test_State::value>(7); + strategy.publish(); + EXPECT_EQ(strategy.render_use_state().value, 0); + EXPECT_EQ(strategy.acquire_render_state(), 1); + EXPECT_EQ(strategy.render_use_state().value, 7); +} +struct Multi_State_A_Base {}; +struct Multi_State_B_Base {}; +struct Multi_State_A { + int value{}; +}; +struct Multi_State_B { + int value{}; +}; +struct Multi_State_A_Strategy : Double_State_Strategy {}; +struct Multi_State_B_Strategy : Double_State_Strategy {}; +struct Multi_State_Product : Multi_State_A_Strategy, Multi_State_B_Strategy {}; +TEST(double_state_strategy_test, supports_named_base_access_in_multiple_inheritance) { + Multi_State_Product product; + product.Multi_State_A_Strategy::set<&Multi_State_A::value>(3); + product.Multi_State_B_Strategy::set<&Multi_State_B::value>(8); + product.Multi_State_A_Strategy::publish(); + product.Multi_State_B_Strategy::publish(); + EXPECT_EQ(product.Multi_State_A_Strategy::render_use_state().value, 3); + EXPECT_EQ(product.Multi_State_B_Strategy::render_use_state().value, 8); +} diff --git a/Kernel/tests/renderive/state/base/State_Strategy_Base_Test.cpp b/Kernel/tests/renderive/state/base/State_Strategy_Base_Test.cpp new file mode 100644 index 0000000..8c8b82e --- /dev/null +++ b/Kernel/tests/renderive/state/base/State_Strategy_Base_Test.cpp @@ -0,0 +1,10 @@ +#include +#include "../State_Test_Types.hpp" +TEST(state_strategy_base_test, publishes_through_virtual_interface) { + State_Plain_Strategy strategy; + State_Strategy_Base* base = &strategy; + strategy.set<&State_Plain_Value::count>(7); + base->publish(); + EXPECT_EQ(base->state_revision(), 1); + EXPECT_EQ(strategy.render_use_state().count, 7); +} diff --git a/Renderive.zip b/Renderive.zip new file mode 100644 index 0000000..dfe316d Binary files /dev/null and b/Renderive.zip differ diff --git a/main.cmake b/main.cmake index d670e08..5fe5840 100644 --- a/main.cmake +++ b/main.cmake @@ -35,10 +35,13 @@ else () ) endif () set(root_dir ${CMAKE_CURRENT_LIST_DIR}) -append_glob_source(kernel_srcs ${root_dir}/Kernel/src) +append_glob_source(kernel_srcs ${root_dir}/Kernel/src ${root_dir}/Kernel/tests) add_executable(Renderive_Kernel ${kernel_srcs} ) +target_include_directories(Renderive_Kernel PUBLIC + ${root_dir}/Kernel/src +) target_link_libraries(Renderive_Kernel PUBLIC ${renderive_private_libraries}) append_glob_source(srcs ${root_dir}/Core) add_library(Renderive_Core STATIC ${srcs}) diff --git a/radio/component/ProgressBar.cpp b/radio/component/ProgressBar.cpp new file mode 100644 index 0000000..55f31a7 --- /dev/null +++ b/radio/component/ProgressBar.cpp @@ -0,0 +1,99 @@ +#include "ProgressBar.h" +#include +#include +#include +#include +#include + +Progress_Bar::Progress_Bar(const std::vector& tick_list, QWidget *parent) : QWidget(parent), tick_list(tick_list) { + setAttribute(Qt::WA_OpaquePaintEvent, true); + timer = startTimer(30); +} + +double Progress_Bar::get_draw_width(double value) { + int n = tick_list.size(); + if (n < 2 || QWidget::width() <= 0) + return 0.0; + if (!std::isfinite(value)) + value = tick_list.front().value; + int i = 1; + for(; i < n - 1; ++i) { + double cur = tick_list[i].value; + if(value < cur || qFuzzyCompare(value, cur)) { + break; + } + } + int lower_offset = i - 1; + int upper_offset = i; + double lower = tick_list[lower_offset].value; + double upper = tick_list[upper_offset].value; + double w_step = (double)QWidget::width()/(double)(n-1); + double last_rate = upper == lower ? 0.0 : (value - lower)/(upper - lower); + last_rate = std::clamp(last_rate, 0.0, 1.0); + double draw_width = w_step * (last_rate + lower_offset); + return std::clamp(draw_width, 0.0, (double)QWidget::width()); +} +void Progress_Bar::setValue(double value) { + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, [this, value]() { + setValue(value); + }, Qt::QueuedConnection); + return; + } + this->value = value; + update(); +} +void Progress_Bar::paintEvent(QPaintEvent *event) { + Q_UNUSED(event) + QPainter painter(this); + painter.setPen(Qt::NoPen); + painter.fillRect(rect(), QApplication::style()->standardPalette().brush(QPalette::Window)); + painter.setPen(QPen(Qt::black)); + auto w = (double)QWidget::width(); + auto hh = (double)height()/2; + int n = tick_list.size(); + int tick_w = 1; + double draw_width = get_draw_width(value); + int fill_width = std::max(0, static_cast(draw_width) - tick_w - 4); + if (fill_width > 0) + painter.fillRect(0, 0, fill_width, height()/2, Qt::black); + int cur_x = 0; + int tick_step = std::max(1, tick_w + (int)tick_space_w); + while(cur_x + tick_w + 4 < draw_width){ + painter.fillRect(cur_x, 0, tick_w, height()/2, Qt::gray); + cur_x += tick_step; + } + int line_y = (int)hh + 2; + int marker_x = std::clamp(cur_x - (int)tick_space_w, 0, std::max(0, QWidget::width() - 4)); + painter.fillRect(marker_x, 0, 4, line_y, Qt::yellow); + painter.drawLine(0, line_y , (int)w, line_y); + QFont font; + font.setBold(true); + QFontMetrics fm(font); + for(int i = 0; i < n; ++i){ + int tick_x = (int)((double)(QWidget::width() * i)/(double)(n - 1)); + int text_w = fm.horizontalAdvance( tick_list[i].text); + int text_x; + if(i == n - 1) { + text_x = tick_x - text_w; + tick_x -=1; + } else if( i == 0) { + text_x = tick_x; + } else { + text_x = tick_x - text_w/2; + } + painter.setPen(Qt::black); + painter.drawLine(tick_x, line_y, tick_x, (line_y + 5)); + painter.setPen(tick_list[i].color); + painter.setFont(font); + painter.drawText(QRectF(text_x, line_y, text_w, line_y), Qt::AlignCenter, tick_list[i].text); + } +} + +void Progress_Bar::timerEvent(QTimerEvent *event) { + if(event->timerId() == timer) { + if(set_value) set_value(); + update(); + } + QObject::timerEvent(event); +} diff --git a/radio/component/ProgressBar.h b/radio/component/ProgressBar.h new file mode 100644 index 0000000..f487313 --- /dev/null +++ b/radio/component/ProgressBar.h @@ -0,0 +1,31 @@ +#ifndef Progress_Bar_H +#define Progress_Bar_H +#include +#include +#include "../Global.h" +struct Tick { + QString text; + QColor color; + double value = 0; + Tick(QString text, QColor color) : text(std::move(text)), color(std::move(color)) { + QString that = (this->text.startsWith('+') || this->text.startsWith('-') || this->text.startsWith('<') || this->text.startsWith('>')) ? this->text.mid(1) : this->text; + value = that.toDouble(); + } + Tick(QString text, QColor color, double value) : text(std::move(text)), color(std::move(color)), value(value) { + } +}; +class Progress_Bar : public QWidget { +public: + explicit Progress_Bar(const std::vector& tick_list, QWidget *parent = nullptr); + double tick_space_w = 1; + double value = 0; + int timer; + std::function set_value = nullptr; + double get_draw_width(double value); + void setValue(double value); +protected: + void paintEvent(QPaintEvent *event) override; + void timerEvent(QTimerEvent *event) override; + std::vector tick_list; +}; +#endif diff --git a/renderive_core_split_files.zip b/renderive_core_split_files.zip new file mode 100644 index 0000000..9081afa Binary files /dev/null and b/renderive_core_split_files.zip differ diff --git a/spectrum_power_at_read_lease_fix.zip b/spectrum_power_at_read_lease_fix.zip new file mode 100644 index 0000000..4cf07f1 Binary files /dev/null and b/spectrum_power_at_read_lease_fix.zip differ diff --git a/spectrum_power_at_triple_buffer_fix.zip b/spectrum_power_at_triple_buffer_fix.zip new file mode 100644 index 0000000..183ec22 Binary files /dev/null and b/spectrum_power_at_triple_buffer_fix.zip differ