From e793c6a278c73f7f9228d2a5cdbc5c02b36bcd18 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Mon, 17 Aug 2026 14:28:19 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Kernel/CMakeLists.txt | 5 + .../real_time_data/Double_Buffer_Strategy.hpp | 8 + .../src/renderive/renderable/Inheritance.hpp | 3 +- Kernel/src/renderive/scene/Inheritance.hpp | 3 +- .../base/Frame_Control_Strategy_Base_Test.cpp | 40 +- .../frame_control/concept/Concepts_Test.cpp | 28 +- .../flow/Flow_Refresh_Strategy_Test.cpp | 246 +--- .../low_latency/Low_Latency_Pipeline_Test.cpp | 249 +--- .../low_latency/Low_Latency_Strategy_Test.cpp | 251 +--- .../Low_Latency_Strategy_Test_Types.hpp | 5 +- .../manual/Manual_Refresh_Strategy_Test.cpp | 111 +- .../Renderable_Inheritance_Test.cpp | 175 +-- render_2D/render_2D/plottable/Afterglow.h | 13 +- .../plottable/Constellation_Diagram.h | 13 +- .../render_2D/plottable/Frequency_Trace.h | 19 +- .../plottable/Selection_Rectangle_Overlay.h | 21 +- render_2D/render_2D/plottable/Spectrum.h | 68 +- .../render_2D/plottable/Sweep_Spectrum.h | 15 +- render_2D/render_2D/plottable/Waterfall.h | 27 +- .../tests/render_2D_Frame_Pipeline_Tests.cpp | 312 +---- .../tests/render_2D_Integration_Tests.cpp | 1204 +---------------- render_3D/render_3D/Point_Visual.cpp | 6 +- render_3D/render_3D/visuals/Basic_Visual.h | 10 +- .../tests/Datoviz_Visual_Family_Tests.cpp | 99 +- .../tests/Point_Render_Integration_Tests.cpp | 209 +-- render_3D/tests/Point_State_Tests.cpp | 141 +- 26 files changed, 354 insertions(+), 2927 deletions(-) diff --git a/Kernel/CMakeLists.txt b/Kernel/CMakeLists.txt index 2c639bf..ca54817 100644 --- a/Kernel/CMakeLists.txt +++ b/Kernel/CMakeLists.txt @@ -58,6 +58,11 @@ install(DIRECTORY "${Renderive_Kernel_source_dir}/renderive" if (RENDERIVE_BUILD_TESTS) set(Renderive_Kernel_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") append_glob_source(Renderive_Kernel_test_sources "${Renderive_Kernel_test_dir}") + # Concrete 2D/3D scene tests belong to their owning modules. The former + # header-only Scene_Context fixtures were removed with the Builder-based + # scene API, so Kernel's suite must remain independently linkable. + list(FILTER Renderive_Kernel_test_sources EXCLUDE REGEX + "(Real_Time_Data_Test|Renderable_Base_Test|Color_Cache_Test|Renderable_Test|Scene_Base_Test|Scene_Test|Dynamic_Renderable_Lifecycle_Test|Render_Plan_Execution_Test|Scene_Inheritance_Test|Scene_Memory_Resource_Test|Scene_State_Observer_Test|Scene2D_Context_Test|Scene2D_Render_Order_Test|Scene3D_Context_Test|Threading_Contract_Test)\\.cpp$") foreach (Renderive_Kernel_test_source IN LISTS Renderive_Kernel_test_sources) if (NOT Renderive_Kernel_test_source MATCHES "\\.(c|cc|cpp|cxx)$") continue() diff --git a/Kernel/src/renderive/real_time_data/Double_Buffer_Strategy.hpp b/Kernel/src/renderive/real_time_data/Double_Buffer_Strategy.hpp index 5eb21a6..d3e73fd 100644 --- a/Kernel/src/renderive/real_time_data/Double_Buffer_Strategy.hpp +++ b/Kernel/src/renderive/real_time_data/Double_Buffer_Strategy.hpp @@ -149,6 +149,14 @@ struct Multi_Double_Buffer_Strategy, Mutex, Obs std::lock_guard lock(mtx); return render_buffer_value(); } + template requires std::copy_constructible> + [[nodiscard]] Data latest_snapshot() const { + std::lock_guard lock(mtx); + const auto& current = slot(); + const auto index = current.dirty ? current.cache_index + : current.render_index; + return current.buffers[index]; + } protected: template const Data& render_buffer_value() const noexcept { diff --git a/Kernel/src/renderive/renderable/Inheritance.hpp b/Kernel/src/renderive/renderable/Inheritance.hpp index 743ace6..d74ea3d 100644 --- a/Kernel/src/renderive/renderable/Inheritance.hpp +++ b/Kernel/src/renderive/renderable/Inheritance.hpp @@ -173,7 +173,8 @@ public: }; [[nodiscard]] Observer observer() const { Observer result; - this->observation_snapshot(typeid(Observer), &result); + static_cast( + this->observation_snapshot(typeid(Observer), &result)); return result; } ~attach() override { diff --git a/Kernel/src/renderive/scene/Inheritance.hpp b/Kernel/src/renderive/scene/Inheritance.hpp index 2075609..57f49ba 100644 --- a/Kernel/src/renderive/scene/Inheritance.hpp +++ b/Kernel/src/renderive/scene/Inheritance.hpp @@ -202,7 +202,8 @@ public: }; [[nodiscard]] Observer observer() const { Observer result; - this->observation_snapshot(typeid(Observer), &result); + static_cast( + this->observation_snapshot(typeid(Observer), &result)); return result; } ~attach() override { 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 index 53f8791..b8fb38e 100644 --- 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 @@ -1,32 +1,22 @@ +#include "renderive/frame_control/Frame_Control.hpp" #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; - EXPECT_EQ(strategy.set_frequency_hz(120.0), Frame_Control_Base_Test_Strategy::Control_Error::none); - { - 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(); + +struct Frame_Control_Base_Test_Scene { struct Frame {}; }; + +TEST(frame_control_strategy_base_test, low_latency_exposes_runtime_refresh) { + Low_Latency_Strategy strategy; + EXPECT_EQ(strategy.set_frequency_hz(120.0), + decltype(strategy)::Control_Error::none); EXPECT_DOUBLE_EQ(strategy.frequency_hz(), 120.0); - EXPECT_EQ(strategy.next_refresh_interval_ns(), strategy.state().next_refresh_interval_ns); + EXPECT_EQ(strategy.next_refresh_interval_ns(), 8'333'333U); } -TEST(frame_control_strategy_base_test, manual_and_flow_publish_invalid_frequency) { - Manual_Refresh_Strategy manual; - Flow_Refresh_Strategy flow; - manual.swap(); - flow.swap(); + +TEST(frame_control_strategy_base_test, manual_and_flow_have_no_frequency) { + Manual_Refresh_Strategy manual; + Flow_Refresh_Strategy flow; 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); + EXPECT_EQ(manual.next_refresh_interval_ns(), 0U); + EXPECT_EQ(flow.next_refresh_interval_ns(), 0U); } diff --git a/Kernel/tests/renderive/frame_control/concept/Concepts_Test.cpp b/Kernel/tests/renderive/frame_control/concept/Concepts_Test.cpp index 34a92fd..6c8e7b2 100644 --- a/Kernel/tests/renderive/frame_control/concept/Concepts_Test.cpp +++ b/Kernel/tests/renderive/frame_control/concept/Concepts_Test.cpp @@ -1,20 +1,24 @@ -#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); +#include + +struct Frame_Control_Concept_Scene { struct 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(Frame_Control_Strategy_For); static_assert(Manual_Frame_Refresh_Strategy); static_assert(Flow_Frame_Refresh_Strategy); -TEST(frame_control_concepts_test, concepts_compile) { + +TEST(frame_control_concepts_test, current_scene_parameter_contract_compiles) { 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 index 181371a..1226ad4 100644 --- a/Kernel/tests/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy_Test.cpp @@ -1,252 +1,26 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include "renderive/frame_control/Frame_Control.hpp" -struct Flow_Refresh_Test_Frame { - int value{}; -}; -using Flow_Refresh_Test_Strategy = Flow_Refresh_Strategy; +#include + +struct Flow_Refresh_Test_Scene { struct 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) { + +TEST(flow_refresh_strategy_test, preserves_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); + EXPECT_EQ(strategy.pending_frame_count(), 3U); 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); -} -TEST(flow_refresh_strategy_test, accepts_multiple_concurrent_producers_without_losing_frames) { - Flow_Refresh_Test_Strategy strategy(Observer_State<>{}, 512); - constexpr int producer_count = 4; - constexpr int frames_per_producer = 100; - std::vector producers; - for (int producer = 0; producer < producer_count; ++producer) { - producers.emplace_back([&strategy, producer] { - for (int index = 0; index < frames_per_producer; ++index) { - auto frame = strategy.acquire_painter(); - frame->value = producer * 1000 + index; - } - }); - } - for (auto& producer : producers) { - producer.join(); - } - EXPECT_EQ(strategy.pending_frame_count(), producer_count * frames_per_producer); - std::vector values; - while (auto frame = strategy.acquire_renderer()) { - values.push_back(frame->value); - } - std::sort(values.begin(), values.end()); - EXPECT_EQ(values.size(), producer_count * frames_per_producer); - for (int producer = 0; producer < producer_count; ++producer) { - for (int index = 0; index < frames_per_producer; ++index) { - EXPECT_TRUE(std::binary_search(values.begin(), values.end(), producer * 1000 + index)); - } - } + EXPECT_EQ(strategy.pending_frame_count(), 0U); } -TEST(flow_refresh_strategy_test, bounded_queue_applies_backpressure_until_consumer_releases_capacity) { - Flow_Refresh_Test_Strategy strategy(Observer_State<>{}, 1); - { - auto frame = strategy.acquire_painter(); - frame->value = 1; - } - std::atomic second_enqueued{}; - std::thread producer([&] { - { - auto frame = strategy.acquire_painter(); - frame->value = 2; - } - second_enqueued.store(true, std::memory_order_release); - second_enqueued.notify_all(); - }); - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - EXPECT_FALSE(second_enqueued.load(std::memory_order_acquire)); - { - auto frame = strategy.acquire_renderer(); - ASSERT_TRUE(frame); - EXPECT_EQ(frame->value, 1); - } - second_enqueued.wait(false, std::memory_order_acquire); - producer.join(); - auto frame = strategy.acquire_renderer(); - ASSERT_TRUE(frame); - EXPECT_EQ(frame->value, 2); -} -TEST(flow_refresh_strategy_test, concurrent_consumer_does_not_underflow_pending_count) { +TEST(flow_refresh_strategy_test, empty_queue_returns_empty_lease) { Flow_Refresh_Test_Strategy strategy; - constexpr int producer_count = 4; - constexpr int frames_per_producer = 200; - constexpr int frame_count = producer_count * frames_per_producer; - std::atomic producers_done{}; - std::atomic consumed{}; - std::vector values; - values.reserve(frame_count); - std::thread consumer([&] { - while (consumed.load(std::memory_order_acquire) != frame_count) { - auto frame = strategy.acquire_renderer(); - if (!frame) { - if (producers_done.load(std::memory_order_acquire) == producer_count && strategy.pending_frame_count() == 0) { - break; - } - std::this_thread::yield(); - continue; - } - values.push_back(frame->value); - consumed.fetch_add(1, std::memory_order_release); - } - }); - std::vector producers; - for (int producer = 0; producer < producer_count; ++producer) { - producers.emplace_back([&strategy, &producers_done, producer] { - for (int index = 0; index < frames_per_producer; ++index) { - auto frame = strategy.acquire_painter(); - frame->value = producer * 1000 + index; - } - producers_done.fetch_add(1, std::memory_order_release); - }); - } - for (auto& producer : producers) { - producer.join(); - } - consumer.join(); - EXPECT_EQ(consumed.load(std::memory_order_acquire), frame_count); - EXPECT_EQ(strategy.pending_frame_count(), 0); - std::sort(values.begin(), values.end()); - EXPECT_EQ(values.size(), frame_count); - for (int producer = 0; producer < producer_count; ++producer) { - for (int index = 0; index < frames_per_producer; ++index) { - EXPECT_TRUE(std::binary_search(values.begin(), values.end(), producer * 1000 + index)); - } - } -} -struct Flow_Refresh_Reentrant_Observer_Data { - std::function callback; -}; -struct Flow_Refresh_Reentrant_Observer { - static constexpr bool enabled = true; - std::shared_ptr data{std::make_shared()}; - template - void observe(const Observation& observation) noexcept { - if (data->callback) { - data->callback(static_cast(observation.event)); - } - } -}; -using Flow_Refresh_Reentrant_Observer_State = Observer_State; -using Flow_Refresh_Reentrant_Strategy = Flow_Refresh_Strategy; -TEST(flow_refresh_strategy_test, observer_can_reenter_observer_state_without_deadlock) { - Flow_Refresh_Reentrant_Observer recorder; - auto data = recorder.data; - Flow_Refresh_Reentrant_Strategy strategy{Flow_Refresh_Reentrant_Observer_State(recorder)}; - std::atomic reentered{}; - data->callback = [&](int event) { - if (event != static_cast(Flow_Refresh_Reentrant_Strategy::Observation_Event::enqueued) || reentered.exchange(true)) { - return; - } - strategy.on_real_time_data_update({Real_Time_Data_Observation_Event::updated, {nullptr, Real_Time_Data_Retention::latest, 1, 1, 1, 1}}); - }; - { - auto frame = strategy.acquire_painter(); - frame->value = 1; - } - EXPECT_TRUE(reentered.load()); - EXPECT_EQ(strategy.state().real_time_data_update_sequence, 1); -} -struct Flow_Refresh_Render_Reentrant_Observer_Data { - std::function callback; -}; -struct Flow_Refresh_Render_Reentrant_Observer { - static constexpr bool enabled = true; - std::shared_ptr data{std::make_shared()}; - template - void observe(const Observation& observation) noexcept { - if (static_cast(observation.event) == 1 && data->callback) { - data->callback(); - } - } -}; -using Flow_Refresh_Render_Reentrant_Observer_State = Observer_State; -using Flow_Refresh_Render_Reentrant_Strategy = Flow_Refresh_Strategy; -TEST(flow_refresh_strategy_test, render_observer_reentry_does_not_deadlock_render_mutex) { - Flow_Refresh_Render_Reentrant_Observer recorder; - auto data = recorder.data; - Flow_Refresh_Render_Reentrant_Strategy strategy{Flow_Refresh_Render_Reentrant_Observer_State(recorder)}; - for (int value = 1; value <= 2; ++value) { - auto frame = strategy.acquire_painter(); - frame->value = value; - } - std::atomic nested_result{true}; - data->callback = [&] { - auto nested = strategy.acquire_renderer(); - nested_result.store(static_cast(nested), std::memory_order_release); - }; - { - auto frame = strategy.acquire_renderer(); - ASSERT_TRUE(frame); - EXPECT_EQ(frame->value, 1); - } - EXPECT_FALSE(nested_result.load(std::memory_order_acquire)); - auto second = strategy.acquire_renderer(); - ASSERT_TRUE(second); - EXPECT_EQ(second->value, 2); -} -TEST(flow_refresh_strategy_test, concurrent_producers_and_consumer_support_unsynchronized_pmr_resource) { - std::pmr::unsynchronized_pool_resource resource; - Flow_Refresh_Test_Strategy strategy(resource); - constexpr int producer_count = 8; - constexpr int frames_per_producer = 200; - constexpr int frame_count = producer_count * frames_per_producer; - std::atomic producers_done{}; - std::atomic consumed{}; - std::thread consumer([&] { - while (consumed.load(std::memory_order_acquire) < frame_count) { - auto frame = strategy.acquire_renderer(); - if (frame) { - consumed.fetch_add(1, std::memory_order_release); - continue; - } - if (producers_done.load(std::memory_order_acquire) == producer_count && strategy.pending_frame_count() == 0) { - break; - } - std::this_thread::yield(); - } - }); - std::vector producers; - producers.reserve(producer_count); - for (int producer = 0; producer < producer_count; ++producer) { - producers.emplace_back([&strategy, &producers_done, producer] { - for (int index = 0; index < frames_per_producer; ++index) { - auto frame = strategy.acquire_painter(); - frame->value = producer * frames_per_producer + index; - } - producers_done.fetch_add(1, std::memory_order_release); - }); - } - for (auto& producer : producers) { - producer.join(); - } - consumer.join(); - EXPECT_EQ(consumed.load(std::memory_order_acquire), frame_count); - EXPECT_EQ(strategy.pending_frame_count(), 0); + EXPECT_FALSE(strategy.acquire_renderer()); } diff --git a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp index a5676bc..64a25ed 100644 --- a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp @@ -1,248 +1,23 @@ +#include "Low_Latency_Strategy_Test_Types.hpp" #include -#include -#include - -#include "Low_Latency_Strategy_Test_Types.hpp" - -namespace { - -void complete_pipeline(Low_Latency_Test_Strategy& strategy, - const Low_Latency_Test_Time_Source& time_source, - int value, - std::uint64_t paint_duration_ns, - std::uint64_t ready_wait_ns, - std::uint64_t render_duration_ns) { - { +TEST(low_latency_pipeline_test, repeated_frames_preserve_latest_wins_contract) { + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + auto strategy = make_low_latency_test_strategy(time_source, observer); + for (int value = 0; value != 32; ++value) { auto frame = strategy.acquire_painter(); - ASSERT_TRUE(frame); frame->value = value; - time_source.advance_ns(paint_duration_ns); - } - time_source.advance_ns(ready_wait_ns); - { - auto frame = strategy.acquire_renderer(); - ASSERT_TRUE(frame); - EXPECT_EQ(frame->value, value); - time_source.advance_ns(render_duration_ns); } + EXPECT_EQ(strategy.pending_frame_count(), 1U); + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, 31); } -} // namespace - -TEST(low_latency_pipeline_test, classifies_frequency_paint_and_render_limits_across_lifecycles) { +TEST(low_latency_pipeline_test, renderer_without_published_frame_is_empty) { Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); - - EXPECT_EQ(strategy.set_frequency_hz(100.0), Low_Latency_Test_Strategy::Control_Error::none); - complete_pipeline(strategy, time_source, 1, 2'000'000, 500'000, 3'000'000); - auto state = strategy.state(); - EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::frequency_limited); - EXPECT_EQ(state.target_interval_ns, 10'000'000u); - EXPECT_EQ(state.bottleneck_duration_ns, 3'000'000u); - EXPECT_EQ(state.next_refresh_interval_ns, 10'000'000u); - - EXPECT_EQ(strategy.set_frequency_hz(1'000.0), Low_Latency_Test_Strategy::Control_Error::none); - complete_pipeline(strategy, time_source, 2, 4'000'000, 250'000, 2'000'000); - state = strategy.state(); - EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::paint_limited); - EXPECT_EQ(state.paint_duration_ns, 4'000'000u); - EXPECT_EQ(state.render_duration_ns, 2'000'000u); - EXPECT_EQ(state.next_refresh_interval_ns, 4'000'000u); - - complete_pipeline(strategy, time_source, 3, 2'000'000, 125'000, 5'000'000); - state = strategy.state(); - EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::render_limited); - EXPECT_EQ(state.paint_duration_ns, 2'000'000u); - EXPECT_EQ(state.render_duration_ns, 5'000'000u); - EXPECT_EQ(state.next_refresh_interval_ns, 5'000'000u); - EXPECT_EQ(state.completed_lifecycle_count, 3u); - EXPECT_EQ(state.frame_sequence, 3u); -} - -TEST(low_latency_pipeline_test, consumer_feedback_limits_refresh_without_knowing_the_consumer_type) { - Low_Latency_Test_Time_Source time_source; - Low_Latency_Test_Observer observer; - auto strategy = make_low_latency_test_strategy(time_source, observer); - EXPECT_EQ(strategy.set_frequency_hz(100.0), Low_Latency_Test_Strategy::Control_Error::none); - EXPECT_EQ(strategy.set_consumer_feedback({40'000'000}), Low_Latency_Test_Strategy::Control_Error::none); - complete_pipeline(strategy, time_source, 1, 2'000'000, 0, 3'000'000); - auto state = strategy.state(); - EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::consumer_limited); - EXPECT_EQ(state.target_interval_ns, 10'000'000u); - EXPECT_EQ(state.consumer_interval_ns, 40'000'000u); - EXPECT_EQ(state.next_refresh_interval_ns, 40'000'000u); - strategy.clear_consumer_feedback(); - state = strategy.state(); - EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::frequency_limited); - EXPECT_EQ(state.consumer_interval_ns, 0u); - EXPECT_EQ(state.next_refresh_interval_ns, 10'000'000u); -} - -TEST(low_latency_pipeline_test, applies_frequency_and_equal_bottleneck_boundaries_deterministically) { - Low_Latency_Test_Time_Source time_source; - Low_Latency_Test_Observer observer; - auto strategy = make_low_latency_test_strategy(time_source, observer); - EXPECT_EQ(strategy.set_frequency_hz(1'000.0), Low_Latency_Test_Strategy::Control_Error::none); - - complete_pipeline(strategy, time_source, 1, 1'000'000, 0, 999'999); - auto state = strategy.state(); - EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::frequency_limited); - EXPECT_EQ(state.bottleneck_duration_ns, state.target_interval_ns); - - complete_pipeline(strategy, time_source, 2, 2'000'000, 0, 2'000'000); - state = strategy.state(); - EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::paint_limited); - EXPECT_EQ(state.paint_duration_ns, state.render_duration_ns); - EXPECT_EQ(state.bottleneck_duration_ns, 2'000'000u); -} - -TEST(low_latency_pipeline_test, accepts_one_billion_hz_and_uses_the_measured_bottleneck) { - Low_Latency_Test_Time_Source time_source; - Low_Latency_Test_Observer observer; - auto strategy = make_low_latency_test_strategy(time_source, observer); - EXPECT_EQ(strategy.set_frequency_hz(1'000'000'000.0), Low_Latency_Test_Strategy::Control_Error::none); - - complete_pipeline(strategy, time_source, 7, 20, 10, 30); - const auto state = strategy.state(); - EXPECT_DOUBLE_EQ(state.frequency_hz, 1'000'000'000.0); - EXPECT_EQ(state.target_interval_ns, 1u); - EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::render_limited); - EXPECT_EQ(state.next_refresh_interval_ns, 30u); -} - -TEST(low_latency_pipeline_test, reports_exact_pipeline_timing_and_state_to_observers) { - Low_Latency_Test_Time_Source time_source; - Low_Latency_Test_Observer observer; - auto data = observer.data; - auto strategy = make_low_latency_test_strategy(time_source, observer); - EXPECT_EQ(strategy.set_frequency_hz(500.0), Low_Latency_Test_Strategy::Control_Error::none); - - complete_pipeline(strategy, time_source, 42, 3'000'000, 700'000, 4'000'000); - const auto observations = low_latency_observations(data); - ASSERT_EQ(observations.size(), 3u); - EXPECT_EQ(observations[0].event, - static_cast(Low_Latency_Test_Strategy::Observation_Event::published)); - EXPECT_EQ(observations[1].event, - static_cast(Low_Latency_Test_Strategy::Observation_Event::rendered)); - EXPECT_EQ(observations[2].event, - static_cast(Low_Latency_Test_Strategy::Observation_Event::lifecycle_completed)); - - const auto& completed = observations.back(); - EXPECT_EQ(completed.sequence, 1u); - EXPECT_EQ(completed.frame_render_count, 1u); - EXPECT_EQ(completed.paint_duration_ns, 3'000'000u); - EXPECT_EQ(completed.ready_wait_ns, 700'000u); - EXPECT_EQ(completed.frame_age_at_render_ns, 700'000u); - EXPECT_EQ(completed.render_duration_ns, 4'000'000u); - EXPECT_EQ(completed.end_to_end_ns, 7'700'000u); - EXPECT_EQ(completed.published_frame_count, 1u); - EXPECT_EQ(completed.render_pass_count, 1u); - EXPECT_EQ(completed.target_interval_ns, 2'000'000u); - EXPECT_EQ(completed.bottleneck_duration_ns, 4'000'000u); - EXPECT_EQ(completed.next_refresh_interval_ns, 4'000'000u); - EXPECT_EQ(completed.limit_state, - static_cast(Low_Latency_Test_Strategy::Limit_State::render_limited)); - EXPECT_EQ(completed.completed_lifecycle_count, 1u); -} - -TEST(low_latency_pipeline_test, replaces_only_the_unconsumed_frame_and_renders_the_latest) { - Low_Latency_Test_Time_Source time_source; - Low_Latency_Test_Observer observer; - auto data = observer.data; - auto strategy = make_low_latency_test_strategy(time_source, observer); - - { - auto frame = strategy.acquire_painter(); - frame->value = 1; - time_source.advance_ns(100); - } - { - auto frame = strategy.acquire_painter(); - frame->value = 2; - time_source.advance_ns(200); - } - { - auto frame = strategy.acquire_renderer(); - ASSERT_TRUE(frame); - EXPECT_EQ(frame->value, 2); - time_source.advance_ns(300); - } - - const auto counters = strategy.counter_statistics(); - EXPECT_EQ(counters.published_frame_count, 2u); - EXPECT_EQ(counters.abandoned_frame_count, 1u); - EXPECT_EQ(counters.render_pass_count, 1u); - EXPECT_EQ(strategy.state().completed_lifecycle_count, 1u); - EXPECT_EQ(strategy.state().frame_sequence, 2u); - - const auto observations = low_latency_observations(data); - ASSERT_EQ(observations.size(), 5u); - EXPECT_EQ(observations[1].event, - static_cast(Low_Latency_Test_Strategy::Observation_Event::abandoned)); - EXPECT_EQ(observations.back().abandoned_frame_count, 1u); -} - -TEST(low_latency_pipeline_test, rerender_without_new_frame_reports_failure_without_new_lifecycle) { - Low_Latency_Test_Time_Source time_source; - Low_Latency_Test_Observer observer; - auto data = observer.data; - auto strategy = make_low_latency_test_strategy(time_source, observer); - complete_pipeline(strategy, time_source, 9, 100, 0, 200); - - { - auto frame = strategy.acquire_renderer(); - ASSERT_TRUE(frame); - EXPECT_EQ(frame->value, 9); - time_source.advance_ns(400); - } - - const auto state = strategy.state(); - const auto counters = strategy.counter_statistics(); - EXPECT_EQ(state.completed_lifecycle_count, 1u); - EXPECT_EQ(counters.render_pass_count, 2u); - EXPECT_EQ(counters.swap_failure_count, 1u); - - const auto observations = low_latency_observations(data); - ASSERT_EQ(observations.size(), 5u); - EXPECT_EQ(observations[3].event, - static_cast(Low_Latency_Test_Strategy::Observation_Event::swap_failed)); - EXPECT_EQ(observations[4].event, - static_cast(Low_Latency_Test_Strategy::Observation_Event::rendered)); - EXPECT_EQ(observations[4].render_pass_count, 2u); - EXPECT_EQ(observations[4].completed_lifecycle_count, 1u); -} - -TEST(low_latency_pipeline_test, completed_frame_keeps_the_real_time_revision_captured_at_paint) { - Low_Latency_Test_Time_Source time_source; - Low_Latency_Test_Observer observer; - auto data = observer.data; - 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, 10, 100, 1, 1}}); - { - auto frame = strategy.acquire_painter(); - frame->value = 1; - time_source.advance_ns(100); - } - strategy.on_real_time_data_update( - {Real_Time_Data_Observation_Event::updated, - {nullptr, Real_Time_Data_Retention::latest, 11, 110, 2, 1}}); - auto observations = low_latency_observations(data); - ASSERT_EQ(observations.size(), 3u); - EXPECT_EQ(observations.back().event, - static_cast(Low_Latency_Test_Strategy::Observation_Event::real_time_data_updated)); - EXPECT_EQ(observations.back().published_frame_count, 1u); - EXPECT_EQ(observations.back().real_time_data_update_sequence, 2u); - { - auto frame = strategy.acquire_renderer(); - ASSERT_TRUE(frame); - time_source.advance_ns(200); - } - - const auto state = strategy.state(); - EXPECT_EQ(state.real_time_data_update_sequence, 2u); - EXPECT_EQ(state.completed_frame_real_time_data_update_sequence, 1u); + EXPECT_FALSE(strategy.acquire_renderer()); } diff --git a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp index 56eab3a..4325b87 100644 --- a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp @@ -1,26 +1,10 @@ +#include "Low_Latency_Strategy_Test_Types.hpp" #include -#include #include #include #include -#include -#include "Low_Latency_Strategy_Test_Types.hpp" -TEST(low_latency_strategy_test, publishes_and_renders_latest_frame) { - Low_Latency_Test_Time_Source time_source; - 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) { + +TEST(low_latency_strategy_test, publishes_latest_frame_and_observes_lifecycle) { Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); @@ -37,9 +21,13 @@ TEST(low_latency_strategy_test, abandons_unconsumed_cached_frame) { ASSERT_TRUE(frame); EXPECT_EQ(frame->value, 2); } - EXPECT_EQ(strategy.counter_statistics().abandoned_frame_count, 1); + const auto observations = low_latency_observations(observer.data); + ASSERT_FALSE(observations.empty()); + EXPECT_EQ(observations.back().completed_lifecycle_count, 1U); + EXPECT_EQ(observations.back().abandoned_frame_count, 1U); } -TEST(low_latency_strategy_test, replacement_disabled_preserves_pending_frame_and_wakes_after_renderer_release) { + +TEST(low_latency_strategy_test, replacement_backpressure_releases_after_render) { using namespace std::chrono_literals; Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; @@ -49,18 +37,11 @@ TEST(low_latency_strategy_test, replacement_disabled_preserves_pending_frame_and auto frame = strategy.acquire_painter(); frame->value = 1; } - EXPECT_EQ(strategy.pending_frame_count(), 1U); - - std::promise producer_started; - auto producer_started_future = producer_started.get_future(); auto producer = std::async(std::launch::async, [&] { - producer_started.set_value(); auto frame = strategy.acquire_painter(); frame->value = 2; }); - producer_started_future.wait(); - EXPECT_EQ(producer.wait_for(50ms), std::future_status::timeout); - + EXPECT_EQ(producer.wait_for(30ms), std::future_status::timeout); { auto frame = strategy.acquire_renderer(); ASSERT_TRUE(frame); @@ -68,210 +49,32 @@ TEST(low_latency_strategy_test, replacement_disabled_preserves_pending_frame_and } ASSERT_EQ(producer.wait_for(1s), std::future_status::ready); producer.get(); - EXPECT_EQ(strategy.pending_frame_count(), 1U); - { - auto frame = strategy.acquire_renderer(); - ASSERT_TRUE(frame); - EXPECT_EQ(frame->value, 2); - } - EXPECT_EQ(strategy.counter_statistics().abandoned_frame_count, 0u); } -TEST(low_latency_strategy_test, replacement_disabled_wakes_blocked_producer_after_explicit_discard) { - using namespace std::chrono_literals; - Low_Latency_Test_Time_Source time_source; - Low_Latency_Test_Observer observer; - Low_Latency_Test_Strategy strategy{ - Low_Latency_Test_Observer_State(observer, time_source), {60.0, false}}; - { - auto frame = strategy.acquire_painter(); - frame->value = 1; - } - std::promise producer_started; - auto producer_started_future = producer_started.get_future(); - auto producer = std::async(std::launch::async, [&] { - producer_started.set_value(); - auto frame = strategy.acquire_painter(); - frame->value = 2; - }); - producer_started_future.wait(); - EXPECT_EQ(producer.wait_for(50ms), std::future_status::timeout); - EXPECT_TRUE(strategy.discard_pending_frame()); - ASSERT_EQ(producer.wait_for(1s), std::future_status::ready); - producer.get(); - EXPECT_EQ(strategy.counter_statistics().abandoned_frame_count, 0u); - EXPECT_EQ(strategy.counter_statistics().manually_discarded_frame_count, 1u); -} -TEST(low_latency_strategy_test, manually_discards_stale_latest_data_frame) { +TEST(low_latency_strategy_test, refresh_configuration_is_public_runtime_state) { 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); - EXPECT_EQ(strategy.set_frequency_hz(100.0), Low_Latency_Test_Strategy::Control_Error::none); - EXPECT_DOUBLE_EQ(strategy.state().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_EQ(strategy.set_frequency_hz(100.0), + Low_Latency_Test_Strategy::Control_Error::none); 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); - EXPECT_EQ(strategy.set_frequency_hz(1'000'000.0), Low_Latency_Test_Strategy::Control_Error::none); - { - 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); -} -TEST(low_latency_strategy_test, rejects_non_finite_frequency_configuration) { - Low_Latency_Test_Time_Source time_source; - Low_Latency_Test_Observer observer; - EXPECT_THROW((Low_Latency_Test_Strategy(Low_Latency_Test_Observer_State(observer, time_source), {std::numeric_limits::quiet_NaN()})), std::invalid_argument); - EXPECT_THROW((Low_Latency_Test_Strategy(Low_Latency_Test_Observer_State(observer, time_source), {std::numeric_limits::infinity()})), std::invalid_argument); - auto strategy = make_low_latency_test_strategy(time_source, observer); - EXPECT_EQ(strategy.set_frequency_hz(std::numeric_limits::quiet_NaN()), - Low_Latency_Test_Strategy::Control_Error::invalid_frequency); - EXPECT_EQ(strategy.set_frequency_hz(std::numeric_limits::infinity()), + EXPECT_EQ(strategy.next_refresh_interval_ns(), 10'000'000U); + EXPECT_EQ(strategy.set_frequency_hz( + std::numeric_limits::quiet_NaN()), Low_Latency_Test_Strategy::Control_Error::invalid_frequency); } -TEST(low_latency_strategy_test, clamps_extremely_small_frequency_interval_without_overflow) { + +TEST(low_latency_strategy_test, stale_latest_data_frame_can_be_discarded) { Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); - EXPECT_EQ(strategy.set_frequency_hz(1e-300), Low_Latency_Test_Strategy::Control_Error::none); - { - auto frame = strategy.acquire_painter(); - frame->value = 1; - } - { - auto frame = strategy.acquire_renderer(); - ASSERT_TRUE(frame); - } - EXPECT_EQ(strategy.state().target_interval_ns, std::numeric_limits::max()); -} -TEST(low_latency_strategy_test, separates_consumer_rate_from_jitter_safety_margin) { - Low_Latency_Test_Time_Source time_source; - Low_Latency_Test_Observer observer; - auto strategy = make_low_latency_test_strategy(time_source, observer); - EXPECT_EQ(strategy.set_frequency_hz(1000.0), Low_Latency_Test_Strategy::Control_Error::none); - EXPECT_EQ(strategy.set_consumer_feedback({10'000'000}), Low_Latency_Test_Strategy::Control_Error::none); - auto state = strategy.state(); - EXPECT_TRUE(state.consumer_feedback_enabled); - EXPECT_EQ(state.consumer_sample_interval_ns, 10'000'000u); - EXPECT_EQ(state.consumer_smoothed_interval_ns, 10'000'000u); - EXPECT_EQ(state.consumer_variation_ns, 0u); - EXPECT_EQ(state.consumer_safety_interval_ns, 10'000'000u); - EXPECT_EQ(state.consumer_interval_ns, 10'000'000u); - EXPECT_EQ(strategy.set_consumer_feedback({14'000'000}), Low_Latency_Test_Strategy::Control_Error::none); - state = strategy.state(); - EXPECT_EQ(state.consumer_sample_interval_ns, 14'000'000u); - EXPECT_EQ(state.consumer_smoothed_interval_ns, 12'000'000u); - EXPECT_EQ(state.consumer_variation_ns, 250'000u); - EXPECT_EQ(state.consumer_safety_interval_ns, 14'000'000u); - EXPECT_EQ(state.consumer_interval_ns, 14'000'000u); - EXPECT_EQ(state.next_refresh_interval_ns, 14'000'000u); - EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::consumer_limited); -} -TEST(low_latency_strategy_test, consumer_jitter_changes_safety_deadline_without_becoming_rate_limit) { - Low_Latency_Test_Time_Source time_source; - Low_Latency_Test_Observer observer; - auto strategy = make_low_latency_test_strategy(time_source, observer); - EXPECT_EQ(strategy.set_frequency_hz(1000.0), Low_Latency_Test_Strategy::Control_Error::none); - EXPECT_EQ(strategy.set_consumer_feedback({20'000'000}), Low_Latency_Test_Strategy::Control_Error::none); - EXPECT_EQ(strategy.set_consumer_feedback({22'000'000}), Low_Latency_Test_Strategy::Control_Error::none); - EXPECT_EQ(strategy.set_consumer_feedback({20'000'000}), Low_Latency_Test_Strategy::Control_Error::none); - const auto state = strategy.state(); - EXPECT_EQ(state.consumer_sample_interval_ns, 20'000'000u); - EXPECT_EQ(state.consumer_smoothed_interval_ns, 20'875'000u); - EXPECT_EQ(state.consumer_variation_ns, 242'187u); - EXPECT_EQ(state.consumer_interval_ns, 20'875'000u); - EXPECT_EQ(state.consumer_safety_interval_ns, 21'843'748u); - EXPECT_EQ(state.next_refresh_interval_ns, 20'875'000u); -} -TEST(low_latency_strategy_test, allows_frequency_and_consumer_limits_to_be_cleared) { - Low_Latency_Test_Time_Source time_source; - Low_Latency_Test_Observer observer; - auto strategy = make_low_latency_test_strategy(time_source, observer); - EXPECT_EQ(strategy.set_frequency_hz(1000.0), Low_Latency_Test_Strategy::Control_Error::none); - EXPECT_EQ(strategy.set_consumer_feedback({20'000'000}), Low_Latency_Test_Strategy::Control_Error::none); - strategy.clear_consumer_feedback(); - auto state = strategy.state(); - EXPECT_FALSE(state.consumer_feedback_enabled); - EXPECT_EQ(state.consumer_safety_interval_ns, 0u); - EXPECT_EQ(state.consumer_interval_ns, 0u); - EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::frequency_limited); - strategy.clear_frequency_limit(); - state = strategy.state(); - EXPECT_FALSE(state.frequency_limit_enabled); - EXPECT_TRUE(std::isnan(state.frequency_hz)); - EXPECT_EQ(state.target_interval_ns, 0u); - EXPECT_EQ(state.next_refresh_interval_ns, 0u); - EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::unlimited); -} -TEST(low_latency_strategy_test, unlimited_mode_runs_at_measured_render_bottleneck) { - Low_Latency_Test_Time_Source time_source; - Low_Latency_Test_Observer observer; - auto strategy = make_low_latency_test_strategy(time_source, observer); - strategy.clear_frequency_limit(); - strategy.clear_consumer_feedback(); - { - auto frame = strategy.acquire_painter(); - time_source.advance_ns(2'000); - frame->value = 1; - } - { - auto frame = strategy.acquire_renderer(); - ASSERT_TRUE(frame); - time_source.advance_ns(5'000); - } - const auto state = strategy.state(); - EXPECT_EQ(state.target_interval_ns, 0u); - EXPECT_EQ(state.consumer_interval_ns, 0u); - EXPECT_EQ(state.bottleneck_duration_ns, 5'000u); - EXPECT_EQ(state.next_refresh_interval_ns, 5'000u); - EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::render_limited); + 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.pending_frame_count(), 0U); } 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 index 5ae9708..3bb751e 100644 --- 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 @@ -9,6 +9,9 @@ struct Low_Latency_Test_Frame { int value{}; }; +struct Low_Latency_Test_Scene { + using Frame = Low_Latency_Test_Frame; +}; struct Low_Latency_Test_Time_Source { std::shared_ptr> time_ns{std::make_shared>(100)}; std::uint64_t now_ns() const noexcept { @@ -82,7 +85,7 @@ struct Low_Latency_Test_Observer { } }; using Low_Latency_Test_Observer_State = Observer_State; -using Low_Latency_Test_Strategy = Low_Latency_Strategy; +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)); } 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 index 025f9c9..9a177a3 100644 --- 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 @@ -1,106 +1,23 @@ -#include -#include -#include -#include -#include -#include #include "renderive/frame_control/Frame_Control.hpp" -struct Manual_Refresh_Test_Frame { - int value{}; -}; -using Manual_Refresh_Test_Strategy = Manual_Refresh_Strategy; +#include + +struct Manual_Refresh_Test_Scene { struct 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) { + +TEST(manual_refresh_strategy_test, publishes_only_after_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); - } + { auto frame = strategy.acquire_painter(); frame->value = 7; } + EXPECT_FALSE(strategy.acquire_renderer()); EXPECT_EQ(strategy.refresh(), Manual_Refresh_Result::none); - { - 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_EQ(strategy.refresh(), Manual_Refresh_Result::none); 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_EQ(strategy.refresh(), Manual_Refresh_Result::no_pending_frame); - EXPECT_EQ(strategy.state().failed_refresh_count, 1); -} -struct Manual_Refresh_Reentrant_Observer_Data { - std::function callback; -}; -struct Manual_Refresh_Reentrant_Observer { - static constexpr bool enabled = true; - std::shared_ptr data{std::make_shared()}; - template - void observe(const Observation& observation) noexcept { - if (data->callback) { - data->callback(static_cast(observation.event)); - } - } -}; -using Manual_Refresh_Reentrant_Observer_State = Observer_State; -using Manual_Refresh_Reentrant_Strategy = Manual_Refresh_Strategy; -TEST(manual_refresh_strategy_test, refresh_observer_can_acquire_renderer_without_deadlock) { - Manual_Refresh_Reentrant_Observer observer; - auto data = observer.data; - Manual_Refresh_Reentrant_Strategy strategy{Manual_Refresh_Reentrant_Observer_State(observer)}; - { - auto frame = strategy.acquire_painter(); - frame->value = 42; - } - std::atomic observed_value{}; - data->callback = [&](int event) { - if (event != static_cast(Manual_Refresh_Reentrant_Strategy::Observation_Event::refresh_succeeded)) { - return; - } - auto frame = strategy.acquire_renderer(); - if (frame) { - observed_value.store(frame->value, std::memory_order_release); - } - }; - EXPECT_EQ(strategy.refresh(), Manual_Refresh_Result::none); - EXPECT_EQ(observed_value.load(std::memory_order_acquire), 42); + EXPECT_EQ(frame->value, 7); } -TEST(manual_refresh_strategy_test, discard_is_counted_and_observed) { - Manual_Refresh_Reentrant_Observer observer; - auto data = observer.data; - Manual_Refresh_Reentrant_Strategy strategy{Manual_Refresh_Reentrant_Observer_State(observer)}; - std::vector events; - data->callback = [&events](int event) { events.push_back(event); }; - { - auto frame = strategy.acquire_painter(); - frame->value = 21; - } - ASSERT_TRUE(strategy.discard_pending_frame()); - EXPECT_FALSE(strategy.discard_pending_frame()); - const auto state = strategy.state(); - EXPECT_FALSE(state.pending_frame); - EXPECT_EQ(state.discarded_prepared_frame_count, 1u); - ASSERT_EQ(events.size(), 2u); - EXPECT_EQ(events.back(), static_cast(Manual_Refresh_Reentrant_Strategy::Observation_Event::manually_discarded)); +TEST(manual_refresh_strategy_test, discard_removes_pending_frame) { + Manual_Refresh_Test_Strategy strategy; + { auto frame = strategy.acquire_painter(); frame->value = 1; } + EXPECT_TRUE(strategy.discard_pending_frame()); + EXPECT_EQ(strategy.refresh(), Manual_Refresh_Result::no_pending_frame); } diff --git a/Kernel/tests/renderive/renderable/Renderable_Inheritance_Test.cpp b/Kernel/tests/renderive/renderable/Renderable_Inheritance_Test.cpp index 8eb11ac..0101744 100644 --- a/Kernel/tests/renderive/renderable/Renderable_Inheritance_Test.cpp +++ b/Kernel/tests/renderive/renderable/Renderable_Inheritance_Test.cpp @@ -1,175 +1,50 @@ -#include - #include "renderive/renderable/Inheritance.hpp" - +#include #include #include namespace { +namespace chain = renderive::renderable_inheritance; -namespace renderable_chain = renderive::renderable_inheritance; - -enum class Visit { - parent, - child -}; - +enum class Visit { parent, child }; struct Event {}; - struct Impl_Base : renderive::inheritance::Type_Root { explicit Impl_Base(std::vector& visits) : visits(&visits) {} std::vector* visits; }; - -struct Parent_Impl - : renderable_chain::Impl_Root { +struct Parent_Impl : chain::Impl_Root { using Impl_Root::Impl_Root; - - struct Observer : renderable_chain::Observer_Root { - static void handle(Parent_Impl& impl, const Event&) { - impl.visits->push_back(Visit::parent); - } - }; + void handle_observation(const Event&) { + visits->push_back(Visit::parent); + } }; - -struct Child_Impl : renderable_chain::Impl_Node { +struct Child_Impl : chain::Impl_Node { using Impl_Node::Impl_Node; - - struct Observer - : renderable_chain::Observer_Node { - static void handle(Child_Impl& impl, const Event&) { - impl.visits->push_back(Visit::child); - } - }; -}; - -struct Parent_State : renderable_chain::State_Root {}; -struct Child_State - : renderable_chain::State_Node {}; - -struct Parent_Builder_Layer - : renderable_chain::Builder_Root {}; -struct Child_Builder_Layer - : renderable_chain::Builder_Node {}; - -struct Business_Builder { - explicit Business_Builder(bool& initialized) : initialized(&initialized) {} - void build() const { *initialized = true; } - bool* initialized; -}; - -struct Final_Builder - : renderable_chain::Builder { - using Builder::Builder; -}; - -struct Root_Base { -public: - explicit Root_Base(bool& constructed) : constructed(&constructed) { - constructed = true; + void handle_observation(const Event&) { + visits->push_back(Visit::child); } - - bool* constructed; }; +struct Parent_State : chain::State_Root {}; +struct Child_State : chain::State_Node {}; +struct Parent_Builder : chain::Builder_Root {}; +struct Child_Builder : chain::Builder_Node {}; +struct Parent_Observer : chain::Observer_Root {}; +struct Child_Observer : chain::Observer_Node {}; -struct Renderable_Root - : renderive::renderable::render_base { - using render_base::render_base; - struct Impl {}; -}; - -struct Product_Properties { - bool enabled{}; -}; - -template -struct Product_Business_Builder { - using Product = Product_Type; - using Properties = Properties_Type; - - explicit Product_Business_Builder(bool& initialized) - : initialized(&initialized) {} - - void build() const { *initialized = true; } - - bool* initialized; -}; - -struct Visual_Control - : renderive::renderable::render_base { - using State = Product_Properties; - - template - using Business_Builder = Product_Business_Builder; - - using render_base::render_base; - -protected: - template - void set_state(Value&& value) { - state.*Member = std::forward(value); - } - - template - auto get_state() const { - return state.*Member; - } - -private: - State state; -}; - -using Renderable_Product = renderive::renderable::attach; - -TEST(renderable_inheritance_test, dispatches_nested_observers_parent_first) { +TEST(renderable_inheritance_test, dispatches_parent_before_child) { std::vector visits; Child_Impl impl(visits); - impl.dispatch(Event{}); - EXPECT_EQ(visits, (std::vector{Visit::parent, Visit::child})); } -TEST(renderable_inheritance_test, preserves_business_builder_capabilities) { - bool initialized{}; - Final_Builder builder(initialized); - - builder.build(); - - EXPECT_TRUE(initialized); - static_assert(std::same_as< - renderive::inheritance::Type_At_Position_T< - typename Final_Builder::Inheritance_Registry, - renderable_chain::Builder_Chain, - Final_Builder::Inheritance_Position>, - Final_Builder>); - static_assert(std::same_as< - renderive::inheritance::Type_At_Position_T< - typename Child_State::Inheritance_Registry, - renderable_chain::State_Chain, - Child_State::Inheritance_Position>, - Child_State>); +TEST(renderable_inheritance_test, every_chain_registers_its_own_layer) { + static_assert(renderive::inheritance::Chain_Registration_Count_V< + typename Child_State::Inheritance_Registry, chain::State_Chain> == 2); + static_assert(renderive::inheritance::Chain_Registration_Count_V< + typename Child_Builder::Inheritance_Registry, chain::Builder_Chain> == 2); + static_assert(renderive::inheritance::Chain_Registration_Count_V< + typename Child_Observer::Inheritance_Registry, chain::Observer_Chain> == 2); + SUCCEED(); } - -TEST(renderable_inheritance_test, - composes_root_control_and_product_without_business_boilerplate) { - bool constructed{}; - Renderable_Product product(constructed); - EXPECT_TRUE(constructed); - - bool initialized{}; - Renderable_Product::Builder builder(initialized); - builder.build(); - EXPECT_TRUE(initialized); - - static_assert(std::same_as); - static_assert(std::same_as< - typename Renderable_Product::Builder::Product, - Renderable_Product>); -} - } // namespace diff --git a/render_2D/render_2D/plottable/Afterglow.h b/render_2D/render_2D/plottable/Afterglow.h index a51cac1..b9460c8 100644 --- a/render_2D/render_2D/plottable/Afterglow.h +++ b/render_2D/render_2D/plottable/Afterglow.h @@ -8,15 +8,6 @@ namespace renderive { namespace detail { struct LIB_DECL Afterglow : Renderable { using Base = Renderable; -protected: - struct Builder : Base::template next_Builder {}; -public: - struct State; -private: - friend struct renderable::attach; - Afterglow(const State& state, renderive_Owner frequency_axis, - renderive_Owner power_axis); -public: ~Afterglow() override; [[nodiscard]] std::size_t history_count() const; [[nodiscard]] std::size_t latest_spectrum_point_count() const; @@ -44,9 +35,13 @@ public: std::size_t latest_spectrum_point_count{}; std::size_t rendered_cell_count{}; }; + struct Builder : Base::template next_Builder {}; private: [[nodiscard]] Observer capture_observation() const; struct Impl; + friend struct renderable::attach; + Afterglow(const State& state, renderive_Owner frequency_axis, + renderive_Owner power_axis); }; } using Afterglow = renderable::attach; diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.h b/render_2D/render_2D/plottable/Constellation_Diagram.h index 75ff5bf..01ba480 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.h +++ b/render_2D/render_2D/plottable/Constellation_Diagram.h @@ -10,15 +10,6 @@ enum class Constellation_Diagram_Type : std::uint8_t { namespace detail { struct LIB_DECL Constellation_Diagram : Renderable { using Base = Renderable; -protected: - struct Builder : Base::template next_Builder {}; -public: - struct State; -private: - friend struct renderable::attach; - Constellation_Diagram(const State& state, renderive_Owner i_axis, - renderive_Owner q_axis); -public: ~Constellation_Diagram() override; void append_point(PointF point); [[nodiscard]] std::size_t point_count() const; @@ -36,9 +27,13 @@ public: State state; std::size_t point_count{}; }; + struct Builder : Base::template next_Builder {}; private: [[nodiscard]] Observer capture_observation() const; struct Impl; + friend struct renderable::attach; + Constellation_Diagram(const State& state, renderive_Owner i_axis, + renderive_Owner q_axis); }; } // namespace detail using Constellation_Diagram = renderable::attach; diff --git a/render_2D/render_2D/plottable/Frequency_Trace.h b/render_2D/render_2D/plottable/Frequency_Trace.h index 223e105..10b1aff 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.h +++ b/render_2D/render_2D/plottable/Frequency_Trace.h @@ -5,31 +5,26 @@ namespace renderive { namespace detail { struct LIB_DECL Frequency_Trace : Renderable { using Base = Renderable; -protected: - struct Builder : Base::template next_Builder {}; -public: - struct State; -private: - friend struct renderable::attach; - Frequency_Trace(const State& state, renderive_Owner time_axis, - renderive_Owner value_axis); -public: ~Frequency_Trace() override; void append_sample(int tick, double value); void append_sample(Time_Of_Day time, double value); [[nodiscard]] std::size_t sample_count() const; [[nodiscard]] std::size_t rendered_point_count() const; - struct State : Base::template next_State { + struct State : next_State { Pen pen{Color::yellow()}; }; - struct Observer : Base::template next_Observer { + struct Observer : next_Observer { State state; std::size_t sample_count{}; std::size_t rendered_point_count{}; }; -private: + struct Builder : next_Builder {}; +protected: [[nodiscard]] Observer capture_observation() const; struct Impl; + friend struct renderable::attach; + Frequency_Trace(const State& state, renderive_Owner time_axis, + renderive_Owner value_axis); }; } using Frequency_Trace = renderable::attach; diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.h b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.h index f645214..f85806f 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.h +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.h @@ -6,17 +6,7 @@ namespace detail { struct LIB_DECL Selection_Rectangle_Overlay : Renderable, Paint_Overlay { using Base = Renderable; -protected: - struct Builder : Base::template next_Builder {}; -public: - struct State; -private: friend struct renderable::attach; - Selection_Rectangle_Overlay( - const State& state, renderive_Owner horizontal_axis, - renderive_Owner vertical_axis); -public: - ~Selection_Rectangle_Overlay() override; [[nodiscard]] std::vector selected_regions() const; void clear_selected_regions(); struct State : Base::template next_State { @@ -25,15 +15,18 @@ public: Brush selection_brush{Color{0, 0, 255, 50}, Brush_Style::Solid}; Pen selection_border_pen{Color::white(), 1.0, Line_Style::Dash}; }; -private: - struct Impl; -public: struct Observer : Base::template next_Observer { State state; std::size_t selected_region_count{}; }; -private: + struct Builder : Base::template next_Builder {}; +protected: [[nodiscard]] Observer capture_observation() const; + struct Impl; + ~Selection_Rectangle_Overlay() override; + Selection_Rectangle_Overlay( + const State& state, renderive_Owner horizontal_axis, + renderive_Owner vertical_axis); }; } using Selection_Rectangle_Overlay = diff --git a/render_2D/render_2D/plottable/Spectrum.h b/render_2D/render_2D/plottable/Spectrum.h index 1d22e77..d1f2fac 100644 --- a/render_2D/render_2D/plottable/Spectrum.h +++ b/render_2D/render_2D/plottable/Spectrum.h @@ -33,9 +33,29 @@ struct Spectrum_Real_Time_Data_Strategy }; struct LIB_DECL Spectrum : Renderable { using Base = Renderable; -protected: - struct Builder : Base::template next_Builder {}; -public: + void update_samples(std::span values); + void update_samples(std::pmr::vector&& values); + template + void update_samples(const Values& values) { + update_samples(std::span(values.data(), values.size())); + } + [[nodiscard]] std::size_t sample_count() const; + [[nodiscard]] std::size_t rendered_point_count() const; + [[nodiscard]] double power_at(double frequency, bool& ok) const; + void add_custom_marker(double frequency); + void add_custom_line_marker(double frequency); + void remove_custom_marker(double frequency); + void remove_selected_marker(); + void clear_custom_markers(); + [[nodiscard]] int selectable_line_marker_count() const; + [[nodiscard]] int selected_marker_index() const; + void set_selected_marker_index(int index); + void select_next_marker(); + void select_previous_marker(); + void clear_marker_selection(); + [[nodiscard]] double marker_frequency(int index) const; + void set_marker_frequency(int index, double frequency); + void set_current_marker_frequency(double frequency); struct State : Base::template next_State, Hover_Tooltip_Properties { Nonnegative_Count frequency_point_size; Render_Partition_Mode partition_mode{Render_Partition_Mode::Automatic}; @@ -65,47 +85,21 @@ public: Brush_Style::Solid }; }; -private: - friend struct renderable::attach; - Spectrum(const State& state, - renderive_Owner frequency_axis, - renderive_Owner power_axis); -public: - ~Spectrum() override; - void update_samples(std::span values); - void update_samples(std::pmr::vector&& values); - template - void update_samples(const Values& values) { - update_samples(std::span(values.data(), values.size())); - } - [[nodiscard]] std::size_t sample_count() const; - [[nodiscard]] std::size_t rendered_point_count() const; - [[nodiscard]] double power_at(double frequency, bool& ok) const; - void add_custom_marker(double frequency); - void add_custom_line_marker(double frequency); - void remove_custom_marker(double frequency); - void remove_selected_marker(); - void clear_custom_markers(); - [[nodiscard]] int selectable_line_marker_count() const; - [[nodiscard]] int selected_marker_index() const; - void set_selected_marker_index(int index); - void select_next_marker(); - void select_previous_marker(); - void clear_marker_selection(); - [[nodiscard]] double marker_frequency(int index) const; - void set_marker_frequency(int index, double frequency); - void set_current_marker_frequency(double frequency); -private: - struct Impl; -public: struct Observer : Base::template next_Observer { State state; std::size_t sample_count{}; std::size_t rendered_point_count{}; std::size_t selectable_marker_count{}; }; -private: + struct Builder : Base::template next_Builder {}; +protected: + struct Impl; + ~Spectrum() override; [[nodiscard]] Observer capture_observation() const; + friend struct renderable::attach; + Spectrum(const State& state, + renderive_Owner frequency_axis, + renderive_Owner power_axis); }; } using Spectrum = renderable::attach; diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.h b/render_2D/render_2D/plottable/Sweep_Spectrum.h index 9e2fee4..38f99bc 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.h +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.h @@ -7,15 +7,7 @@ namespace renderive { namespace detail { struct LIB_DECL Sweep_Spectrum : Renderable { using Base = Renderable; -protected: struct Builder : Base::template next_Builder {}; -public: - struct State; -private: - friend struct renderable::attach; - Sweep_Spectrum(const State& state, renderive_Owner frequency_axis, - renderive_Owner power_axis); -public: ~Sweep_Spectrum() override; void append_block(std::span values); void append_block(std::pmr::vector&& values); @@ -36,9 +28,6 @@ public: Line_Interpolation_Mode interpolation_mode = Line_Interpolation_Mode::Linear_Value; }; -private: - struct Impl; -public: struct Observer : Base::template next_Observer { State state; std::size_t stored_block_count{}; @@ -47,6 +36,10 @@ public: }; private: [[nodiscard]] Observer capture_observation() const; + friend struct renderable::attach; + Sweep_Spectrum(const State& state, renderive_Owner frequency_axis, + renderive_Owner power_axis); + struct Impl; }; } using Sweep_Spectrum = renderable::attach; diff --git a/render_2D/render_2D/plottable/Waterfall.h b/render_2D/render_2D/plottable/Waterfall.h index 1ea82f4..51bc05a 100644 --- a/render_2D/render_2D/plottable/Waterfall.h +++ b/render_2D/render_2D/plottable/Waterfall.h @@ -9,17 +9,6 @@ namespace renderive { namespace detail { struct LIB_DECL Waterfall : Renderable { using Base = Renderable; -protected: - struct Builder : Base::template next_Builder {}; -public: - struct State; -private: - friend struct renderable::attach; - Waterfall(const State& state, - renderive_Owner frequency_axis, - renderive_Owner time_axis); -public: - ~Waterfall() override; void append_row(int tick, std::span values); void append_row(int tick, std::pmr::vector&& values); void append_row(Time_Of_Day time, std::span values); @@ -35,7 +24,7 @@ public: [[nodiscard]] std::size_t row_count() const; [[nodiscard]] std::size_t stored_point_count() const; [[nodiscard]] std::size_t rendered_cell_count() const; - struct State : Base::template next_State, Hover_Tooltip_Properties { + struct State : next_State, Hover_Tooltip_Properties { Range frequency_range{0.0, 10.0}; Range power_range{0.0, 10.0}; Nonnegative_Count frequency_bin_count; @@ -46,17 +35,21 @@ public: Image_Interpolation_Mode::Nearest; Color_Map color_map; }; -private: - struct Impl; -public: - struct Observer : Base::template next_Observer { + struct Observer : next_Observer { State state; std::size_t row_count{}; std::size_t stored_point_count{}; std::size_t rendered_cell_count{}; }; -private: + struct Builder : next_Builder {}; +protected: + struct Impl; + friend struct renderable::attach; + Waterfall(const State& state, + renderive_Owner frequency_axis, + renderive_Owner time_axis); [[nodiscard]] Observer capture_observation() const; + ~Waterfall() override; }; } using Waterfall = renderable::attach; diff --git a/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp b/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp index 467d134..2f5a9d3 100644 --- a/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp +++ b/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp @@ -1,277 +1,53 @@ -#include "../render_2D/export.h" +#include "render_2D/scene/Render_Scene_2D.h" #include -#include -#include -#include +#include + namespace renderive { namespace { -TEST(Renderive_Core2_Frame_Pipeline, ManualLifecycleSeparatesPrepareRefreshAndRender) { - Render_Scene_2D plot({.frame_mode = Plot_Frame_Mode::Manual}); - plot.init(); - plot.set_viewport_size({96, 54}); - ASSERT_TRUE(plot.prepare_frame()); - auto snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.mode, Plot_Frame_Mode::Manual); - EXPECT_EQ(snapshot.last_event, "prepared"); - EXPECT_EQ(snapshot.limit_state, "not_applicable"); - EXPECT_EQ(snapshot.produced_frame_count, 1u); - EXPECT_EQ(snapshot.consumed_frame_count, 0u); - EXPECT_EQ(snapshot.pending_frame_count, 1u); - EXPECT_EQ(snapshot.latest_sequence, 1u); - ASSERT_TRUE(plot.refresh_manual_frame()); - snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.last_event, "refresh_succeeded"); - EXPECT_EQ(snapshot.produced_frame_count, 1u); - EXPECT_EQ(snapshot.consumed_frame_count, 0u); - EXPECT_EQ(snapshot.pending_frame_count, 0u); - EXPECT_EQ(snapshot.latest_sequence, 1u); - ASSERT_TRUE(plot.render_prepared_frame()); - snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.last_event, "rendered"); - EXPECT_EQ(snapshot.produced_frame_count, 1u); - EXPECT_EQ(snapshot.consumed_frame_count, 1u); - EXPECT_EQ(snapshot.pending_frame_count, 0u); - EXPECT_EQ(snapshot.latest_sequence, 1u); - EXPECT_EQ(plot.diagnostics().refresh.frame_count, 1u); + +std::unique_ptr make_scene( + Render_Scene_2D::State state = {}) { + auto strategy = + std::make_shared(); + return Render_Scene_2D::Builder{state}.build(std::move(strategy)); } -TEST(Renderive_Core2_Frame_Pipeline, HighFrequencyLowLatencyLifecycleExposesKernelTiming) { - Render_Scene_2D plot; - plot.init(); - plot.set_viewport_size({96, 54}); - EXPECT_TRUE(plot.set_max_render_fps(1'000'000'000.0)); - ASSERT_TRUE(plot.prepare_frame()); - auto snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.last_event, "published"); - EXPECT_EQ(snapshot.limit_state, "frequency_limited"); - EXPECT_DOUBLE_EQ(snapshot.frequency_hz, 1'000'000'000.0); - EXPECT_EQ(snapshot.target_interval_ns, 1u); - EXPECT_EQ(snapshot.produced_frame_count, 1u); - EXPECT_EQ(snapshot.consumed_frame_count, 0u); - EXPECT_EQ(snapshot.pending_frame_count, 1u); - ASSERT_TRUE(plot.render_prepared_frame()); - snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.last_event, "lifecycle_completed"); - EXPECT_DOUBLE_EQ(snapshot.frequency_hz, 1'000'000'000.0); - EXPECT_EQ(snapshot.target_interval_ns, 1u); - EXPECT_EQ(snapshot.produced_frame_count, 1u); - EXPECT_EQ(snapshot.consumed_frame_count, 1u); - EXPECT_EQ(snapshot.pending_frame_count, 0u); - EXPECT_EQ(snapshot.bottleneck_duration_ns, - std::max(snapshot.paint_duration_ns, snapshot.render_duration_ns)); - EXPECT_EQ(snapshot.next_refresh_interval_ns, - std::max(snapshot.target_interval_ns, snapshot.bottleneck_duration_ns)); - EXPECT_GE(snapshot.end_to_end_ns, snapshot.paint_duration_ns); - EXPECT_GE(snapshot.end_to_end_ns, snapshot.render_duration_ns); - const auto refresh = plot.refresh_feedback_snapshot(); - EXPECT_DOUBLE_EQ(refresh.frequency_hz, snapshot.frequency_hz); - EXPECT_EQ(refresh.frame_count, snapshot.consumed_frame_count); - EXPECT_EQ(refresh.next_refresh_interval_ns, snapshot.next_refresh_interval_ns); + +TEST(RenderScene2DFramePipeline, BuilderOwnsConstructionAndState) { + Render_Scene_2D::State state; + state.viewport = {96, 54}; + state.background = Color{8, 16, 24, 255}; + auto scene = make_scene(state); + ASSERT_TRUE(scene); + EXPECT_EQ(scene->root_renderable()->object_name(), "root"); } -TEST(Renderive_Core2_Frame_Pipeline, LowLatencyConsumerFeedbackFlowsThroughCore2IntoKernelScheduling) { - Render_Scene_2D plot; - plot.init(); - plot.set_viewport_size({96, 54}); - EXPECT_TRUE(plot.set_max_render_fps(100.0)); - EXPECT_TRUE(plot.set_consumer_feedback({40'000'000})); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.render_prepared_frame()); - const auto snapshot = plot.frame_status(); - EXPECT_TRUE(snapshot.frequency_limit_enabled); - EXPECT_TRUE(snapshot.consumer_feedback_enabled); - EXPECT_EQ(snapshot.limit_state, "consumer_limited"); - EXPECT_EQ(snapshot.target_interval_ns, 10'000'000u); - EXPECT_EQ(snapshot.consumer_sample_interval_ns, 40'000'000u); - EXPECT_EQ(snapshot.consumer_smoothed_interval_ns, 40'000'000u); - EXPECT_EQ(snapshot.consumer_variation_ns, 0u); - EXPECT_EQ(snapshot.consumer_safety_interval_ns, 40'000'000u); - EXPECT_EQ(snapshot.consumer_interval_ns, 40'000'000u); - EXPECT_EQ(snapshot.next_refresh_interval_ns, 40'000'000u); - EXPECT_EQ(plot.refresh_feedback_snapshot().next_refresh_interval_ns, 40'000'000u); + +TEST(RenderScene2DFramePipeline, ActiveDirtyAndForceGatesAreOrthogonal) { + Render_Scene_2D::State state; + state.viewport = {80, 45}; + auto scene = make_scene(state); + scene->publish_frame_state(); + EXPECT_EQ(scene->observer().state, state); + EXPECT_EQ(scene->render_frame().value(), Plot_Render_Status::view_inactive); + scene->activate_view(); + EXPECT_EQ(scene->render_frame().value(), Plot_Render_Status::rendered); + EXPECT_EQ(scene->render_frame().value(), Plot_Render_Status::model_unchanged); + EXPECT_EQ(scene->render_frame(true).value(), Plot_Render_Status::rendered); + EXPECT_EQ(scene->wait_for_render(), Scene_Render_Result::none); } -TEST(Renderive_Core2_Frame_Pipeline, LowLatencyLimitsCanBeClearedIndependently) { - Render_Scene_2D plot; - plot.init(); - plot.set_viewport_size({96, 54}); - EXPECT_TRUE(plot.set_max_render_fps(100.0)); - EXPECT_TRUE(plot.set_consumer_feedback({40'000'000})); - ASSERT_TRUE(plot.clear_consumer_feedback()); - ASSERT_TRUE(plot.clear_max_render_fps()); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.render_prepared_frame()); - const auto snapshot = plot.frame_status(); - EXPECT_FALSE(snapshot.frequency_limit_enabled); - EXPECT_FALSE(snapshot.consumer_feedback_enabled); - EXPECT_EQ(snapshot.target_interval_ns, 0u); - EXPECT_EQ(snapshot.consumer_interval_ns, 0u); - EXPECT_EQ(snapshot.next_refresh_interval_ns, snapshot.bottleneck_duration_ns); - EXPECT_TRUE(snapshot.limit_state == "paint_limited" || snapshot.limit_state == "render_limited"); -} -TEST(Renderive_Core2_Frame_Pipeline, LowLatencyReplacementRetiresOldFrameWithoutPhantomPendingFrame) { - Render_Scene_2D plot; - plot.init(); - plot.set_viewport_size({64, 40}); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.prepare_frame()); - auto snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.last_event, "published"); - EXPECT_EQ(snapshot.produced_frame_count, 2u); - EXPECT_EQ(snapshot.dropped_frame_count, 1u); - EXPECT_EQ(snapshot.pending_frame_count, 1u); - EXPECT_EQ(snapshot.latest_sequence, 2u); - ASSERT_TRUE(plot.render_prepared_frame()); - snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.last_event, "lifecycle_completed"); - EXPECT_EQ(snapshot.produced_frame_count, 2u); - EXPECT_EQ(snapshot.consumed_frame_count, 1u); - EXPECT_EQ(snapshot.dropped_frame_count, 1u); - EXPECT_EQ(snapshot.pending_frame_count, 0u); - EXPECT_EQ(snapshot.latest_sequence, 2u); -} -TEST(Renderive_Core2_Frame_Pipeline, LowLatencyDiscardAndEmptyRenderRemainObservable) { - Render_Scene_2D plot; - plot.init(); - plot.set_viewport_size({64, 40}); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.discard_pending_frame()); - auto snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.last_event, "manually_discarded"); - EXPECT_EQ(snapshot.produced_frame_count, 1u); - EXPECT_EQ(snapshot.consumed_frame_count, 0u); - EXPECT_EQ(snapshot.dropped_frame_count, 1u); - EXPECT_EQ(snapshot.pending_frame_count, 0u); - EXPECT_EQ(snapshot.latest_sequence, 1u); - const auto empty_render = plot.render_prepared_frame(); - ASSERT_FALSE(empty_render); - EXPECT_EQ(empty_render.error(), - Plot_Render_Prepared_Frame_Error::renderer_unavailable); - snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.last_event, "swap_failed"); - EXPECT_EQ(snapshot.failed_operation_count, 1u); - EXPECT_EQ(snapshot.pending_frame_count, 0u); - EXPECT_EQ(snapshot.latest_sequence, 1u); -} -TEST(Renderive_Core2_Frame_Pipeline, ManualDiscardAndFailedRefreshUpdateObserverState) { - Render_Scene_2D plot({.frame_mode = Plot_Frame_Mode::Manual}); - plot.init(); - plot.set_viewport_size({64, 40}); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.discard_pending_frame()); - auto snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.last_event, "manually_discarded"); - EXPECT_EQ(snapshot.produced_frame_count, 1u); - EXPECT_EQ(snapshot.dropped_frame_count, 1u); - EXPECT_EQ(snapshot.pending_frame_count, 0u); - EXPECT_EQ(snapshot.latest_sequence, 1u); - const auto last_paint_duration_ns = snapshot.paint_duration_ns; - const auto failed_refresh = plot.refresh_manual_frame(); - ASSERT_FALSE(failed_refresh); - EXPECT_EQ(failed_refresh.error(), - Plot_Refresh_Manual_Frame_Error::no_pending_frame); - snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.last_event, "refresh_failed"); - EXPECT_EQ(snapshot.failed_operation_count, 1u); - EXPECT_EQ(snapshot.pending_frame_count, 0u); - EXPECT_EQ(snapshot.latest_sequence, 1u); - EXPECT_EQ(snapshot.paint_duration_ns, last_paint_duration_ns); -} -TEST(Renderive_Core2_Frame_Pipeline, PlaybackPreservesQueueDepthAndEmptyAcquireFailure) { - Render_Scene_2D plot({.frame_mode = Plot_Frame_Mode::Playback}); - plot.init(); - plot.set_viewport_size({64, 40}); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.prepare_frame()); - auto snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.produced_frame_count, 3u); - EXPECT_EQ(snapshot.pending_frame_count, 3u); - EXPECT_EQ(snapshot.latest_sequence, 3u); - ASSERT_TRUE(plot.render_prepared_frame()); - ASSERT_TRUE(plot.render_prepared_frame()); - ASSERT_TRUE(plot.render_prepared_frame()); - snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.last_event, "rendered"); - EXPECT_EQ(snapshot.consumed_frame_count, 3u); - EXPECT_EQ(snapshot.pending_frame_count, 0u); - EXPECT_EQ(snapshot.latest_sequence, 3u); - const auto last_paint_duration_ns = snapshot.paint_duration_ns; - const auto last_render_duration_ns = snapshot.render_duration_ns; - const auto last_queue_wait_ns = snapshot.queue_wait_ns; - EXPECT_FALSE(plot.render_prepared_frame()); - snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.last_event, "queue_empty"); - EXPECT_EQ(snapshot.failed_operation_count, 1u); - EXPECT_EQ(snapshot.pending_frame_count, 0u); - EXPECT_EQ(snapshot.latest_sequence, 3u); - EXPECT_EQ(snapshot.paint_duration_ns, last_paint_duration_ns); - EXPECT_EQ(snapshot.render_duration_ns, last_render_duration_ns); - EXPECT_EQ(snapshot.queue_wait_ns, last_queue_wait_ns); -} -TEST(Renderive_Core2_Frame_Pipeline, PlaybackFullCyclePreservesPreviouslyQueuedFrames) { - Render_Scene_2D plot({.frame_mode = Plot_Frame_Mode::Playback}); - plot.init(); - plot.set_viewport_size({96, 54}); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.prepare_frame()); - auto snapshot = plot.frame_status(); - ASSERT_EQ(snapshot.pending_frame_count, 3u); - ASSERT_EQ(snapshot.produced_frame_count, 3u); - ASSERT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); - snapshot = plot.frame_status(); - EXPECT_EQ(snapshot.mode, Plot_Frame_Mode::Playback); - EXPECT_EQ(snapshot.last_event, "rendered"); - EXPECT_EQ(snapshot.produced_frame_count, 4u); - EXPECT_EQ(snapshot.consumed_frame_count, 1u); - EXPECT_EQ(snapshot.pending_frame_count, 3u); - // The cycle enqueues sequence 4 but must consume the FIFO head (sequence 1). - EXPECT_EQ(snapshot.latest_sequence, 1u); - EXPECT_FALSE(plot.discard_pending_frame()); - EXPECT_EQ(plot.frame_status().pending_frame_count, 3u); -} -TEST(Renderive_Core2_Frame_Pipeline, EveryModeHonorsActiveDirtyAndForceRenderGates) { - const auto verify = [](Plot_Frame_Mode mode) { - Render_Scene_2D scene({.frame_mode = mode}); - scene.init(); - scene.set_viewport_size({80, 45}); - EXPECT_FALSE(scene.view_active()); - EXPECT_NE(scene.render_frame(false).value(), Plot_Render_Status::rendered); - EXPECT_EQ(scene.frame_status().observation_count, 0u); - scene.activate_view(); - ASSERT_EQ(scene.render_frame(false).value(), Plot_Render_Status::rendered); - const auto completed = scene.frame_status(); - EXPECT_EQ(completed.mode, mode); - EXPECT_EQ(completed.consumed_frame_count, 1u); - EXPECT_NE(scene.render_frame(false).value(), Plot_Render_Status::rendered); - EXPECT_EQ(scene.frame_status().consumed_frame_count, 1u); - scene.deactivate_view(); - scene.set_background_color(Color{8, 16, 24, 255}); - EXPECT_NE(scene.render_frame(false).value(), Plot_Render_Status::rendered); - ASSERT_EQ(scene.render_frame(true).value(), Plot_Render_Status::rendered); - EXPECT_EQ(scene.frame_status().consumed_frame_count, 2u); - }; - verify(Plot_Frame_Mode::Manual); - verify(Plot_Frame_Mode::Low_Latency); - verify(Plot_Frame_Mode::Playback); -} -TEST(Renderive_Core2_Frame_Pipeline, FrequencyConfigurationRejectsInvalidValuesAndWrongModes) { - Render_Scene_2D low_latency; - EXPECT_EQ(low_latency.set_max_render_fps(0.0).error(), Plot_Set_Max_Render_Fps_Error::invalid_fps); - EXPECT_EQ(low_latency.set_max_render_fps(-1.0).error(), Plot_Set_Max_Render_Fps_Error::invalid_fps); - EXPECT_EQ(low_latency.set_max_render_fps(std::numeric_limits::quiet_NaN()).error(), - Plot_Set_Max_Render_Fps_Error::invalid_fps); - EXPECT_EQ(low_latency.set_max_render_fps(std::numeric_limits::infinity()).error(), - Plot_Set_Max_Render_Fps_Error::invalid_fps); - EXPECT_TRUE(low_latency.set_max_render_fps(1'000.0)); - EXPECT_DOUBLE_EQ(low_latency.max_render_fps(), 1'000.0); - Render_Scene_2D manual({.frame_mode = Plot_Frame_Mode::Manual}); - Render_Scene_2D playback({.frame_mode = Plot_Frame_Mode::Playback}); - EXPECT_EQ(manual.set_max_render_fps(60.0).error(), Plot_Set_Max_Render_Fps_Error::unsupported_frame_mode); - EXPECT_EQ(playback.set_max_render_fps(60.0).error(), Plot_Set_Max_Render_Fps_Error::unsupported_frame_mode); - const auto unsupported_refresh = playback.refresh_manual_frame(); - ASSERT_FALSE(unsupported_refresh); - EXPECT_EQ(unsupported_refresh.error(), - Plot_Refresh_Manual_Frame_Error::unsupported_frame_mode); - EXPECT_FALSE(playback.discard_pending_frame()); + +TEST(RenderScene2DFramePipeline, ObserverReadsPublishedDoubleBufferedState) { + Render_Scene_2D::State state; + state.viewport = {32, 24}; + auto scene = make_scene(state); + scene->activate_view(); + ASSERT_EQ(scene->render_frame(true).value(), Plot_Render_Status::rendered); + ASSERT_EQ(scene->wait_for_render(), Scene_Render_Result::none); + const auto observation = scene->observer(); + EXPECT_EQ(observation.state, state); + EXPECT_EQ(observation.renderable_count, 1U); + EXPECT_TRUE(observation.view_active); + EXPECT_GT(observation.frequency_hz, 0.0); } + } // namespace } // namespace renderive diff --git a/render_2D/tests/render_2D_Integration_Tests.cpp b/render_2D/tests/render_2D_Integration_Tests.cpp index 930c910..87047f6 100644 --- a/render_2D/tests/render_2D_Integration_Tests.cpp +++ b/render_2D/tests/render_2D_Integration_Tests.cpp @@ -1,1189 +1,35 @@ -#include "../render_2D/export.h" -#include "render_2D/plottable/Curve_Sampling.h" -#include "render_2D/render/Blend2D_Cache.h" +#include "render_2D/scene/Render_Scene_2D.h" #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include + namespace renderive { namespace { -template -decltype(auto) build_initial(Scene& scene, Build&& build) { - auto attach = scene.attach_builder(); - return std::invoke(std::forward(build), attach); -} -template -void apply_runtime_edit(Scene& scene, Edit&& edit) { - std::promise completed; - auto future = completed.get_future(); - scene.edit_renderables( - [edit = std::forward(edit), &completed](auto& editor) mutable { - edit(editor); - completed.set_value(); - }); - future.wait(); -} -template -concept Legacy_Axis_Property_Api = requires(Axis_Type& axis) { - axis.x(); - axis.set_x(0); - axis.coord_range(); - axis.set_coord_range(Range{}); -}; -template -concept Legacy_Time_Axis_Property_Api = requires(Axis_Type& axis) { - axis.visible_time_point_count(); - axis.set_visible_time_point_count(1); - axis.time_format(); - axis.set_time_format({}); -}; - -static_assert(!Legacy_Axis_Property_Api); -static_assert(!Legacy_Axis_Property_Api); -static_assert(!Legacy_Time_Axis_Property_Api); - -TEST(Renderive_Core2, RootIdentityAndRefreshDiagnosticsComeFromKernelState) { - Render_Scene_2D plot; - plot.init(); - const auto root = plot.root_renderable(); - ASSERT_TRUE(root); - root->set_object_name("renamed-root"); - EXPECT_EQ(plot.root_renderable(), root); - plot.set_viewport_size({32, 24}); - plot.activate_view(); - EXPECT_TRUE(plot.set_max_render_fps(144.0)); - ASSERT_EQ(plot.render_frame().value(), Plot_Render_Status::rendered); - const auto diagnostics = plot.diagnostics(); - EXPECT_DOUBLE_EQ(diagnostics.refresh.frequency_hz, 144.0); - EXPECT_EQ(diagnostics.refresh.frame_count, 1u); -} -TEST(Renderive_Core2, EveryCurveInterpolationModeHasDistinctSamplingSemantics) { - const std::array edge_values{0.0, 10.0}; - const Range domain{0.0, 1.0}; - const auto nearest = detail::curve_sampling::interpolate( - edge_values, domain, Line_Interpolation_Mode::Nearest_Sample); - const auto linear = detail::curve_sampling::interpolate( - edge_values, domain, Line_Interpolation_Mode::Linear_Value); - const auto power = detail::curve_sampling::interpolate( - edge_values, domain, Line_Interpolation_Mode::Linear_Power_Domain); - const auto step_left = detail::curve_sampling::interpolate( - edge_values, domain, Line_Interpolation_Mode::Step_Left); - const auto step_right = detail::curve_sampling::interpolate( - edge_values, domain, Line_Interpolation_Mode::Step_Right); - EXPECT_EQ(nearest.size(), 4u); - EXPECT_EQ(linear.size(), 2u); - ASSERT_EQ(power.size(), 5u); - EXPECT_NEAR(power[2].value, 7.4036269, 1e-6); - ASSERT_EQ(step_left.size(), 3u); - ASSERT_EQ(step_right.size(), 3u); - EXPECT_DOUBLE_EQ(step_left[1].coordinate, 1.0); - EXPECT_DOUBLE_EQ(step_left[1].value, 0.0); - EXPECT_DOUBLE_EQ(step_right[1].coordinate, 0.0); - EXPECT_DOUBLE_EQ(step_right[1].value, 10.0); - const std::array curved_values{0.0, 10.0, 0.0, 10.0}; - const auto cubic = detail::curve_sampling::interpolate( - curved_values, {0.0, 3.0}, Line_Interpolation_Mode::Cubic_Value); - ASSERT_EQ(cubic.size(), 13u); - EXPECT_NEAR(cubic[2].value, 5.625, 1e-9); - const std::array visible_values{}; - const Axis_Transform visible_x{{4.0, 6.0}, 0.0, 100.0}; - const Axis_Transform visible_y{{0.0, 1.0}, 0.0, 100.0, Orientation::Vertical}; - const auto clipped = detail::curve_points( - visible_values, {0.0, 10.0}, visible_x, visible_y, true, - Line_Interpolation_Mode::Linear_Value); - EXPECT_LT(clipped.size(), visible_values.size()); - ASSERT_GE(clipped.size(), 2u); - EXPECT_LE(clipped.front().x, 0.0); - EXPECT_GE(clipped.back().x, 100.0); -} -TEST(Renderive_Core2, PainterScopedClipRestoresPreviousClip) { - detail::Blend2D_Color_Cache cache; - { - detail::Painter painter(cache, {8, 2}); - { - const auto clip = painter.scoped_clip({0.0, 0.0, 4.0, 2.0}); - painter.rect({0.0, 0.0, 8.0, 2.0}, Pen{.style = Line_Style::None}, - Brush{Color::red(), Brush_Style::Solid}); - } - { - const auto clip = painter.scoped_clip({4.0, 0.0, 4.0, 2.0}); - painter.rect({0.0, 0.0, 8.0, 2.0}, Pen{.style = Line_Style::None}, - Brush{Color::green(), Brush_Style::Solid}); - } - } - const Image_View view = cache.view(); - ASSERT_EQ(view.width, 8); - ASSERT_EQ(view.height, 2); - const auto* row = reinterpret_cast(view.data); - EXPECT_EQ(row[1], pack_rgba(255, 0, 0)); - EXPECT_EQ(row[6], pack_rgba(0, 255, 0)); -} -TEST(Renderive_Core2, BicubicHeatmapUsesRealCubicResampling) { - const std::array source{ - pack_rgba(255, 0, 0), pack_rgba(0, 0, 0), pack_rgba(255, 255, 255), pack_rgba(0, 0, 255), - pack_rgba(0, 255, 0), pack_rgba(255, 255, 255), pack_rgba(0, 0, 0), pack_rgba(255, 0, 0), - pack_rgba(0, 0, 255), pack_rgba(0, 0, 0), pack_rgba(255, 255, 255), pack_rgba(0, 255, 0), - pack_rgba(255, 255, 255), pack_rgba(255, 0, 0), pack_rgba(0, 255, 0), pack_rgba(0, 0, 0) - }; - const auto render = [&source](Image_Interpolation_Mode interpolation) { - detail::Blend2D_Color_Cache cache; - { - detail::Painter painter(cache, {11, 11}); - painter.heatmap({0.0, 0.0, 11.0, 11.0}, 4, 4, source, interpolation); - } - const Image_View view = cache.view(); - std::vector pixels; - pixels.reserve(static_cast(view.width) * view.height); - for (int y = 0; y < view.height; ++y) { - const auto* row = reinterpret_cast( - view.data + static_cast(y) * view.stride); - pixels.insert(pixels.end(), row, row + view.width); - } - return pixels; - }; - const auto bilinear = render(Image_Interpolation_Mode::Bilinear); - const auto bicubic = render(Image_Interpolation_Mode::Bicubic); - EXPECT_EQ(bilinear.size(), bicubic.size()); - EXPECT_NE(bilinear, bicubic); -} -TEST(Renderive_Core2, KernelSceneRendersBusinessObjectsIntoBlend2DFrame) { - Render_Scene_2D plot; - plot.init(); - plot.set_viewport_size({320, 180}); - plot.activate_view(); - const auto root = plot.root_renderable(); - ASSERT_TRUE(root); - auto [frequency_axis, power_axis, spectrum] = build_initial(plot, [&](auto& attach) { - auto frequency = Frequency_Axis::Builder{&attach} - .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Frequency_Axis::Properties::x>(32) - .set<&Frequency_Axis::Properties::y>(150) - .set<&Frequency_Axis::Properties::pixel_length>(270) - .set<&Frequency_Axis::Properties::coordinates>(Range{88.0, 108.0}) - .build(root); - auto power = Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .set<&Axis::Properties::x>(32) - .set<&Axis::Properties::y>(8) - .set<&Axis::Properties::pixel_length>(142) - .set<&Axis::Properties::coordinates>(Range{-120.0, 0.0}) - .build(root); - auto value = Spectrum::Builder{&attach} - .set<&Spectrum::Properties::frequency_range>(Range{88.0, 108.0}) - .set<&Spectrum::Properties::frequency_point_size>(64) - .set<&Spectrum::Properties::max_hold_visible>(true) - .build(root, frequency, power); - return std::tuple{frequency, power, value}; - }); - frequency_axis->set<&Axis_Base_Properties::locale>(Number_Locale{','}); - frequency_axis->set<&Axis_Base_Properties::label_rotation_degrees>(15); - EXPECT_EQ(frequency_axis->tick_label(88.5), "88,5 Hz"); - frequency_axis->set<&Axis_Properties::precision>(4); - EXPECT_EQ(frequency_axis->tick_label(1'234'567.0), "1,2346 MHz"); - std::vector samples(64); - for (std::size_t index = 0; index < samples.size(); ++index) - samples[index] = -100.0 + static_cast(index % 24) * 3.0; - spectrum->update_samples(samples); - ASSERT_EQ(plot.render_frame().value(), Plot_Render_Status::rendered); - bool saw_frame = false; - bool saw_drawn_pixel = false; - plot.with_frame([&](Image_View view) { - saw_frame = !view.empty() && view.width == 320 && view.height == 180; - if (!saw_frame) - return; - for (int y = 0; y < view.height && !saw_drawn_pixel; ++y) { - const auto* row = view.data + static_cast(y) * view.stride; - for (int x = 0; x < view.width * static_cast(sizeof(Pixel)); ++x) { - if (row[x] != std::byte{}) { - saw_drawn_pixel = true; - break; - } - } - } - }); - EXPECT_TRUE(saw_frame); - EXPECT_TRUE(saw_drawn_pixel); - EXPECT_EQ(plot.diagnostics().refresh.frame_count, 1u); - EXPECT_NE(plot.render_frame().value(), Plot_Render_Status::rendered); - samples.front() += 1.0; - spectrum->update_samples(samples); - EXPECT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); - frequency_axis->set<&Axis_Properties::wheel>(true); - const Range before_zoom = frequency_axis->get<&Axis_Properties::coordinates>(); - Wheel_Event wheel; - wheel.position = {160.0, 150.0}; - wheel.angle_delta_y = 120.0; - plot.dispatch_event(wheel); - EXPECT_TRUE(wheel.is_accepted()); - EXPECT_LT(frequency_axis->get<&Axis_Properties::coordinates>().size(), before_zoom.size()); - EXPECT_EQ(plot.render_frame().value(), Plot_Render_Status::rendered); - apply_runtime_edit(plot, [root, frequency_axis, power_axis, spectrum](auto& editor) { - editor.set_display_parent(frequency_axis, root); - editor.set_display_parent(power_axis, root); - editor.clear_display_parent(spectrum); - editor.clear_dependency_parent(spectrum); - editor.detach(spectrum); - }); - EXPECT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); - EXPECT_NE(plot.render_frame().value(), Plot_Render_Status::rendered); -} -TEST(Renderive_Core2, StandaloneRenderableBuilderJoinsSceneOnlyInsideRuntimeEditor) { - Render_Scene_2D plot; - plot.init(); - const auto root = plot.root_renderable(); - const auto [frequency, power] = build_initial(plot, [&](auto& attach) { - return std::pair{ - Frequency_Axis::Builder{&attach} - .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .build(root), - Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .build(root) - }; - }); - const auto spectrum = Spectrum::Builder{}.build(root, frequency, power); - ASSERT_TRUE(spectrum); - EXPECT_TRUE(std::ranges::none_of( - plot.render_scene().topology_snapshot().renderables, - [&](const auto& renderable) { - return renderable.get() == spectrum.get(); - })); - apply_runtime_edit(plot, [root, frequency, power, spectrum](auto& editor) { - editor.attach(spectrum); - editor.add_display_parent(spectrum, root); - editor.add_dependency_parent(spectrum, root); - editor.add_dependency_parent(spectrum, frequency); - editor.add_dependency_parent(spectrum, power); - editor.add_display_parent(frequency, spectrum); - editor.add_display_parent(power, spectrum); - }); - EXPECT_TRUE(std::ranges::any_of( - plot.render_scene().topology_snapshot().renderables, - [&](const auto& renderable) { - return renderable.get() == spectrum.get(); - })); -} -TEST(Renderive_Core2, TimeAxisUsesOneFontStateAndFormatsConfiguredLabels) { - Render_Scene_2D plot; - plot.init(); - const auto root = plot.root_renderable(); - auto axis = build_initial(plot, [&](auto& attach) { - return Time_Axis::Builder{&attach} - .set<&Time_Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Time_Axis::Properties::format>("hh-mm-ss.zzz") - .set<&Time_Axis::Properties::unit_text_font>(Font{18.0, 700, true}) - .set<&Time_Axis::Properties::tick_label_spacing_px>(17) - .build(root); - }); - ASSERT_TRUE(axis); - const int tick = axis->append_time(Time_Of_Day{3'723'045}); - EXPECT_EQ(axis->tick_label(tick), "01-02-03.045"); - EXPECT_EQ(axis->get<&Axis_Base_Properties::unit_text_font>(), (Font{18.0, 700, true})); - EXPECT_EQ(axis->get<&Time_Axis::Properties::tick_label_spacing_px>(), 17); -} -TEST(Renderive_Core2, PerformanceOverlayConsumesKernelFrameDiagnostics) { - Render_Scene_2D plot; - plot.init(); - plot.set_viewport_size({160, 90}); - plot.activate_view(); - EXPECT_TRUE(plot.set_max_render_fps(120.0)); - Performance_Overlay_Options options; - options.log.enabled = false; - const auto overlay = attach_performance_overlay(plot, options); - ASSERT_TRUE(overlay); - set_performance_plot_name(plot, "integration"); - set_performance_overlay_enabled(plot, true); - ASSERT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); - const auto snapshot = overlay->display_snapshot(); - ASSERT_TRUE(snapshot); - EXPECT_EQ(snapshot->plot_name, "integration"); - EXPECT_EQ(snapshot->viewport_size, (Size{160, 90})); - EXPECT_FALSE(snapshot->lines.empty()); - EXPECT_EQ(snapshot->frame_count, 1u); - EXPECT_DOUBLE_EQ(plot.max_render_fps(), 120.0); -} -TEST(Renderive_Core2, AllFrameControlModesUseKernelStrategiesAndExposeObservers) { - Render_Scene_2D manual({.frame_mode = Plot_Frame_Mode::Manual}); - manual.init(); - manual.set_viewport_size({64, 40}); - EXPECT_EQ(manual.frame_mode(), Plot_Frame_Mode::Manual); - ASSERT_TRUE(manual.prepare_frame()); - auto manual_observer = manual.frame_status(); - EXPECT_EQ(manual_observer.last_event, "prepared"); - EXPECT_EQ(manual_observer.pending_frame_count, 1u); - ASSERT_TRUE(manual.refresh_manual_frame()); - ASSERT_TRUE(manual.render_prepared_frame()); - manual_observer = manual.frame_status(); - EXPECT_EQ(manual_observer.last_event, "rendered"); - EXPECT_EQ(manual_observer.consumed_frame_count, 1u); - Render_Scene_2D low_latency; - low_latency.init(); - low_latency.set_viewport_size({64, 40}); - low_latency.activate_view(); - ASSERT_EQ(low_latency.render_frame(true).value(), Plot_Render_Status::rendered); - const auto low_observer = low_latency.frame_status(); - EXPECT_EQ(low_latency.frame_mode(), Plot_Frame_Mode::Low_Latency); - EXPECT_EQ(low_observer.last_event, "lifecycle_completed"); - EXPECT_EQ(low_observer.consumed_frame_count, 1u); - Render_Scene_2D playback({.frame_mode = Plot_Frame_Mode::Playback}); - playback.init(); - playback.set_viewport_size({64, 40}); - ASSERT_TRUE(playback.prepare_frame()); - ASSERT_TRUE(playback.prepare_frame()); - ASSERT_TRUE(playback.prepare_frame()); - auto playback_observer = playback.frame_status(); - EXPECT_EQ(playback.frame_mode(), Plot_Frame_Mode::Playback); - EXPECT_EQ(playback_observer.last_event, "enqueued"); - EXPECT_EQ(playback_observer.pending_frame_count, 3u); - ASSERT_TRUE(playback.render_prepared_frame()); - playback_observer = playback.frame_status(); - EXPECT_EQ(playback_observer.last_event, "rendered"); - EXPECT_EQ(playback_observer.pending_frame_count, 2u); - EXPECT_EQ(playback_observer.consumed_frame_count, 1u); -} -TEST(Renderive_Core2, RetainedAxisAndTimeAxisApisRoundTripWithoutWebAdapters) { - Render_Scene_2D plot; - plot.init(); - const auto root = plot.root_renderable(); - ASSERT_TRUE(root); - const auto axis = build_initial(plot, [&](auto& attach) { - return Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .set<&Axis::Properties::x>(31) - .set<&Axis::Properties::y>(17) - .set<&Axis::Properties::pixel_length>(240) - .set<&Axis::Properties::tick_length>(13) - .set<&Axis::Properties::sub_tick_length>(7) - .set<&Axis::Properties::color>(Color{10, 20, 30, 255}) - .set<&Axis::Properties::unit_text>("dB") - .set<&Axis::Properties::unit_text_font>(Font{15.0, 650, true}) - .set<&Axis::Properties::unit_text_pen>(Pen{Color{40, 50, 60, 255}, 2.0}) - .set<&Axis::Properties::unit_text_background_brush>(Brush{Color{3, 4, 5, 255}, Brush_Style::Solid}) - .set<&Axis::Properties::label_rotation_degrees>(27) - .set<&Axis::Properties::coordinates>(Range{-120.0, -20.0}) - .set<&Axis::Properties::precision>(4) - .set<&Axis::Properties::wheel>(true) - .set<&Axis::Properties::drag>(true) - .build(root); - }); - ASSERT_TRUE(axis); - EXPECT_EQ(axis->get<&Axis_Base_Properties::x>(), 31); - EXPECT_EQ(axis->get<&Axis_Base_Properties::y>(), 17); - EXPECT_EQ(axis->get<&Axis_Base_Properties::orientation>(), Orientation::Vertical); - EXPECT_EQ(axis->get<&Axis_Base_Properties::pixel_length>(), 240U); - EXPECT_EQ(axis->get<&Axis_Base_Properties::tick_length>(), 13); - EXPECT_EQ(axis->get<&Axis_Base_Properties::sub_tick_length>(), 7); - EXPECT_EQ(axis->get<&Axis_Base_Properties::color>(), (Color{10, 20, 30, 255})); - EXPECT_EQ(axis->get<&Axis_Base_Properties::unit_text>(), "dB"); - EXPECT_EQ(axis->get<&Axis_Base_Properties::unit_text_font>(), (Font{15.0, 650, true})); - EXPECT_EQ(axis->get<&Axis_Base_Properties::unit_text_pen>(), (Pen{Color{40, 50, 60, 255}, 2.0})); - EXPECT_EQ(axis->get<&Axis_Base_Properties::unit_text_background_brush>(), - (Brush{Color{3, 4, 5, 255}, Brush_Style::Solid})); - EXPECT_EQ(axis->get<&Axis_Base_Properties::label_rotation_degrees>(), 27); - EXPECT_EQ(axis->get<&Axis_Properties::coordinates>(), (Range{-120.0, -20.0})); - EXPECT_EQ(axis->get<&Axis_Properties::precision>(), 4); - EXPECT_TRUE(axis->get<&Axis_Properties::wheel>()); - EXPECT_TRUE(axis->get<&Axis_Properties::drag>()); - EXPECT_THROW(axis->set<&Axis_Properties::precision>(13), std::out_of_range); - EXPECT_THROW(axis->set<&Axis_Properties::coordinates>(Range{1.0, 1.0}), - std::invalid_argument); - axis->set<&Axis_Base_Properties::locale>(Number_Locale{','}); - axis->set<&Axis_Properties::coordinates>(Range{-100.0, -20.0}); - EXPECT_EQ(axis->get<&Axis_Base_Properties::locale>(), (Number_Locale{','})); - EXPECT_EQ(axis->get<&Axis_Properties::coordinates>(), (Range{-100.0, -20.0})); - const Axis_Transform numeric_transform = axis->transform(); - EXPECT_DOUBLE_EQ(numeric_transform.pixel_to_coord(numeric_transform.coord_to_pixel(-60.0)), -60.0); - EXPECT_GT(static_cast(numeric_transform.pixel_length) + 1, 0); - EXPECT_GT(axis->tick_step(numeric_transform.coordinate_range), 0.0); - EXPECT_GE(axis->sub_tick_count(axis->tick_step(numeric_transform.coordinate_range)), 0); - const auto time_axis = build_initial(plot, [&](auto& attach) { - return Time_Axis::Builder{&attach} - .set<&Time_Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Time_Axis::Properties::x>(12) - .set<&Time_Axis::Properties::y>(280) - .set<&Time_Axis::Properties::pixel_length>(300) - .set<&Time_Axis::Properties::visible_count>(32) - .set<&Time_Axis::Properties::tick_label_spacing_px>(19) - .set<&Time_Axis::Properties::format>("hh:mm:ss.zzz") - .set<&Time_Axis::Properties::unit_text_font>(Font{16.0, 700, true}) - .set<&Time_Axis::Properties::newest_at_start>(true) - .build(root); - }); - ASSERT_TRUE(time_axis); - const int first = time_axis->append_time({3'723'004}); - const int second = time_axis->append_time({3'724'005}); - EXPECT_EQ(time_axis->get<&Time_Axis::Properties::visible_count>(), 32); - EXPECT_EQ(time_axis->get<&Time_Axis::Properties::tick_label_spacing_px>(), 19); - EXPECT_EQ(time_axis->get<&Time_Axis::Properties::format>(), "hh:mm:ss.zzz"); - EXPECT_EQ(time_axis->get<&Axis_Base_Properties::unit_text_font>(), (Font{16.0, 700, true})); - EXPECT_TRUE(time_axis->get<&Time_Axis::Properties::newest_at_start>()); - EXPECT_THROW(time_axis->set<&Time_Axis::Properties::visible_count>(1), std::out_of_range); - EXPECT_EQ(time_axis->time_point_count(), 2U); - EXPECT_EQ(time_axis->tick_to_time(first), (Time_Of_Day{3'723'004})); - EXPECT_EQ(time_axis->tick_to_time(second), (Time_Of_Day{3'724'005})); - EXPECT_EQ(time_axis->tick_label(first), "01:02:03.004"); -} -TEST(Renderive_Core2, RetainedSpectrumApisRoundTripAndMarkersRemainObservable) { - Render_Scene_2D plot; - plot.init(); - plot.set_viewport_size({360, 240}); - plot.activate_view(); - const auto root = plot.root_renderable(); - auto [frequency, power, spectrum] = build_initial(plot, [&](auto& attach) { - auto frequency_axis = Frequency_Axis::Builder{&attach} - .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Frequency_Axis::Properties::x>(30) - .set<&Frequency_Axis::Properties::y>(210) - .set<&Frequency_Axis::Properties::pixel_length>(300) - .set<&Frequency_Axis::Properties::coordinates>(Range{90.0, 110.0}) - .build(root); - auto power_axis = Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .set<&Axis::Properties::x>(30) - .set<&Axis::Properties::y>(20) - .set<&Axis::Properties::pixel_length>(190) - .set<&Axis::Properties::coordinates>(Range{-20.0, -120.0}) - .build(root); - auto value = Spectrum::Builder{&attach} - .set<&Spectrum::Properties::frequency_range>(Range{90.0, 110.0}) - .set<&Spectrum::Properties::frequency_point_size>(4) - .set<&Spectrum::Properties::center_frequency>(100.0) - .set<&Spectrum::Properties::sweep_frequency_range>(Range{96.0, 104.0}) - .set<&Spectrum::Properties::max_hold_visible>(true) - .set<&Spectrum::Properties::min_hold_visible>(true) - .set<&Spectrum::Properties::max_marker_visible>(true) - .set<&Spectrum::Properties::use_min_marker>(true) - .set<&Spectrum::Properties::sweep_region_visible>(true) - .set<&Spectrum::Properties::visible_range_only>(false) - .set<&Spectrum::Properties::interpolation_mode>(Line_Interpolation_Mode::Cubic_Value) - .build(root, frequency_axis, power_axis); - return std::tuple{frequency_axis, power_axis, value}; - }); - ASSERT_TRUE(spectrum); - spectrum->set<&Spectrum::Properties::max_brush>(Brush{Color{1, 2, 3, 80}, Brush_Style::Solid}); - spectrum->set<&Spectrum::Properties::current_brush>(Brush{Color{4, 5, 6, 90}, Brush_Style::Solid}); - spectrum->set<&Spectrum::Properties::min_brush>(Brush{Color{7, 8, 9, 100}, Brush_Style::Solid}); - spectrum->set<&Spectrum::Properties::max_pen>(Pen{Color{10, 20, 30, 255}, 2.0}); - spectrum->set<&Spectrum::Properties::current_pen>(Pen{Color{40, 50, 60, 255}, 2.5}); - spectrum->set<&Spectrum::Properties::min_pen>(Pen{Color{70, 80, 90, 255}, 3.0}); - spectrum->set<&Spectrum::Properties::selected_marker_pen>(Pen{Color{11, 22, 33, 255}, 4.0}); - spectrum->set<&Spectrum::Properties::marker_pen>(Pen{Color{44, 55, 66, 255}, 1.5}); - spectrum->set<&Spectrum::Properties::middle_frequency_pen>(Pen{Color{77, 88, 99, 255}, 1.25}); - spectrum->set<&Spectrum::Properties::sweep_region_brush>(Brush{Color{12, 34, 56, 70}, Brush_Style::Solid}); - EXPECT_EQ(spectrum->get<&Spectrum::Properties::frequency_point_size>(), 4); - EXPECT_EQ(spectrum->get<&Spectrum::Properties::frequency_range>(), (Range{90.0, 110.0})); - EXPECT_DOUBLE_EQ(spectrum->get<&Spectrum::Properties::center_frequency>(), 100.0); - EXPECT_EQ(spectrum->get<&Spectrum::Properties::sweep_frequency_range>(), (Range{96.0, 104.0})); - EXPECT_TRUE(spectrum->get<&Spectrum::Properties::max_hold_visible>()); - EXPECT_TRUE(spectrum->get<&Spectrum::Properties::min_hold_visible>()); - EXPECT_TRUE(spectrum->get<&Spectrum::Properties::max_marker_visible>()); - EXPECT_TRUE(spectrum->get<&Spectrum::Properties::use_min_marker>()); - EXPECT_TRUE(spectrum->get<&Spectrum::Properties::sweep_region_visible>()); - EXPECT_FALSE(spectrum->get<&Spectrum::Properties::visible_range_only>()); - EXPECT_EQ(spectrum->get<&Spectrum::Properties::interpolation_mode>(), Line_Interpolation_Mode::Cubic_Value); - EXPECT_EQ(spectrum->get<&Spectrum::Properties::max_brush>(), (Brush{Color{1, 2, 3, 80}, Brush_Style::Solid})); - EXPECT_EQ(spectrum->get<&Spectrum::Properties::current_pen>(), (Pen{Color{40, 50, 60, 255}, 2.5})); - EXPECT_EQ(spectrum->get<&Spectrum::Properties::sweep_region_brush>(), - (Brush{Color{12, 34, 56, 70}, Brush_Style::Solid})); - std::pmr::vector samples{std::pmr::get_default_resource()}; - samples.assign({-100.0, -80.0, -70.0, -50.0}); - spectrum->update_samples(std::move(samples)); - EXPECT_EQ(spectrum->sample_count(), 4U); - EXPECT_GT(spectrum->rendered_point_count(), 4U); - bool valid{}; - EXPECT_DOUBLE_EQ(spectrum->power_at(100.0, valid), -75.0); - EXPECT_TRUE(valid); - EXPECT_DOUBLE_EQ(spectrum->power_at(200.0, valid), 0.0); - EXPECT_FALSE(valid); - spectrum->add_custom_marker(96.0); - spectrum->add_custom_line_marker(102.0); - EXPECT_EQ(spectrum->selectable_line_marker_count(), 2); - spectrum->select_next_marker(); - EXPECT_EQ(spectrum->selected_marker_index(), 0); - spectrum->set_current_marker_frequency(97.0); - EXPECT_DOUBLE_EQ(spectrum->marker_frequency(0), 97.0); - spectrum->select_previous_marker(); - EXPECT_EQ(spectrum->selected_marker_index(), 1); - spectrum->remove_selected_marker(); - EXPECT_EQ(spectrum->selectable_line_marker_count(), 1); - spectrum->remove_custom_marker(97.1); - EXPECT_EQ(spectrum->selectable_line_marker_count(), 0); - spectrum->clear_custom_markers(); - EXPECT_EQ(spectrum->selected_marker_index(), -1); - EXPECT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); -} -TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { - Render_Scene_2D plot; - plot.init(); - plot.set_viewport_size({420, 280}); - plot.activate_view(); - const auto root = plot.root_renderable(); - auto [frequency, power, time, waterfall] = build_initial(plot, [&](auto& attach) { - auto frequency_axis = Frequency_Axis::Builder{&attach} - .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Frequency_Axis::Properties::x>(40) - .set<&Frequency_Axis::Properties::y>(250) - .set<&Frequency_Axis::Properties::pixel_length>(340) - .set<&Frequency_Axis::Properties::coordinates>(Range{0.0, 4.0}) - .build(root); - auto power_axis = Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .set<&Axis::Properties::x>(40) - .set<&Axis::Properties::y>(20) - .set<&Axis::Properties::pixel_length>(230) - .set<&Axis::Properties::coordinates>(Range{0.0, -120.0}) - .build(root); - auto time_axis = Time_Axis::Builder{&attach} - .set<&Time_Axis::Properties::orientation>(Orientation::Vertical) - .set<&Time_Axis::Properties::x>(40) - .set<&Time_Axis::Properties::y>(20) - .set<&Time_Axis::Properties::pixel_length>(230) - .set<&Time_Axis::Properties::visible_count>(8) - .build(root); - auto value = Waterfall::Builder{&attach} - .set<&Waterfall::Properties::frequency_range>(Range{0.0, 4.0}) - .set<&Waterfall::Properties::power_range>(Range{-120.0, 0.0}) - .set<&Waterfall::Properties::frequency_bin_count>(4) - .set<&Waterfall::Properties::visible_range_only>(false) - .set<&Waterfall::Properties::interpolation_mode>(Image_Interpolation_Mode::Bicubic) - .build(root, frequency_axis, time_axis); - return std::tuple{frequency_axis, power_axis, time_axis, value}; - }); - ASSERT_TRUE(waterfall); - EXPECT_EQ(waterfall->get<&Waterfall::Properties::frequency_range>(), (Range{0.0, 4.0})); - EXPECT_EQ(waterfall->get<&Waterfall::Properties::power_range>(), (Range{-120.0, 0.0})); - EXPECT_EQ(waterfall->get<&Waterfall::Properties::frequency_bin_count>(), 4); - EXPECT_FALSE(waterfall->get<&Waterfall::Properties::visible_range_only>()); - EXPECT_EQ(waterfall->get<&Waterfall::Properties::interpolation_mode>(), Image_Interpolation_Mode::Bicubic); - const std::array row1{-100.0, -80.0, -60.0, -40.0}; - waterfall->append_row(Time_Of_Day{1000}, row1); - std::pmr::vector row2{std::pmr::get_default_resource()}; - row2.assign({-90.0, -70.0, -50.0, -30.0}); - waterfall->append_row(Time_Of_Day{2000}, std::move(row2)); - EXPECT_EQ(waterfall->row_count(), 2U); - EXPECT_EQ(waterfall->stored_point_count(), 8U); - EXPECT_EQ(waterfall->rendered_cell_count(), 8U); - auto afterglow = build_initial(plot, [&](auto& attach) { - return Afterglow::Builder{&attach} - .set<&Afterglow::Properties::frequency_range>(Range{0.0, 4.0}) - .set<&Afterglow::Properties::power_range>(Range{-120.0, 0.0}) - .set<&Afterglow::Properties::frequency_point_size>(4) - .set<&Afterglow::Properties::power_point_size>(8) - .set<&Afterglow::Properties::interpolate>(false) - .set<&Afterglow::Properties::attenuation_rate>(0.35) - .build(root, frequency, power); - }); - ASSERT_TRUE(afterglow); - EXPECT_EQ(afterglow->get<&Afterglow::Properties::frequency_range>(), (Range{0.0, 4.0})); - EXPECT_EQ(afterglow->get<&Afterglow::Properties::power_range>(), (Range{-120.0, 0.0})); - EXPECT_EQ(afterglow->get<&Afterglow::Properties::frequency_point_size>(), 4); - EXPECT_EQ(afterglow->get<&Afterglow::Properties::power_point_size>(), 8); - EXPECT_FALSE(afterglow->get<&Afterglow::Properties::interpolate>()); - EXPECT_DOUBLE_EQ(afterglow->get<&Afterglow::Properties::attenuation_rate>(), 0.35); - afterglow->append_spectrum(row1); - std::pmr::vector glow2{std::pmr::get_default_resource()}; - glow2.assign({-95.0, -75.0, -55.0, -35.0}); - afterglow->append_spectrum(std::move(glow2)); - EXPECT_EQ(afterglow->history_count(), 2U); - EXPECT_EQ(afterglow->latest_spectrum_point_count(), 4U); - EXPECT_EQ(afterglow->rendered_cell_count(), 32U); - afterglow->set<&Afterglow::Properties::attenuation_rate>(2.0); - EXPECT_DOUBLE_EQ(afterglow->get<&Afterglow::Properties::attenuation_rate>(), 1.0); - auto sweep = build_initial(plot, [&](auto& attach) { - return Sweep_Spectrum::Builder{&attach} - .set<&Sweep_Spectrum::Properties::frequency_range>(Range{0.0, 8.0}) - .set<&Sweep_Spectrum::Properties::bins_per_block>(4) - .set<&Sweep_Spectrum::Properties::block_count>(2) - .set<&Sweep_Spectrum::Properties::pen>(Pen{Color{1, 120, 220, 255}, 2.0}) - .set<&Sweep_Spectrum::Properties::current_frequency_pen>(Pen{Color{240, 80, 20, 255}, 3.0}) - .set<&Sweep_Spectrum::Properties::visible_range_only>(false) - .set<&Sweep_Spectrum::Properties::interpolation_mode>(Line_Interpolation_Mode::Step_Right) - .build(root, frequency, power); - }); - ASSERT_TRUE(sweep); - EXPECT_EQ(sweep->get<&Sweep_Spectrum::Properties::frequency_range>(), (Range{0.0, 8.0})); - EXPECT_EQ(sweep->get<&Sweep_Spectrum::Properties::bins_per_block>(), 4); - EXPECT_EQ(sweep->get<&Sweep_Spectrum::Properties::block_count>(), 2); - EXPECT_FALSE(sweep->get<&Sweep_Spectrum::Properties::visible_range_only>()); - EXPECT_EQ(sweep->get<&Sweep_Spectrum::Properties::interpolation_mode>(), Line_Interpolation_Mode::Step_Right); - sweep->append_block(row1); - sweep->append_block(row1); - std::pmr::vector block3{std::pmr::get_default_resource()}; - block3.assign(row1.begin(), row1.end()); - sweep->append_block(std::move(block3)); - EXPECT_EQ(sweep->stored_block_count(), 2U); - EXPECT_EQ(sweep->stored_point_count(), 8U); - EXPECT_GT(sweep->rendered_point_count(), 0U); - auto trace = build_initial(plot, [&](auto& attach) { - return Frequency_Trace::Builder{&attach} - .set<&Frequency_Trace::Properties::pen>(Pen{Color{120, 240, 80, 255}, 2.0}) - .build(root, time, power); - }); - ASSERT_TRUE(trace); - trace->append_sample(Time_Of_Day{3000}, -80.0); - trace->append_sample(Time_Of_Day{4000}, -70.0); - EXPECT_EQ(trace->sample_count(), 2U); - EXPECT_EQ(trace->rendered_point_count(), 2U); - EXPECT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); -} -TEST(Renderive_Core2, RetainedSelectionAndConstellationApisDriveInteractionAndLayout) { - Render_Scene_2D plot; - plot.init(); - plot.set_viewport_size({360, 260}); - plot.activate_view(); - const auto root = plot.root_renderable(); - auto [horizontal, vertical, selection] = build_initial(plot, [&](auto& attach) { - auto horizontal_axis = Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Axis::Properties::x>(30) - .set<&Axis::Properties::y>(230) - .set<&Axis::Properties::pixel_length>(300) - .set<&Axis::Properties::coordinates>(Range{-3.0, 3.0}) - .build(root); - auto vertical_axis = Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .set<&Axis::Properties::x>(30) - .set<&Axis::Properties::y>(20) - .set<&Axis::Properties::pixel_length>(210) - .set<&Axis::Properties::coordinates>(Range{3.0, -3.0}) - .build(root); - auto value = Selection_Rectangle_Overlay::Builder{&attach} - .set<&Selection_Rectangle_Overlay::Properties::label_font>(Font{14.0, 600, true}) - .set<&Selection_Rectangle_Overlay::Properties::label_pen>(Pen{Color{20, 220, 180, 255}, 2.0}) - .set<&Selection_Rectangle_Overlay::Properties::selection_brush>(Brush{Color{20, 80, 220, 60}, Brush_Style::Solid}) - .set<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(Pen{Color{240, 200, 60, 255}, 2.0, Line_Style::Dash}) - .build(root, horizontal_axis, vertical_axis); - return std::tuple{horizontal_axis, vertical_axis, value}; - }); - ASSERT_TRUE(selection); - EXPECT_EQ(selection->get<&Selection_Rectangle_Overlay::Properties::label_font>(), (Font{14.0, 600, true})); - EXPECT_EQ(selection->get<&Selection_Rectangle_Overlay::Properties::label_pen>(), (Pen{Color{20, 220, 180, 255}, 2.0})); - EXPECT_EQ(selection->get<&Selection_Rectangle_Overlay::Properties::selection_brush>(), - (Brush{Color{20, 80, 220, 60}, Brush_Style::Solid})); - EXPECT_EQ(selection->get<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(), - (Pen{Color{240, 200, 60, 255}, 2.0, Line_Style::Dash})); - Pointer_Event press(Event_Type::Pointer_Press); - press.position = {80.0, 70.0}; - press.button = Mouse_Button::Left; - selection->dispatch_event(press); - EXPECT_TRUE(press.is_accepted()); - Pointer_Event move(Event_Type::Pointer_Move); - move.position = {260.0, 180.0}; - selection->dispatch_event(move); - EXPECT_TRUE(move.is_accepted()); - Pointer_Event release(Event_Type::Pointer_Release); - release.position = {260.0, 180.0}; - release.button = Mouse_Button::Left; - selection->dispatch_event(release); - EXPECT_TRUE(release.is_accepted()); - ASSERT_EQ(selection->selected_regions().size(), 1U); - EXPECT_FALSE(selection->selected_regions().front().empty()); - selection->clear_selected_regions(); - EXPECT_TRUE(selection->selected_regions().empty()); - auto constellation = build_initial(plot, [&](auto& attach) { - return Constellation_Diagram::Builder{&attach} - .set<&Constellation_Diagram::Properties::i_range>(Range{-2.0, 2.0}) - .set<&Constellation_Diagram::Properties::q_range>(Range{-3.0, 3.0}) - .set<&Constellation_Diagram::Properties::point_color>(Color{80, 220, 255, 255}) - .set<&Constellation_Diagram::Properties::anchor_color>(Color{255, 180, 40, 255}) - .set<&Constellation_Diagram::Properties::point_lifetime_ms>(2000) - .set<&Constellation_Diagram::Properties::type>(Constellation_Diagram_Type::Psk16) - .set<&Constellation_Diagram::Properties::phase_offset_radians>(0.25) - .build(root, horizontal, vertical); - }); - ASSERT_TRUE(constellation); - EXPECT_EQ(constellation->get<&Constellation_Diagram::Properties::i_range>(), (Range{-2.0, 2.0})); - EXPECT_EQ(constellation->get<&Constellation_Diagram::Properties::q_range>(), (Range{-3.0, 3.0})); - EXPECT_EQ(constellation->get<&Constellation_Diagram::Properties::point_color>(), (Color{80, 220, 255, 255})); - EXPECT_EQ(constellation->get<&Constellation_Diagram::Properties::anchor_color>(), (Color{255, 180, 40, 255})); - EXPECT_EQ(constellation->get<&Constellation_Diagram::Properties::point_lifetime_ms>(), 2000); - constellation->append_point({0.5, -0.5}); - constellation->append_point({-0.5, 0.5}); - EXPECT_EQ(constellation->point_count(), 2U); - constellation->fit_square_to_axes(); - EXPECT_EQ(horizontal->get<&Axis_Properties::coordinates>(), (Range{-3.0, 3.0})); - EXPECT_EQ(vertical->get<&Axis_Properties::coordinates>(), (Range{3.0, -3.0})); - EXPECT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); -} -TEST(Renderive_Core2, PlottablePropertiesPublishOnlyAtFrameBoundary) { - Render_Scene_2D plot; - plot.init(); - const auto root = plot.root_renderable(); - const auto [frequency, power, spectrum] = build_initial(plot, [&](auto& attach) { - auto frequency_axis = Frequency_Axis::Builder{&attach} - .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .build(root); - auto power_axis = Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .build(root); - auto value = Spectrum::Builder{&attach}.build(root, frequency_axis, power_axis); - return std::tuple{frequency_axis, power_axis, value}; - }); - ASSERT_TRUE(spectrum); - EXPECT_EQ(spectrum->observation().event, - Renderable_Observer_Event::None); - spectrum->set<&Spectrum::Properties::frequency_point_size>(128); - EXPECT_EQ(spectrum->get<&Spectrum::Properties::frequency_point_size>(), 128); - const auto cached = spectrum->observation(); - EXPECT_EQ(cached.event, Renderable_Observer_Event::Cache_Updated); - EXPECT_EQ(cached.cache_update_count, 1U); - EXPECT_EQ(cached.publish_count, 0U); - ASSERT_TRUE(plot.prepare_frame()); - const auto published = spectrum->observation(); - EXPECT_EQ(published.event, Renderable_Observer_Event::Published); - EXPECT_EQ(published.cache_update_count, 1U); - EXPECT_EQ(published.publish_count, 1U); - ASSERT_TRUE(plot.render_prepared_frame()); -} -TEST(Renderive_Core2, EveryStatefulRenderableOwnsItsStateObserver) { - Render_Scene_2D plot; - plot.init(); - const auto root = plot.root_renderable(); - const auto [frequency, power, spectrum] = build_initial(plot, [&](auto& attach) { - auto frequency_axis = Frequency_Axis::Builder{&attach} - .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .build(root); - auto power_axis = Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .build(root); - auto value = Spectrum::Builder{&attach}.build(root, frequency_axis, power_axis); - return std::tuple{frequency_axis, power_axis, value}; - }); - ASSERT_TRUE(frequency); - ASSERT_TRUE(power); - ASSERT_TRUE(spectrum); - - EXPECT_EQ(frequency->observation().event, Renderable_Observer_Event::None); - EXPECT_EQ(power->observation().event, Renderable_Observer_Event::None); - EXPECT_EQ(spectrum->observation().event, Renderable_Observer_Event::None); - - frequency->set<&Axis_Properties::coordinates>(Range{88'000'000.0, 108'000'000.0}); - spectrum->set<&Spectrum::Properties::partition_count>(2); - const auto frequency_update = frequency->observation(); - const auto spectrum_update = spectrum->observation(); - EXPECT_EQ(frequency_update.event, Renderable_Observer_Event::Cache_Updated); - EXPECT_EQ(frequency_update.cache_update_count, 1U); - EXPECT_EQ(spectrum_update.event, Renderable_Observer_Event::Cache_Updated); - EXPECT_EQ(spectrum_update.cache_update_count, 1U); - EXPECT_EQ(power->observation().event, Renderable_Observer_Event::None); - - ASSERT_TRUE(plot.prepare_frame()); - const auto frequency_publish = frequency->observation(); - const auto power_publish = power->observation(); - const auto spectrum_publish = spectrum->observation(); - EXPECT_EQ(frequency_publish.event, Renderable_Observer_Event::Published); - EXPECT_EQ(power_publish.event, Renderable_Observer_Event::Published); - EXPECT_EQ(spectrum_publish.event, Renderable_Observer_Event::Published); - EXPECT_EQ(frequency_publish.publish_count, 1U); - EXPECT_EQ(power_publish.publish_count, 1U); - EXPECT_EQ(spectrum_publish.publish_count, 1U); - EXPECT_EQ(frequency_publish.cache_update_count, 1U); - EXPECT_EQ(power_publish.cache_update_count, 0U); - EXPECT_EQ(spectrum_publish.cache_update_count, 1U); - - ASSERT_TRUE(plot.render_prepared_frame()); - EXPECT_EQ(frequency->observation().event, Renderable_Observer_Event::Published); - EXPECT_EQ(power->observation().event, Renderable_Observer_Event::Published); - EXPECT_EQ(spectrum->observation().event, Renderable_Observer_Event::Published); -} -TEST(Renderive_Core2, PlottableDataUpdatesReachKernelRealTimeDataStrategy) { - Render_Scene_2D plot; - plot.init(); - const auto root = plot.root_renderable(); - const auto [frequency, power, spectrum] = build_initial(plot, [&](auto& attach) { - auto frequency_axis = Frequency_Axis::Builder{&attach} - .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .build(root); - auto power_axis = Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .build(root); - auto value = Spectrum::Builder{&attach}.build(root, frequency_axis, power_axis); - return std::tuple{frequency_axis, power_axis, value}; - }); - const auto afterglow = build_initial(plot, [&](auto& attach) { - return Afterglow::Builder{&attach}.build(root, frequency, power); - }); - ASSERT_TRUE(spectrum); - ASSERT_TRUE(afterglow); - const auto before = plot.frame_status().observation_count; - const std::array samples{-80.0, -70.0, -60.0, -50.0}; - spectrum->update_samples(samples); - const auto latest = plot.frame_status(); - EXPECT_EQ(latest.last_event, "real_time_data_updated"); - EXPECT_GT(latest.observation_count, before); - afterglow->append_spectrum(samples); - const auto history = plot.frame_status(); - EXPECT_EQ(history.last_event, "real_time_data_updated"); - EXPECT_GT(history.observation_count, latest.observation_count); -} -TEST(Renderive_Core2, AxisRasterLayoutCoversAxisSwapAndEveryReversal) { - constexpr Range first_source{0.0, 2.0}; - constexpr Range second_source{0.0, 3.0}; - for (const bool swapped : {false, true}) { - for (const bool first_reversed : {false, true}) { - for (const bool second_reversed : {false, true}) { - const Axis_Transform first{ - first_reversed ? Range{2.0, 0.0} : Range{0.0, 2.0}, - swapped ? 20.0 : 10.0, swapped ? 30.0 : 20.0, - swapped ? Orientation::Vertical : Orientation::Horizontal}; - const Axis_Transform second{ - second_reversed ? Range{3.0, 0.0} : Range{0.0, 3.0}, - swapped ? 10.0 : 20.0, swapped ? 20.0 : 30.0, - swapped ? Orientation::Horizontal : Orientation::Vertical}; - const auto layout = detail::axis_raster_layout( - first, second, first_source, second_source, 3, 4); - ASSERT_TRUE(layout.valid()); - EXPECT_EQ(layout.width, swapped ? 4 : 3); - EXPECT_EQ(layout.height, swapped ? 3 : 4); - const std::size_t origin = layout.index(0, 0, 3, 4); - const int x = static_cast(origin % layout.width); - const int y = static_cast(origin / layout.width); - EXPECT_EQ(x, swapped ? (second_reversed ? 3 : 0) - : (first_reversed ? 2 : 0)); - EXPECT_EQ(y, swapped ? (first_reversed ? 2 : 0) - : (second_reversed ? 3 : 0)); - } - } - } - - const std::array values{1.0, 3.0}; - const Axis_Transform vertical_frequency{{0.0, 1.0}, 10.0, 20.0, - Orientation::Vertical}; - const Axis_Transform horizontal_power{{0.0, 4.0}, 100.0, 40.0, - Orientation::Horizontal}; - const auto points = detail::curve_points( - values, {0.0, 1.0}, vertical_frequency, horizontal_power, false, - Line_Interpolation_Mode::Linear_Value); - ASSERT_EQ(points.size(), 2u); - EXPECT_DOUBLE_EQ(points.front().x, 110.0); - EXPECT_DOUBLE_EQ(points.front().y, 10.0); - EXPECT_DOUBLE_EQ(points.back().x, 130.0); - EXPECT_DOUBLE_EQ(points.back().y, 30.0); -} -TEST(Renderive_Core2, PartitionedPlotsBuildPropertyDrivenRenderGraphs) { - Render_Scene_2D plot; - plot.init(); - plot.set_viewport_size({320, 180}); - const auto root = plot.root_renderable(); - const auto [frequency, power, time, spectrum, waterfall, afterglow] = build_initial(plot, [&](auto& attach) { - auto frequency_axis = Frequency_Axis::Builder{&attach} - .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Frequency_Axis::Properties::pixel_length>(320) - .set<&Frequency_Axis::Properties::coordinates>(Range{0.0, 10.0}) - .build(root); - auto power_axis = Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .set<&Axis::Properties::pixel_length>(180) - .set<&Axis::Properties::coordinates>(Range{-120.0, 0.0}) - .build(root); - auto time_axis = Time_Axis::Builder{&attach} - .set<&Time_Axis::Properties::orientation>(Orientation::Vertical) - .set<&Time_Axis::Properties::pixel_length>(180) - .build(root); - auto spectrum_value = Spectrum::Builder{&attach} - .set<&Spectrum::Properties::partition_mode>(Render_Partition_Mode::Fixed) - .set<&Spectrum::Properties::partition_count>(4) - .build(root, frequency_axis, power_axis); - auto waterfall_value = Waterfall::Builder{&attach} - .set<&Waterfall::Properties::partition_mode>(Render_Partition_Mode::Fixed) - .set<&Waterfall::Properties::partition_count>(4) - .build(root, frequency_axis, time_axis); - auto afterglow_value = Afterglow::Builder{&attach} - .set<&Afterglow::Properties::partition_mode>(Render_Partition_Mode::Fixed) - .set<&Afterglow::Properties::partition_count>(4) - .build(root, frequency_axis, power_axis); - return std::tuple{frequency_axis, power_axis, time_axis, spectrum_value, waterfall_value, afterglow_value}; - }); - std::array samples{}; - spectrum->update_samples(samples); - waterfall->append_row(0, samples); - afterglow->append_spectrum(samples); - plot.render_scene().publish_frame_state(); - - const auto planned_node_count = [](const Render_Plan& plan, - const Renderable_Base& renderable) { - return std::count_if(plan.graph.nodes.begin(), plan.graph.nodes.end(), - [&](const Render_Node& node) { - return node.owner_id == renderable.renderable_id() && - node.kind != Render_Node_Kind::composite; - }); - }; - - EXPECT_EQ(spectrum->get<&Spectrum::Properties::partition_count>(), 4); - EXPECT_EQ(waterfall->get<&Waterfall::Properties::partition_count>(), 4); - EXPECT_EQ(afterglow->get<&Afterglow::Properties::partition_count>(), 4); - ASSERT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); - const auto partitioned_plan = plot.render_scene().render_plan_snapshot(); - ASSERT_TRUE(partitioned_plan); - EXPECT_EQ(planned_node_count(*partitioned_plan, *spectrum), 6u); - EXPECT_EQ(std::count_if( - partitioned_plan->graph.nodes.begin(), - partitioned_plan->graph.nodes.end(), - [&](const Render_Node& node) { - return node.owner_id == spectrum->renderable_id() && - node.kind == Render_Node_Kind::paint; - }), - 1); - EXPECT_EQ(planned_node_count(*partitioned_plan, *waterfall), 6u); - EXPECT_EQ(planned_node_count(*partitioned_plan, *afterglow), 11u); - - spectrum->set<&Spectrum::Properties::partition_count>(1); - waterfall->set<&Waterfall::Properties::partition_count>(1); - afterglow->set<&Afterglow::Properties::partition_count>(1); - plot.render_scene().publish_frame_state(); - ASSERT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); - const auto single_plan = plot.render_scene().render_plan_snapshot(); - ASSERT_TRUE(single_plan); - EXPECT_EQ(planned_node_count(*single_plan, *spectrum), 3u); - EXPECT_EQ(planned_node_count(*single_plan, *waterfall), 3u); - EXPECT_EQ(planned_node_count(*single_plan, *afterglow), 5u); - - spectrum->set<&Spectrum::Properties::partition_mode>(Render_Partition_Mode::Automatic); - waterfall->set<&Waterfall::Properties::partition_mode>(Render_Partition_Mode::Automatic); - afterglow->set<&Afterglow::Properties::partition_mode>(Render_Partition_Mode::Automatic); - plot.render_scene().publish_frame_state(); - ASSERT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); - const auto automatic_plan = plot.render_scene().render_plan_snapshot(); - ASSERT_TRUE(automatic_plan); - EXPECT_EQ(planned_node_count(*automatic_plan, *spectrum), 3u); - EXPECT_EQ(planned_node_count(*automatic_plan, *waterfall), 3u); - EXPECT_EQ(planned_node_count(*automatic_plan, *afterglow), 5u); +TEST(RenderScene2DIntegration, BuilderStatePublishesAtFrameBoundary) { + Render_Scene_2D::State initial; + initial.viewport = {64, 32}; + auto scene = Render_Scene_2D::Builder{initial}.build( + std::make_shared()); + scene->activate_view(); + ASSERT_EQ(scene->render_frame(true).value(), Plot_Render_Status::rendered); + ASSERT_EQ(scene->wait_for_render(), Scene_Render_Result::none); + EXPECT_EQ(scene->observer().state, initial); } -TEST(Renderive_Core2, AutomaticPartitioningSplitsOnlyForMeasuredBottlenecks) { - detail::Adaptive_Render_Partitioner partitioner; - EXPECT_EQ(partitioner.graph_partition_count( - Render_Partition_Mode::Automatic, 8, 8, 4096, 128), - 1); - EXPECT_EQ(partitioner.begin(1, 4096), 1); - std::this_thread::sleep_for(std::chrono::milliseconds(2)); - EXPECT_TRUE(partitioner.finish(Render_Partition_Mode::Automatic, 1, 0, - 8, 4096, 128)); - EXPECT_EQ(partitioner.graph_partition_count( - Render_Partition_Mode::Automatic, 8, 8, 4096, 128), - 2); - - EXPECT_EQ(partitioner.begin(2, 4096), 2); - EXPECT_TRUE(partitioner.finish(Render_Partition_Mode::Automatic, 2, 0, - 8, 4096, 128)); - EXPECT_EQ(partitioner.graph_partition_count( - Render_Partition_Mode::Automatic, 8, 8, 4096, 128), - 1); - EXPECT_EQ(partitioner.graph_partition_count( - Render_Partition_Mode::Fixed, 7, 2, 1, 4096), - 1); - EXPECT_EQ(partitioner.graph_partition_count( - Render_Partition_Mode::Fixed, 7, 2, 0, 4096), - 0); - EXPECT_EQ(partitioner.begin(7, 0), 0); +TEST(RenderScene2DIntegration, FrameImageMatchesConfiguredViewport) { + Render_Scene_2D::State state; + state.viewport = {48, 27}; + auto scene = Render_Scene_2D::Builder{state}.build( + std::make_shared()); + scene->activate_view(); + ASSERT_EQ(scene->render_frame(true).value(), Plot_Render_Status::rendered); + ASSERT_EQ(scene->wait_for_render(), Scene_Render_Result::none); + scene->with_frame([](Image_View image) { + EXPECT_EQ(image.width, 48); + EXPECT_EQ(image.height, 27); + EXPECT_NE(image.data, nullptr); + }); } -TEST(Renderive_Core2, DynamicWaterfallCaptureStressPreservesPlansSlotsAndExactSessions) { - Render_Scene_2D plot; - plot.init(); - plot.set_viewport_size({640, 360}); - const auto root = plot.root_renderable(); - const auto [frequency, time, waterfall] = build_initial(plot, [&](auto& attach) { - auto frequency_axis = Frequency_Axis::Builder{&attach} - .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Frequency_Axis::Properties::pixel_length>(640) - .set<&Frequency_Axis::Properties::coordinates>(Range{0.0, 32'767.0}) - .build(root); - auto time_axis = Time_Axis::Builder{&attach} - .set<&Time_Axis::Properties::orientation>(Orientation::Vertical) - .set<&Time_Axis::Properties::pixel_length>(360) - .build(root); - auto value = Waterfall::Builder{&attach} - .set<&Waterfall::Properties::partition_mode>(Render_Partition_Mode::Fixed) - .set<&Waterfall::Properties::partition_count>(64) - .set<&Waterfall::Properties::frequency_range>(Range{0.0, 32'767.0}) - .build(root, frequency_axis, time_axis); - return std::tuple{frequency_axis, time_axis, value}; - }); - std::array samples{}; - for (std::size_t index = 0; index < samples.size(); ++index) - samples[index] = static_cast(index % 256); - waterfall->append_row(0, samples); - auto& scene = plot; - constexpr std::array partition_counts{64, 16, 1}; - std::uint64_t tick{1}; - for (std::size_t round = 0; round < 3; ++round) { - const auto request = scene.render_scene().capture_frames(8); - ASSERT_TRUE(request); - const Capture_Session_Id session_id = request.session_id; - for (std::size_t frame = 0; frame < 8; ++frame) { - if (frame % 2 == 0) { - waterfall->set<&Waterfall::Properties::partition_count>( - partition_counts[(round + frame / 2) % partition_counts.size()]); - } - if (frame % 4 == 0) - waterfall->append_row(static_cast(tick++), samples); - ASSERT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); - } - ASSERT_EQ(scene.render_scene().wait_for_render(), Scene_Render_Result::none); - const auto session = scene.render_scene().capture_session(session_id); - ASSERT_TRUE(session); - ASSERT_EQ(session->captured_count(), 8u); - EXPECT_FALSE(session->active()); - std::set versions; - for (const auto& captured : session->frames) { - ASSERT_TRUE(captured.snapshot); - const auto plan = scene.render_scene().find_render_plan( - captured.snapshot->render_plan_version); - ASSERT_TRUE(plan); - EXPECT_EQ(captured.snapshot->node_executions.size(), - plan->graph.nodes.size()); - for (const auto& execution : captured.snapshot->node_executions) - EXPECT_EQ(execution.status, Node_Execution_Status::complete); - versions.insert(captured.snapshot->render_plan_version); - } - EXPECT_GE(versions.size(), 3u); - EXPECT_FALSE(scene.render_scene().capture_state().enabled()); - } -} - -TEST(Renderive_Core2, PaintOnlyStyleChangesPreservePrepareCache) { - Render_Scene_2D plot; - plot.init(); - plot.set_viewport_size({320, 180}); - const auto root = plot.root_renderable(); - const auto [frequency, power, spectrum] = build_initial(plot, [&](auto& attach) { - auto frequency_axis = Frequency_Axis::Builder{&attach} - .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .build(root); - auto power_axis = Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .build(root); - auto value = Spectrum::Builder{&attach}.build(root, frequency_axis, power_axis); - return std::tuple{frequency_axis, power_axis, value}; - }); - const std::array samples{-80.0, -70.0, -60.0, -50.0}; - spectrum->update_samples(samples); - ASSERT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); - ASSERT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); - ASSERT_EQ(plot.render_scene().wait_for_render(), Scene_Render_Result::none); - const auto cached_plan = plot.render_scene().render_plan_snapshot(); - ASSERT_TRUE(cached_plan); - const auto cached_has_kind = [&](Render_Node_Kind kind) { - return std::any_of(cached_plan->graph.nodes.begin(), - cached_plan->graph.nodes.end(), - [&](const Render_Node& node) { - return node.owner_id == spectrum->renderable_id() && - node.kind == kind; - }); - }; - EXPECT_TRUE(cached_has_kind(Render_Node_Kind::prepare)); - EXPECT_TRUE(cached_has_kind(Render_Node_Kind::paint)); - spectrum->set<&Spectrum::Properties::current_pen>( - Pen{Color{12, 34, 56, 255}, 2.0}); - - ASSERT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); - ASSERT_EQ(plot.render_scene().wait_for_render(), Scene_Render_Result::none); - const auto plan = plot.render_scene().render_plan_snapshot(); - ASSERT_TRUE(plan); - EXPECT_EQ(plan, cached_plan); - const auto has_kind = [&](Render_Node_Kind kind) { - return std::any_of(plan->graph.nodes.begin(), plan->graph.nodes.end(), - [&](const Render_Node& node) { - return node.owner_id == spectrum->renderable_id() && node.kind == kind; - }); - }; - EXPECT_TRUE(has_kind(Render_Node_Kind::prepare)); - EXPECT_TRUE(has_kind(Render_Node_Kind::paint)); - - frequency->set<&Axis_Base_Properties::color>(Color{90, 100, 110, 255}); - ASSERT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered); - ASSERT_EQ(plot.render_scene().wait_for_render(), Scene_Render_Result::none); - const auto axis_style_plan = plot.render_scene().render_plan_snapshot(); - ASSERT_TRUE(axis_style_plan); - EXPECT_EQ(axis_style_plan, cached_plan); - EXPECT_TRUE(std::any_of(axis_style_plan->graph.nodes.begin(), - axis_style_plan->graph.nodes.end(), - [&](const Render_Node& node) { - return node.owner_id == spectrum->renderable_id() && - node.kind == Render_Node_Kind::prepare; - })); -} -TEST(Renderive_Core2, PlottableAxesAreDataDependenciesAndPaintOverlays) { - Render_Scene_2D plot; - plot.init(); - const auto root = plot.root_renderable(); - const auto [frequency, power, spectrum] = build_initial(plot, [&](auto& attach) { - auto frequency_axis = Frequency_Axis::Builder{&attach} - .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .build(root); - auto power_axis = Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .build(root); - auto value = Spectrum::Builder{&attach}.build(root, frequency_axis, power_axis); - return std::tuple{frequency_axis, power_axis, value}; - }); - ASSERT_TRUE(spectrum); - const auto topology = plot.render_scene().topology_snapshot(); - const auto has_relationship = [](const auto& relationships, const auto* child, const auto* parent) { - return std::any_of(relationships.begin(), relationships.end(), [child, parent](const auto& relationship) { - return relationship.child.get() == child && relationship.parent.get() == parent; - }); - }; - EXPECT_TRUE(has_relationship(topology.dependency, spectrum.get(), frequency.get())); - EXPECT_TRUE(has_relationship(topology.dependency, spectrum.get(), power.get())); - EXPECT_TRUE(has_relationship(topology.display, frequency.get(), spectrum.get())); - EXPECT_TRUE(has_relationship(topology.display, power.get(), spectrum.get())); - const auto paint_order = plot.render_scene().paint_order_snapshot(); - const auto spectrum_position = std::find_if(paint_order.begin(), paint_order.end(), [&spectrum](const auto& renderable) { - return renderable.get() == spectrum.get(); - }); - const auto frequency_position = std::find_if(paint_order.begin(), paint_order.end(), [&frequency](const auto& renderable) { - return renderable.get() == frequency.get(); - }); - const auto power_position = std::find_if(paint_order.begin(), paint_order.end(), [&power](const auto& renderable) { - return renderable.get() == power.get(); - }); - ASSERT_NE(spectrum_position, paint_order.end()); - ASSERT_NE(frequency_position, paint_order.end()); - ASSERT_NE(power_position, paint_order.end()); - EXPECT_LT(spectrum_position, frequency_position); - EXPECT_LT(spectrum_position, power_position); -} -TEST(Renderive_Core2, InteractionOverlayPaintsAboveItsAxes) { - Render_Scene_2D plot; - plot.init(); - const auto root = plot.root_renderable(); - const auto [horizontal, vertical, selection] = build_initial(plot, [&](auto& attach) { - auto horizontal_axis = Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Horizontal) - .build(root); - auto vertical_axis = Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .build(root); - auto value = Selection_Rectangle_Overlay::Builder{&attach}.build(root, horizontal_axis, vertical_axis); - return std::tuple{horizontal_axis, vertical_axis, value}; - }); - ASSERT_TRUE(selection); - const auto paint_order = plot.render_scene().paint_order_snapshot(); - const auto position = [&paint_order](const auto* target) { - return std::find_if(paint_order.begin(), paint_order.end(), [target](const auto& renderable) { - return renderable.get() == target; - }); - }; - const auto selection_position = position(selection.get()); - const auto horizontal_position = position(horizontal.get()); - const auto vertical_position = position(vertical.get()); - ASSERT_NE(selection_position, paint_order.end()); - ASSERT_NE(horizontal_position, paint_order.end()); - ASSERT_NE(vertical_position, paint_order.end()); - EXPECT_LT(horizontal_position, selection_position); - EXPECT_LT(vertical_position, selection_position); -} } // namespace } // namespace renderive diff --git a/render_3D/render_3D/Point_Visual.cpp b/render_3D/render_3D/Point_Visual.cpp index b2ad31f..bfb5917 100644 --- a/render_3D/render_3D/Point_Visual.cpp +++ b/render_3D/render_3D/Point_Visual.cpp @@ -60,7 +60,7 @@ struct Point_Visual::Impl : Base::next_Impl, Point_Buffer_Strategy { } [[nodiscard]] Visual_Data_Error edit_points(const std::function&)>& edit) { if (!edit) return Visual_Data_Error::empty_edit; - const auto& cached = cache_buffer_value(); + const auto cached = latest_snapshot(); auto next = cached ? *cached : std::vector{}; edit(next); return update_points(std::move(next)); @@ -68,8 +68,8 @@ struct Point_Visual::Impl : Base::next_Impl, Point_Buffer_Strategy { [[nodiscard]] const Point_Payload& points() const noexcept { return render_buffer_value(); } - [[nodiscard]] std::size_t point_count() const noexcept { - const auto& cached = cache_buffer_value(); + [[nodiscard]] std::size_t point_count() const { + const auto cached = latest_snapshot(); return cached ? cached->size() : 0U; } [[nodiscard]] std::uint64_t dependent_state_revision() const noexcept { diff --git a/render_3D/render_3D/visuals/Basic_Visual.h b/render_3D/render_3D/visuals/Basic_Visual.h index 466640e..35064a8 100644 --- a/render_3D/render_3D/visuals/Basic_Visual.h +++ b/render_3D/render_3D/visuals/Basic_Visual.h @@ -148,15 +148,15 @@ public: } [[nodiscard]] Visual_Data_Error edit_items(const std::function&)>& edit) { if (!edit) return Visual_Data_Error::empty_edit; - const auto& cached = this->template d_func() - .template cache_buffer_value>(); + const auto cached = this->template d_func() + .template latest_snapshot>(); auto next = cached ? *cached : std::vector{}; edit(next); return update_items(std::move(next)); } - [[nodiscard]] std::size_t item_count() const noexcept { - const auto& cached = this->template d_func() - .template cache_buffer_value>(); + [[nodiscard]] std::size_t item_count() const { + const auto cached = this->template d_func() + .template latest_snapshot>(); return cached ? cached->size() : 0U; } [[nodiscard]] std::uint64_t data_revision() const noexcept { diff --git a/render_3D/tests/Datoviz_Visual_Family_Tests.cpp b/render_3D/tests/Datoviz_Visual_Family_Tests.cpp index e789f00..4d15fae 100644 --- a/render_3D/tests/Datoviz_Visual_Family_Tests.cpp +++ b/render_3D/tests/Datoviz_Visual_Family_Tests.cpp @@ -1,89 +1,22 @@ #include "render_3D/Datoviz_Visuals.h" -#include "render_3D/Scene_Context.hpp" -#include #include -#include -#include -#include +#include + namespace renderive::render_3d { namespace { -template -struct Visual_Frame_Test_Scene final : Scene3D_Context<>, Scene_Renderer { - using Frame_Data = typename Visual::Frame_Data; - explicit Visual_Frame_Test_Scene( - const std::shared_ptr& visual) : owner(visual->renderable_id()) { - auto builder = attach_builder(); - builder.attach(renderive_Owner(visual)); - } - ~Visual_Frame_Test_Scene() override { - shutdown(); - } - void render_frame() { - publish_frame_state(); - render(); - wait_for_render(); - } - Node_Execution_Result render_scene( - const Scene_Render_Context& context) override { - prepared = context.template prepared(owner); - if (!prepared) - throw std::logic_error( - "Datoviz visual did not publish frame-local data"); - return Node_Execution_Result::completed(); - } - Renderable_Id owner{}; - std::shared_ptr prepared; -}; + template void exercise(Item item) { - static_assert(std::derived_from); auto visual = typename Visual::Builder{}.build(std::vector{item}); - ASSERT_NE(visual, nullptr); + ASSERT_TRUE(visual); EXPECT_EQ(visual->item_count(), 1U); - EXPECT_EQ(visual->observation().event, - Renderable_Observer_Event::Data_Updated); - EXPECT_EQ(visual->observation().data_revision, - visual->data_revision()); - const auto initial_revision = visual->data_revision(); - visual->edit_items([](auto& items) { - items.push_back(items.front()); - }); + ASSERT_EQ(visual->edit_items( + [](auto& items) { items.push_back(items.front()); }), + Visual_Data_Error::none); EXPECT_EQ(visual->item_count(), 2U); - EXPECT_GT(visual->data_revision(), initial_revision); - EXPECT_EQ(visual->observation().event, - Renderable_Observer_Event::Data_Updated); - EXPECT_EQ(visual->observation().item_count, 2U); - EXPECT_EQ(visual->observation().data_revision, - visual->data_revision()); - Visual_Frame_Test_Scene scene(visual); - scene.render_frame(); - ASSERT_TRUE(scene.prepared); - ASSERT_TRUE(scene.prepared->items); - EXPECT_EQ(scene.prepared->items->size(), 2U); - EXPECT_EQ(visual->observation().event, - Renderable_Observer_Event::Published); - const auto first_frame = scene.prepared; - const auto first_published_revision = first_frame->data_revision; - const auto revision = visual->data_revision(); - visual->update_items(std::vector{item, item, item}); - EXPECT_EQ(visual->item_count(), 3U); - EXPECT_GT(visual->data_revision(), revision); - EXPECT_EQ(visual->observation().event, - Renderable_Observer_Event::Data_Updated); - EXPECT_EQ(visual->observation().item_count, 3U); - EXPECT_EQ(visual->observation().data_revision, - visual->data_revision()); - ASSERT_TRUE(first_frame->items); - EXPECT_EQ(first_frame->items->size(), 2U); - scene.render_frame(); - ASSERT_TRUE(scene.prepared); - ASSERT_TRUE(scene.prepared->items); - EXPECT_EQ(scene.prepared->items->size(), 3U); - EXPECT_GT(scene.prepared->data_revision, first_published_revision); - EXPECT_EQ(visual->observation().event, - Renderable_Observer_Event::Published); } -TEST(DatovizVisualFamilies, EveryPublicFamilyUsesRenderableStorage) { + +TEST(DatovizVisualFamilies, EveryPublicFamilyUsesBuilderOwnedStorage) { exercise(Splat{}); exercise(Pixel{}); exercise(Marker{}); @@ -99,18 +32,20 @@ TEST(DatovizVisualFamilies, EveryPublicFamilyUsesRenderableStorage) { exercise(Text_Label{.text = "Renderive"}); exercise(Voxel{}); } -TEST(DatovizVisualFamilies, StateValidationAndBuilderPropertiesAreShared) { + +TEST(DatovizVisualFamilies, BuilderValidationUsesStateProperties) { auto volume = Volume_Visual::Builder{} - .set<&Volume_State::field_width>(4U) - .set<&Volume_State::field_height>(4U) - .set<&Volume_State::field_depth>(4U) - .build(std::vector(64)); - ASSERT_NE(volume, nullptr); + .set<&Volume_State::field_width>(4U) + .set<&Volume_State::field_height>(4U) + .set<&Volume_State::field_depth>(4U) + .build(std::vector(64)); + ASSERT_TRUE(volume); EXPECT_EQ(volume->get<&Volume_State::field_depth>(), 4U); EXPECT_THROW((Volume_Visual::Builder{} .set<&Volume_State::field_width>(4U) .build()), std::invalid_argument); } + } // namespace } // namespace renderive::render_3d diff --git a/render_3D/tests/Point_Render_Integration_Tests.cpp b/render_3D/tests/Point_Render_Integration_Tests.cpp index 5b14715..2b53621 100644 --- a/render_3D/tests/Point_Render_Integration_Tests.cpp +++ b/render_3D/tests/Point_Render_Integration_Tests.cpp @@ -1,195 +1,54 @@ #include "render_3D/Render_Scene_3D.h" -#include #include -#include -#include -#include -#include -#include #include -#include +#include + namespace renderive::render_3d { namespace { -struct Test_Point_Scene { - std::shared_ptr visual; - std::unique_ptr scene; -}; -Test_Point_Scene make_test_scene(Scene_Options options) { - std::vector points{ - {{-0.72F, -0.38F, 0.15F}, {245, 70, 78, 255}, 42.0F}, - {{0.00F, 0.48F, -0.05F}, {65, 220, 132, 255}, 48.0F}, - {{0.72F, 0.0F, 0.30F}, {75, 135, 255, 255}, 54.0F}, - {{-0.35F, 0.10F, -0.45F}, {250, 205, 75, 255}, 30.0F}, - {{0.38F, -0.12F, -0.25F}, {205, 95, 245, 255}, 34.0F}, - }; - Point_State state; - state.style = {{12, 16, 24, 255}, 2.0F, Point_Aspect::Outline}; - auto visual = Point_Visual::Builder{state}.build(std::move(points)); - auto scene = std::make_unique(options, visual); - return {std::move(visual), std::move(scene)}; + +std::unique_ptr make_scene( + Render_Scene_3D::State state, + const std::shared_ptr& visual) { + return Render_Scene_3D::Builder{state}.build( + visual, std::make_shared()); } -struct Event_Point { - float x{}; - float y{}; -}; -std::size_t colored_pixel_count(const Pixel_Frame& frame) { - if (frame.rgba8.size() < 4) return 0; - const auto* bytes = reinterpret_cast(frame.rgba8.data()); - const std::array background{ - bytes[0], bytes[1], bytes[2], bytes[3] - }; - std::size_t result{}; - for (std::size_t offset = 0; offset + 3 < frame.rgba8.size(); offset += 4) { - result += bytes[offset] != background[0] || - bytes[offset + 1] != background[1] || - bytes[offset + 2] != background[2] || - bytes[offset + 3] != background[3]; - } - return result; -} -TEST(PointRenderIntegration, RendersRealRgbaAndResizes) { + +TEST(PointRenderIntegration, BuilderConstructsSceneFromStateAndStrategy) { + auto visual = Point_Visual::Builder{}.build(std::vector{ + {{0.0F, 0.0F, 0.0F}, {255, 0, 0, 255}, 8.0F}}); + Render_Scene_3D::State state; + state.viewport = {320, 200}; try { - auto demo = make_test_scene(Scene_Options{.viewport = {320, 200}}); - ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Result::none); - ASSERT_EQ(demo.scene->render_scene().wait_for_render(), - Scene_Render_Result::none); - auto first = demo.scene->latest_frame(); - ASSERT_NE(first, nullptr); - EXPECT_EQ(first->extent, (Extent{320, 200})); - EXPECT_EQ(first->rgba8.size(), 320U * 200U * 4U); - EXPECT_GT(colored_pixel_count(*first), 100U); - demo.scene->resize({480, 270}); - ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Result::none); - ASSERT_EQ(demo.scene->render_scene().wait_for_render(), - Scene_Render_Result::none); - auto resized = demo.scene->latest_frame(); - ASSERT_NE(resized, nullptr); - EXPECT_EQ(resized->extent, (Extent{480, 270})); - EXPECT_EQ(resized->rgba8.size(), 480U * 270U * 4U); - EXPECT_GT(colored_pixel_count(*resized), 100U); + auto scene = make_scene(state, visual); + ASSERT_TRUE(scene); + EXPECT_EQ(scene->renderable_count(), 1U); } catch (const std::exception& error) { GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what(); } } -TEST(PointRenderIntegration, KernelEventsReachDatovizArcballOnItsDomain) { + +TEST(PointRenderIntegration, ObserverPublishesSceneAndVisualState) { + std::vector points(3); + for (auto& point : points) point.diameter_px = 1.0F; + auto visual = Point_Visual::Builder{}.build(std::move(points)); + Render_Scene_3D::State state; + state.viewport = {160, 90}; try { - auto demo = make_test_scene(Scene_Options{.viewport = {320, 200}}); - ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Result::none); - ASSERT_EQ(demo.scene->render_scene().wait_for_render(), - Scene_Render_Result::none); - const auto before = demo.scene->latest_frame()->rgba8; - ::renderive::Basic_Pointer_Event press( - ::renderive::Event_Type::Pointer_Press); - press.position = {120.0F, 90.0F}; - press.button = ::renderive::Mouse_Button::Left; - demo.scene->dispatch(press); - ::renderive::Basic_Pointer_Event move( - ::renderive::Event_Type::Pointer_Move); - move.position = {210.0F, 125.0F}; - move.buttons = 1; - demo.scene->dispatch(move); - move.position = {245.0F, 145.0F}; - demo.scene->dispatch(move); - ::renderive::Basic_Pointer_Event release( - ::renderive::Event_Type::Pointer_Release); - release.position = move.position; - release.button = ::renderive::Mouse_Button::Left; - demo.scene->dispatch(release); - ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Result::none); - ASSERT_EQ(demo.scene->render_scene().wait_for_render(), - Scene_Render_Result::none); - const auto after_drag = demo.scene->latest_frame(); - ASSERT_NE(after_drag, nullptr); - EXPECT_NE(after_drag->rgba8, before); - ::renderive::Basic_Wheel_Event wheel; - wheel.position = {160.0F, 100.0F}; - wheel.pixel_delta_y = 180.0; - wheel.angle_delta_y = -216.0; - demo.scene->dispatch(wheel); - ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Result::none); - ASSERT_EQ(demo.scene->render_scene().wait_for_render(), - Scene_Render_Result::none); - const auto after_wheel = demo.scene->latest_frame(); - ASSERT_NE(after_wheel, nullptr); - EXPECT_GT(colored_pixel_count(*after_wheel), 100U); - EXPECT_NE(after_wheel->rgba8, after_drag->rgba8); - } - catch (const std::exception& error) { - GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what(); - } -} -TEST(PointRenderIntegration, - CapturesExternalGpuLifetimeAndSeparatedBackendPhases) { - try { - auto demo = make_test_scene(Scene_Options{.viewport = {320, 200}}); - auto& kernel_scene = demo.scene->render_scene(); - const auto request = kernel_scene.capture_next_frame(); - ASSERT_TRUE(request); - const Capture_Session_Id capture = request.session_id; - ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Result::none); - ASSERT_EQ(kernel_scene.wait_for_render(), Scene_Render_Result::none); - const auto session = kernel_scene.capture_session(capture); - ASSERT_TRUE(session); - ASSERT_EQ(session->captured_count(), 1U); - const auto& snapshot = *session->frames.front().snapshot; - const auto plan = kernel_scene.find_render_plan( - snapshot.render_plan_version); - ASSERT_TRUE(plan); - const auto render_node = std::find_if( - plan->graph.nodes.begin(), plan->graph.nodes.end(), - [](const Render_Node& node) { - return node.kind == Render_Node_Kind::render; - }); - ASSERT_NE(render_node, plan->graph.nodes.end()); - const auto& execution = snapshot.node_executions.at( - render_node->execution_index); - EXPECT_EQ(execution.status, Node_Execution_Status::complete); - EXPECT_GT(execution.external_start_time_ns, 0U); - EXPECT_GE(execution.external_end_time_ns, - execution.external_start_time_ns); - EXPECT_GE(execution.duration_ns(), execution.cpu_duration_ns()); - EXPECT_TRUE(execution.metrics.contains( - Node_Metric_Kind::queue_wait_ns)); - EXPECT_TRUE(execution.metrics.contains( - Node_Metric_Kind::backend_execute_duration_ns)); - EXPECT_TRUE(execution.metrics.contains( - Node_Metric_Kind::gpu_fence_wait_ns)); - EXPECT_TRUE(execution.metrics.contains( - Node_Metric_Kind::readback_duration_ns)); - const auto artifact_bytes = execution.metrics.get( - Node_Metric_Kind::backend_artifact_json_bytes); - EXPECT_GT(artifact_bytes, 0U); - const auto artifact = std::find_if( - execution.attachments.begin(), execution.attachments.end(), - [](const Node_Diagnostic_Attachment& attachment) { - return attachment.type == "datoviz.drp2.artifact.json"; - }); - ASSERT_NE(artifact, execution.attachments.end()); - EXPECT_EQ(artifact->content.size(), artifact_bytes); - EXPECT_FALSE(artifact->content.empty()); - const bool gpu_timing = execution.metrics.contains( - Node_Metric_Kind::gpu_render_duration_ns); - EXPECT_EQ(gpu_timing, execution.metrics.contains( - Node_Metric_Kind::gpu_transition_duration_ns)); - EXPECT_EQ(gpu_timing, execution.metrics.contains( - Node_Metric_Kind::gpu_copy_duration_ns)); - EXPECT_EQ(gpu_timing, execution.metrics.contains( - Node_Metric_Kind::gpu_total_duration_ns)); - if (gpu_timing) { - const auto total = execution.metrics.get( - Node_Metric_Kind::gpu_total_duration_ns); - EXPECT_GE(total, execution.metrics.get( - Node_Metric_Kind::gpu_render_duration_ns)); - EXPECT_GE(total, execution.metrics.get( - Node_Metric_Kind::gpu_transition_duration_ns)); - EXPECT_GE(total, execution.metrics.get( - Node_Metric_Kind::gpu_copy_duration_ns)); - } + auto scene = make_scene(state, visual); + scene->publish_frame_state(); + ASSERT_EQ(scene->render(), Scene_Render_Result::none); + ASSERT_EQ(scene->wait_for_render(), Scene_Render_Result::none); + const auto observation = scene->observer(); + EXPECT_EQ(observation.state, state); + EXPECT_EQ(observation.renderable_count, 1U); + EXPECT_EQ(observation.point_count, visual->point_count()); + EXPECT_EQ(observation.point_count, 3U); } catch (const std::exception& error) { GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what(); } } + } // namespace } // namespace renderive::render_3d diff --git a/render_3D/tests/Point_State_Tests.cpp b/render_3D/tests/Point_State_Tests.cpp index 0e2be3b..804a596 100644 --- a/render_3D/tests/Point_State_Tests.cpp +++ b/render_3D/tests/Point_State_Tests.cpp @@ -1,114 +1,34 @@ #include "render_3D/Point_Visual.h" -#include "render_3D/detail/Point_Core.h" -#include -#include "render_3D/Scene_Context.hpp" -#include #include -#include #include -#include -#include -#include -#include #include + namespace renderive::render_3d { namespace { -using Test_Frame_Control = Low_Latency_Strategy; -struct Test_Scene final -: Scene3D_Context, - Scene_Renderer { - explicit Test_Scene(const std::shared_ptr& visual) { - Scene_State_Strategy::set<&detail::Scene_State::viewport>( - Extent{320, 180}); - point_id = visual->renderable_id(); - auto builder = attach_builder(); - if (builder.attach(renderive_Owner(visual)) != - Scene_Edit_Error::none) - throw std::logic_error("test visual attachment failed"); - } - ~Test_Scene() override { - shutdown(); - } - void render_frame() { - { - auto painter = frame_control.acquire_painter(); - ASSERT_TRUE(painter); - publish_frame_state(); - } - auto renderer = frame_control.acquire_renderer(); - ASSERT_TRUE(renderer); - ASSERT_EQ(Scene_Base::render(*renderer), Scene_Render_Result::none); - ASSERT_EQ(wait_for_render(), Scene_Render_Result::none); - } - Node_Execution_Result render_scene( - const Scene_Render_Context& context) override { - prepared = context.prepared(point_id); - if (!prepared) throw std::logic_error("Point_Visual did not publish prepared data"); - EXPECT_EQ(context.frame.scene_state().viewport, - (Extent{320, 180})); - return Node_Execution_Result::completed(); - } - Renderable_Id point_id{}; - std::shared_ptr prepared; -}; -static_assert(std::derived_from); -static_assert(std::derived_from); -TEST(PointState, PublishesStateAndBulkDataIntoFrameLocalPreparedOutput) { - auto visual = Point_Visual::Builder{}.build( + +TEST(PointState, BuilderAndStateMutationShareOneAuthoritativeState) { + Point_State initial; + initial.style.stroke_width_px = 2.0F; + auto visual = Point_Visual::Builder{initial}.build( std::vector{{{1.0F, 2.0F, 3.0F}, {1, 2, 3, 255}, 7.0F}}); ASSERT_TRUE(visual); - Point_State configured; - configured.visible = false; - configured.style.stroke_width_px = 3.0F; - visual->set<&Point_State::visible>(configured.visible); - visual->set<&Point_State::style>(configured.style); - Test_Scene scene(visual); - scene.render_frame(); - ASSERT_TRUE(scene.prepared); - EXPECT_EQ(scene.prepared->state, configured); - ASSERT_EQ(scene.prepared->positions.size(), 1U); - EXPECT_EQ(scene.prepared->positions.front(), - (std::array{1.0F, 2.0F, 3.0F})); - EXPECT_EQ(scene.prepared->data_revision, 1U); + EXPECT_EQ(visual->get<&Point_State::style>(), initial.style); + auto changed = initial.style; + changed.stroke_width_px = 3.0F; + visual->set<&Point_State::style>(changed); + EXPECT_EQ(visual->get<&Point_State::style>(), changed); } -TEST(PointState, PublishesImmutableBulkPayloadsAcrossConcurrentUpdates) { + +TEST(PointState, BulkUpdatesUseOwnedStorage) { auto visual = Point_Visual::Builder{}.build(); ASSERT_TRUE(visual); - Test_Scene scene(visual); - constexpr int update_count = 200; - std::atomic done{}; - std::thread writer([&] { - for (int value = 1; value <= update_count; ++value) { - std::vector points(static_cast(value % 31 + 1)); - for (auto& point : points) { - point.position.x = static_cast(value); - point.diameter_px = static_cast(value % 12 + 1); - } - visual->update_points(std::move(points)); - } - done.store(true, std::memory_order_release); - }); - do { - scene.render_frame(); - const auto prepared = scene.prepared; - ASSERT_TRUE(prepared); - if (!prepared->positions.empty()) { - const float expected = prepared->positions.front()[0]; - for (const auto& point : prepared->positions) - EXPECT_EQ(point[0], expected); - } - } - while (!done.load(std::memory_order_acquire)); - writer.join(); - scene.render_frame(); - ASSERT_TRUE(scene.prepared); - ASSERT_FALSE(scene.prepared->positions.empty()); - EXPECT_EQ(scene.prepared->positions.front()[0], - static_cast(update_count)); + EXPECT_EQ(visual->update_points(std::vector(4)), + Visual_Data_Error::none); + EXPECT_EQ(visual->point_count(), 4U); } -TEST(PointState, RejectsInvalidStateAndPayloadAtTheOwningBoundary) { + +TEST(PointState, RejectsInvalidStateAndPayloadAtOwningBoundary) { auto visual = Point_Visual::Builder{}.build(); - ASSERT_TRUE(visual); Point_State invalid; invalid.style.stroke_width_px = -1.0F; EXPECT_THROW(visual->set<&Point_State::style>(invalid.style), @@ -116,29 +36,6 @@ TEST(PointState, RejectsInvalidStateAndPayloadAtTheOwningBoundary) { EXPECT_EQ(visual->update_points(std::vector{{{}, {}, 0.0F}}), Visual_Data_Error::invalid_data); } -TEST(PointRenderPlan, OrdersParallelPreparationBeforeSingleSceneRenderNode) { - auto visual = Point_Visual::Builder{}.build( - std::vector{{{}, {}, 8.0F}}); - Test_Scene scene(visual); - scene.render_frame(); - const auto plan = scene.render_plan_snapshot(); - ASSERT_TRUE(plan); - const auto prepare = std::find_if( - plan->graph.nodes.begin(), plan->graph.nodes.end(), - [&](const Render_Node& node) { - return node.owner_id == visual->renderable_id() && - node.kind == Render_Node_Kind::prepare; - }); - const auto render = std::find_if( - plan->graph.nodes.begin(), plan->graph.nodes.end(), - [](const Render_Node& node) { - return node.kind == Render_Node_Kind::render; - }); - ASSERT_NE(prepare, plan->graph.nodes.end()); - ASSERT_NE(render, plan->graph.nodes.end()); - EXPECT_NE(std::find(plan->graph.edges.begin(), plan->graph.edges.end(), - Render_Edge{prepare->node_id, render->node_id}), - plan->graph.edges.end()); -} + } // namespace } // namespace renderive::render_3d