#include "../render_2D/export.h" #include "render_2D/plottable/Curve_Sampling.h" #include "render_2D/render/Blend2D_Cache.h" #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) { Plot_Scene 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(); plot.set_max_render_fps(144.0); ASSERT_TRUE(plot.render_frame()); 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) { Plot_Scene 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_TRUE(plot.render_frame()); 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_FALSE(plot.render_frame()); samples.front() += 1.0; spectrum->update_samples(samples); EXPECT_TRUE(plot.render_frame(true)); 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_TRUE(plot.render_frame()); 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_TRUE(plot.render_frame(true)); EXPECT_FALSE(plot.render_frame()); } TEST(Renderive_Core2, StandaloneRenderableBuilderJoinsSceneOnlyInsideRuntimeEditor) { Plot_Scene 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) { Plot_Scene 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) { Plot_Scene plot; plot.init(); plot.set_viewport_size({160, 90}); plot.activate_view(); 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_TRUE(plot.render_frame(true)); 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) { Plot_Scene 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); Plot_Scene low_latency; low_latency.init(); low_latency.set_viewport_size({64, 40}); low_latency.activate_view(); ASSERT_TRUE(low_latency.render_frame(true)); 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); Plot_Scene 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) { Plot_Scene 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) { Plot_Scene 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_TRUE(plot.render_frame(true)); } TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { Plot_Scene 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_TRUE(plot.render_frame(true)); } TEST(Renderive_Core2, RetainedSelectionAndConstellationApisDriveInteractionAndLayout) { Plot_Scene 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_TRUE(plot.render_frame(true)); } TEST(Renderive_Core2, PlottablePropertiesPublishOnlyAtFrameBoundary) { Plot_Scene 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) { Plot_Scene 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) { Plot_Scene 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) { Plot_Scene 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_TRUE(plot.render_frame(true)); 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_TRUE(plot.render_frame(true)); 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_TRUE(plot.render_frame(true)); 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(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(Renderive_Core2, DynamicWaterfallCaptureStressPreservesPlansSlotsAndExactSessions) { Plot_Scene 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 Capture_Session_Id session_id = scene.render_scene().capture_frames(8); 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_TRUE(plot.render_frame(true)); } 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) { Plot_Scene 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_TRUE(plot.render_frame(true)); ASSERT_TRUE(plot.render_frame(true)); 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_FALSE(cached_has_kind(Render_Node_Kind::prepare)); EXPECT_FALSE(cached_has_kind(Render_Node_Kind::paint)); spectrum->set<&Spectrum::Properties::current_pen>( Pen{Color{12, 34, 56, 255}, 2.0}); ASSERT_TRUE(plot.render_frame(true)); const auto plan = plot.render_scene().render_plan_snapshot(); ASSERT_TRUE(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_FALSE(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_TRUE(plot.render_frame(true)); const auto axis_style_plan = plot.render_scene().render_plan_snapshot(); ASSERT_TRUE(axis_style_plan); EXPECT_FALSE(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) { Plot_Scene 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) { Plot_Scene 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