From ef9e36c4b99e598a01129c33f5023acfa4829e16 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Wed, 26 Aug 2026 20:35:02 +0800 Subject: [PATCH] =?UTF-8?q?blend2D=E5=B9=B6=E8=A1=8C=E7=BB=98=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render_2D/render_2D/plottable/Afterglow.ipp | 28 ++-- .../render_2D/plottable/Frequency_Trace.ipp | 22 ++- render_2D/render_2D/plottable/Spectrum.ipp | 71 +++++++-- .../render_2D/plottable/Sweep_Spectrum.ipp | 40 ++++- render_2D/render_2D/plottable/Waterfall.ipp | 51 ++++-- .../render_2D/plottable/common/Curve_Plot.cpp | 26 ++++ .../render_2D/plottable/common/Curve_Plot.hpp | 2 + .../plottable/common/Raster_Plot.cpp | 18 +++ .../plottable/common/Raster_Plot.hpp | 4 + render_2D/render_2D/render/Blend2D_Cache.cpp | 136 +++++++++++----- render_2D/render_2D/render/Blend2D_Cache.hpp | 5 + render_2D/render_2D/render/Blend2D_Cache.ipp | 12 +- render_2D/render_2D/scene/Render_Scene_2D.ipp | 146 ++++++++++++++---- .../tests/Async_Render_Contract_Test.cpp | 108 +++++++++++++ render_2D/tests/Frame_Pixel_Format_Test.cpp | 49 ++++++ render_2D/tests/Spectrum_Test.cpp | 2 +- 16 files changed, 603 insertions(+), 117 deletions(-) create mode 100644 render_2D/tests/Async_Render_Contract_Test.cpp diff --git a/render_2D/render_2D/plottable/Afterglow.ipp b/render_2D/render_2D/plottable/Afterglow.ipp index 29fd075..3a81491 100644 --- a/render_2D/render_2D/plottable/Afterglow.ipp +++ b/render_2D/render_2D/plottable/Afterglow.ipp @@ -29,6 +29,8 @@ struct Afterglow::Private : Prev_Private { template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); template + [[nodiscard]] bool should_rebuild_paint_graph(Object* object, const Prop& state); + template void prepare_frame(Object* object); template void accumulate_partition(Object* object, Plot_Partition_Count index); @@ -36,7 +38,7 @@ struct Afterglow::Private : Prev_Private { template void color_partition(Object* object, Plot_Partition_Count index); template - void paint_frame(Object* object); + void paint_partition(Object* object, Plot_Partition_Count index); /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ template void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); @@ -85,6 +87,10 @@ bool Afterglow::Private::should_rebuild_prepare_graph(Object* object, const Prop return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, std::max(1, columns)); } template +bool Afterglow::Private::should_rebuild_paint_graph(Object*, const Prop&) { + return !paint_graph || paint_graph->size() != graph_partition_count; +} +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) { @@ -115,9 +121,11 @@ 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"}; - graph.add("frame", [this, object] { - paint_frame(object); - }); + for (Plot_Partition_Count index = 0; + index < graph_partition_count; ++index) + graph.add("partition", [this, object, index] { + paint_partition(object, index); + }); return graph; } template @@ -196,12 +204,12 @@ void Afterglow::Private::color_partition(Object* object, Plot_Partition_Count in } } template -void Afterglow::Private::paint_frame(Object* object) { - auto& cache = this->paint_surface(); - if (!prepared.valid) { - return; - } - detail::Painter painter(cache, prepared.canvas); +void Afterglow::Private::paint_partition(Object*, Plot_Partition_Count index) { + if (!prepared.valid || index >= graph_partition_count) return; + const Rect_F region = detail::raster_paint_region( + prepared.layout, index, graph_partition_count); + 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 diff --git a/render_2D/render_2D/plottable/Frequency_Trace.ipp b/render_2D/render_2D/plottable/Frequency_Trace.ipp index 3fb6417..2f81310 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.ipp +++ b/render_2D/render_2D/plottable/Frequency_Trace.ipp @@ -20,9 +20,10 @@ struct Frequency_Trace::Private : Prev_Private { template [[nodiscard]] Task_Graph build_paint_graph(Object* object, const Prop& state); /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); + template [[nodiscard]] bool should_rebuild_paint_graph(Object* object, const Prop& state); template void prepare_frame(Object* object, Plot_Partition_Count partition_count); template void prepare_partition(Object* object, Plot_Partition_Count partition_index); - template void paint_frame(Object* object); + template void paint_partition(Object* object, Plot_Partition_Count partition_index); /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ template void after_prop_set(Object* object, Member Owner::* member, Prop_Access pending_states); }; @@ -36,6 +37,8 @@ std::expected, Dependency_Graph_Error> Frequency_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(); })); } 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"}; auto begin = graph.add("frame", [this, object] { prepare_frame(object, graph_partition_count); }); @@ -43,7 +46,12 @@ Task_Graph Frequency_Trace::Private::build_prepare_graph(Object* object, const P return graph; } template -Task_Graph Frequency_Trace::Private::build_paint_graph(Object* object, const Prop&) { Task_Graph graph{"frequency_trace.paint"}; graph.add("frame", [this, object] { paint_frame(object); }); return graph; } +Task_Graph Frequency_Trace::Private::build_paint_graph(Object* object, const Prop&) { + Task_Graph graph{"frequency_trace.paint"}; + for (Plot_Partition_Count index = 0; index < prepared.partitions.size(); ++index) + graph.add("partition", [this, object, index] { paint_partition(object, index); }); + return graph; +} template void Frequency_Trace::Private::prepare_frame(Object* object, Plot_Partition_Count partition_count) { const auto& state = object->template read_prop(); const auto& time_layout = time_axis->template read_prop(); const auto& value_layout = value_axis->template read_prop(); @@ -66,8 +74,14 @@ void Frequency_Trace::Private::prepare_partition(Object* object, Plot_Partition_ prepared.partitions[partition_index] = detail::prepare_curve(std::span(prepared.samples).subspan(range.first_sample, range.sample_count), true, time_axis->coordinate_range(), value_state.coordinate_range, time_axis, value_axis, time_layout.orientation, value_layout.orientation); } template -void Frequency_Trace::Private::paint_frame(Object* object) { - const auto& state = object->template read_prop(); auto& cache = this->paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); +void Frequency_Trace::Private::paint_partition(Object* object, Plot_Partition_Count partition_index) { + if (!prepared.valid || partition_index >= prepared.partitions.size()) return; + const auto& state = object->template read_prop(); + const auto& curve = prepared.partitions[partition_index]; + const Rect_F region = detail::curve_paint_region(curve, state.pen.width + 2.0); + if (region.empty()) return; + detail::Painter painter(this->paint_surface(), prepared.canvas, region); + 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(); } diff --git a/render_2D/render_2D/plottable/Spectrum.ipp b/render_2D/render_2D/plottable/Spectrum.ipp index 5d9a16f..f5fe6f3 100644 --- a/render_2D/render_2D/plottable/Spectrum.ipp +++ b/render_2D/render_2D/plottable/Spectrum.ipp @@ -52,13 +52,19 @@ struct Spectrum::Private : Prev_Private { template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); 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); template - void paint_frame(Object* object); + void begin_paint(Object* object); + template + void paint_partition(Object* object, std::size_t partition_index); + template + 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) {} @@ -103,6 +109,10 @@ bool Spectrum::Private::should_rebuild_prepare_graph(Object* object, const Prop& return prepare_graph_partition_count != desired_partition_count(object, state); } template +bool Spectrum::Private::should_rebuild_paint_graph(Object*, const Prop&) { + return !paint_graph || paint_graph->size() != prepared.partitions.size() + 2; +} +template Task_Graph Spectrum::Private::build_prepare_graph(Object* object, const Prop& state) { const std::size_t partition_count = desired_partition_count(object, state); prepare_graph_partition_count = partition_count; @@ -121,9 +131,20 @@ Task_Graph Spectrum::Private::build_prepare_graph(Object* object, const Prop& st template Task_Graph Spectrum::Private::build_paint_graph(Object* object, const Prop&) { Task_Graph graph{"spectrum.paint"}; - graph.add("frame", [this, object] { - paint_frame(object); + auto begin = graph.add("begin", [this, object] { + begin_paint(object); }); + auto overlays = graph.add("overlays", [this, object] { + paint_overlays(object); + }); + for (std::size_t index = 0; index < prepared.partitions.size(); ++index) { + auto partition = graph.add("partition", [this, object, index] { + paint_partition(object, index); + }); + begin.precede(partition); + partition.precede(overlays); + } + if (prepared.partitions.empty()) begin.precede(overlays); return graph; } template @@ -189,7 +210,7 @@ void Spectrum::Private::prepare_partition(Object* object, std::size_t partition_ if (state.min_hold_visible && minima.size() == frame.samples.size()) partition.minimum = detail::prepare_curve(std::span(minima).subspan(range.first_sample, range.sample_count), range.domain, state.interpolation_mode, state.visible_range_only, frequency_state.coordinate_range, power_state.coordinate_range, frequency_axis, power_axis, frequency_layout.orientation, power_layout.orientation); } template -void Spectrum::Private::paint_frame(Object* object) { +void Spectrum::Private::begin_paint(Object* object) { auto& private_data = static_cast(*this); const auto& state = static_cast(*private_data.current); auto& published = static_cast(*private_data.state.pending); @@ -200,15 +221,39 @@ void Spectrum::Private::paint_frame(Object* object) { published.selectable_marker_count = state.custom_markers.size(); auto& cache = private_data.paint_surface(); if (!prepared.valid) return; - detail::Painter painter(cache, prepared.canvas_size); - if (state.sweep_region_visible && !prepared.sweep_region.empty()) painter.rect(prepared.sweep_region, Pen{.style = Line_Style::none}, state.sweep_region_brush); - for (const auto& partition : prepared.partitions) { - if (partition.clip.empty()) continue; - const auto clip = painter.scoped_clip(partition.clip); - detail::paint_curve(painter, partition.maximum, state.max_pen, state.max_brush); - detail::paint_curve(painter, partition.minimum, state.min_pen, state.min_brush); - detail::paint_curve(painter, partition.current, state.current_pen, state.current_brush); - } + if (!state.sweep_region_visible || prepared.sweep_region.empty()) return; + detail::Painter painter(cache, prepared.canvas_size, prepared.sweep_region); + painter.rect(prepared.sweep_region, Pen{.style = Line_Style::none}, + state.sweep_region_brush); +} +template +void Spectrum::Private::paint_partition(Object* object, + std::size_t partition_index) { + if (!prepared.valid || partition_index >= prepared.partitions.size()) return; + auto& private_data = static_cast(*this); + const auto& state = static_cast(*private_data.current); + const auto& partition = prepared.partitions[partition_index]; + if (partition.clip.empty()) return; + const Rect_F clip = partition.clip.normalized(); + constexpr double overlap{2.0}; + detail::Painter painter( + private_data.paint_surface(), prepared.canvas_size, + {clip.x - overlap, clip.y - overlap, + clip.width + overlap * 2.0, clip.height + overlap * 2.0}); + const auto clip_scope = painter.scoped_clip(clip); + detail::paint_curve(painter, partition.maximum, state.max_pen, + state.max_brush); + detail::paint_curve(painter, partition.minimum, state.min_pen, + state.min_brush); + detail::paint_curve(painter, partition.current, state.current_pen, + state.current_brush); +} +template +void Spectrum::Private::paint_overlays(Object* object) { + if (!prepared.valid) return; + auto& private_data = static_cast(*this); + const auto& state = static_cast(*private_data.current); + detail::Painter painter(private_data.paint_surface(), prepared.canvas_size); for (const auto& marker : prepared.markers) { const Pen& pen = marker.style == Marker_Style::middle ? state.middle_frequency_pen : marker.style == Marker_Style::selected ? state.selected_marker_pen : state.marker_pen; if (pen.enabled()) painter.line(marker.first, marker.second, pen); diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.ipp b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp index 232c32c..0a497d3 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.ipp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp @@ -32,9 +32,11 @@ struct Sweep_Spectrum::Private : Prev_Private { template [[nodiscard]] Task_Graph build_paint_graph(Object* object, const Prop& state); /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); + template [[nodiscard]] bool should_rebuild_paint_graph(Object* object, const Prop& state); template void prepare_frame(Object* object); template void prepare_partition(Object* object, Plot_Partition_Count index); - template void paint_frame(Object* object); + template void paint_partition(Object* object, Plot_Partition_Count index); + template void paint_marker(Object* object); [[nodiscard]] std::size_t stored_point_count() const; template void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); }; @@ -48,9 +50,19 @@ std::expected, Dependency_Graph_Error> Sweep_Spectrum::B 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()); } 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; } template -Task_Graph Sweep_Spectrum::Private::build_paint_graph(Object* object, const Prop&) { Task_Graph graph{"sweep_spectrum.paint"}; graph.add("frame", [this, object] { paint_frame(object); }); return graph; } +Task_Graph Sweep_Spectrum::Private::build_paint_graph(Object* object, const Prop&) { + Task_Graph graph{"sweep_spectrum.paint"}; + auto marker = graph.add("marker", [this, object] { paint_marker(object); }); + for (Plot_Partition_Count index = 0; index < prepared.partitions.size(); ++index) { + auto partition = graph.add("partition", [this, object, index] { paint_partition(object, index); }); + partition.precede(marker); + } + return graph; +} template void Sweep_Spectrum::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop(); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& power_layout = power_axis->template read_prop(); const auto& power_state = power_axis->template read_prop(); prepared = {}; prepared.canvas = scene->template read_prop().viewport; @@ -94,7 +106,29 @@ void Sweep_Spectrum::Private::prepare_partition(Object* object, Plot_Partition_C prepared.partitions[index] = detail::prepare_curve(std::span(prepared.values).subspan(range.first_sample, range.sample_count), range.domain, state.interpolation_mode, state.visible_range_only, frequency_state.coordinate_range, power_state.coordinate_range, frequency_axis, power_axis, frequency_layout.orientation, power_layout.orientation); } template -void Sweep_Spectrum::Private::paint_frame(Object* object) { const auto& state = object->template read_prop(); auto& cache = this->paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); painter.line(prepared.marker_first, prepared.marker_second, state.current_frequency_pen); } +void Sweep_Spectrum::Private::paint_partition(Object* object, Plot_Partition_Count index) { + if (!prepared.valid || index >= prepared.partitions.size()) return; + const auto& state = object->template read_prop(); + const auto& curve = prepared.partitions[index]; + const Rect_F region = detail::curve_paint_region(curve, state.pen.width + 2.0); + if (region.empty()) return; + detail::Painter painter(this->paint_surface(), prepared.canvas, region); + detail::paint_curve(painter, curve, state.pen); +} +template +void Sweep_Spectrum::Private::paint_marker(Object* object) { + if (!prepared.valid) return; + const auto& state = object->template read_prop(); + const double padding = state.current_frequency_pen.width + 2.0; + const auto [left, right] = std::minmax(prepared.marker_first.x, prepared.marker_second.x); + const auto [top, bottom] = std::minmax(prepared.marker_first.y, prepared.marker_second.y); + detail::Painter painter(this->paint_surface(), prepared.canvas, + {left - padding, top - padding, + right - left + padding * 2.0, + bottom - top + padding * 2.0}); + painter.line(prepared.marker_first, prepared.marker_second, + 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(); } 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; } diff --git a/render_2D/render_2D/plottable/Waterfall.ipp b/render_2D/render_2D/plottable/Waterfall.ipp index 1e5f860..4c13ca7 100644 --- a/render_2D/render_2D/plottable/Waterfall.ipp +++ b/render_2D/render_2D/plottable/Waterfall.ipp @@ -40,11 +40,15 @@ struct Waterfall::Private : Prev_Private { template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); template + [[nodiscard]] bool should_rebuild_paint_graph(Object* object, const Prop& state); + template void prepare_frame(Object* object); template void prepare_partition(Object* object, Plot_Partition_Count index); template - void paint_frame(Object* object); + void paint_partition(Object* object, Plot_Partition_Count index); + template + void paint_tooltip(Object* object); /* CRTP 覆盖:更新 hover 位置并请求重绘。 */ template void handle_event(Object* object, const Event& event); @@ -96,6 +100,10 @@ bool Waterfall::Private::should_rebuild_prepare_graph(Object* object, const Prop return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, cells); } template +bool Waterfall::Private::should_rebuild_paint_graph(Object*, const Prop&) { + return !paint_graph || paint_graph->size() != graph_partition_count + 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()); @@ -116,9 +124,16 @@ Task_Graph Waterfall::Private::build_prepare_graph(Object* object, const Prop& s template Task_Graph Waterfall::Private::build_paint_graph(Object* object, const Prop&) { Task_Graph graph{"waterfall.paint"}; - graph.add("frame", [this, object] { - paint_frame(object); + auto tooltip_task = graph.add("tooltip", [this, object] { + paint_tooltip(object); }); + for (Plot_Partition_Count index = 0; + index < graph_partition_count; ++index) { + auto partition = graph.add("partition", [this, object, index] { + paint_partition(object, index); + }); + partition.precede(tooltip_task); + } return graph; } template @@ -189,18 +204,28 @@ void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count } } template -void Waterfall::Private::paint_frame(Object* object) { +void Waterfall::Private::paint_partition(Object* object, + Plot_Partition_Count index) { const auto& state = object->template read_prop(); - auto& cache = this->paint_surface(); - if (!prepared.valid) { - return; - } - detail::Painter painter(cache, prepared.canvas); + if (!prepared.valid || index >= graph_partition_count) return; + const Rect_F region = detail::raster_paint_region( + prepared.layout, index, graph_partition_count); + if (region.empty()) return; + detail::Painter painter(this->paint_surface(), prepared.canvas, region); detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode); - if (!prepared.tooltip_text.empty()) { - painter.rect(prepared.tooltip_box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); - painter.text({prepared.tooltip_box.x + 4.0, prepared.tooltip_box.y + 3.0}, prepared.tooltip_text, state.tooltip_font, state.tooltip_text_pen); - } +} +template +void Waterfall::Private::paint_tooltip(Object* object) { + if (!prepared.valid || prepared.tooltip_text.empty()) return; + const auto& state = object->template read_prop(); + detail::Painter painter(this->paint_surface(), prepared.canvas, + prepared.tooltip_box); + painter.rect(prepared.tooltip_box, Pen{state.tooltip_text_pen.color}, + state.tooltip_background_brush); + painter.text({prepared.tooltip_box.x + 4.0, + prepared.tooltip_box.y + 3.0}, + prepared.tooltip_text, state.tooltip_font, + state.tooltip_text_pen); } template void Waterfall::Private::handle_event(Object* object, const Event& event) { diff --git a/render_2D/render_2D/plottable/common/Curve_Plot.cpp b/render_2D/render_2D/plottable/common/Curve_Plot.cpp index ec8e399..b35f480 100644 --- a/render_2D/render_2D/plottable/common/Curve_Plot.cpp +++ b/render_2D/render_2D/plottable/common/Curve_Plot.cpp @@ -88,6 +88,32 @@ Curve_Prepared prepare_curve(std::span values, Axis_Range doma const auto samples = interpolate_curve(values, domain, mode); return prepare_curve(samples, visible_only, visible_coordinate_range, value_range, coordinate_axis, value_axis, coordinate_orientation, value_orientation); } +Rect_F curve_paint_region(const Curve_Prepared& curve, double padding) { + bool initialized{}; + double left{}; + double top{}; + double right{}; + double bottom{}; + const auto include = [&](const Point_F& point) { + if (!initialized) { + left = right = point.x; + top = bottom = point.y; + initialized = true; + return; + } + left = std::min(left, point.x); + top = std::min(top, point.y); + right = std::max(right, point.x); + bottom = std::max(bottom, point.y); + }; + for (const auto& point : curve.points) include(point); + for (const auto& point : curve.fill) include(point); + if (!initialized) return {}; + padding = std::max(1.0, padding); + return {left - padding, top - padding, + right - left + padding * 2.0, + bottom - top + padding * 2.0}; +} void paint_curve(Painter& painter, const Curve_Prepared& curve, const Pen& pen, const Brush& brush) { if (curve.points.size() < 2) return; if (brush.enabled()) painter.polygon(curve.fill, Pen{.style = Line_Style::none}, brush); diff --git a/render_2D/render_2D/plottable/common/Curve_Plot.hpp b/render_2D/render_2D/plottable/common/Curve_Plot.hpp index e8a781f..6d3e524 100644 --- a/render_2D/render_2D/plottable/common/Curve_Plot.hpp +++ b/render_2D/render_2D/plottable/common/Curve_Plot.hpp @@ -26,5 +26,7 @@ struct Curve_Partition_Range { [[nodiscard]] Rect_F map_plot_rect(const Abs_Axis* coordinate_axis, Axis_Range coordinate_range, const Abs_Axis* value_axis, Axis_Range value_range, Axis_Orientation coordinate_orientation); [[nodiscard]] Curve_Prepared prepare_curve(std::span samples, bool visible_only, Axis_Range visible_coordinate_range, Axis_Range value_range, const Abs_Axis* coordinate_axis, const Abs_Axis* value_axis, Axis_Orientation coordinate_orientation, Axis_Orientation value_orientation); [[nodiscard]] Curve_Prepared prepare_curve(std::span values, Axis_Range domain, Line_Interpolation_Mode mode, bool visible_only, Axis_Range visible_coordinate_range, Axis_Range value_range, const Abs_Axis* coordinate_axis, const Abs_Axis* value_axis, Axis_Orientation coordinate_orientation, Axis_Orientation value_orientation); +/* 返回曲线及填充覆盖的 framebuffer 视图,padding 用于描边与抗锯齿重叠边界。 */ +[[nodiscard]] Rect_F curve_paint_region(const Curve_Prepared& curve, double padding); void paint_curve(Painter& painter, const Curve_Prepared& curve, const Pen& pen, const Brush& brush = {}); } diff --git a/render_2D/render_2D/plottable/common/Raster_Plot.cpp b/render_2D/render_2D/plottable/common/Raster_Plot.cpp index fece53f..6992e4c 100644 --- a/render_2D/render_2D/plottable/common/Raster_Plot.cpp +++ b/render_2D/render_2D/plottable/common/Raster_Plot.cpp @@ -39,6 +39,24 @@ std::pair raster_partition_range(std::size_t work_size if (partition_count == 0) return {}; return {work_size * partition_index / partition_count, work_size * (partition_index + 1) / partition_count}; } +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 {}; + 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); + constexpr double overlap{1.0}; + return {first - overlap, target.y - overlap, + last - first + overlap * 2.0, + target.height + 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 328e17d..087d9aa 100644 --- a/render_2D/render_2D/plottable/common/Raster_Plot.hpp +++ b/render_2D/render_2D/plottable/common/Raster_Plot.hpp @@ -27,5 +27,9 @@ struct Raster_Axis_Selection { [[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]] Rect_F raster_paint_region(const Raster_Layout& layout, + Plot_Partition_Count partition_index, + Plot_Partition_Count partition_count); void paint_raster(Painter& painter, const Raster_Layout& layout, std::span pixels, Image_Interpolation_Mode interpolation); } diff --git a/render_2D/render_2D/render/Blend2D_Cache.cpp b/render_2D/render_2D/render/Blend2D_Cache.cpp index 85adafd..dbd0a07 100644 --- a/render_2D/render_2D/render/Blend2D_Cache.cpp +++ b/render_2D/render_2D/render/Blend2D_Cache.cpp @@ -10,18 +10,34 @@ #include #include namespace aethera::render_2d { +namespace { +constexpr std::size_t framebuffer_stride_alignment{64}; + +std::size_t aligned_stride(int width) { + const auto row_bytes = static_cast(width) * sizeof(Pixel); + return (row_bytes + framebuffer_stride_alignment - 1) & + ~(framebuffer_stride_alignment - 1); +} +} void Blend2D_Cache::Private::require_success(BLResult result, const char* operation) { if (result == BL_SUCCESS) return; throw std::runtime_error(std::string(operation) + " failed with Blend2D result " + std::to_string(result)); } -void Blend2D_Cache::Private::clear_image(BLImage& image) { - if (image.is_empty()) return; - BLImageData data{}; - require_success(image.get_data(&data), "read Blend2D image data"); - for (int y = 0; y < data.size.h; ++y) { - auto* row = static_cast(data.pixel_data) + static_cast(y) * data.stride; - std::memset(row, 0, static_cast(data.size.w) * sizeof(Pixel)); - } +BLImage Blend2D_Cache::Private::image_view(BLDataAccessFlags access) const { + BLImage image; + if (image_size.empty()) return image; + require_success( + image.create_from_data(image_size.width, image_size.height, + BL_FORMAT_PRGB32, + const_cast(pixels.data()), row_stride(), + access), + "create Blend2D framebuffer view"); + return image; +} +std::intptr_t Blend2D_Cache::Private::row_stride() const noexcept { + return image_size.empty() + ? 0 + : static_cast(aligned_stride(image_size.width)); } const BLPixelConverter& Blend2D_Cache::Private::rgba_converter() { static const BLPixelConverter converter = [] { @@ -41,14 +57,11 @@ const BLPixelConverter& Blend2D_Cache::Private::rgba_converter() { }(); return converter; } -Blend2D_Cache::Private::Private(const Private& other) { - require_success(image.assign_deep(other.image), "copy Blend2D image"); -} +Blend2D_Cache::Private::Private(const Private& other) = default; Blend2D_Cache::Private& Blend2D_Cache::Private::operator=(const Private& other) { if (this == &other) return *this; - BLImage copy; - require_success(copy.assign_deep(other.image), "copy Blend2D image"); - image = std::move(copy); + pixels = other.pixels; + image_size = other.image_size; return *this; } Blend2D_Cache::Blend2D_Cache() : d(std::make_unique()) {} @@ -61,44 +74,49 @@ Blend2D_Cache& Blend2D_Cache::operator=(const Blend2D_Cache& other) { Blend2D_Cache::Blend2D_Cache(Blend2D_Cache&& other) noexcept = default; Blend2D_Cache& Blend2D_Cache::operator=(Blend2D_Cache&& other) noexcept = default; void Blend2D_Cache::clear() { - Private::clear_image(d->image); + std::fill(d->pixels.begin(), d->pixels.end(), std::byte{}); } void Blend2D_Cache::composite(const Blend2D_Cache& source) { - if (source.d->image.is_empty()) return; + if (source.d->image_size.empty()) return; const Size source_size = source.size(); - if (d->image.is_empty() || size() != source_size) { + if (d->image_size.empty() || size() != source_size) { ensure_size(source_size); clear(); } + BLImage destination_image = d->image_view(BL_DATA_ACCESS_RW); + BLImage source_image = source.d->image_view(BL_DATA_ACCESS_READ); BLContext context; - Private::require_success(context.begin(d->image), "begin Blend2D cache composite"); + Private::require_success(context.begin(destination_image), "begin Blend2D cache composite"); context.set_comp_op(BL_COMP_OP_SRC_OVER); Private::require_success( context.blit_image(BLRect(0.0, 0.0, source_size.width, source_size.height), - source.d->image, + source_image, BLRectI(0, 0, source_size.width, source_size.height)), "composite Blend2D cache"); Private::require_success(context.end(), "end Blend2D cache composite"); } void Blend2D_Cache::ensure_size(Size requested) { if (requested.empty()) { - d->image.reset(); + d->pixels.clear(); + d->image_size = {}; return; } if (size() == requested) return; - Private::require_success(d->image.create(requested.width, requested.height, BL_FORMAT_PRGB32), - "create Blend2D cache"); - clear(); + const std::size_t stride = aligned_stride(requested.width); + const auto height = static_cast(requested.height); + if (stride > std::numeric_limits::max() / height) + throw std::length_error("Blend2D framebuffer size overflow"); + std::vector pixels(stride * height); + d->pixels = std::move(pixels); + d->image_size = requested; } Size Blend2D_Cache::size() const noexcept { - return {d->image.width(), d->image.height()}; + return d->image_size; } Image_View Blend2D_Cache::view() const { - if (d->image.is_empty()) return {}; - BLImageData data{}; - Private::require_success(d->image.get_data(&data), "read Blend2D cache"); - return {static_cast(data.pixel_data), data.size.w, data.size.h, - static_cast(data.stride), Pixel_Format::bgra8_premultiplied}; + if (d->image_size.empty()) return {}; + return {d->pixels.data(), d->image_size.width, d->image_size.height, + static_cast(d->row_stride()), Pixel_Format::bgra8_premultiplied}; } Pixel_Buffer Blend2D_Cache::output_pixels(Pixel_Format format) const { const Image_View source = view(); @@ -234,12 +252,45 @@ std::vector Painter::Private::bicubic_resample(std::span sou } return destination; } +void Painter::Private::begin(Blend2D_Cache& cache, Size size, Rect_F view) { + const Size framebuffer = cache.size(); + view = view.normalized(); + const int left = std::clamp(static_cast(std::floor(view.x)), 0, + framebuffer.width); + const int top = std::clamp(static_cast(std::floor(view.y)), 0, + framebuffer.height); + const int right = std::clamp(static_cast(std::ceil(view.x + view.width)), + left, framebuffer.width); + const int bottom = std::clamp(static_cast(std::ceil(view.y + view.height)), + top, framebuffer.height); + if (left == right || top == bottom) return; + const auto stride = cache.d->row_stride(); + auto* pixels = cache.d->pixels.data() + + static_cast(top) * stride + + static_cast(left) * sizeof(Pixel); + Blend2D_Cache::Private::require_success( + image.create_from_data(right - left, bottom - top, BL_FORMAT_PRGB32, + pixels, stride, BL_DATA_ACCESS_RW), + "create Blend2D painter view"); + Blend2D_Cache::Private::require_success(context.begin(image), + "begin Blend2D painter"); + context.set_comp_op(BL_COMP_OP_SRC_OVER); + context.translate(-static_cast(left), -static_cast(top)); + active = true; +} Painter::Painter(Blend2D_Cache& cache, Size size) : d(std::make_unique()) { cache.ensure_size(size); - if (cache.d->image.is_empty()) return; - Blend2D_Cache::Private::require_success(d->context.begin(cache.d->image), "begin Blend2D painter"); - d->context.set_comp_op(BL_COMP_OP_SRC_OVER); - d->active = true; + d->begin(cache, size, + {0.0, 0.0, static_cast(size.width), + static_cast(size.height)}); +} +Painter::Painter(Blend2D_Cache& cache, Size size, Rect_F view) + : d(std::make_unique()) { + if (cache.size() != size) { + throw std::invalid_argument( + "Blend2D painter view requires a pre-sized framebuffer"); + } + d->begin(cache, size, view); } Painter::~Painter() { if (d && d->active) d->context.end(); @@ -263,9 +314,10 @@ Painter::operator bool() const noexcept { return d->active; } Painter::Clip_Scope Painter::scoped_clip(Rect_F rect) { return Clip_Scope(*this, rect); } void Painter::clear() { if (d->active) d->context.clear_all(); } void Painter::composite(const Blend2D_Cache& source) { - if (!d->active || source.d->image.is_empty()) return; + if (!d->active || source.d->image_size.empty()) return; const Size value = source.size(); - d->context.blit_image(BLRect(0, 0, value.width, value.height), source.d->image, + BLImage source_image = source.d->image_view(BL_DATA_ACCESS_READ); + d->context.blit_image(BLRect(0, 0, value.width, value.height), source_image, BLRectI(0, 0, value.width, value.height)); } void Painter::line(Point_F first, Point_F second, const Pen& pen) { @@ -340,13 +392,13 @@ void Painter::heatmap(Rect_F target, int width, int height, std::span(static_cast(image_data.pixel_data) + - static_cast(y) * image_data.stride); - std::copy_n(pixels.data() + static_cast(y) * width, width, destination); - } + BLImage image; + Blend2D_Cache::Private::require_success( + image.create_from_data(width, height, BL_FORMAT_PRGB32, + const_cast(pixels.data()), + static_cast(width) * sizeof(Pixel), + BL_DATA_ACCESS_READ), + "create Blend2D heatmap view"); d->context.set_pattern_quality(interpolation == Image_Interpolation_Mode::bilinear ? BL_PATTERN_QUALITY_BILINEAR : BL_PATTERN_QUALITY_NEAREST); d->context.blit_image(BLRect(target.x, target.y, target.width, target.height), image, diff --git a/render_2D/render_2D/render/Blend2D_Cache.hpp b/render_2D/render_2D/render/Blend2D_Cache.hpp index e6ff78b..568cdcb 100644 --- a/render_2D/render_2D/render/Blend2D_Cache.hpp +++ b/render_2D/render_2D/render/Blend2D_Cache.hpp @@ -56,6 +56,11 @@ struct Painter { Painter* painter{}; /* 非空时析构恢复对应 Painter 的裁剪状态。 */ }; Painter(Blend2D_Cache& cache, Size size); + /* + * 在 cache 的同一像素块上建立局部可写视图;坐标仍使用完整画布坐标。 + * 不同 Painter 拥有独立后端图像和上下文,view 允许彼此重叠。 + */ + Painter(Blend2D_Cache& cache, Size size, Rect_F view); ~Painter(); Painter(const Painter&) = delete; Painter& operator=(const Painter&) = delete; diff --git a/render_2D/render_2D/render/Blend2D_Cache.ipp b/render_2D/render_2D/render/Blend2D_Cache.ipp index 40e3293..65fe412 100644 --- a/render_2D/render_2D/render/Blend2D_Cache.ipp +++ b/render_2D/render_2D/render/Blend2D_Cache.ipp @@ -7,23 +7,29 @@ #include namespace aethera::render_2d { struct Blend2D_Cache::Private { - BLImage image{}; /* 缓存拥有的 PRGB32 Blend2D 图像。 */ + std::vector pixels{}; /* 自管 PRGB32 大块;所有 BLImage 视图仅在绘制调用内借用。 */ + Size image_size{}; /* pixels 当前表示的二维像素尺寸。 */ Private() = default; Private(const Private& other); Private& operator=(const Private& other); /* 将 Blend2D 失败转换为 Unknown Failure。 */ static void require_success(BLResult result, const char* operation); - /* 将图像全部清零为透明像素。 */ - static void clear_image(BLImage& image); + /* 创建借用当前自管像素的完整 Blend2D 图像;返回对象不拥有像素。 */ + [[nodiscard]] BLImage image_view(BLDataAccessFlags access) const; + /* 从权威 image_size 计算相邻像素行的缓存行对齐跨度。 */ + [[nodiscard]] std::intptr_t row_stride() const noexcept; /* 进程级 PRGB32 -> RGBA8 转换器,只在后端实现中可见。 */ [[nodiscard]] static const BLPixelConverter& rgba_converter(); }; namespace detail { struct Painter::Private { + BLImage image{}; /* 本 Painter 独占、借用目标像素块的局部 BLImage 视图。 */ BLContext context{}; /* 当前缓存图像上的 Blend2D 绘制上下文。 */ bool active{}; /* context 是否已经成功 begin 且尚未 end。 */ std::optional fill_color{}; /* 已应用的填充颜色,减少重复状态写入。 */ std::optional stroke_pen{}; /* 已应用的描边参数,减少重复状态写入。 */ + /* 裁剪 view 到 framebuffer 后建立独立图像与上下文,并保持完整画布坐标系。 */ + void begin(Blend2D_Cache& cache, Size size, Rect_F view); /* 将项目颜色转换为 Blend2D 浮点颜色。 */ [[nodiscard]] static BLRgba rgba(Color color) noexcept; /* 仅当颜色变化时更新当前填充样式。 */ diff --git a/render_2D/render_2D/scene/Render_Scene_2D.ipp b/render_2D/render_2D/scene/Render_Scene_2D.ipp index b510cce..407b100 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.ipp +++ b/render_2D/render_2D/scene/Render_Scene_2D.ipp @@ -52,7 +52,6 @@ struct Render_Scene_2D::Private : Prev_Private { Root* object{}; /* Paint 拓扑位置对应的最终对象;Scene 不拥有。 */ Renderable_2D_Base::Private* private_data{}; /* 对象的二维能力层;对象存活期间有效。 */ Root* cache_owner{}; /* 所属缓存根;空值表示直接绘制最终帧。 */ - bool cache_group_last{}; /* 是否为所属缓存组在拓扑序中的最后节点。 */ bool paint_requested{}; /* Scene 按本轮组级失效结果计算的执行许可。 */ }; struct Cache_Group { @@ -83,9 +82,9 @@ struct Render_Scene_2D::Private : Prev_Private { std::vector active_prepare_executions{}; std::chrono::steady_clock::time_point active_prepare_started{}; ~Private(); - Frame_2D* active_frame{}; /* 当前同步 process 借用的外部帧;render 返回前清空。 */ - Blend2D_Cache* frame_target{}; /* 当前 render(frame) 所属外部颜色层;调用返回后清空。 */ - /* Impl CRTP 实现:在对象锁内执行 Kernel Scene,再按 Paint 图拓扑顺序合成颜色层。 */ + Frame_2D* active_frame{}; /* 异步帧 DAG 借用的外部帧;完成回调前保持存活。 */ + Blend2D_Cache* frame_target{}; /* 当前异步帧的最终颜色层;帧 DAG 完成后清空。 */ + /* Impl CRTP 实现:Prepare 完成后按 Paint 图拓扑边并行绘制并合成颜色层。 */ std::vector paint_order{}; /* Paint 图当前拓扑序及 Scene 缓存分组结果。 */ std::vector cache_groups{}; /* 仅保存显式缓存根对应的执行分组。 */ template void ensure_frame_taskflow(Object* object); @@ -144,18 +143,19 @@ void Render_Scene_2D::Private::after_advance(Object* object, Prop*, State_Access if (conflict) cache_owner = nullptr; } cache_owner_by_object.emplace(node.object, cache_owner); - paint_order.push_back(Paint_Node{node.object, data, cache_owner, false, false}); + paint_order.push_back(Paint_Node{node.object, data, cache_owner, false}); if (!cache_owner) return; auto [group, inserted] = cache_group_by_owner.emplace(cache_owner, cache_groups.size()); if (inserted) cache_groups.push_back(Cache_Group{cache_owner}); cache_groups[group->second].members.push_back(node.object); }); if (!order_result) throw std::logic_error("render scene paint graph became invalid while building cache groups"); - std::unordered_set closed_groups; - for (auto current = paint_order.rbegin(); current != paint_order.rend(); ++current) - if (current->cache_owner && closed_groups.insert(current->cache_owner).second) current->cache_group_last = true; - Task_Node previous; - bool has_previous{}; + struct Stage_Tasks { + Task_Node entry; /* Renderable Paint 条件入口。 */ + Task_Node exit; /* Renderable Paint 状态发布出口。 */ + }; + std::unordered_map stage_tasks; + std::unordered_map cache_composites; for (std::size_t index = 0; index < paint_order.size(); ++index) { auto& paint_node = paint_order[index]; Root* root = paint_node.object; @@ -213,25 +213,115 @@ void Render_Scene_2D::Private::after_advance(Object* object, Prop*, State_Access paint_run.precede(paint_extension); paint_extension.precede(paint_done); } - if (has_previous) previous.precede(paint_if); - previous = paint_done; - if (paint_node.cache_group_last) { - Root* cache_owner = paint_node.cache_owner; - auto composite = taskflow.add(task_prefix + ".cache_composite", [this, object, cache_owner] { - const auto group = std::find_if(cache_groups.begin(), cache_groups.end(), [cache_owner](const Cache_Group& value) { return value.owner == cache_owner; }); - const auto owner_node = std::find_if(paint_order.begin(), paint_order.end(), [cache_owner](const Paint_Node& value) { return value.object == cache_owner; }); - if (group == cache_groups.end() || owner_node == paint_order.end()) return; - if (group->rebuild) owner_node->private_data->valid_cache = owner_node->private_data->paint_target; - if (owner_node->private_data->valid_cache) { - if (!frame_target) throw std::logic_error("2D paint graph has no external frame target"); - frame_target->composite(*owner_node->private_data->valid_cache); - } - }); - paint_done.precede(composite); - previous = composite; - } - has_previous = true; + stage_tasks.emplace(root, Stage_Tasks{paint_if, paint_done}); } + /* + * 缓存组是最终 framebuffer 上的一个原子颜色层:组内 Renderable 仍按 + * Paint_Tag 连边,组外后继必须等待该层完成合成。未重建时成员条件会 + * 直接走完成分支,但缓存层仍按同一拓扑位置合成。 + */ + for (auto& group : cache_groups) { + Root* cache_owner = group.owner; + auto owner_node = std::find_if( + paint_order.begin(), paint_order.end(), + [cache_owner](const Paint_Node& value) { + return value.object == cache_owner; + }); + if (owner_node == paint_order.end()) + throw std::logic_error("2D cache group lost its owner node"); + const auto task_prefix = + std::string(owner_node->private_data->dispatch->business_name) + + ".paint.cache_composite"; + auto composite = taskflow.add(task_prefix, [this, cache_owner] { + const auto group = std::find_if( + cache_groups.begin(), cache_groups.end(), + [cache_owner](const Cache_Group& value) { + return value.owner == cache_owner; + }); + const auto owner_node = std::find_if( + paint_order.begin(), paint_order.end(), + [cache_owner](const Paint_Node& value) { + return value.object == cache_owner; + }); + if (group == cache_groups.end() || owner_node == paint_order.end()) + return; + if (group->rebuild) + owner_node->private_data->valid_cache = + owner_node->private_data->paint_target; + if (!owner_node->private_data->valid_cache) return; + if (!frame_target) + throw std::logic_error( + "2D paint graph has no external frame target"); + frame_target->composite(*owner_node->private_data->valid_cache); + }); + for (Root* member : group.members) { + const auto tasks = stage_tasks.find(member); + if (tasks != stage_tasks.end()) tasks->second.exit.precede(composite); + } + cache_composites.emplace(cache_owner, composite); + } + /* + * Paint_Tag 是绘制顺序的唯一权威来源。只连接最近的二维依赖;没有 + * 显式前驱的节点在 Scene Prepare 与 framebuffer 初始化完成后直接就绪。 + */ + dependencies.for_each([&](const Dependency_Graph::Node& target_node) { + if (!dependencies.private_data(target_node)) return; + const auto target = stage_tasks.find(target_node.object); + if (target == stage_tasks.end()) return; + const auto target_paint = std::find_if( + paint_order.begin(), paint_order.end(), + [&](const Paint_Node& value) { + return value.object == target_node.object; + }); + if (target_paint == paint_order.end()) return; + std::unordered_set visited; + std::vector pending{ + target_node.dependencies.begin(), target_node.dependencies.end()}; + while (!pending.empty()) { + const auto* source_node = pending.back(); + pending.pop_back(); + if (!visited.insert(source_node->object).second) continue; + if (!dependencies.private_data(*source_node)) { + for (const auto* dependency : source_node->dependencies) + pending.push_back(dependency); + continue; + } + const auto source = stage_tasks.find(source_node->object); + if (source == stage_tasks.end()) continue; + const auto source_paint = std::find_if( + paint_order.begin(), paint_order.end(), + [&](const Paint_Node& value) { + return value.object == source_node->object; + }); + if (source_paint == paint_order.end()) continue; + if (source_paint->cache_owner == target_paint->cache_owner) { + source->second.exit.precede(target->second.entry); + continue; + } + const auto source_composite = source_paint->cache_owner + ? cache_composites.find(source_paint->cache_owner) + : cache_composites.end(); + const auto target_composite = target_paint->cache_owner + ? cache_composites.find(target_paint->cache_owner) + : cache_composites.end(); + if ((source_paint->cache_owner && + source_composite == cache_composites.end()) || + (target_paint->cache_owner && + target_composite == cache_composites.end())) + throw std::logic_error( + "2D cached Paint dependency has no composite task"); + if (source_paint->cache_owner) { + if (target_paint->cache_owner) + source_composite->second.precede( + target_composite->second); + else + source_composite->second.precede(target->second.entry); + } + else { + source->second.exit.precede(target_composite->second); + } + } + }); object->template access_pending_dependency_graph([](auto& paint_state) { if (paint_state.dirty()) paint_state.take_dirty(); }); } template diff --git a/render_2D/tests/Async_Render_Contract_Test.cpp b/render_2D/tests/Async_Render_Contract_Test.cpp new file mode 100644 index 0000000..91e98a4 --- /dev/null +++ b/render_2D/tests/Async_Render_Contract_Test.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +using namespace aethera; +using namespace aethera::render_2d; + +template +Object* build_object(Arguments&&... arguments) { + typename Object::Builder builder(std::forward(arguments)...); + auto result = builder.build(); + if (!result) std::_Exit(2); + return std::move(result).value().release(); +} + +bool contains_color(Image_View view) { + for (int y = 0; y < view.height; ++y) { + const auto* row = reinterpret_cast( + view.data + static_cast(y) * view.stride); + for (int x = 0; x < view.width; ++x) + if (row[x] != 0) return true; + } + return false; +} +} + +int main() { + using Frequency = Impl; + using Power = Impl; + using Spectrum_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_painted = new std::atomic_bool{}; + auto* topology_valid = new std::atomic_bool{true}; + spectrum->paint_taskflow().add("test.spectrum.paint.complete", [spectrum_painted] { + spectrum_painted->store(true, std::memory_order_release); + }); + const auto verify_spectrum_precedes_axis = + [spectrum_painted, topology_valid] { + if (!spectrum_painted->load(std::memory_order_acquire)) + topology_valid->store(false, std::memory_order_relaxed); + }; + frequency->paint_taskflow().add("test.frequency.paint.order", + verify_spectrum_precedes_axis); + power->paint_taskflow().add("test.power.paint.order", + verify_spectrum_precedes_axis); + + typename Scene_Object::Builder scene_builder; + scene_builder.add_renderable(spectrum); + auto scene_result = scene_builder.build(); + if (!scene_result) return 2; + auto* scene = std::move(scene_result).value().release(); + + frequency->set<&Abs_Axis::Prop::position>(Point_F{20.0, 100.0}); + frequency->set<&Abs_Axis::Prop::pixel_length>(120.0); + frequency->set<&Numeric_Axis::Prop::coordinate_range>( + Axis_Range{0.0, 100.0}); + power->set<&Abs_Axis::Prop::position>(Point_F{20.0, 100.0}); + power->set<&Abs_Axis::Prop::pixel_length>(-80.0); + power->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical); + power->set<&Numeric_Axis::Prop::coordinate_range>( + Axis_Range{-100.0, 0.0}); + spectrum->set<&Spectrum::Prop::frequency_range>( + Axis_Range{0.0, 100.0}); + spectrum->set<&Spectrum::Prop::partition_mode>( + Plot_Partition_Mode::fixed); + spectrum->set<&Spectrum::Prop::partition_count>(4); + spectrum->pending_buffer() = + Spectrum_Frame{std::vector(512, -50.0)}; + spectrum->mark_dirty(); + + scene->set<&Render_Scene_2D::Prop::viewport>(Size{160, 120}); + scene->activate_view(); + auto* frame = new Frame_2D(Frame_Identity{1, 0}); + scene->set_frame_callback( + [spectrum, frequency, power, frame, + topology_valid](Frame_2D* completed) { + const auto& state = + spectrum->read_state(); + const bool valid = completed == frame && + state.prepare_executed && state.paint_executed && + spectrum->read_state().sample_count == + 512 && + frequency->read_state().paint_executed && + power->read_state().paint_executed && + topology_valid->load(std::memory_order_relaxed) && + contains_color(completed->image()); + std::_Exit(valid ? 0 : 1); + }); + if (!scene->render(frame)) return 2; + + /* + * render() 是异步入口。结束提交线程但保持 Taskflow worker 和进程存活, + * 完成回调负责给出测试结果;此处没有轮询、future::get 或条件变量等待。 + */ + _endthreadex(3); +} diff --git a/render_2D/tests/Frame_Pixel_Format_Test.cpp b/render_2D/tests/Frame_Pixel_Format_Test.cpp index e737f9e..9b7b806 100644 --- a/render_2D/tests/Frame_Pixel_Format_Test.cpp +++ b/render_2D/tests/Frame_Pixel_Format_Test.cpp @@ -33,3 +33,52 @@ TEST(frame_pixel_format, uses_blend2d_to_publish_straight_rgba) { EXPECT_EQ(std::to_integer(pixels.bytes[2]), 0U); EXPECT_EQ(std::to_integer(pixels.bytes[3]), 128U); } +TEST(framebuffer_view, writes_only_the_requested_external_image_region) { + Blend2D_Cache framebuffer; + framebuffer.ensure_size(Size{8, 6}); + { + aethera::render_2d::detail::Painter painter( + framebuffer, Size{8, 6}, Rect_F{2.0, 1.0, 3.0, 2.0}); + painter.rect(Rect_F{0.0, 0.0, 8.0, 6.0}, + Pen{.style = Line_Style::none}, + Brush{Color::red_color(), Brush_Style::solid}); + } + const Image_View view = framebuffer.view(); + ASSERT_EQ(view.width, 8); + ASSERT_EQ(view.height, 6); + for (int y = 0; y < view.height; ++y) { + const auto* row = reinterpret_cast( + view.data + static_cast(y) * view.stride); + for (int x = 0; x < view.width; ++x) { + const bool inside = x >= 2 && x < 5 && y >= 1 && y < 3; + EXPECT_EQ(row[x] != 0, inside) << "pixel " << x << ',' << y; + } + } +} +TEST(framebuffer_view, overlapping_views_share_the_same_authoritative_pixels) { + Blend2D_Cache framebuffer; + framebuffer.ensure_size(Size{8, 6}); + { + aethera::render_2d::detail::Painter first( + framebuffer, Size{8, 6}, Rect_F{1.0, 1.0, 4.0, 3.0}); + first.rect(Rect_F{0.0, 0.0, 8.0, 6.0}, + Pen{.style = Line_Style::none}, + Brush{Color::red_color(), Brush_Style::solid}); + } + { + aethera::render_2d::detail::Painter second( + framebuffer, Size{8, 6}, Rect_F{3.0, 2.0, 4.0, 3.0}); + second.rect(Rect_F{0.0, 0.0, 8.0, 6.0}, + Pen{.style = Line_Style::none}, + Brush{Color{0, 0, 255, 255}, Brush_Style::solid}); + } + const Image_View view = framebuffer.view(); + const auto pixel = [&](int x, int y) { + const auto* row = reinterpret_cast( + view.data + static_cast(y) * view.stride); + return row[x]; + }; + EXPECT_NE(pixel(2, 2), 0u); + EXPECT_NE(pixel(4, 3), 0u); + EXPECT_NE(pixel(2, 2), pixel(4, 3)); +} diff --git a/render_2D/tests/Spectrum_Test.cpp b/render_2D/tests/Spectrum_Test.cpp index ddae2c0..000a420 100644 --- a/render_2D/tests/Spectrum_Test.cpp +++ b/render_2D/tests/Spectrum_Test.cpp @@ -147,7 +147,7 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) { EXPECT_GT(spectrum->read_state().rendered_point_count, 0u); const auto& render_state = spectrum->read_state(); EXPECT_EQ(render_state.prepare_task_count, 4u); - EXPECT_EQ(render_state.paint_task_count, 1u); + EXPECT_EQ(render_state.paint_task_count, 5u); const Image_View frame = output_frame->image(); ASSERT_FALSE(frame.empty()); EXPECT_TRUE(contains_color(frame));