From c50afae9c516a4dd0a24f56cb30a9588a84e03c3 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Wed, 26 Aug 2026 21:32:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E6=97=B6=E5=88=86=E5=9D=97=E6=8E=A7?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render_2D/render_2D/plottable/Afterglow.hpp | 6 +- render_2D/render_2D/plottable/Afterglow.ipp | 110 +++++++++++------ .../plottable/Constellation_Diagram.hpp | 4 +- .../plottable/Constellation_Diagram.ipp | 108 ++++++++++++++-- .../render_2D/plottable/Frequency_Trace.hpp | 6 +- .../render_2D/plottable/Frequency_Trace.ipp | 18 ++- render_2D/render_2D/plottable/Plot_Types.hpp | 12 +- render_2D/render_2D/plottable/Spectrum.hpp | 7 +- render_2D/render_2D/plottable/Spectrum.ipp | 30 +++-- .../render_2D/plottable/Sweep_Spectrum.hpp | 6 +- .../render_2D/plottable/Sweep_Spectrum.ipp | 20 ++- render_2D/render_2D/plottable/Waterfall.hpp | 6 +- render_2D/render_2D/plottable/Waterfall.ipp | 115 +++++++++++------- .../render_2D/plottable/common/Curve_Plot.cpp | 22 ++-- .../render_2D/plottable/common/Curve_Plot.hpp | 1 - .../plottable/common/Raster_Plot.cpp | 74 ++++++++--- .../plottable/common/Raster_Plot.hpp | 20 ++- .../tests/Async_Render_Contract_Test.cpp | 108 ++++++++++++++-- .../tests/Partition_Configuration_Test.cpp | 91 ++++++++++++++ render_2D/tests/Plottable_Migration_Test.cpp | 16 ++- render_2D/tests/Spectrum_Test.cpp | 4 +- web_server/src/Gallery_Plots_2D.cpp | 27 ++-- web_server/src/Renderable_Adapter.ipp | 3 +- webapp_gallery/src/app.tsx | 10 +- 24 files changed, 621 insertions(+), 203 deletions(-) create mode 100644 render_2D/tests/Partition_Configuration_Test.cpp diff --git a/render_2D/render_2D/plottable/Afterglow.hpp b/render_2D/render_2D/plottable/Afterglow.hpp index ddfebb0..179589b 100644 --- a/render_2D/render_2D/plottable/Afterglow.hpp +++ b/render_2D/render_2D/plottable/Afterglow.hpp @@ -18,12 +18,11 @@ struct Afterglow : Def struct Builder : Prev_Builder { using Base = Prev_Builder; - Builder(Frequency_Object* frequency_axis, Power_Object* power_axis); + Builder(Frequency_Object* frequency_axis, Power_Object* power_axis, + Plot_Partition_Grid partition_grid = {}); [[nodiscard]] std::expected, Dependency_Graph_Error> build(); private: Frequency_Object* frequency_axis{}; /* 不拥有的频率轴;生命周期必须覆盖 Afterglow。 */ diff --git a/render_2D/render_2D/plottable/Afterglow.ipp b/render_2D/render_2D/plottable/Afterglow.ipp index 3a81491..bc11767 100644 --- a/render_2D/render_2D/plottable/Afterglow.ipp +++ b/render_2D/render_2D/plottable/Afterglow.ipp @@ -17,7 +17,7 @@ struct Afterglow::Private : Prev_Private { Frequency_Object* frequency_axis{}; /* 不拥有的频率轴。 */ Power_Object* power_axis{}; /* 不拥有的功率轴。 */ Prepared prepared{}; /* 当前权威状态推导出的 Paint 输入。 */ - Plot_Partition_Count graph_partition_count{}; /* Prepare 子图当前固化的分块数。 */ + Plot_Partition_Grid graph_partition_grid{}; /* Prepare/Paint 子图当前固化的 framebuffer 列×行网格。 */ void bind_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value); /* CRTP 覆盖:构建累加、归一化和着色三阶段 Prepare 子图。 */ template @@ -44,7 +44,14 @@ struct Afterglow::Private : Prev_Private { void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); }; template -Afterglow::Builder::Builder(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {} +Afterglow::Builder::Builder( + Frequency_Object* frequency_axis_value, + Power_Object* power_axis_value, + Plot_Partition_Grid partition_grid) + : Base(), frequency_axis(frequency_axis_value), + power_axis(power_axis_value) { + this->prop.partition_grid = partition_grid; +} template std::expected, Dependency_Graph_Error> Afterglow::Builder::build() { auto result = Base::build(); @@ -78,26 +85,20 @@ std::expected, Dependency_Graph_Error> Afterglow::Builde return plot; } template -bool Afterglow::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { - const std::size_t available = object->template access_query_stream( - [](std::span>> spectra) { - return spectra.empty() ? 0 : spectra.back()->size(); - }); - const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; - return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, std::max(1, columns)); +bool Afterglow::Private::should_rebuild_prepare_graph(Object*, + const Prop& state) { + return graph_partition_grid != state.partition_grid; } template bool Afterglow::Private::should_rebuild_paint_graph(Object*, const Prop&) { - return !paint_graph || paint_graph->size() != graph_partition_count; + return !paint_graph || paint_graph->size() != + detail::raster_partition_count(graph_partition_grid); } template Task_Graph Afterglow::Private::build_prepare_graph(Object* object, const Prop& state) { - const std::size_t available = object->template access_query_stream( - [](std::span>> spectra) { - return spectra.empty() ? 0 : spectra.back()->size(); - }); - const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; - graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, std::max(1, columns)); + graph_partition_grid = state.partition_grid; + const Plot_Partition_Count partition_count = + detail::raster_partition_count(graph_partition_grid); Task_Graph graph{"afterglow.prepare"}; auto begin = graph.add("frame", [this, object] { prepare_frame(object); @@ -105,7 +106,8 @@ Task_Graph Afterglow::Private::build_prepare_graph(Object* object, const Prop& s auto normalize = graph.add("normalize", [this] { normalize_frame(); }); - for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { + if (partition_count == 0) begin.precede(normalize); + for (Plot_Partition_Count index = 0; index < partition_count; ++index) { auto accumulate = graph.add("accumulate", [this, object, index] { accumulate_partition(object, index); }); @@ -121,8 +123,9 @@ Task_Graph Afterglow::Private::build_prepare_graph(Object* object, const Prop& s template Task_Graph Afterglow::Private::build_paint_graph(Object* object, const Prop&) { Task_Graph graph{"afterglow.paint"}; - for (Plot_Partition_Count index = 0; - index < graph_partition_count; ++index) + const Plot_Partition_Count partition_count = + detail::raster_partition_count(graph_partition_grid); + for (Plot_Partition_Count index = 0; index < partition_count; ++index) graph.add("partition", [this, object, index] { paint_partition(object, index); }); @@ -170,20 +173,43 @@ void Afterglow::Private::accumulate_partition(Object* object, Plot_Partition_Cou const auto& state = object->template read_prop(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; - const auto [first, last] = detail::raster_partition_range(static_cast(columns), index, graph_partition_count); + const detail::Raster_Partition partition = detail::raster_partition( + prepared.layout, index, graph_partition_grid); + if (partition.empty()) return; Plot_Ratio attenuation{1.0}; const Plot_Ratio decay = 1.0 - std::clamp(state.attenuation_rate, 0.0, 1.0); const auto& spectra = prepared.source_spectra; const Plot_Ratio retained = std::pow(decay, static_cast(spectra.size())); - for (std::size_t column = first; column < last; ++column) - for (int row = 0; row < rows; ++row) - prepared.intensity[static_cast(row) * columns + column] *= retained; + for (int y = partition.first_y; y < partition.last_y; ++y) + for (int x = partition.first_x; x < partition.last_x; ++x) { + const auto [column, row] = + detail::raster_coordinates(prepared.layout, x, y); + prepared.intensity[static_cast(row) * columns + + column] *= retained; + } for (auto spectrum = spectra.rbegin(); spectrum != spectra.rend() && attenuation >= 0.01; ++spectrum, attenuation *= decay) { const std::size_t count = std::min(columns, (*spectrum)->size()); - for (std::size_t column = first; column < std::min(last, count); ++column) { + for (std::size_t column = 0; column < count; ++column) { const int row = std::clamp(static_cast(detail::normalized_plot_value((**spectrum)[column], state.power_range) * (rows - 1)), 0, rows - 1); - prepared.intensity[static_cast(row) * columns + column] += attenuation; - if (state.interpolate && row + 1 < rows) prepared.intensity[static_cast(row + 1) * columns + column] += attenuation * 0.35; + const std::size_t pixel = prepared.layout.index( + static_cast(column), row); + const int x = static_cast(pixel % prepared.layout.width); + const int y = static_cast(pixel / prepared.layout.width); + if (partition.contains(x, y)) + prepared.intensity[static_cast(row) * columns + + column] += attenuation; + if (state.interpolate && row + 1 < rows) { + const std::size_t adjacent = prepared.layout.index( + static_cast(column), row + 1); + const int adjacent_x = static_cast( + adjacent % prepared.layout.width); + const int adjacent_y = static_cast( + adjacent / prepared.layout.width); + if (partition.contains(adjacent_x, adjacent_y)) + prepared.intensity[ + static_cast(row + 1) * columns + + column] += attenuation * 0.35; + } } } } @@ -195,26 +221,38 @@ void Afterglow::Private::color_partition(Object* object, Plot_Partition_Count in if (!prepared.valid) return; const auto& state = object->template read_prop(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; - const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; - const auto [first, last] = detail::raster_partition_range(static_cast(columns), index, graph_partition_count); - for (std::size_t column = first; column < last; ++column) - for (int row = 0; row < rows; ++row) { - const std::size_t cell = static_cast(row) * columns + column; - prepared.pixels[prepared.layout.index(static_cast(column), row)] = premultiply(state.color_map.sample(prepared.intensity[cell] / prepared.maximum)); + const detail::Raster_Partition partition = detail::raster_partition( + prepared.layout, index, graph_partition_grid); + for (int y = partition.first_y; y < partition.last_y; ++y) + for (int x = partition.first_x; x < partition.last_x; ++x) { + const auto [column, row] = + detail::raster_coordinates(prepared.layout, x, y); + const std::size_t cell = static_cast(row) * + columns + column; + prepared.pixels[static_cast(y) * + prepared.layout.width + x] = + premultiply(state.color_map.sample( + prepared.intensity[cell] / prepared.maximum)); } } template void Afterglow::Private::paint_partition(Object*, Plot_Partition_Count index) { - if (!prepared.valid || index >= graph_partition_count) return; + if (!prepared.valid || index >= + detail::raster_partition_count(graph_partition_grid)) + return; const Rect_F region = detail::raster_paint_region( - prepared.layout, index, graph_partition_count); + prepared.layout, index, graph_partition_grid); if (region.empty()) return; detail::Painter painter(this->paint_surface(), prepared.canvas, region); detail::paint_raster(painter, prepared.layout, prepared.pixels, Image_Interpolation_Mode::bilinear); } template -void Afterglow::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { - if constexpr (std::same_as) object->template mark_dirty(); +void Afterglow::Private::after_prop_set(Object* object, + Member Owner::*, + Prop_Access) { + if constexpr (std::same_as) { + object->template mark_dirty(); + } } inline void Afterglow::Private::bind_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) { frequency_axis = frequency_axis_value; diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.hpp b/render_2D/render_2D/plottable/Constellation_Diagram.hpp index c05c801..61a3948 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.hpp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.hpp @@ -23,6 +23,7 @@ struct Constellation_Diagram : Def struct Builder : Prev_Builder { using Base = Prev_Builder; - Builder(Axis_Object* i_axis, Axis_Object* q_axis); + Builder(Axis_Object* i_axis, Axis_Object* q_axis, + Plot_Partition_Count partition_count = 1); [[nodiscard]] std::expected, Dependency_Graph_Error> build(); private: Axis_Object* i_axis{}; /* 不拥有的同相分量轴;生命周期必须覆盖星座图。 */ diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.ipp b/render_2D/render_2D/plottable/Constellation_Diagram.ipp index bd976f8..e9c35bb 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.ipp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.ipp @@ -18,15 +18,30 @@ struct Constellation_Diagram::Private : Prev_Private { /* CRTP 覆盖:从当前点集和轴状态准备画布坐标。 */ template void prepare_data(Object* object); - /* CRTP 覆盖:直接绘制已准备的星座点与锚点。 */ + /* CRTP 覆盖:构建锚点前驱与并行接收点 Paint 子图。 */ template - void paint(Object* object); + [[nodiscard]] Task_Graph build_paint_graph(Object* object, + const Prop& state); + template + [[nodiscard]] bool should_rebuild_paint_graph(Object* object, + const Prop& state); + template + void paint_anchors(Object* object); + template + void paint_partition(Object* object, Plot_Partition_Count index); + [[nodiscard]] static Rect_F point_region( + std::span points, double radius); /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ template void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); }; template -Constellation_Diagram::Builder::Builder(Axis_Object* i_axis_value, Axis_Object* q_axis_value) : Base(), i_axis(i_axis_value), q_axis(q_axis_value) {} +Constellation_Diagram::Builder::Builder( + Axis_Object* i_axis_value, Axis_Object* q_axis_value, + Plot_Partition_Count partition_count) + : Base(), i_axis(i_axis_value), q_axis(q_axis_value) { + this->prop.partition_count = partition_count; +} template std::expected, Dependency_Graph_Error> Constellation_Diagram::Builder::build() { auto result = Base::build(); @@ -94,24 +109,93 @@ void Constellation_Diagram::Private::prepare_data(Object* object) { object->template mark_dirty(); } template -void Constellation_Diagram::Private::paint(Object* object) { - const auto& state = object->template read_prop(); - auto& cache = this->paint_surface(); - if (!prepared.valid) { - return; +Task_Graph Constellation_Diagram::Private::build_paint_graph( + Object* object, const Prop& state) { + Task_Graph graph{"constellation.paint"}; + auto anchors = graph.add("anchors", [this, object] { + paint_anchors(object); + }); + for (Plot_Partition_Count index = 0; + index < state.partition_count; ++index) { + auto partition = graph.add("partition", [this, object, index] { + paint_partition(object, index); + }); + anchors.precede(partition); } - detail::Painter painter(cache, prepared.canvas); + return graph; +} +template +bool Constellation_Diagram::Private::should_rebuild_paint_graph( + Object*, const Prop& state) { + return !paint_graph || paint_graph->size() != state.partition_count + 1; +} +inline Rect_F Constellation_Diagram::Private::point_region( + std::span points, double radius) { + if (points.empty()) return {}; + double left = points.front().x; + double right = left; + double top = points.front().y; + double bottom = top; + for (const Point_F point : points.subspan(1)) { + left = std::min(left, point.x); + right = std::max(right, point.x); + top = std::min(top, point.y); + bottom = std::max(bottom, point.y); + } + constexpr double antialias_padding{1.0}; + const double padding = radius + antialias_padding; + return {left - padding, top - padding, + right - left + padding * 2.0, + bottom - top + padding * 2.0}; +} +template +void Constellation_Diagram::Private::paint_anchors(Object* object) { + const auto& state = object->template read_prop(); + if (!prepared.valid || prepared.anchors.empty()) return; + detail::Painter painter(this->paint_surface(), prepared.canvas, + point_region(prepared.anchors, 4.5)); /* * 原实现每个点都会创建 BLPath,再分别 fill + stroke;这里改为同色实心圆批量入口。 * 旧样式是 radius + 1px 同色描边,最终外半径约增加 0.5px,因此直接使用 4.5/2.5 * 保持视觉尺寸,同时只做一次填充状态设置并走后端原生 circle primitive。 */ painter.solid_circles(prepared.anchors, 4.5, state.anchor_color); - painter.solid_circles(prepared.points, 2.5, state.point_color); +} +template +void Constellation_Diagram::Private::paint_partition( + Object* object, Plot_Partition_Count index) { + const auto& state = + object->template read_prop(); + if (!prepared.valid || state.partition_count == 0 || + index >= state.partition_count) + return; + const std::size_t point_count = prepared.points.size(); + const auto boundary = [point_count, count = state.partition_count]( + Plot_Partition_Count position) { + return point_count / count * position + + std::min(position, point_count % count); + }; + const std::size_t first = boundary(index); + const std::size_t last = boundary(index + 1); + const std::span points{prepared.points.data() + first, + last - first}; + if (points.empty()) return; + detail::Painter painter(this->paint_surface(), prepared.canvas, + point_region(points, 2.5)); + painter.solid_circles(points, 2.5, state.point_color); } template -void Constellation_Diagram::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { - if constexpr (std::same_as) object->template mark_dirty(); +void Constellation_Diagram::Private::after_prop_set( + Object* object, Member Owner::* member, Prop_Access) { + if constexpr (std::same_as) { + if constexpr (std::same_as) { + if (member == &Prop::partition_count) { + object->template mark_dirty(); + return; + } + } + object->template mark_dirty(); + } } inline void Constellation_Diagram::Private::bind_sources(Axis_Object* i_axis_value, Axis_Object* q_axis_value) { i_axis = i_axis_value; diff --git a/render_2D/render_2D/plottable/Frequency_Trace.hpp b/render_2D/render_2D/plottable/Frequency_Trace.hpp index e3049a3..db294af 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.hpp +++ b/render_2D/render_2D/plottable/Frequency_Trace.hpp @@ -20,9 +20,8 @@ struct Frequency_Trace : Def; struct Prop : Prev_Prop { Prop() { cache_enabled = true; } - Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ + Plot_Partition_Count partition_count{1}; /* 曲线 Prepare/Paint 子图的显式分块数;零值禁用曲线分块任务。 */ Pen pen{Color::yellow()}; /* 频率轨迹折线样式。 */ - Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { @@ -32,7 +31,8 @@ struct Frequency_Trace : Def struct Builder : Prev_Builder { using Base = Prev_Builder; - Builder(Time_Object* time_axis, Value_Object* value_axis); + Builder(Time_Object* time_axis, Value_Object* value_axis, + Plot_Partition_Count partition_count = 1); [[nodiscard]] std::expected, Dependency_Graph_Error> build(); private: Time_Object* time_axis{}; /* 不拥有的时间轴。 */ diff --git a/render_2D/render_2D/plottable/Frequency_Trace.ipp b/render_2D/render_2D/plottable/Frequency_Trace.ipp index 2f81310..2b5c21b 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.ipp +++ b/render_2D/render_2D/plottable/Frequency_Trace.ipp @@ -28,19 +28,24 @@ struct Frequency_Trace::Private : Prev_Private { template void after_prop_set(Object* object, Member Owner::* member, Prop_Access pending_states); }; template -Frequency_Trace::Builder::Builder(Time_Object* time_axis_value, Value_Object* value_axis_value) : Base(), time_axis(time_axis_value), value_axis(value_axis_value) {} +Frequency_Trace::Builder::Builder( + Time_Object* time_axis_value, Value_Object* value_axis_value, + Plot_Partition_Count partition_count) + : Base(), time_axis(time_axis_value), value_axis(value_axis_value) { + this->prop.partition_count = partition_count; +} template std::expected, Dependency_Graph_Error> Frequency_Trace::Builder::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto trace = std::move(result).value(); auto& private_data = static_cast(*trace->d); private_data.bind_sources(time_axis, value_axis); private_data.scene_attach = [object = trace.get()](Root* root) -> std::expected { auto* scene = static_cast(root); auto& data = static_cast(*object->d); data.scene = scene; auto* time_axis = data.time_axis; auto* value_axis = data.value_axis; return scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { prepare.add_dependency(object, scene); prepare.add_dependency(object, time_axis); prepare.add_dependency(object, value_axis); paint.add_dependency(time_axis, object); paint.add_dependency(value_axis, object); cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(object, scene); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, time_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, time_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, time_axis); cache.template add_prop_dependency<&Time_Axis::Prop::visible_count>(object, time_axis); cache.template add_prop_dependency<&Time_Axis::Prop::newest_at_start>(object, time_axis); cache.template add_dependency<&Time_Axis::State::next_tick>(object, time_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, value_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, value_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, value_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, value_axis); }); }; return trace; } template -bool Frequency_Trace::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, object->template access_query_stream([](std::span samples) { return samples.size(); })); } +bool Frequency_Trace::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { return graph_partition_count != state.partition_count; } template bool Frequency_Trace::Private::should_rebuild_paint_graph(Object*, const Prop&) { return !paint_graph || paint_graph->size() != prepared.partitions.size(); } template Task_Graph Frequency_Trace::Private::build_prepare_graph(Object* object, const Prop& state) { - graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, object->template access_query_stream([](std::span samples) { return samples.size(); })); Task_Graph graph{"frequency_trace.prepare"}; + graph_partition_count = state.partition_count; Task_Graph graph{"frequency_trace.prepare"}; auto begin = graph.add("frame", [this, object] { prepare_frame(object, graph_partition_count); }); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.add("partition", [this, object, index] { prepare_partition(object, index); }); begin.precede(task); } return graph; @@ -84,6 +89,11 @@ void Frequency_Trace::Private::paint_partition(Object* object, Plot_Partition_Co detail::paint_curve(painter, curve, state.pen); } template -void Frequency_Trace::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +void Frequency_Trace::Private::after_prop_set( + Object* object, Member Owner::*, Prop_Access) { + if constexpr (std::same_as) { + object->template mark_dirty(); + } +} inline void Frequency_Trace::Private::bind_sources(Time_Object* time_axis_value, Value_Object* value_axis_value) { time_axis = time_axis_value; value_axis = value_axis_value; } } diff --git a/render_2D/render_2D/plottable/Plot_Types.hpp b/render_2D/render_2D/plottable/Plot_Types.hpp index fcd96c8..f08b67c 100644 --- a/render_2D/render_2D/plottable/Plot_Types.hpp +++ b/render_2D/render_2D/plottable/Plot_Types.hpp @@ -2,6 +2,7 @@ #include "../base/Types.hpp" #include #include +#include #include namespace aethera::render_2d { using Plot_Coordinate = double; @@ -11,9 +12,14 @@ using Plot_Time_Tick = int; using Plot_Duration_Milliseconds = Duration_Milliseconds; using Plot_Partition_Count = std::size_t; using Plot_Index = std::ptrdiff_t; -enum struct Plot_Partition_Mode : std::uint8_t { - automatic, - fixed +struct Plot_Partition_Grid { + Plot_Partition_Count columns{1}; /* framebuffer 水平方向的分块数。 */ + Plot_Partition_Count rows{1}; /* framebuffer 垂直方向的分块数。 */ + [[nodiscard]] bool valid() const noexcept { + return columns != 0 && rows != 0 && + columns <= std::numeric_limits::max() / rows; + } + bool operator==(const Plot_Partition_Grid&) const = default; }; struct Color_Map { std::vector stops{}; /* 从低值到高值的离散颜色停靠点;空值使用透明色。 */ diff --git a/render_2D/render_2D/plottable/Spectrum.hpp b/render_2D/render_2D/plottable/Spectrum.hpp index d2fc4c4..19ceee9 100644 --- a/render_2D/render_2D/plottable/Spectrum.hpp +++ b/render_2D/render_2D/plottable/Spectrum.hpp @@ -12,7 +12,6 @@ using Spectrum_Frequency = Plot_Coordinate; using Spectrum_Power = Plot_Value; using Spectrum_Interpolation_Ratio = Plot_Ratio; using Spectrum_Marker_Index = Plot_Index; -using Spectrum_Partition_Mode = Plot_Partition_Mode; struct Spectrum_Frame_Tag {}; struct Spectrum_Frame { std::vector samples{}; /* 最近提交的当前频谱功率样本。 */ @@ -27,7 +26,7 @@ struct Spectrum : Def struct Builder : Prev_Builder { using Base = Prev_Builder; - Builder(Frequency_Object* frequency_axis, Power_Object* power_axis); + Builder(Frequency_Object* frequency_axis, Power_Object* power_axis, + Plot_Partition_Count partition_count = 1); [[nodiscard]] std::expected, Dependency_Graph_Error> build(); private: Frequency_Object* frequency_axis{}; /* 不拥有的频率轴;生命周期必须覆盖 Spectrum。 */ diff --git a/render_2D/render_2D/plottable/Spectrum.ipp b/render_2D/render_2D/plottable/Spectrum.ipp index f5fe6f3..1db0208 100644 --- a/render_2D/render_2D/plottable/Spectrum.ipp +++ b/render_2D/render_2D/plottable/Spectrum.ipp @@ -42,7 +42,7 @@ struct Spectrum::Private : Prev_Private { /* CRTP State 钩子:Spectrum 业务状态写入后标记自身 Prepare;其他继承层状态由各自 Private 负责。 */ template void after_prop_set(Object* object, Member Owner::* member, Prop_Access pending_states); - /* CRTP 子图能力:按当前样本数和 State 分块策略构建并行 Prepare 图。 */ +/* CRTP 子图能力:按显式 partition_count 构建并行 Prepare 图。 */ template [[nodiscard]] Task_Graph build_prepare_graph(Object* object, const Prop& state); /* CRTP 子图能力:构建背景、分块曲线和覆盖标记的 Paint 图。 */ @@ -54,8 +54,6 @@ struct Spectrum::Private : Prev_Private { template [[nodiscard]] bool should_rebuild_paint_graph(Object* object, const Prop& state); template - [[nodiscard]] std::size_t desired_partition_count(const Object* object, const Prop& state) const; - template void prepare_frame(Object* object, std::size_t partition_count); template void prepare_partition(Object* object, std::size_t partition_index); @@ -67,7 +65,14 @@ struct Spectrum::Private : Prev_Private { void paint_overlays(Object* object); }; template -Spectrum::Builder::Builder(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {} +Spectrum::Builder::Builder( + Frequency_Object* frequency_axis_value, + Power_Object* power_axis_value, + Plot_Partition_Count partition_count) + : Base(), frequency_axis(frequency_axis_value), + power_axis(power_axis_value) { + this->prop.partition_count = partition_count; +} template std::expected, Dependency_Graph_Error> Spectrum::Builder::build() { auto result = Base::build(); @@ -101,12 +106,8 @@ std::expected, Dependency_Graph_Error> Spectrum::Builder return spectrum; } template -std::size_t Spectrum::Private::desired_partition_count(const Object* object, const Prop& state) const { - return detail::curve_partition_count(state.partition_mode, state.partition_count, object->template current_buffer().samples.size()); -} -template -bool Spectrum::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { - return prepare_graph_partition_count != desired_partition_count(object, state); +bool Spectrum::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { + return prepare_graph_partition_count != state.partition_count; } template bool Spectrum::Private::should_rebuild_paint_graph(Object*, const Prop&) { @@ -114,7 +115,7 @@ bool Spectrum::Private::should_rebuild_paint_graph(Object*, const Prop&) { } template Task_Graph Spectrum::Private::build_prepare_graph(Object* object, const Prop& state) { - const std::size_t partition_count = desired_partition_count(object, state); + const Plot_Partition_Count partition_count = state.partition_count; prepare_graph_partition_count = partition_count; Task_Graph graph{"spectrum.prepare"}; auto begin = graph.add("frame", [this, object, partition_count] { @@ -264,8 +265,11 @@ void Spectrum::Private::paint_overlays(Object* object) { } } template -void Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { - if constexpr (std::same_as) object->template mark_dirty(); +void Spectrum::Private::after_prop_set(Object* object, Member Owner::*, + Prop_Access) { + if constexpr (std::same_as) { + object->template mark_dirty(); + } } inline void Spectrum::Private::bind_render_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) { frequency_axis = frequency_axis_value; diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.hpp b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp index a3ef884..953851d 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.hpp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp @@ -28,10 +28,9 @@ struct Sweep_Spectrum : Def struct Builder : Prev_Builder { using Base = Prev_Builder; - Builder(Frequency_Object* frequency_axis, Power_Object* power_axis); + Builder(Frequency_Object* frequency_axis, Power_Object* power_axis, + Plot_Partition_Count partition_count = 1); [[nodiscard]] std::expected, Dependency_Graph_Error> build(); private: Frequency_Object* frequency_axis{}; /* 不拥有的频率轴。 */ diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.ipp b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp index 0a497d3..5d521bf 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.ipp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp @@ -41,18 +41,25 @@ struct Sweep_Spectrum::Private : Prev_Private { template void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); }; template -Sweep_Spectrum::Builder::Builder(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {} +Sweep_Spectrum::Builder::Builder( + Frequency_Object* frequency_axis_value, + Power_Object* power_axis_value, + Plot_Partition_Count partition_count) + : Base(), frequency_axis(frequency_axis_value), + power_axis(power_axis_value) { + this->prop.partition_count = partition_count; +} template std::expected, Dependency_Graph_Error> Sweep_Spectrum::Builder::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto sweep = std::move(result).value(); auto& private_data = static_cast(*sweep->d); private_data.bind_sources(frequency_axis, power_axis); private_data.scene_attach = [object = sweep.get()](Root* root) -> std::expected { auto* scene = static_cast(root); auto& data = static_cast(*object->d); data.scene = scene; auto* frequency_axis = data.frequency_axis; auto* power_axis = data.power_axis; return scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { prepare.add_dependency(object, scene); prepare.add_dependency(object, frequency_axis); prepare.add_dependency(object, power_axis); paint.add_dependency(frequency_axis, object); paint.add_dependency(power_axis, object); cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(object, scene); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, frequency_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, power_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, power_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, power_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, power_axis); }); }; return sweep; } template -bool Sweep_Spectrum::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, stored_point_count()); } +bool Sweep_Spectrum::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { return graph_partition_count != state.partition_count; } template bool Sweep_Spectrum::Private::should_rebuild_paint_graph(Object*, const Prop&) { return !paint_graph || paint_graph->size() != prepared.partitions.size() + 1; } template -Task_Graph Sweep_Spectrum::Private::build_prepare_graph(Object* object, const Prop& state) { graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, stored_point_count()); Task_Graph graph{"sweep_spectrum.prepare"}; auto begin = graph.add("frame", [this, object] { prepare_frame(object); }); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.add("partition", [this, object, index] { prepare_partition(object, index); }); begin.precede(task); } return graph; } +Task_Graph Sweep_Spectrum::Private::build_prepare_graph(Object* object, const Prop& state) { graph_partition_count = state.partition_count; Task_Graph graph{"sweep_spectrum.prepare"}; auto begin = graph.add("frame", [this, object] { prepare_frame(object); }); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.add("partition", [this, object, index] { prepare_partition(object, index); }); begin.precede(task); } return graph; } template Task_Graph Sweep_Spectrum::Private::build_paint_graph(Object* object, const Prop&) { Task_Graph graph{"sweep_spectrum.paint"}; @@ -130,7 +137,12 @@ void Sweep_Spectrum::Private::paint_marker(Object* object) { state.current_frequency_pen); } template -void Sweep_Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +void Sweep_Spectrum::Private::after_prop_set( + Object* object, Member Owner::*, Prop_Access) { + if constexpr (std::same_as) { + object->template mark_dirty(); + } +} inline void Sweep_Spectrum::Private::bind_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) { frequency_axis = frequency_axis_value; power_axis = power_axis_value; } inline std::size_t Sweep_Spectrum::Private::stored_point_count() const { std::size_t count{}; diff --git a/render_2D/render_2D/plottable/Waterfall.hpp b/render_2D/render_2D/plottable/Waterfall.hpp index c39a524..d99dd5f 100644 --- a/render_2D/render_2D/plottable/Waterfall.hpp +++ b/render_2D/render_2D/plottable/Waterfall.hpp @@ -28,11 +28,10 @@ struct Waterfall : Def struct Builder : Prev_Builder { using Base = Prev_Builder; - Builder(Frequency_Object* frequency_axis, Time_Object* time_axis); + Builder(Frequency_Object* frequency_axis, Time_Object* time_axis, + Plot_Partition_Grid partition_grid = {}); [[nodiscard]] std::expected, Dependency_Graph_Error> build(); private: Frequency_Object* frequency_axis{}; /* 不拥有的频率轴;生命周期必须覆盖 Waterfall。 */ diff --git a/render_2D/render_2D/plottable/Waterfall.ipp b/render_2D/render_2D/plottable/Waterfall.ipp index 4c13ca7..ebadfae 100644 --- a/render_2D/render_2D/plottable/Waterfall.ipp +++ b/render_2D/render_2D/plottable/Waterfall.ipp @@ -8,15 +8,10 @@ #include namespace aethera::render_2d { struct Waterfall::Private : Prev_Private { - struct Prepared_Row { - std::size_t source{}; /* 本帧冻结 Waterfall_Stream 中可见行的下标。 */ - int slot{}; /* 该 tick 在当前固定时间窗口中的离散槽位。 */ - }; struct Prepared { - std::vector> source_rows{}; /* Rendering-role row ownership retained through parallel Prepare tasks. */ + std::vector> rows_by_slot{}; /* 时间槽到本帧源行的唯一映射;空槽保持空指针。 */ detail::Raster_Layout layout{}; /* 可视频段与完整时间窗口组成的色块布局。 */ std::vector pixels{}; /* 固定时间窗口的像素矩阵;无数据槽保持透明。 */ - std::vector rows{}; /* 可见源行到固定时间槽位的映射。 */ Rect_F tooltip_box{}; /* 当前 hover 提示框的画布矩形。 */ std::string tooltip_text{}; /* 当前 hover 频率文本;空值表示不绘制。 */ Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */ @@ -28,7 +23,7 @@ struct Waterfall::Private : Prev_Private { Time_Object* time_axis{}; /* 不拥有的时间轴,同时权威决定保留行数。 */ Prepared prepared{}; /* 当前权威状态推导出的 Paint 输入。 */ detail::Hover_Tooltip_Runtime tooltip{}; /* 事件侧当前 hover 位置。 */ - Plot_Partition_Count graph_partition_count{}; /* Prepare 子图当前固化的分块数。 */ + Plot_Partition_Grid graph_partition_grid{}; /* Prepare/Paint 子图当前固化的 framebuffer 列×行网格。 */ void bind_sources(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value); /* CRTP 覆盖:按当前色块工作量构建分块 Prepare 子图。 */ template @@ -57,7 +52,13 @@ struct Waterfall::Private : Prev_Private { void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); }; template -Waterfall::Builder::Builder(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) : Base(), frequency_axis(frequency_axis_value), time_axis(time_axis_value) {} +Waterfall::Builder::Builder( + Frequency_Object* frequency_axis_value, Time_Object* time_axis_value, + Plot_Partition_Grid partition_grid) + : Base(), frequency_axis(frequency_axis_value), + time_axis(time_axis_value) { + this->prop.partition_grid = partition_grid; +} template std::expected, Dependency_Graph_Error> Waterfall::Builder::build() { auto result = Base::build(); @@ -93,27 +94,25 @@ std::expected, Dependency_Graph_Error> Waterfall::Builde return plot; } template -bool Waterfall::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { - const std::size_t cells = object->template access_query_stream([&](std::span> rows) { - return rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : rows.empty() ? 1 : rows.back()->values.size()); - }); - return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, cells); +bool Waterfall::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { + return graph_partition_grid != state.partition_grid; } template bool Waterfall::Private::should_rebuild_paint_graph(Object*, const Prop&) { - return !paint_graph || paint_graph->size() != graph_partition_count + 1; + return !paint_graph || paint_graph->size() != + detail::raster_partition_count(graph_partition_grid) + 1; } template -Task_Graph Waterfall::Private::build_prepare_graph(Object* object, const Prop& state) { - const std::size_t cells = object->template access_query_stream([&](std::span> rows) { - return rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : rows.empty() ? 1 : rows.back()->values.size()); - }); - graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, cells); +Task_Graph Waterfall::Private::build_prepare_graph(Object* object, + const Prop& state) { + graph_partition_grid = state.partition_grid; + const Plot_Partition_Count partition_count = + detail::raster_partition_count(graph_partition_grid); Task_Graph graph{"waterfall.prepare"}; auto begin = graph.add("frame", [this, object] { prepare_frame(object); }); - for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { + for (Plot_Partition_Count index = 0; index < partition_count; ++index) { auto task = graph.add("partition", [this, object, index] { prepare_partition(object, index); }); @@ -127,8 +126,9 @@ Task_Graph Waterfall::Private::build_paint_graph(Object* object, const Prop&) { auto tooltip_task = graph.add("tooltip", [this, object] { paint_tooltip(object); }); - for (Plot_Partition_Count index = 0; - index < graph_partition_count; ++index) { + const Plot_Partition_Count partition_count = + detail::raster_partition_count(graph_partition_grid); + for (Plot_Partition_Count index = 0; index < partition_count; ++index) { auto partition = graph.add("partition", [this, object, index] { paint_partition(object, index); }); @@ -144,17 +144,18 @@ void Waterfall::Private::prepare_frame(Object* object) { 2, time_axis->template read_prop().visible_count)); object->template accumulate_stream(row_limit); prepared = {}; - object->template access_rendering_stream([&](std::span> rows) { - prepared.source_rows.assign(rows.begin(), rows.end()); - }); + std::vector> source_rows; + object->template access_rendering_stream( + [&](std::span> rows) { + source_rows.assign(rows.begin(), rows.end()); + }); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& time_layout = time_axis->template read_prop(); prepared.canvas = scene->template read_prop().viewport; - const auto& rows = prepared.source_rows; - if (rows.empty()) { + if (source_rows.empty()) { return; } - const auto shortest = std::min_element(rows.begin(), rows.end(), [](const auto& left, const auto& right) { + const auto shortest = std::min_element(source_rows.begin(), source_rows.end(), [](const auto& left, const auto& right) { return left->values.size() < right->values.size(); }); const std::size_t available = (*shortest)->values.size(); @@ -173,11 +174,18 @@ void Waterfall::Private::prepare_frame(Object* object) { prepared.pixels.assign(static_cast(prepared.layout.width) * prepared.layout.height, 0); const Axis_Coordinate direction = time_range.length() < 0.0 ? -1.0 : 1.0; const Axis_Coordinate first_center = time_range.origin + direction * 0.5; - for (std::size_t source = 0; source < rows.size(); ++source) { - const int slot = static_cast(std::llround((static_cast(rows[source]->tick) - first_center) / direction)); - if (slot >= 0 && slot < time_slots) prepared.rows.push_back({source, slot}); + prepared.rows_by_slot.resize(static_cast(time_slots)); + bool has_visible_row{}; + for (const auto& source_row : source_rows) { + const int slot = static_cast(std::llround( + (static_cast(source_row->tick) - first_center) / + direction)); + if (slot >= 0 && slot < time_slots) { + prepared.rows_by_slot[static_cast(slot)] = source_row; + has_visible_row = true; + } } - if (prepared.rows.empty()) { + if (!has_visible_row) { return; } if (state.tooltip_enabled && tooltip.active && prepared.layout.target.contains(tooltip.position)) { @@ -192,24 +200,37 @@ template void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop(); - const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; - const std::size_t cells = static_cast(columns) * prepared.rows.size(); - const auto [first, last] = detail::raster_partition_range(cells, index, graph_partition_count); - for (std::size_t cell = first; cell < last; ++cell) { - const auto& row = prepared.rows[cell / static_cast(columns)]; - const int column = static_cast(cell % static_cast(columns)); - const auto& values = prepared.source_rows[row.source]->values; - const std::size_t source = static_cast(prepared.source_first + column); - prepared.pixels[prepared.layout.index(column, row.slot)] = premultiply(state.color_map.sample(detail::normalized_plot_value(values[source], state.power_range))); + const detail::Raster_Partition partition = detail::raster_partition( + prepared.layout, index, graph_partition_grid); + for (int y = partition.first_y; y < partition.last_y; ++y) { + for (int x = partition.first_x; x < partition.last_x; ++x) { + const auto [column, slot] = + detail::raster_coordinates(prepared.layout, x, y); + if (slot < 0 || static_cast(slot) >= + prepared.rows_by_slot.size()) + continue; + const auto& row = + prepared.rows_by_slot[static_cast(slot)]; + if (!row) continue; + const std::size_t source = static_cast( + prepared.source_first + column); + prepared.pixels[static_cast(y) * + prepared.layout.width + x] = + premultiply(state.color_map.sample( + detail::normalized_plot_value(row->values[source], + state.power_range))); + } } } template void Waterfall::Private::paint_partition(Object* object, Plot_Partition_Count index) { const auto& state = object->template read_prop(); - if (!prepared.valid || index >= graph_partition_count) return; + if (!prepared.valid || index >= + detail::raster_partition_count(graph_partition_grid)) + return; const Rect_F region = detail::raster_paint_region( - prepared.layout, index, graph_partition_count); + prepared.layout, index, graph_partition_grid); if (region.empty()) return; detail::Painter painter(this->paint_surface(), prepared.canvas, region); detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode); @@ -232,8 +253,12 @@ void Waterfall::Private::handle_event(Object* object, const Event& event) { if (detail::update_hover_tooltip(tooltip, event)) object->template mark_dirty(); } template -void Waterfall::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { - if constexpr (std::same_as) object->template mark_dirty(); +void Waterfall::Private::after_prop_set(Object* object, + Member Owner::*, + Prop_Access) { + if constexpr (std::same_as) { + object->template mark_dirty(); + } } inline void Waterfall::Private::bind_sources(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) { frequency_axis = frequency_axis_value; diff --git a/render_2D/render_2D/plottable/common/Curve_Plot.cpp b/render_2D/render_2D/plottable/common/Curve_Plot.cpp index b35f480..dbe6160 100644 --- a/render_2D/render_2D/plottable/common/Curve_Plot.cpp +++ b/render_2D/render_2D/plottable/common/Curve_Plot.cpp @@ -1,9 +1,14 @@ #include "Curve_Plot.hpp" #include #include -#include namespace aethera::render_2d::detail { namespace { +std::size_t partition_boundary(std::size_t work_size, + Plot_Partition_Count index, + Plot_Partition_Count count) { + return work_size / count * index + + std::min(index, work_size % count); +} Plot_Value power_domain_lerp(Plot_Value first, Plot_Value second, Plot_Ratio ratio) { const Plot_Value first_power = std::pow(10.0, std::clamp(first, -3'000.0, 3'000.0) / 10.0); const Plot_Value second_power = std::pow(10.0, std::clamp(second, -3'000.0, 3'000.0) / 10.0); @@ -15,17 +20,16 @@ Plot_Value cubic_value(Plot_Value previous, Plot_Value first, Plot_Value second, return 0.5 * ((2.0 * first) + (-previous + second) * ratio + (2.0 * previous - 5.0 * first + 4.0 * second - next) * ratio2 + (-previous + 3.0 * first - 3.0 * second + next) * ratio3); } } -Plot_Partition_Count curve_partition_count(Plot_Partition_Mode mode, Plot_Partition_Count requested_count, std::size_t sample_count) { - const Plot_Partition_Count segment_count = sample_count > 1 ? sample_count - 1 : 1; - if (mode == Plot_Partition_Mode::fixed) return std::clamp(requested_count, Plot_Partition_Count{1}, segment_count); - const Plot_Partition_Count worker_count = std::max(1, std::thread::hardware_concurrency()); - return std::min({segment_count, worker_count, std::max(1, (segment_count + 127) / 128)}); -} Curve_Partition_Range curve_partition_range(std::size_t sample_count, Plot_Partition_Count partition_index, Plot_Partition_Count partition_count, Axis_Range domain) { if (sample_count == 0 || partition_count == 0) return {}; const std::size_t segment_count = sample_count > 1 ? sample_count - 1 : 1; - const std::size_t first_sample = std::min(segment_count * partition_index / partition_count, sample_count - 1); - const std::size_t last_sample = std::min(segment_count * (partition_index + 1) / partition_count, sample_count - 1); + const std::size_t first_sample = std::min( + partition_boundary(segment_count, partition_index, partition_count), + sample_count - 1); + const std::size_t last_sample = std::min( + partition_boundary(segment_count, partition_index + 1, + partition_count), + sample_count - 1); const Plot_Ratio denominator = sample_count > 1 ? static_cast(sample_count - 1) : 1.0; return {first_sample, last_sample - first_sample + 1, {domain.origin + domain.length() * static_cast(first_sample) / denominator, domain.origin + domain.length() * static_cast(last_sample) / denominator}}; } diff --git a/render_2D/render_2D/plottable/common/Curve_Plot.hpp b/render_2D/render_2D/plottable/common/Curve_Plot.hpp index 6d3e524..024a473 100644 --- a/render_2D/render_2D/plottable/common/Curve_Plot.hpp +++ b/render_2D/render_2D/plottable/common/Curve_Plot.hpp @@ -18,7 +18,6 @@ struct Curve_Partition_Range { std::size_t sample_count{}; /* 分块包含的样本数;相邻块共享边界样本。 */ Axis_Range domain{}; /* 分块样本对应的业务坐标范围。 */ }; -[[nodiscard]] Plot_Partition_Count curve_partition_count(Plot_Partition_Mode mode, Plot_Partition_Count requested_count, std::size_t sample_count); [[nodiscard]] Curve_Partition_Range curve_partition_range(std::size_t sample_count, Plot_Partition_Count partition_index, Plot_Partition_Count partition_count, Axis_Range domain); [[nodiscard]] std::vector interpolate_curve(std::span values, Axis_Range domain, Line_Interpolation_Mode mode); [[nodiscard]] std::vector visible_curve_samples(std::vector samples, Axis_Range visible_range); diff --git a/render_2D/render_2D/plottable/common/Raster_Plot.cpp b/render_2D/render_2D/plottable/common/Raster_Plot.cpp index 6992e4c..66e7ed0 100644 --- a/render_2D/render_2D/plottable/common/Raster_Plot.cpp +++ b/render_2D/render_2D/plottable/common/Raster_Plot.cpp @@ -2,8 +2,22 @@ #include "Curve_Plot.hpp" #include namespace aethera::render_2d::detail { +namespace { +Plot_Partition_Count partition_boundary(Plot_Partition_Count work_size, + Plot_Partition_Count index, + Plot_Partition_Count count) { + return work_size / count * index + + std::min(index, work_size % count); +} +} bool Raster_Layout::valid() const noexcept { return width > 0 && height > 0 && !target.empty(); } int Raster_Axis_Selection::count() const noexcept { return last - first + 1; } +bool Raster_Partition::empty() const noexcept { + return first_x >= last_x || first_y >= last_y; +} +bool Raster_Partition::contains(int x, int y) const noexcept { + return x >= first_x && x < last_x && y >= first_y && y < last_y; +} std::size_t Raster_Layout::index(int first, int second) const noexcept { if (first_reversed) first = (first_horizontal ? width : height) - 1 - first; if (second_reversed) second = (first_horizontal ? height : width) - 1 - second; @@ -35,27 +49,55 @@ Raster_Layout raster_layout(const Abs_Axis* first_axis, Axis_Range first_range, const bool horizontal = first_orientation == Axis_Orientation::horizontal; return {horizontal ? first_count : second_count, horizontal ? second_count : first_count, map_plot_rect(first_axis, first_range, second_axis, second_range, first_orientation), first_axis->coordinate_to_pixel(first_range.origin) > first_axis->coordinate_to_pixel(first_range.target), second_axis->coordinate_to_pixel(second_range.origin) > second_axis->coordinate_to_pixel(second_range.target), horizontal}; } -std::pair raster_partition_range(std::size_t work_size, Plot_Partition_Count partition_index, Plot_Partition_Count partition_count) { - if (partition_count == 0) return {}; - return {work_size * partition_index / partition_count, work_size * (partition_index + 1) / partition_count}; +Plot_Partition_Count raster_partition_count(Plot_Partition_Grid grid) noexcept { + return grid.valid() ? grid.columns * grid.rows : 0; +} +Raster_Partition raster_partition(const Raster_Layout& layout, + Plot_Partition_Count partition_index, + Plot_Partition_Grid grid) { + const Plot_Partition_Count count = raster_partition_count(grid); + if (!layout.valid() || partition_index >= count) return {}; + const auto column = partition_index % grid.columns; + const auto row = partition_index / grid.columns; + return { + static_cast(partition_boundary(layout.width, column, + grid.columns)), + static_cast(partition_boundary(layout.width, column + 1, + grid.columns)), + static_cast(partition_boundary(layout.height, row, + grid.rows)), + static_cast(partition_boundary(layout.height, row + 1, + grid.rows))}; +} +std::pair raster_coordinates(const Raster_Layout& layout, + int x, int y) noexcept { + int first = layout.first_horizontal ? x : y; + int second = layout.first_horizontal ? y : x; + if (layout.first_reversed) + first = (layout.first_horizontal ? layout.width : layout.height) - 1 - first; + if (layout.second_reversed) + second = (layout.first_horizontal ? layout.height : layout.width) - 1 - second; + return {first, second}; } Rect_F raster_paint_region(const Raster_Layout& layout, Plot_Partition_Count partition_index, - Plot_Partition_Count partition_count) { - if (!layout.valid() || partition_count == 0 || - partition_index >= partition_count) - return {}; + Plot_Partition_Grid grid) { + const Raster_Partition partition = + raster_partition(layout, partition_index, grid); + if (partition.empty()) return {}; const Rect_F target = layout.target.normalized(); - const double first = target.x + target.width * - static_cast(partition_index) / - static_cast(partition_count); - const double last = target.x + target.width * - static_cast(partition_index + 1) / - static_cast(partition_count); + const double left = target.x + target.width * partition.first_x / + static_cast(layout.width); + const double right = target.x + target.width * partition.last_x / + static_cast(layout.width); + const double top = target.y + target.height * partition.first_y / + static_cast(layout.height); + const double bottom = target.y + target.height * partition.last_y / + static_cast(layout.height); constexpr double overlap{1.0}; - return {first - overlap, target.y - overlap, - last - first + overlap * 2.0, - target.height + overlap * 2.0}; + return {left - overlap, top - overlap, + right - left + overlap * 2.0, + bottom - top + overlap * 2.0}; } void paint_raster(Painter& painter, const Raster_Layout& layout, std::span pixels, Image_Interpolation_Mode interpolation) { if (layout.valid() && pixels.size() == static_cast(layout.width) * static_cast(layout.height)) painter.heatmap(layout.target, layout.width, layout.height, pixels, interpolation); diff --git a/render_2D/render_2D/plottable/common/Raster_Plot.hpp b/render_2D/render_2D/plottable/common/Raster_Plot.hpp index 087d9aa..ae12de4 100644 --- a/render_2D/render_2D/plottable/common/Raster_Plot.hpp +++ b/render_2D/render_2D/plottable/common/Raster_Plot.hpp @@ -23,13 +23,27 @@ struct Raster_Axis_Selection { Axis_Range range{}; /* 选中格点覆盖的业务坐标范围。 */ [[nodiscard]] int count() const noexcept; }; +struct Raster_Partition { + int first_x{}; /* framebuffer 矩阵中包含的首列。 */ + int last_x{}; /* framebuffer 矩阵中不包含的尾列。 */ + int first_y{}; /* framebuffer 矩阵中包含的首行。 */ + int last_y{}; /* framebuffer 矩阵中不包含的尾行。 */ + [[nodiscard]] bool empty() const noexcept; + [[nodiscard]] bool contains(int x, int y) const noexcept; +}; [[nodiscard]] Plot_Ratio normalized_plot_value(Plot_Value value, Axis_Range range); [[nodiscard]] std::optional raster_axis_selection(Axis_Range data_range, Axis_Range visible_range, int source_count, bool visible_only); [[nodiscard]] Raster_Layout raster_layout(const Abs_Axis* first_axis, Axis_Range first_range, int first_count, const Abs_Axis* second_axis, Axis_Range second_range, int second_count, Axis_Orientation first_orientation, Axis_Orientation second_orientation); -[[nodiscard]] std::pair raster_partition_range(std::size_t work_size, Plot_Partition_Count partition_index, Plot_Partition_Count partition_count); -/* 将目标矩形切成可并行写入的竖向 framebuffer 视图;相邻视图保留一像素重叠。 */ +[[nodiscard]] Plot_Partition_Count raster_partition_count( + Plot_Partition_Grid grid) noexcept; +[[nodiscard]] Raster_Partition raster_partition( + const Raster_Layout& layout, Plot_Partition_Count partition_index, + Plot_Partition_Grid grid); +[[nodiscard]] std::pair raster_coordinates( + const Raster_Layout& layout, int x, int y) noexcept; +/* 将目标矩形按列×行网格切成 framebuffer 视图;相邻 tile 保留一像素重叠。 */ [[nodiscard]] Rect_F raster_paint_region(const Raster_Layout& layout, Plot_Partition_Count partition_index, - Plot_Partition_Count partition_count); + Plot_Partition_Grid grid); void paint_raster(Painter& painter, const Raster_Layout& layout, std::span pixels, Image_Interpolation_Mode interpolation); } diff --git a/render_2D/tests/Async_Render_Contract_Test.cpp b/render_2D/tests/Async_Render_Contract_Test.cpp index 91e98a4..577b5d7 100644 --- a/render_2D/tests/Async_Render_Contract_Test.cpp +++ b/render_2D/tests/Async_Render_Contract_Test.cpp @@ -1,6 +1,8 @@ #include #include +#include #include +#include #include #include #include @@ -34,13 +36,22 @@ bool contains_color(Image_View view) { int main() { using Frequency = Impl; using Power = Impl; + using Time = Impl; using Spectrum_Object = Impl; + using Constellation_Object = Impl; + using Waterfall_Object = Impl; using Scene_Object = Impl; initialize_runtime({.workers = 4}); auto* frequency = build_object(); auto* power = build_object(); - auto* spectrum = build_object(frequency, power); + auto* spectrum = build_object(frequency, power, 4u); + auto* time = build_object