blend2D并行绘制

This commit is contained in:
2026-08-26 20:35:02 +08:00
parent 2de8a35fff
commit ef9e36c4b9
16 changed files with 603 additions and 117 deletions
+18 -10
View File
@@ -29,6 +29,8 @@ struct Afterglow::Private : Prev_Private {
template <Attached Object>
[[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object>
[[nodiscard]] bool should_rebuild_paint_graph(Object* object, const Prop& state);
template <Attached Object>
void prepare_frame(Object* object);
template <Attached Object>
void accumulate_partition(Object* object, Plot_Partition_Count index);
@@ -36,7 +38,7 @@ struct Afterglow::Private : Prev_Private {
template <Attached Object>
void color_partition(Object* object, Plot_Partition_Count index);
template <Attached Object>
void paint_frame(Object* object);
void paint_partition(Object* object, Plot_Partition_Count index);
/* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */
template <typename Object, typename Owner, typename Member, typename Prop_Type>
void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> 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<std::size_t>(1, columns));
}
template <Attached Object>
bool Afterglow::Private::should_rebuild_paint_graph(Object*, const Prop&) {
return !paint_graph || paint_graph->size() != graph_partition_count;
}
template <Attached Object>
Task_Graph Afterglow::Private::build_prepare_graph(Object* object, const Prop& state) {
const std::size_t available = object->template access_query_stream<Afterglow_Stream_Tag>(
[](std::span<const std::shared_ptr<const std::vector<Plot_Value>>> spectra) {
@@ -115,9 +121,11 @@ Task_Graph Afterglow::Private::build_prepare_graph(Object* object, const Prop& s
template <Attached Object>
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 <Attached Object>
@@ -196,12 +204,12 @@ void Afterglow::Private::color_partition(Object* object, Plot_Partition_Count in
}
}
template <Attached Object>
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 <typename Object, typename Owner, typename Member, typename Prop_Type>
@@ -20,9 +20,10 @@ struct Frequency_Trace::Private : Prev_Private {
template <Attached Object> [[nodiscard]] Task_Graph build_paint_graph(Object* object, const Prop& state);
/* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */
template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object> [[nodiscard]] bool should_rebuild_paint_graph(Object* object, const Prop& state);
template <Attached Object> void prepare_frame(Object* object, Plot_Partition_Count partition_count);
template <Attached Object> void prepare_partition(Object* object, Plot_Partition_Count partition_index);
template <Attached Object> void paint_frame(Object* object);
template <Attached Object> void paint_partition(Object* object, Plot_Partition_Count partition_index);
/* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */
template <typename Object, typename Owner, typename Member, typename Prop_Type> void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> pending_states);
};
@@ -36,6 +37,8 @@ std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Frequency_Trace::
template <Attached Object>
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<Frequency_Trace_Stream_Tag>([](std::span<const Frequency_Trace_Sample> samples) { return samples.size(); })); }
template <Attached Object>
bool Frequency_Trace::Private::should_rebuild_paint_graph(Object*, const Prop&) { return !paint_graph || paint_graph->size() != prepared.partitions.size(); }
template <Attached Object>
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<Frequency_Trace_Stream_Tag>([](std::span<const Frequency_Trace_Sample> 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 <Attached Object>
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 <Attached Object>
void Frequency_Trace::Private::prepare_frame(Object* object, Plot_Partition_Count partition_count) {
const auto& state = object->template read_prop<Frequency_Trace::Base_Tag>(); const auto& time_layout = time_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& value_layout = value_axis->template read_prop<Abs_Axis::Base_Tag>();
@@ -66,8 +74,14 @@ void Frequency_Trace::Private::prepare_partition(Object* object, Plot_Partition_
prepared.partitions[partition_index] = detail::prepare_curve(std::span<const detail::Curve_Sample>(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 <Attached Object>
void Frequency_Trace::Private::paint_frame(Object* object) {
const auto& state = object->template read_prop<Frequency_Trace::Base_Tag>(); 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<Frequency_Trace::Base_Tag>();
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 <typename Object, typename Owner, typename Member, typename Prop_Type>
void Frequency_Trace::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
+58 -13
View File
@@ -52,13 +52,19 @@ struct Spectrum::Private : Prev_Private {
template <Attached Object>
[[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object>
[[nodiscard]] bool should_rebuild_paint_graph(Object* object, const Prop& state);
template <Attached Object>
[[nodiscard]] std::size_t desired_partition_count(const Object* object, const Prop& state) const;
template <Attached Object>
void prepare_frame(Object* object, std::size_t partition_count);
template <Attached Object>
void prepare_partition(Object* object, std::size_t partition_index);
template <Attached Object>
void paint_frame(Object* object);
void begin_paint(Object* object);
template <Attached Object>
void paint_partition(Object* object, std::size_t partition_index);
template <Attached Object>
void paint_overlays(Object* object);
};
template <typename Object>
Spectrum::Builder<Object>::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 <Attached Object>
bool Spectrum::Private::should_rebuild_paint_graph(Object*, const Prop&) {
return !paint_graph || paint_graph->size() != prepared.partitions.size() + 2;
}
template <Attached Object>
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 <Attached Object>
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 <Attached Object>
@@ -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<const Spectrum_Power>(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 <Attached Object>
void Spectrum::Private::paint_frame(Object* object) {
void Spectrum::Private::begin_paint(Object* object) {
auto& private_data = static_cast<typename Object::Private&>(*this);
const auto& state = static_cast<const Prop&>(*private_data.current);
auto& published = static_cast<State&>(*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 <Attached Object>
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<typename Object::Private&>(*this);
const auto& state = static_cast<const Prop&>(*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 <Attached Object>
void Spectrum::Private::paint_overlays(Object* object) {
if (!prepared.valid) return;
auto& private_data = static_cast<typename Object::Private&>(*this);
const auto& state = static_cast<const Prop&>(*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);
@@ -32,9 +32,11 @@ struct Sweep_Spectrum::Private : Prev_Private {
template <Attached Object> [[nodiscard]] Task_Graph build_paint_graph(Object* object, const Prop& state);
/* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */
template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object> [[nodiscard]] bool should_rebuild_paint_graph(Object* object, const Prop& state);
template <Attached Object> void prepare_frame(Object* object);
template <Attached Object> void prepare_partition(Object* object, Plot_Partition_Count index);
template <Attached Object> void paint_frame(Object* object);
template <Attached Object> void paint_partition(Object* object, Plot_Partition_Count index);
template <Attached Object> void paint_marker(Object* object);
[[nodiscard]] std::size_t stored_point_count() const;
template <typename Object, typename Owner, typename Member, typename Prop_Type> void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> states);
};
@@ -48,9 +50,19 @@ std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Sweep_Spectrum::B
template <Attached Object>
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 <Attached Object>
bool Sweep_Spectrum::Private::should_rebuild_paint_graph(Object*, const Prop&) { return !paint_graph || paint_graph->size() != prepared.partitions.size() + 1; }
template <Attached Object>
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 <Attached Object>
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 <Attached Object>
void Sweep_Spectrum::Private::prepare_frame(Object* object) {
const auto& state = object->template read_prop<Sweep_Spectrum::Base_Tag>(); const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& power_layout = power_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& power_state = power_axis->template read_prop<Numeric_Axis::Base_Tag>(); prepared = {}; prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport;
@@ -94,7 +106,29 @@ void Sweep_Spectrum::Private::prepare_partition(Object* object, Plot_Partition_C
prepared.partitions[index] = detail::prepare_curve(std::span<const Plot_Value>(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 <Attached Object>
void Sweep_Spectrum::Private::paint_frame(Object* object) { const auto& state = object->template read_prop<Sweep_Spectrum::Base_Tag>(); 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<Sweep_Spectrum::Base_Tag>();
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 <Attached Object>
void Sweep_Spectrum::Private::paint_marker(Object* object) {
if (!prepared.valid) return;
const auto& state = object->template read_prop<Sweep_Spectrum::Base_Tag>();
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 <typename Object, typename Owner, typename Member, typename Prop_Type>
void Sweep_Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
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; }
+38 -13
View File
@@ -40,11 +40,15 @@ struct Waterfall::Private : Prev_Private {
template <Attached Object>
[[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object>
[[nodiscard]] bool should_rebuild_paint_graph(Object* object, const Prop& state);
template <Attached Object>
void prepare_frame(Object* object);
template <Attached Object>
void prepare_partition(Object* object, Plot_Partition_Count index);
template <Attached Object>
void paint_frame(Object* object);
void paint_partition(Object* object, Plot_Partition_Count index);
template <Attached Object>
void paint_tooltip(Object* object);
/* CRTP 覆盖:更新 hover 位置并请求重绘。 */
template <Attached Object>
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 <Attached Object>
bool Waterfall::Private::should_rebuild_paint_graph(Object*, const Prop&) {
return !paint_graph || paint_graph->size() != graph_partition_count + 1;
}
template <Attached Object>
Task_Graph Waterfall::Private::build_prepare_graph(Object* object, const Prop& state) {
const std::size_t cells = object->template access_query_stream<Waterfall_Stream_Tag>([&](std::span<const std::shared_ptr<const Waterfall_Row>> 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 <Attached Object>
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 <Attached Object>
@@ -189,18 +204,28 @@ void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count
}
}
template <Attached Object>
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<Waterfall::Base_Tag>();
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 <Attached Object>
void Waterfall::Private::paint_tooltip(Object* object) {
if (!prepared.valid || prepared.tooltip_text.empty()) return;
const auto& state = object->template read_prop<Waterfall::Base_Tag>();
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 <Attached Object>
void Waterfall::Private::handle_event(Object* object, const Event& event) {
@@ -88,6 +88,32 @@ Curve_Prepared prepare_curve(std::span<const Plot_Value> 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);
@@ -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<const Curve_Sample> 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<const Plot_Value> 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 = {});
}
@@ -39,6 +39,24 @@ std::pair<std::size_t, std::size_t> 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<double>(partition_index) /
static_cast<double>(partition_count);
const double last = target.x + target.width *
static_cast<double>(partition_index + 1) /
static_cast<double>(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<const Pixel> pixels, Image_Interpolation_Mode interpolation) {
if (layout.valid() && pixels.size() == static_cast<std::size_t>(layout.width) * static_cast<std::size_t>(layout.height)) painter.heatmap(layout.target, layout.width, layout.height, pixels, interpolation);
}
@@ -27,5 +27,9 @@ struct Raster_Axis_Selection {
[[nodiscard]] std::optional<Raster_Axis_Selection> 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<std::size_t, std::size_t> 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<const Pixel> pixels, Image_Interpolation_Mode interpolation);
}
+94 -42
View File
@@ -10,18 +10,34 @@
#include <stdexcept>
#include <string>
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<std::size_t>(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<std::byte*>(data.pixel_data) + static_cast<std::ptrdiff_t>(y) * data.stride;
std::memset(row, 0, static_cast<std::size_t>(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<std::byte*>(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<std::intptr_t>(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<Private>()) {}
@@ -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<std::size_t>(requested.height);
if (stride > std::numeric_limits<std::size_t>::max() / height)
throw std::length_error("Blend2D framebuffer size overflow");
std::vector<std::byte> 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<const std::byte*>(data.pixel_data), data.size.w, data.size.h,
static_cast<int>(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<int>(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<Pixel> Painter::Private::bicubic_resample(std::span<const Pixel> 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<int>(std::floor(view.x)), 0,
framebuffer.width);
const int top = std::clamp(static_cast<int>(std::floor(view.y)), 0,
framebuffer.height);
const int right = std::clamp(static_cast<int>(std::ceil(view.x + view.width)),
left, framebuffer.width);
const int bottom = std::clamp(static_cast<int>(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<std::ptrdiff_t>(top) * stride +
static_cast<std::ptrdiff_t>(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<double>(left), -static_cast<double>(top));
active = true;
}
Painter::Painter(Blend2D_Cache& cache, Size size) : d(std::make_unique<Private>()) {
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<double>(size.width),
static_cast<double>(size.height)});
}
Painter::Painter(Blend2D_Cache& cache, Size size, Rect_F view)
: d(std::make_unique<Private>()) {
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<const Pixe
resampled = Private::bicubic_resample(pixels, width, height, destination_width, destination_height);
pixels = resampled; width = destination_width; height = destination_height;
}
BLImage image(width, height, BL_FORMAT_PRGB32); BLImageData image_data{};
Blend2D_Cache::Private::require_success(image.get_data(&image_data), "write Blend2D heatmap image");
for (int y = 0; y < height; ++y) {
auto* destination = reinterpret_cast<Pixel*>(static_cast<std::byte*>(image_data.pixel_data) +
static_cast<std::ptrdiff_t>(y) * image_data.stride);
std::copy_n(pixels.data() + static_cast<std::size_t>(y) * width, width, destination);
}
BLImage image;
Blend2D_Cache::Private::require_success(
image.create_from_data(width, height, BL_FORMAT_PRGB32,
const_cast<Pixel*>(pixels.data()),
static_cast<std::intptr_t>(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,
@@ -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;
+9 -3
View File
@@ -7,23 +7,29 @@
#include <vector>
namespace aethera::render_2d {
struct Blend2D_Cache::Private {
BLImage image{}; /* 缓存拥有的 PRGB32 Blend2D 图像。 */
std::vector<std::byte> 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<Color> fill_color{}; /* 已应用的填充颜色,减少重复状态写入。 */
std::optional<Pen> stroke_pen{}; /* 已应用的描边参数,减少重复状态写入。 */
/* 裁剪 view 到 framebuffer 后建立独立图像与上下文,并保持完整画布坐标系。 */
void begin(Blend2D_Cache& cache, Size size, Rect_F view);
/* 将项目颜色转换为 Blend2D 浮点颜色。 */
[[nodiscard]] static BLRgba rgba(Color color) noexcept;
/* 仅当颜色变化时更新当前填充样式。 */
+118 -28
View File
@@ -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<Renderable*> 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_Node> paint_order{}; /* Paint 图当前拓扑序及 Scene 缓存分组结果。 */
std::vector<Cache_Group> cache_groups{}; /* 仅保存显式缓存根对应的执行分组。 */
template <Attached Object> 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<Root*> 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<Root*, Stage_Tasks> stage_tasks;
std::unordered_map<Root*, Task_Node> 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<Root*> visited;
std::vector<const Dependency_Graph::Node*> 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<Paint_Tag>([](auto& paint_state) { if (paint_state.dirty()) paint_state.take_dirty(); });
}
template <Attached Object>
@@ -0,0 +1,108 @@
#include <render_2D/axis/Axis.hpp>
#include <render_2D/base/Frame_2D.hpp>
#include <render_2D/plottable/Spectrum.hpp>
#include <render_2D/scene/Render_Scene_2D.hpp>
#include <process.h>
#include <atomic>
#include <cstdlib>
#include <memory>
#include <vector>
namespace {
using namespace aethera;
using namespace aethera::render_2d;
template <typename Object, typename... Arguments>
Object* build_object(Arguments&&... arguments) {
typename Object::Builder builder(std::forward<Arguments>(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<const Pixel*>(
view.data + static_cast<std::ptrdiff_t>(y) * view.stride);
for (int x = 0; x < view.width; ++x)
if (row[x] != 0) return true;
}
return false;
}
}
int main() {
using Frequency = Impl<Frequency_Axis>;
using Power = Impl<Numeric_Axis>;
using Spectrum_Object = Impl<Spectrum>;
using Scene_Object = Impl<Render_Scene_2D>;
initialize_runtime({.workers = 4});
auto* frequency = build_object<Frequency>();
auto* power = build_object<Power>();
auto* spectrum = build_object<Spectrum_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_Tag>() =
Spectrum_Frame{std::vector<Spectrum_Power>(512, -50.0)};
spectrum->mark_dirty<Prepare_Data_Tag>();
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<Renderable::Base_Tag>();
const bool valid = completed == frame &&
state.prepare_executed && state.paint_executed &&
spectrum->read_state<Spectrum::Base_Tag>().sample_count ==
512 &&
frequency->read_state<Renderable::Base_Tag>().paint_executed &&
power->read_state<Renderable::Base_Tag>().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);
}
@@ -33,3 +33,52 @@ TEST(frame_pixel_format, uses_blend2d_to_publish_straight_rgba) {
EXPECT_EQ(std::to_integer<unsigned>(pixels.bytes[2]), 0U);
EXPECT_EQ(std::to_integer<unsigned>(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<const Pixel*>(
view.data + static_cast<std::ptrdiff_t>(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<const Pixel*>(
view.data + static_cast<std::ptrdiff_t>(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));
}
+1 -1
View File
@@ -147,7 +147,7 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
EXPECT_GT(spectrum->read_state<Spectrum::Base_Tag>().rendered_point_count, 0u);
const auto& render_state = spectrum->read_state<Renderable::Base_Tag>();
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));