From 9331b4cbf692206e82df49086b4dcfcdb83d9bf5 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Wed, 12 Aug 2026 09:01:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=9B=E4=B8=AA=E6=96=B9=E5=90=91=20=20?= =?UTF-8?q?=E5=88=86=E5=9D=97=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + render_2D/axis/Axis_State_Strategy.hpp | 3 +- render_2D/axis/Axis_Types.h | 5 + render_2D/plottable/Afterglow.cpp | 137 ++++++++++++-- render_2D/plottable/Afterglow.h | 6 + render_2D/plottable/Constellation_Diagram.cpp | 9 +- render_2D/plottable/Curve_Sampling.h | 4 +- render_2D/plottable/Frequency_Trace.cpp | 3 +- render_2D/plottable/Heatmap_Utils.h | 77 +++++++- .../plottable/Selection_Rectangle_Overlay.cpp | 21 ++- render_2D/plottable/Spectrum.cpp | 176 +++++++++++++++--- render_2D/plottable/Spectrum.h | 4 + render_2D/plottable/Sweep_Spectrum.cpp | 13 +- render_2D/plottable/Waterfall.cpp | 112 +++++++++-- render_2D/plottable/Waterfall.h | 4 + render_2D/render/Blend2D_Cache.cpp | 18 ++ render_2D/render/Blend2D_Cache.h | 2 + render_2D/renderable/Render_Partition.h | 69 +++++++ render_2D/renderable/Renderable.cpp | 29 +++ render_2D/renderable/Renderable.h | 9 + .../tests/render_2D_Integration_Tests.cpp | 78 +++++++- web_server/app/Gallery_Plot_Session.cpp | 27 ++- web_server/app/Gallery_Renderables.h | 3 + web_server/tests/Web_Bridge_Tests.cpp | 86 ++++++++- 24 files changed, 788 insertions(+), 108 deletions(-) create mode 100644 render_2D/renderable/Render_Partition.h diff --git a/.gitignore b/.gitignore index 97ec3b5..427d063 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.idea /cmake-build*/ +/.playwright-cli/ diff --git a/render_2D/axis/Axis_State_Strategy.hpp b/render_2D/axis/Axis_State_Strategy.hpp index 512c090..de491c1 100644 --- a/render_2D/axis/Axis_State_Strategy.hpp +++ b/render_2D/axis/Axis_State_Strategy.hpp @@ -81,7 +81,8 @@ private: coordinates, state.orientation == Orientation::Horizontal ? static_cast(state.x) : static_cast(state.y), - static_cast(state.pixel_length) + static_cast(state.pixel_length), + state.orientation }; } diff --git a/render_2D/axis/Axis_Types.h b/render_2D/axis/Axis_Types.h index 3df2963..0a72bca 100644 --- a/render_2D/axis/Axis_Types.h +++ b/render_2D/axis/Axis_Types.h @@ -11,6 +11,7 @@ struct Axis_Transform { Range coordinate_range; double pixel_origin{}; double pixel_length{}; + Orientation orientation = Orientation::Horizontal; [[nodiscard]] double coord_to_pixel(double coordinate) const noexcept { const double span = coordinate_range.length(); @@ -23,6 +24,10 @@ struct Axis_Transform { return coordinate_range.origin; return coordinate_range.origin + (pixel - pixel_origin) / pixel_length * coordinate_range.length(); } + + [[nodiscard]] double point_to_coord(PointF point) const noexcept { + return pixel_to_coord(orientation == Orientation::Horizontal ? point.x : point.y); + } }; struct Axis_Base_Properties { diff --git a/render_2D/plottable/Afterglow.cpp b/render_2D/plottable/Afterglow.cpp index 87881a5..23c1858 100644 --- a/render_2D/plottable/Afterglow.cpp +++ b/render_2D/plottable/Afterglow.cpp @@ -2,12 +2,26 @@ #include "Heatmap_Utils.h" #include "Plottable_Real_Time_Data.h" #include "../render/Blend2D_Cache.h" +#include "../renderable/Render_Partition.h" #include +#include #include namespace renderive { namespace detail { namespace { using Afterglow_History = Plottable_History_Real_Time_Data, std::deque>>; +struct Afterglow_Render_Frame { + Adaptive_Render_Partitioner partitioner; + Axis_Raster_Layout layout; + std::vector intensity; + std::vector pixels; + int source_width{}; + int source_height{}; + int active_partitions{1}; + double maximum{1.0}; + std::size_t work_size{}; + bool valid{}; +}; } struct Afterglow_Control::Impl { Impl(Afterglow_Control& owner, std::shared_ptr frequency, std::shared_ptr power) @@ -15,6 +29,7 @@ struct Afterglow_Control::Impl { std::shared_ptr frequency_axis; std::shared_ptr power_axis; Afterglow_History history; + Afterglow_Render_Frame render_frame; }; Afterglow_Control::Afterglow_Control(Plot_Core& plot, const Afterglow_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) : Plottable_State(plot, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} @@ -49,9 +64,47 @@ void Afterglow_Control::append_spectrum(std::pmr::vector&& values) { void Afterglow_Control::publish() { publish_properties(); } -void Afterglow_Control::paint(Painter& painter, const Render_State_View& view) { + +void Afterglow_Control::build_paint_task_graph(Renderable_Task_Graph& graph) { + const auto prepare = graph.emplace([this](const Scene_Render_Context&) { + prepare_render_frame(render_state_view()); + }, "prepare afterglow"); + std::array accumulation; + std::array coloring; + for (int index = 0; index < maximum_render_partitions; ++index) { + accumulation[static_cast(index)] = graph.emplace( + [this, index](const Scene_Render_Context&) { + accumulate_partition(render_state_view(), index); + }, + "accumulate afterglow partition"); + graph.precede(prepare, accumulation[static_cast(index)]); + } + const auto normalize = graph.emplace([this](const Scene_Render_Context&) { + normalize_render_frame(); + }, "normalize afterglow"); + for (const auto task : accumulation) + graph.precede(task, normalize); + for (int index = 0; index < maximum_render_partitions; ++index) { + coloring[static_cast(index)] = graph.emplace( + [this, index](const Scene_Render_Context&) { + color_partition(render_state_view(), index); + }, + "color afterglow partition"); + graph.precede(normalize, coloring[static_cast(index)]); + } + const auto compose = add_paint_task( + graph, "compose afterglow", + [this](Painter& painter, const Render_State_View& view) { paint(painter, view); }); + for (const auto task : coloring) + graph.precede(task, compose); +} + +void Afterglow_Control::prepare_render_frame(const Render_State_View& view) { const auto& state = render_properties(view); const auto& history = view.get(impl_->history); + auto& output = impl_->render_frame; + output.valid = false; + output.work_size = 0; if (history.empty()) return; const int width = std::min(state.frequency_point_size.get(), static_cast(history.back().size())); @@ -60,27 +113,83 @@ void Afterglow_Control::paint(Painter& painter, const Render_State_View& view) { : std::max(1, static_cast(impl_->power_axis->transform(view).pixel_length)); if (width <= 0 || height <= 0) return; - std::vector intensity(static_cast(width) * height); + const auto layout = axis_raster_layout(impl_->frequency_axis->transform(view), + impl_->power_axis->transform(view), + state.frequency_range, state.power_range, + width, height); + if (!layout.valid()) + return; + output.layout = layout; + output.source_width = width; + output.source_height = height; + output.work_size = static_cast(width) * height; + output.intensity.resize(output.work_size); + output.pixels.resize(output.work_size); + output.active_partitions = + output.partitioner.begin(state.partition_count.get(), output.work_size); + output.valid = true; +} + +void Afterglow_Control::accumulate_partition(const Render_State_View& view, + int partition_index) { + auto& output = impl_->render_frame; + if (!output.valid || partition_index >= output.active_partitions) + return; + const auto& state = render_properties(view); + const auto& history = view.get(impl_->history); + const auto columns = render_partition_range(static_cast(output.source_width), + partition_index, output.active_partitions); + for (std::size_t x = columns.first; x < columns.last; ++x) + for (int y = 0; y < output.source_height; ++y) + output.intensity[static_cast(y) * output.source_width + x] = 0.0; double weight = 1.0; const double decay = 1.0 - state.attenuation_rate.get(); for (auto iterator = history.rbegin(); iterator != history.rend(); ++iterator) { - const int count = std::min(width, static_cast(iterator->size())); - for (int x = 0; x < count; ++x) { - const double normalized = normalized_value((*iterator)[static_cast(x)], state.power_range); - const int y = std::clamp(height - 1 - static_cast(normalized * (height - 1)), 0, height - 1); - intensity[static_cast(y) * width + x] += weight; - if (state.interpolate && y + 1 < height) - intensity[static_cast(y + 1) * width + x] += weight * 0.35; + const std::size_t count = std::min(output.source_width, iterator->size()); + for (std::size_t x = columns.first; x < std::min(columns.last, count); ++x) { + const double normalized = normalized_value((*iterator)[x], state.power_range); + const int y = std::clamp(static_cast(normalized * (output.source_height - 1)), + 0, output.source_height - 1); + output.intensity[static_cast(y) * output.source_width + x] += weight; + if (state.interpolate && y + 1 < output.source_height) + output.intensity[static_cast(y + 1) * output.source_width + x] += weight * 0.35; } weight *= decay; if (weight < 0.01) break; } - const double maximum = std::max(1.0, *std::max_element(intensity.begin(), intensity.end())); - std::vector pixels(intensity.size()); - for (std::size_t index = 0; index < pixels.size(); ++index) - pixels[index] = state.color_map.at_normalized(intensity[index] / maximum); - painter.heatmap(mapped_rect(impl_->frequency_axis->transform(view), impl_->power_axis->transform(view), state.frequency_range, state.power_range), width, height, pixels, Image_Interpolation_Mode::Bilinear); +} + +void Afterglow_Control::normalize_render_frame() { + auto& output = impl_->render_frame; + if (output.valid) + output.maximum = std::max(1.0, *std::max_element(output.intensity.begin(), + output.intensity.end())); +} + +void Afterglow_Control::color_partition(const Render_State_View& view, + int partition_index) { + auto& output = impl_->render_frame; + if (!output.valid || partition_index >= output.active_partitions) + return; + const auto& state = render_properties(view); + const auto range = render_partition_range(output.work_size, partition_index, + output.active_partitions); + for (std::size_t cell = range.first; cell < range.last; ++cell) { + const int y = static_cast(cell / static_cast(output.source_width)); + const int x = static_cast(cell % static_cast(output.source_width)); + output.pixels[output.layout.index(x, y, output.source_width, output.source_height)] = + state.color_map.at_normalized(output.intensity[cell] / output.maximum); + } +} + +void Afterglow_Control::paint(Painter& painter, const Render_State_View& view) { + auto& output = impl_->render_frame; + if (!output.valid) + return; + painter.heatmap(output.layout.target, output.layout.width, output.layout.height, + output.pixels, Image_Interpolation_Mode::Bilinear); + output.partitioner.finish(output.work_size); } } } diff --git a/render_2D/plottable/Afterglow.h b/render_2D/plottable/Afterglow.h index ba11f29..4e31c54 100644 --- a/render_2D/plottable/Afterglow.h +++ b/render_2D/plottable/Afterglow.h @@ -9,6 +9,7 @@ struct Afterglow_Properties { Range power_range{0.0, 10.0}; Nonnegative_Count frequency_point_size; Nonnegative_Count power_point_size; + Nonnegative_Count partition_count; bool interpolate = true; Unit_Interval attenuation_rate{0.2}; Color_Map color_map; @@ -29,9 +30,14 @@ public: } protected: void paint(Painter& painter, const Render_State_View& state) override; + void build_paint_task_graph(Renderable_Task_Graph& graph) override; private: struct Impl; std::unique_ptr impl_; + void prepare_render_frame(const Render_State_View& state); + void accumulate_partition(const Render_State_View& state, int partition_index); + void normalize_render_frame(); + void color_partition(const Render_State_View& state, int partition_index); void publish() override; }; } diff --git a/render_2D/plottable/Constellation_Diagram.cpp b/render_2D/plottable/Constellation_Diagram.cpp index 2527e5d..6d1ea7d 100644 --- a/render_2D/plottable/Constellation_Diagram.cpp +++ b/render_2D/plottable/Constellation_Diagram.cpp @@ -1,5 +1,6 @@ #include "Constellation_Diagram.h" #include "Plottable_Real_Time_Data.h" +#include "Heatmap_Utils.h" #include "../render/Blend2D_Cache.h" #include #include @@ -58,13 +59,17 @@ void Constellation_Diagram_Control::paint(Painter& painter, const Render_State_V for(int index = 0; index < count; ++index) { const double angle = state.phase_offset_radians + 2.0 * std::numbers::pi * index / count; const PointF point{state.i_range.center() + std::cos(angle) * radius, state.q_range.center() + std::sin(angle) * radius}; - painter.circle({x.coord_to_pixel(point.x), y.coord_to_pixel(point.y)}, 3.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::Solid}); + painter.circle(mapped_point(x, point.x, y, point.y), 3.0, + Pen{state.anchor_color}, + Brush{state.anchor_color, Brush_Style::Solid}); } const auto cutoff = std::chrono::steady_clock::now() - std::chrono::milliseconds(state.point_lifetime_ms.get()); for(const auto& value : points) { if(value.time < cutoff) continue; - painter.circle({x.coord_to_pixel(value.point.x), y.coord_to_pixel(value.point.y)}, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::Solid}); + painter.circle(mapped_point(x, value.point.x, y, value.point.y), 2.0, + Pen{state.point_color}, + Brush{state.point_color, Brush_Style::Solid}); } } } diff --git a/render_2D/plottable/Curve_Sampling.h b/render_2D/plottable/Curve_Sampling.h index 9c2736c..ed5fbbe 100644 --- a/render_2D/plottable/Curve_Sampling.h +++ b/render_2D/plottable/Curve_Sampling.h @@ -1,6 +1,7 @@ #pragma once #include "../axis/Axis.h" +#include "Heatmap_Utils.h" #include #include @@ -141,8 +142,7 @@ inline std::vector curve_points(std::span values, points.reserve(samples.size()); for (const auto& sample : samples) { if (std::isfinite(sample.coordinate) && std::isfinite(sample.value)) - points.push_back({x_axis.coord_to_pixel(sample.coordinate), - y_axis.coord_to_pixel(sample.value)}); + points.push_back(mapped_point(x_axis, sample.coordinate, y_axis, sample.value)); } return points; } diff --git a/render_2D/plottable/Frequency_Trace.cpp b/render_2D/plottable/Frequency_Trace.cpp index e9e0d05..75e85a0 100644 --- a/render_2D/plottable/Frequency_Trace.cpp +++ b/render_2D/plottable/Frequency_Trace.cpp @@ -1,5 +1,6 @@ #include "Frequency_Trace.h" #include "Plottable_Real_Time_Data.h" +#include "Heatmap_Utils.h" #include "../render/Blend2D_Cache.h" #include #include @@ -47,7 +48,7 @@ void Frequency_Trace_Control::paint(Painter& painter, const Render_State_View& v std::vector points; points.reserve(samples.size()); for(const auto& [tick, value] : samples) - points.push_back({x.coord_to_pixel(tick), y.coord_to_pixel(value)}); + points.push_back(mapped_point(x, tick, y, value)); painter.polyline(points, state.pen); } } diff --git a/render_2D/plottable/Heatmap_Utils.h b/render_2D/plottable/Heatmap_Utils.h index e3bb112..9918120 100644 --- a/render_2D/plottable/Heatmap_Utils.h +++ b/render_2D/plottable/Heatmap_Utils.h @@ -4,12 +4,77 @@ #include #include namespace renderive::detail { -inline RectF mapped_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical, Range horizontal_range, Range vertical_range) { - const double x1 = horizontal.coord_to_pixel(horizontal_range.origin); - const double x2 = horizontal.coord_to_pixel(horizontal_range.target); - const double y1 = vertical.coord_to_pixel(vertical_range.origin); - const double y2 = vertical.coord_to_pixel(vertical_range.target); - return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; +inline bool axes_are_orthogonal(const Axis_Transform& first, + const Axis_Transform& second) noexcept { + return first.orientation != second.orientation; +} + +inline PointF mapped_point(const Axis_Transform& first, + double first_coordinate, + const Axis_Transform& second, + double second_coordinate) noexcept { + if (!axes_are_orthogonal(first, second)) + return {}; + const double first_pixel = first.coord_to_pixel(first_coordinate); + const double second_pixel = second.coord_to_pixel(second_coordinate); + return first.orientation == Orientation::Horizontal + ? PointF{first_pixel, second_pixel} + : PointF{second_pixel, first_pixel}; +} + +inline RectF mapped_rect(const Axis_Transform& first, + const Axis_Transform& second, + Range first_range, + Range second_range) noexcept { + if (!axes_are_orthogonal(first, second)) + return {}; + const PointF origin = mapped_point(first, first_range.origin, second, second_range.origin); + const PointF target = mapped_point(first, first_range.target, second, second_range.target); + return {std::min(origin.x, target.x), std::min(origin.y, target.y), + std::abs(target.x - origin.x), std::abs(target.y - origin.y)}; +} + +struct Axis_Raster_Layout { + int width{}; + int height{}; + RectF target; + bool first_reversed{}; + bool second_reversed{}; + bool first_is_horizontal{}; + + [[nodiscard]] bool valid() const noexcept { + return width > 0 && height > 0 && !target.empty(); + } + + [[nodiscard]] std::size_t index(int first, int second, + int first_count, int second_count) const noexcept { + if (first_reversed) + first = first_count - 1 - first; + if (second_reversed) + second = second_count - 1 - second; + const int x = first_is_horizontal ? first : second; + const int y = first_is_horizontal ? second : first; + return static_cast(y) * width + x; + } +}; + +inline Axis_Raster_Layout axis_raster_layout(const Axis_Transform& first, + const Axis_Transform& second, + Range first_range, + Range second_range, + int first_count, + int second_count) noexcept { + if (!axes_are_orthogonal(first, second) || first_count <= 0 || second_count <= 0) + return {}; + const bool first_horizontal = first.orientation == Orientation::Horizontal; + return { + first_horizontal ? first_count : second_count, + first_horizontal ? second_count : first_count, + mapped_rect(first, second, first_range, second_range), + first.coord_to_pixel(first_range.origin) > first.coord_to_pixel(first_range.target), + second.coord_to_pixel(second_range.origin) > second.coord_to_pixel(second_range.target), + first_horizontal + }; } inline double normalized_value(double value, Range range) { if(range.length() == 0.0) diff --git a/render_2D/plottable/Selection_Rectangle_Overlay.cpp b/render_2D/plottable/Selection_Rectangle_Overlay.cpp index ef8c944..10b6e9b 100644 --- a/render_2D/plottable/Selection_Rectangle_Overlay.cpp +++ b/render_2D/plottable/Selection_Rectangle_Overlay.cpp @@ -1,5 +1,6 @@ #include "Selection_Rectangle_Overlay.h" #include "Plottable_Real_Time_Data.h" +#include "Heatmap_Utils.h" #include "../render/Blend2D_Cache.h" #include #include @@ -14,12 +15,8 @@ struct Selection_Interaction { PointF selection_current{}; }; using Selection_Interaction_State = Double_State_Strategy; -RectF axis_content_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) { - const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin); - const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target); - const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin); - const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target); - return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; +RectF axis_content_rect(const Axis_Transform& first, const Axis_Transform& second) { + return mapped_rect(first, second, first.coordinate_range, second.coordinate_range); } } struct Selection_Rectangle_Overlay_Control::Impl { @@ -88,9 +85,11 @@ void Selection_Rectangle_Overlay_Control::handle_event(const Event& event) { return; const Axis_Transform horizontal = impl_->horizontal_axis->transform(); const Axis_Transform vertical = impl_->vertical_axis->transform(); - const RectF region{horizontal.pixel_to_coord(start.x), vertical.pixel_to_coord(start.y), - horizontal.pixel_to_coord(pointer.position.x) - horizontal.pixel_to_coord(start.x), - vertical.pixel_to_coord(pointer.position.y) - vertical.pixel_to_coord(start.y)}; + const double first_start = horizontal.point_to_coord(start); + const double second_start = vertical.point_to_coord(start); + const RectF region{first_start, second_start, + horizontal.point_to_coord(pointer.position) - first_start, + vertical.point_to_coord(pointer.position) - second_start}; if(std::abs(region.width) > 1e-9 && std::abs(region.height) > 1e-9) impl_->regions.update(region.normalized()); event.accept(); @@ -106,7 +105,9 @@ void Selection_Rectangle_Overlay_Control::paint(Painter& painter, const Render_S const auto horizontal = impl_->horizontal_axis->transform(view); const auto vertical = impl_->vertical_axis->transform(view); for(const RectF& region : view.get(impl_->regions)) { - RectF pixels{horizontal.coord_to_pixel(region.x), vertical.coord_to_pixel(region.y), horizontal.coord_to_pixel(region.right()) - horizontal.coord_to_pixel(region.x), vertical.coord_to_pixel(region.bottom()) - vertical.coord_to_pixel(region.y)}; + const RectF pixels = mapped_rect(horizontal, vertical, + {region.x, region.right()}, + {region.y, region.bottom()}); painter.rect(pixels, state.selection_border_pen, state.selection_brush); std::ostringstream text; text << region.width << " x " << region.height; diff --git a/render_2D/plottable/Spectrum.cpp b/render_2D/plottable/Spectrum.cpp index 7c5771a..b6b8a94 100644 --- a/render_2D/plottable/Spectrum.cpp +++ b/render_2D/plottable/Spectrum.cpp @@ -1,7 +1,10 @@ #include "Spectrum.h" #include "Curve_Sampling.h" #include "Plottable_Real_Time_Data.h" +#include "../render/Blend2D_Cache.h" +#include "../renderable/Render_Partition.h" #include +#include #include #include #include @@ -20,23 +23,56 @@ struct Spectrum_Interaction { Hover_Tooltip_Runtime tooltip; }; using Spectrum_Interaction_State = Double_State_Strategy; -RectF axes_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) { - const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin); - const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target); - const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin); - const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target); - return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; +struct Spectrum_Render_Frame { + Adaptive_Render_Partitioner partitioner; + std::array layers; + int active_partitions{1}; + std::size_t work_size{}; + bool valid{}; +}; + +struct Curve_Partition { + std::span values; + Range domain; + Range clip_domain; +}; + +Curve_Partition curve_partition(std::span values, Range domain, + int partition_index, int partition_count) { + if (values.size() < 2 || partition_count <= 0) + return {}; + const std::size_t segment_count = values.size() - 1; + const auto core = render_partition_range(segment_count, partition_index, partition_count); + if (core.first == core.last) + return {}; + const std::size_t first = core.first == 0 ? 0 : core.first - 1; + const std::size_t last = std::min(segment_count, core.last + 1); + const auto coordinate = [domain, segment_count](std::size_t index) { + return domain.origin + domain.length() * static_cast(index) / + static_cast(segment_count); + }; + return { + values.subspan(first, last - first + 1), + {coordinate(first), coordinate(last)}, + {coordinate(core.first), coordinate(core.last)} + }; } -void draw_curve(Painter& painter, std::span values, Range domain, const Axis_Transform& x_axis, const Axis_Transform& y_axis, bool visible_only, Line_Interpolation_Mode interpolation, const Pen& pen, const Brush& brush) { - auto points = curve_points(values, domain, x_axis, y_axis, visible_only, interpolation); +void draw_curve(Painter& painter, std::span values, Range domain, + const Axis_Transform& frequency_axis, const Axis_Transform& power_axis, + bool visible_only, Line_Interpolation_Mode interpolation, + const Pen& pen, const Brush& brush) { + auto points = curve_points(values, domain, frequency_axis, power_axis, + visible_only, interpolation); if(points.size() < 2) return; if(brush.enabled()) { std::vector polygon; polygon.reserve(points.size() + 2); - polygon.push_back({points.front().x, y_axis.coord_to_pixel(y_axis.coordinate_range.target)}); + polygon.push_back(mapped_point(frequency_axis, domain.origin, power_axis, + power_axis.coordinate_range.target)); polygon.insert(polygon.end(), points.begin(), points.end()); - polygon.push_back({points.back().x, y_axis.coord_to_pixel(y_axis.coordinate_range.target)}); + polygon.push_back(mapped_point(frequency_axis, domain.target, power_axis, + power_axis.coordinate_range.target)); painter.polygon(polygon, Pen{.style = Line_Style::None}, brush); } painter.polyline(points, pen); @@ -62,6 +98,7 @@ struct Spectrum_Control::Impl { Plottable_Latest_Real_Time_Data frame; Spectrum_Interaction_State interaction; std::mutex frame_update_mutex; + Spectrum_Render_Frame render_frame; }; Spectrum_Control::Spectrum_Control(Plot_Core& plot, const Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) : Plottable_State(plot, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} @@ -205,32 +242,116 @@ void Spectrum_Control::publish() { publish_properties(); impl_->interaction.publish(); } + +void Spectrum_Control::build_paint_task_graph(Renderable_Task_Graph& graph) { + const auto prepare = graph.emplace([this](const Scene_Render_Context&) { + prepare_render_frame(render_state_view()); + }, "prepare spectrum"); + std::array partitions; + for (int index = 0; index < maximum_render_partitions; ++index) { + partitions[static_cast(index)] = graph.emplace( + [this, index](const Scene_Render_Context&) { + render_partition(render_state_view(), index); + }, + "paint spectrum partition"); + graph.precede(prepare, partitions[static_cast(index)]); + } + const auto compose = add_paint_task( + graph, "compose spectrum", + [this](Painter& painter, const Render_State_View& view) { paint(painter, view); }); + for (const auto task : partitions) + graph.precede(task, compose); +} + +void Spectrum_Control::prepare_render_frame(const Render_State_View& view) { + const auto& state = render_properties(view); + const auto& published_frame = view.get(impl_->frame); + auto& output = impl_->render_frame; + const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view); + const Axis_Transform power_axis = impl_->power_axis->transform(view); + output.valid = axes_are_orthogonal(frequency_axis, power_axis); + output.work_size = published_frame ? published_frame->samples.size() : 0; + output.active_partitions = + output.partitioner.begin(state.partition_count.get(), output.work_size); + for (int index = 0; index < output.active_partitions; ++index) + output.layers[static_cast(index)].clear(); +} + +void Spectrum_Control::render_partition(const Render_State_View& view, + int partition_index) { + auto& output = impl_->render_frame; + if (!output.valid || partition_index >= output.active_partitions) + return; + const auto& state = render_properties(view); + const auto& published_frame = view.get(impl_->frame); + if (!published_frame) + return; + const Spectrum_Frame& frame = *published_frame; + const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view); + const Axis_Transform power_axis = impl_->power_axis->transform(view); + const auto current = curve_partition(frame.samples, state.frequency_range, + partition_index, output.active_partitions); + if (current.values.empty()) + return; + + Painter painter(output.layers[static_cast(partition_index)], viewport_size()); + if (!painter) + return; + painter.clip(mapped_rect(frequency_axis, power_axis, current.clip_domain, + power_axis.coordinate_range)); + if (state.max_hold_visible) { + const auto maximum = curve_partition(frame.maxima, state.frequency_range, + partition_index, output.active_partitions); + draw_curve(painter, maximum.values, maximum.domain, frequency_axis, power_axis, + state.visible_range_only, state.interpolation_mode, + state.max_pen, state.max_brush); + } + if (state.min_hold_visible) { + const auto minimum = curve_partition(frame.minima, state.frequency_range, + partition_index, output.active_partitions); + draw_curve(painter, minimum.values, minimum.domain, frequency_axis, power_axis, + state.visible_range_only, state.interpolation_mode, + state.min_pen, state.min_brush); + } + draw_curve(painter, current.values, current.domain, frequency_axis, power_axis, + state.visible_range_only, state.interpolation_mode, + state.current_pen, state.current_brush); +} + void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) { const auto& state = render_properties(view); const auto& published_frame = view.get(impl_->frame); const Spectrum_Frame empty_frame; const Spectrum_Frame& frame = published_frame ? *published_frame : empty_frame; const auto& interaction = view.get(impl_->interaction); - const Axis_Transform horizontal = impl_->frequency_axis->transform(view); - const Axis_Transform vertical = impl_->power_axis->transform(view); - const RectF content = axes_rect(horizontal, vertical); + const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view); + const Axis_Transform power_axis = impl_->power_axis->transform(view); + const RectF content = mapped_rect(frequency_axis, power_axis, + frequency_axis.coordinate_range, + power_axis.coordinate_range); + if (content.empty()) + return; + auto& output = impl_->render_frame; if(state.sweep_region_visible) { - const double first = horizontal.coord_to_pixel(state.sweep_frequency_range.origin); - const double last = horizontal.coord_to_pixel(state.sweep_frequency_range.target); - painter.rect({std::min(first, last), content.y, std::abs(last - first), content.height}, Pen{.style = Line_Style::None}, state.sweep_region_brush); + painter.rect(mapped_rect(frequency_axis, power_axis, state.sweep_frequency_range, + power_axis.coordinate_range), + Pen{.style = Line_Style::None}, state.sweep_region_brush); } - if(state.max_hold_visible) - draw_curve(painter, frame.maxima, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.max_pen, state.max_brush); - if(state.min_hold_visible) - draw_curve(painter, frame.minima, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.min_pen, state.min_brush); - draw_curve(painter, frame.samples, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.current_pen, state.current_brush); + for (int index = 0; output.valid && index < output.active_partitions; ++index) + painter.composite(output.layers[static_cast(index)]); if(state.middle_frequency_pen.enabled()) { - const double x = horizontal.coord_to_pixel(state.center_frequency); - painter.line({x, content.y}, {x, content.bottom()}, state.middle_frequency_pen); + painter.line(mapped_point(frequency_axis, state.center_frequency, power_axis, + power_axis.coordinate_range.origin), + mapped_point(frequency_axis, state.center_frequency, power_axis, + power_axis.coordinate_range.target), + state.middle_frequency_pen); } for(std::size_t index = 0; index < interaction.markers.size(); ++index) { - const double x = horizontal.coord_to_pixel(interaction.markers[index]); - painter.line({x, content.y}, {x, content.bottom()}, static_cast(index) == interaction.selected_marker ? state.selected_marker_pen : state.marker_pen); + painter.line(mapped_point(frequency_axis, interaction.markers[index], power_axis, + power_axis.coordinate_range.origin), + mapped_point(frequency_axis, interaction.markers[index], power_axis, + power_axis.coordinate_range.target), + static_cast(index) == interaction.selected_marker ? state.selected_marker_pen : state.marker_pen); } if(!frame.samples.empty() && (state.max_marker_visible || state.use_min_marker)) { const auto draw_extreme = [&](bool maximum) { @@ -238,7 +359,7 @@ void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) { const std::size_t index = static_cast(std::distance(frame.samples.begin(), iterator)); const double denominator = frame.samples.size() > 1 ? frame.samples.size() - 1.0 : 1.0; const double frequency = state.frequency_range.origin + state.frequency_range.length() * index / denominator; - const PointF point{horizontal.coord_to_pixel(frequency), vertical.coord_to_pixel(*iterator)}; + const PointF point = mapped_point(frequency_axis, frequency, power_axis, *iterator); const Pen& pen = maximum ? state.max_pen : state.min_pen; painter.circle(point, 3.0, pen, Brush{pen.color, Brush_Style::Solid}); }; @@ -248,7 +369,7 @@ void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) { draw_extreme(false); } if(state.tooltip_enabled && interaction.tooltip.active && content.contains(interaction.tooltip.position)) { - const double frequency = horizontal.pixel_to_coord(interaction.tooltip.position.x); + const double frequency = frequency_axis.point_to_coord(interaction.tooltip.position); bool ok{}; const double power = spectrum_power_at(state, frame, frequency, ok); if(ok) { @@ -259,6 +380,7 @@ void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) { painter.text({box.x + 4.0, box.y + 3.0}, text.str(), state.tooltip_font, state.tooltip_text_pen); } } + output.partitioner.finish(output.work_size); } } } diff --git a/render_2D/plottable/Spectrum.h b/render_2D/plottable/Spectrum.h index 1939394..2d9378e 100644 --- a/render_2D/plottable/Spectrum.h +++ b/render_2D/plottable/Spectrum.h @@ -8,6 +8,7 @@ namespace renderive { struct Spectrum_Properties : Hover_Tooltip_Properties { Nonnegative_Count frequency_point_size; + Nonnegative_Count partition_count; Range frequency_range{}; double center_frequency = 50.0; Range sweep_frequency_range{40.0, 60.0}; @@ -60,9 +61,12 @@ public: void handle_event(const Event& event) override; protected: void paint(Painter& painter, const Render_State_View& state) override; + void build_paint_task_graph(Renderable_Task_Graph& graph) override; private: struct Impl; std::unique_ptr impl_; + void prepare_render_frame(const Render_State_View& state); + void render_partition(const Render_State_View& state, int partition_index); void publish() override; }; } diff --git a/render_2D/plottable/Sweep_Spectrum.cpp b/render_2D/plottable/Sweep_Spectrum.cpp index aae40ab..185c6ab 100644 --- a/render_2D/plottable/Sweep_Spectrum.cpp +++ b/render_2D/plottable/Sweep_Spectrum.cpp @@ -9,13 +9,6 @@ namespace renderive { namespace detail { namespace { using Sweep_Spectrum_History = Plottable_History_Real_Time_Data, std::deque>>; -RectF axis_content_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) { - const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin); - const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target); - const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin); - const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target); - return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; -} std::vector flatten(const std::deque>& blocks) { std::vector values; for(const auto& block : blocks) @@ -74,9 +67,9 @@ void Sweep_Spectrum_Control::paint(Painter& painter, const Render_State_View& vi painter.polyline(curve_points(values, state.frequency_range, x, y, state.visible_range_only, state.interpolation_mode), state.pen); const double completed = std::min(1.0, static_cast(blocks.size()) / state.block_count.get()); const double frequency = state.frequency_range.origin + state.frequency_range.length() * completed; - const RectF content = axis_content_rect(x, y); - const double marker_x = x.coord_to_pixel(frequency); - painter.line({marker_x, content.y}, {marker_x, content.bottom()}, state.current_frequency_pen); + painter.line(mapped_point(x, frequency, y, y.coordinate_range.origin), + mapped_point(x, frequency, y, y.coordinate_range.target), + state.current_frequency_pen); } } } diff --git a/render_2D/plottable/Waterfall.cpp b/render_2D/plottable/Waterfall.cpp index d2247b6..57aa686 100644 --- a/render_2D/plottable/Waterfall.cpp +++ b/render_2D/plottable/Waterfall.cpp @@ -1,7 +1,9 @@ #include "Waterfall.h" #include "Heatmap_Utils.h" #include "Plottable_Real_Time_Data.h" +#include "../renderable/Render_Partition.h" #include +#include #include #include #include @@ -18,6 +20,17 @@ struct Waterfall_Interaction { }; using Waterfall_History = Plottable_History_Real_Time_Data>; using Waterfall_Interaction_State = Double_State_Strategy; +struct Waterfall_Render_Frame { + Adaptive_Render_Partitioner partitioner; + Axis_Raster_Layout layout; + Frequency_Columns columns; + std::vector pixels; + int source_width{}; + int source_height{}; + int active_partitions{1}; + std::size_t work_size{}; + bool valid{}; +}; } struct Waterfall_Control::Impl { Impl(Waterfall_Control& owner, std::shared_ptr frequency, std::shared_ptr time) @@ -26,6 +39,7 @@ struct Waterfall_Control::Impl { std::shared_ptr time_axis; Waterfall_History rows; Waterfall_Interaction_State interaction; + Waterfall_Render_Frame render_frame; }; Waterfall_Control::Waterfall_Control(Plot_Core& plot, const Waterfall_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr time_axis) : Plottable_State(plot, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(time_axis))) {} @@ -82,35 +96,95 @@ void Waterfall_Control::publish() { publish_properties(); impl_->interaction.publish(); } -void Waterfall_Control::paint(Painter& painter, const Render_State_View& view) { + +void Waterfall_Control::build_paint_task_graph(Renderable_Task_Graph& graph) { + const auto prepare = graph.emplace([this](const Scene_Render_Context&) { + prepare_render_frame(render_state_view()); + }, "prepare waterfall"); + std::array partitions; + for (int index = 0; index < maximum_render_partitions; ++index) { + partitions[static_cast(index)] = graph.emplace( + [this, index](const Scene_Render_Context&) { + render_partition(render_state_view(), index); + }, + "raster waterfall partition"); + graph.precede(prepare, partitions[static_cast(index)]); + } + const auto compose = add_paint_task( + graph, "compose waterfall", + [this](Painter& painter, const Render_State_View& view) { paint(painter, view); }); + for (const auto partition : partitions) + graph.precede(partition, compose); +} + +void Waterfall_Control::prepare_render_frame(const Render_State_View& view) { const auto& state = render_properties(view); const auto& rows = view.get(impl_->rows); - const auto& interaction = view.get(impl_->interaction); - if(rows.empty()) + auto& output = impl_->render_frame; + output.valid = false; + output.work_size = 0; + if (rows.empty()) return; const int source_width = std::min(state.frequency_bin_count.get(), static_cast(std::min_element(rows.begin(), rows.end(), [](const auto& left, const auto& right) { return left.values.size() < right.values.size(); })->values.size())); const int height = static_cast(rows.size()); - if(source_width <= 0 || height <= 0) + if (source_width <= 0 || height <= 0) return; - const Axis_Transform horizontal = impl_->frequency_axis->transform(view); - const auto columns = frequency_columns(state.frequency_range, horizontal.coordinate_range, source_width, state.visible_range_only); - if(!columns) + const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view); + const Axis_Transform time_axis = impl_->time_axis->transform(view); + const auto columns = frequency_columns(state.frequency_range, frequency_axis.coordinate_range, + source_width, state.visible_range_only); + if (!columns) return; const int width = columns->last - columns->first + 1; - std::vector pixels(static_cast(width) * height); - for(int y = 0; y < height; ++y) { + const Range time_range = rows.size() == 1 + ? time_axis.coordinate_range + : Range{static_cast(rows.front().tick), + static_cast(rows.back().tick)}; + const auto layout = axis_raster_layout(frequency_axis, time_axis, columns->range, + time_range, width, height); + if (!layout.valid()) + return; + output.layout = layout; + output.columns = *columns; + output.source_width = width; + output.source_height = height; + output.work_size = static_cast(width) * height; + output.pixels.resize(output.work_size); + output.active_partitions = + output.partitioner.begin(state.partition_count.get(), output.work_size); + output.valid = true; +} + +void Waterfall_Control::render_partition(const Render_State_View& view, int partition_index) { + auto& output = impl_->render_frame; + if (!output.valid || partition_index >= output.active_partitions) + return; + const auto& state = render_properties(view); + const auto& rows = view.get(impl_->rows); + const auto range = render_partition_range(output.work_size, partition_index, + output.active_partitions); + for (std::size_t cell = range.first; cell < range.last; ++cell) { + const int y = static_cast(cell / static_cast(output.source_width)); + const int x = static_cast(cell % static_cast(output.source_width)); const auto& row = rows[static_cast(y)].values; - for(int x = 0; x < width; ++x) - pixels[static_cast(y) * width + x] = state.color_map.at_normalized(normalized_value(row[static_cast(columns->first + x)], state.power_range)); + output.pixels[output.layout.index(x, y, output.source_width, output.source_height)] = + state.color_map.at_normalized(normalized_value( + row[static_cast(output.columns.first + x)], state.power_range)); } - const Axis_Transform vertical = impl_->time_axis->transform(view); - const Range time_range{static_cast(rows.front().tick), static_cast(rows.back().tick)}; - RectF target = mapped_rect(horizontal, vertical, columns->range, time_range); - if(target.height < 1.0) - target.height = std::max(1.0, impl_->time_axis->transform(view).pixel_length); - painter.heatmap(target, width, height, pixels, state.interpolation_mode); - if(state.tooltip_enabled && interaction.tooltip.active && target.contains(interaction.tooltip.position)) { - const double frequency = horizontal.pixel_to_coord(interaction.tooltip.position.x); +} + +void Waterfall_Control::paint(Painter& painter, const Render_State_View& view) { + const auto& state = render_properties(view); + const auto& interaction = view.get(impl_->interaction); + auto& output = impl_->render_frame; + if (!output.valid) + return; + painter.heatmap(output.layout.target, output.layout.width, output.layout.height, + output.pixels, state.interpolation_mode); + output.partitioner.finish(output.work_size); + if(state.tooltip_enabled && interaction.tooltip.active && output.layout.target.contains(interaction.tooltip.position)) { + const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view); + const double frequency = frequency_axis.point_to_coord(interaction.tooltip.position); std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency << " Hz"; const RectF box{interaction.tooltip.position.x + 8.0, interaction.tooltip.position.y + 8.0, 110.0, 24.0}; diff --git a/render_2D/plottable/Waterfall.h b/render_2D/plottable/Waterfall.h index 4790a27..2123ea0 100644 --- a/render_2D/plottable/Waterfall.h +++ b/render_2D/plottable/Waterfall.h @@ -9,6 +9,7 @@ struct Waterfall_Properties : Hover_Tooltip_Properties { Range frequency_range{0.0, 10.0}; Range power_range{0.0, 10.0}; Nonnegative_Count frequency_bin_count; + Nonnegative_Count partition_count; bool visible_range_only{true}; Image_Interpolation_Mode interpolation_mode = Image_Interpolation_Mode::Nearest; Color_Map color_map; @@ -36,9 +37,12 @@ public: void handle_event(const Event& event) override; protected: void paint(Painter& painter, const Render_State_View& state) override; + void build_paint_task_graph(Renderable_Task_Graph& graph) override; private: struct Impl; std::unique_ptr impl_; + void prepare_render_frame(const Render_State_View& state); + void render_partition(const Render_State_View& state, int partition_index); void publish() override; }; } diff --git a/render_2D/render/Blend2D_Cache.cpp b/render_2D/render/Blend2D_Cache.cpp index d70e9cc..dadbe83 100644 --- a/render_2D/render/Blend2D_Cache.cpp +++ b/render_2D/render/Blend2D_Cache.cpp @@ -222,6 +222,24 @@ Painter::~Painter() { context_.end(); } +void Painter::clip(RectF value) { + value = value.normalized(); + if (!active_ || value.empty()) + return; + context_.clip_to_rect(BLRect(value.x, value.y, value.width, value.height)); +} + +void Painter::composite(const Blend2D_Color_Cache& source) { + if (!active_ || source.image_.is_empty()) + return; + const Size source_size = source.size(); + context_.blit_image(BLRect(0.0, 0.0, + static_cast(source_size.width), + static_cast(source_size.height)), + source.image_, + BLRectI(0, 0, source_size.width, source_size.height)); +} + BLRgba Painter::rgba(Color color) noexcept { constexpr double scale = 1.0 / 255.0; return BLRgba(color.r * scale, color.g * scale, color.b * scale, color.a * scale); diff --git a/render_2D/render/Blend2D_Cache.h b/render_2D/render/Blend2D_Cache.h index a79a50c..2663238 100644 --- a/render_2D/render/Blend2D_Cache.h +++ b/render_2D/render/Blend2D_Cache.h @@ -34,6 +34,8 @@ public: Painter& operator=(const Painter&) = delete; [[nodiscard]] explicit operator bool() const noexcept { return active_; } + void clip(RectF rect); + void composite(const Blend2D_Color_Cache& source); void line(PointF first, PointF second, const Pen& pen); void polyline(std::span points, const Pen& pen); void polygon(std::span points, const Pen& pen, const Brush& brush); diff --git a/render_2D/renderable/Render_Partition.h b/render_2D/renderable/Render_Partition.h new file mode 100644 index 0000000..7113339 --- /dev/null +++ b/render_2D/renderable/Render_Partition.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include + +namespace renderive::detail { + +inline constexpr int maximum_render_partitions = 16; + +struct Render_Partition_Range { + std::size_t first{}; + std::size_t last{}; +}; + +inline Render_Partition_Range render_partition_range(std::size_t size, + int index, + int count) noexcept { + if (size == 0 || count <= 0 || index < 0 || index >= count) + return {}; + return {size * static_cast(index) / static_cast(count), + size * static_cast(index + 1) / static_cast(count)}; +} + +class Adaptive_Render_Partitioner { +public: + Adaptive_Render_Partitioner() noexcept + : automatic_count_(std::clamp( + static_cast(std::thread::hardware_concurrency()), 1, + maximum_render_partitions)) {} + + int begin(int configured_count, std::size_t work_size) noexcept { + automatic_ = configured_count == 0; + const int requested = automatic_ ? automatic_count_ : configured_count; + const int useful = static_cast( + std::min(maximum_render_partitions, + std::max(1, work_size))); + active_count_ = std::clamp(requested, 1, + std::min(maximum_render_partitions, useful)); + started_at_ = Clock::now(); + return active_count_; + } + + void finish(std::size_t work_size) noexcept { + if (!automatic_) + return; + const auto elapsed = + std::chrono::duration_cast(Clock::now() - started_at_); + constexpr auto target = std::chrono::microseconds(1500); + if (elapsed > target * 2 && active_count_ < maximum_render_partitions && + work_size / static_cast(active_count_) >= 512) { + automatic_count_ = std::min(maximum_render_partitions, active_count_ + 1); + } else if (elapsed < target / 2 && active_count_ > 1) { + automatic_count_ = active_count_ - 1; + } else { + automatic_count_ = active_count_; + } + } + +private: + using Clock = std::chrono::steady_clock; + Clock::time_point started_at_{}; + int automatic_count_{1}; + int active_count_{1}; + bool automatic_{true}; +}; + +} // namespace renderive::detail diff --git a/render_2D/renderable/Renderable.cpp b/render_2D/renderable/Renderable.cpp index ecdf5c6..602d803 100644 --- a/render_2D/renderable/Renderable.cpp +++ b/render_2D/renderable/Renderable.cpp @@ -54,6 +54,35 @@ void Renderable::render(const ::Scene_Render_Context& context) { } } +void Renderable::build_task_graph(Renderable_Task_Graph& graph) { + build_paint_task_graph(graph); +} + +void Renderable::build_paint_task_graph(Renderable_Task_Graph& graph) { + add_paint_task(graph, "paint", [this](detail::Painter& painter, + const Render_State_View& state) { + paint(painter, state); + }); +} + +Renderable_Task_Graph::Task Renderable::add_paint_task( + Renderable_Task_Graph& graph, + std::string name, + Paint_Task_Function function) { + return graph.emplace( + [this, function = std::move(function)](const Scene_Render_Context& context) { + if (!is_visible() || context.color_cache == nullptr) + return; + auto* cache = dynamic_cast(context.color_cache); + if (!cache) + return; + detail::Painter painter(*cache, viewport_size()); + if (painter) + function(painter, render_state_view()); + }, + std::move(name)); +} + void Renderable::changed() noexcept { invalidate_cache(); plot_.notify_model_dirty(); diff --git a/render_2D/renderable/Renderable.h b/render_2D/renderable/Renderable.h index 8b38db1..c415aef 100644 --- a/render_2D/renderable/Renderable.h +++ b/render_2D/renderable/Renderable.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -40,11 +41,19 @@ public: void render(const ::Scene_Render_Context& context) final; protected: + using Paint_Task_Function = + std::function; + virtual void paint(detail::Painter& painter, const Render_State_View& state) = 0; + virtual void build_paint_task_graph(Renderable_Task_Graph& graph); + Renderable_Task_Graph::Task add_paint_task(Renderable_Task_Graph& graph, + std::string name, + Paint_Task_Function function); void changed() noexcept; [[nodiscard]] Size viewport_size() const noexcept; private: + void build_task_graph(Renderable_Task_Graph& graph) final; friend class detail::Renderable_State_Observer; Plot_Core& plot_; mutable std::mutex metadata_mutex_; diff --git a/render_2D/tests/render_2D_Integration_Tests.cpp b/render_2D/tests/render_2D_Integration_Tests.cpp index c9f3bdc..08dc810 100644 --- a/render_2D/tests/render_2D_Integration_Tests.cpp +++ b/render_2D/tests/render_2D_Integration_Tests.cpp @@ -74,7 +74,7 @@ TEST(Renderive_Core2, EveryCurveInterpolationModeHasDistinctSamplingSemantics) { 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}; + 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); @@ -608,6 +608,82 @@ TEST(Renderive_Core2, PlottableDataUpdatesReachKernelRealTimeDataStrategy) { 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, PartitionedPlotsBuildExplicitInternalTaskGraphs) { + Plot_Core plot; + plot.init(); + const auto root = plot.root_renderable(); + const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal) + .set_pixel_length(320) + .set_coord_range({0.0, 10.0}) + .build(); + const auto power = Axis::Builder(root, Orientation::Vertical) + .set_pixel_length(180) + .set_coord_range({-120.0, 0.0}) + .build(); + const auto time = Time_Axis::Builder(root, Orientation::Vertical) + .set_pixel_length(180) + .build(); + const auto spectrum = Spectrum::Builder{} + .set<&Spectrum::Properties::partition_count>(4) + .build(root, frequency, power); + const auto waterfall = Waterfall::Builder{} + .set<&Waterfall::Properties::partition_count>(4) + .build(root, frequency, time); + const auto afterglow = Afterglow::Builder{} + .set<&Afterglow::Properties::partition_count>(4) + .build(root, frequency, power); + + 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); + EXPECT_EQ(spectrum->task_graph()->nodes().size(), 18u); + EXPECT_EQ(waterfall->task_graph()->nodes().size(), 18u); + EXPECT_EQ(afterglow->task_graph()->nodes().size(), 35u); +} TEST(Renderive_Core2, PlottableAxesAreDataDependenciesAndPaintOverlays) { Plot_Core plot; plot.init(); diff --git a/web_server/app/Gallery_Plot_Session.cpp b/web_server/app/Gallery_Plot_Session.cpp index db88fd3..8eccf75 100644 --- a/web_server/app/Gallery_Plot_Session.cpp +++ b/web_server/app/Gallery_Plot_Session.cpp @@ -1054,25 +1054,24 @@ private: const int bottom = 46; const int width = std::max(1, viewport.width - left - right); const int height = std::max(1, viewport.height - top - bottom); - if (const auto horizontal = horizontal_axis()) { - update_axis_state(horizontal, [=](Axis_Base_Properties& axis) { + const auto layout_axis = [=](const std::shared_ptr& target) { + if (!target) + return; + update_axis_state(target, [=](Axis_Base_Properties& axis) { axis.x = left; - axis.y = top + height; - axis.pixel_length = static_cast(width); + axis.y = axis.orientation == Orientation::Horizontal ? top + height : top; + axis.pixel_length = static_cast( + axis.orientation == Orientation::Horizontal ? width : height); }); - } - if (const auto vertical = vertical_axis()) { - update_axis_state(vertical, [=](Axis_Base_Properties& axis) { - axis.x = left; - axis.y = top; - axis.pixel_length = static_cast(height); - }); - } + }; + layout_axis(horizontal_axis()); + layout_axis(vertical_axis()); if (case_id_ == "axis_lab" && time_axis_) { time_axis_->update([&](auto& axis) { axis.x = left; - axis.y = 26; - axis.pixel_length = static_cast(width); + axis.y = axis.orientation == Orientation::Horizontal ? 26 : top; + axis.pixel_length = static_cast( + axis.orientation == Orientation::Horizontal ? width : height); }); } } diff --git a/web_server/app/Gallery_Renderables.h b/web_server/app/Gallery_Renderables.h index a6bb46f..289af82 100644 --- a/web_server/app/Gallery_Renderables.h +++ b/web_server/app/Gallery_Renderables.h @@ -348,6 +348,7 @@ struct Type_Descriptor { RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Spectrum, Spectrum_Properties, "spectrum", "Spectrum", property_number("frequency_point_size", "Frequency points"), + property_number("partition_count", "Render partitions"), property_object("frequency_range", "Frequency range"), property_number("center_frequency", "Center frequency"), property_object("sweep_frequency_range", "Sweep range"), @@ -377,6 +378,7 @@ RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Waterfall, Waterfall_Properties, "waterfall property_object("frequency_range", "Frequency range"), property_object("power_range", "Power range"), property_number("frequency_bin_count", "Frequency bins"), + property_number("partition_count", "Render partitions"), property_boolean("visible_range_only", "Visible range only"), property_select("interpolation_mode", "Interpolation"), property_select("color_map", "Color map"), @@ -390,6 +392,7 @@ RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Afterglow, Afterglow_Properties, "afterglow property_object("power_range", "Power range"), property_number("frequency_point_size", "Frequency points"), property_number("power_point_size", "Power points"), + property_number("partition_count", "Render partitions"), property_boolean("interpolate", "Interpolate power"), property_number("attenuation_rate", "Attenuation"), property_select("color_map", "Color map")); diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index 75484c5..a0bb4d8 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -346,10 +347,11 @@ TEST(RenderiveWebGallery, AdminiveResourcesPatchTheRealRenderableState) { response = response_json(session.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"spectrum","patch":{"frequency_point_size":1024,"frequency_range":{"origin":90000000},"current_pen":{"color":"#ff8844"},"interpolation_mode":"Cubic_Value"}})"))); + R"({"category":"event","type":"gallery_patch","target":"spectrum","patch":{"frequency_point_size":1024,"partition_count":4,"frequency_range":{"origin":90000000},"current_pen":{"color":"#ff8844"},"interpolation_mode":"Cubic_Value"}})"))); ASSERT_EQ(response.at("type"), "case_state"); const auto& updated = find_resource(response, "spectrum").at("data"); EXPECT_EQ(updated.at("frequency_point_size"), 1024); + EXPECT_EQ(updated.at("partition_count"), 4); EXPECT_EQ(updated.at("frequency_range").at("origin"), 90'000'000); EXPECT_EQ(updated.at("current_pen").at("color"), "#ff8844"); EXPECT_EQ(updated.at("interpolation_mode"), "Cubic_Value"); @@ -365,6 +367,88 @@ TEST(RenderiveWebGallery, AdminiveResourcesPatchTheRealRenderableState) { EXPECT_EQ(foreign.at("type"), "error"); } +TEST(RenderiveWebGallery, AxisSwapAndEveryPlotDirectionAreEditableAndRenderable) { + const auto patch = [](Gallery_Plot_Session& session, std::string_view target, + nlohmann::json value) { + nlohmann::json request{{"category", "event"}, {"type", "gallery_patch"}, + {"target", target}, {"patch", std::move(value)}}; + return response_json(session.handle(gallery_request( + Gallery_Request_Kind::Patch, request.dump()))); + }; + const auto resource_data = [](const nlohmann::json& state, std::string_view target) + -> const nlohmann::json& { + const auto& resources = state.at("controls").at("resources"); + const auto found = std::find_if(resources.begin(), resources.end(), + [target](const auto& resource) { + return resource.at("target").template get() == target; + }); + if (found == resources.end()) + throw std::out_of_range("missing gallery resource"); + return found->at("data"); + }; + + for (const std::string_view case_id : {"spectrum", "afterglow"}) { + Gallery_Plot_Session session; + auto state = response_json(session.handle(gallery_request( + Gallery_Request_Kind::Open, open_message(case_id, "manual")))); + ASSERT_EQ(state.at("type"), "case_state"); + state = patch(session, "frequency_axis", + {{"orientation", "Vertical"}, + {"coordinates", {{"origin", 108'000'000.0}, + {"target", 88'000'000.0}}}}); + ASSERT_EQ(state.at("type"), "case_state"); + state = patch(session, "value_axis", + {{"orientation", "Horizontal"}, + {"coordinates", {{"origin", -20.0}, {"target", -120.0}}}}); + ASSERT_EQ(state.at("type"), "case_state"); + state = patch(session, case_id, {{"partition_count", 4}}); + ASSERT_EQ(state.at("type"), "case_state"); + EXPECT_EQ(resource_data(state, "frequency_axis").at("orientation"), "Vertical"); + EXPECT_EQ(resource_data(state, "value_axis").at("orientation"), "Horizontal"); + EXPECT_EQ(resource_data(state, case_id).at("partition_count"), 4); + } + + Gallery_Plot_Session waterfall; + auto state = response_json(waterfall.handle(gallery_request( + Gallery_Request_Kind::Open, open_message("waterfall", "manual")))); + ASSERT_EQ(state.at("type"), "case_state"); + state = patch(waterfall, "frequency_axis", + {{"orientation", "Vertical"}, + {"coordinates", {{"origin", 108'000'000.0}, + {"target", 88'000'000.0}}}}); + ASSERT_EQ(state.at("type"), "case_state"); + state = patch(waterfall, "time_axis", + {{"orientation", "Horizontal"}, {"newest_at_start", true}}); + ASSERT_EQ(state.at("type"), "case_state"); + state = patch(waterfall, "waterfall", {{"partition_count", 4}}); + ASSERT_EQ(state.at("type"), "case_state"); + EXPECT_EQ(resource_data(state, "frequency_axis").at("orientation"), "Vertical"); + EXPECT_EQ(resource_data(state, "time_axis").at("orientation"), "Horizontal"); + EXPECT_TRUE(resource_data(state, "time_axis").at("newest_at_start")); + EXPECT_EQ(resource_data(state, "waterfall").at("partition_count"), 4); + + for (const auto& [frequency_orientation, time_orientation, newest_at_start] : + std::array{ + std::tuple{"Horizontal", "Vertical", false}, + std::tuple{"Horizontal", "Vertical", true}, + std::tuple{"Vertical", "Horizontal", false}, + std::tuple{"Vertical", "Horizontal", true}}) { + state = patch(waterfall, "frequency_axis", + {{"orientation", frequency_orientation}}); + ASSERT_EQ(state.at("type"), "case_state"); + state = patch(waterfall, "time_axis", + {{"orientation", time_orientation}, + {"newest_at_start", newest_at_start}}); + ASSERT_EQ(state.at("type"), "case_state"); + EXPECT_EQ(resource_data(state, "frequency_axis").at("orientation"), + frequency_orientation); + EXPECT_EQ(resource_data(state, "time_axis").at("orientation"), + time_orientation); + EXPECT_EQ(resource_data(state, "time_axis").at("newest_at_start"), + newest_at_start); + } +} + TEST(RenderiveWebGallery, EveryPublishedActionDispatchesToItsCanvasAndFrameMode) { constexpr std::array cases{ "axis_lab", "spectrum", "afterglow", "sweep_spectrum",