blend2D并行绘制
This commit is contained in:
@@ -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>(); }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
/* 仅当颜色变化时更新当前填充样式。 */
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user