完美一版

This commit is contained in:
2026-08-22 00:57:37 +08:00
parent e3d3bfd767
commit a8f102bdb1
36 changed files with 1374 additions and 354 deletions
+14
View File
@@ -0,0 +1,14 @@
#include "Frame_2D.hpp"
#include <stdexcept>
namespace aethera::render_2d {
struct Frame_2D::Private {
Blend2D_Cache color{}; /* Scene 直接合成且由外部 Frame 生命周期持有的最终颜色层。 */
};
Frame_2D::Frame_2D(Frame_Identity identity) : Render_Frame(identity), d(std::make_unique<Private>()) {}
Frame_2D::~Frame_2D() = default;
Image_View Frame_2D::image() const { return d->color.view(); }
Blend2D_Cache& detail::Frame_2D_Access::render_target(Frame_2D* frame) {
if (!frame) throw std::invalid_argument("Render_Scene_2D requires a non-null external frame");
return frame->d->color;
}
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "../render/Blend2D_Cache.hpp"
#include <frame.hpp>
#include <memory>
namespace aethera::render_2d {
class Frame_2D;
namespace detail {
struct Frame_2D_Access {
[[nodiscard]] static Blend2D_Cache& render_target(Frame_2D* frame);
};
}
class Frame_2D final : public Render_Frame {
public:
explicit Frame_2D(Frame_Identity identity);
~Frame_2D() override;
[[nodiscard]] Image_View image() const;
private:
struct Private;
std::unique_ptr<Private> d; /* 本帧最终二维颜色结果的唯一所有权。 */
friend struct detail::Frame_2D_Access;
};
}
@@ -20,12 +20,14 @@ struct Selection_Rectangle_Overlay::Private : Prev_Private {
Point_F press_position{}; /* 仅用于区分点击与拖动的按下像素位置。 */
bool dragging{}; /* 是否正在构造尚未提交的选择矩形。 */
const Dispatch* dispatch{}; /* 最终类型公开薄壳分派表。 */
static constexpr double minimum_drag_distance_pixels{3.0}; /* 允许提交和绘制选择框的最小像素距离。 */
/* CRTP 覆盖:绑定 Paint-only、事件能力和最终 Overlay 分派表。 */
template <Attached Object> void bind_private_crtp(Object* object);
void bind_sources(Abs_Axis* horizontal_axis_value, Abs_Axis* vertical_axis_value);
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
[[nodiscard]] static Axis_Rectangle axis_rectangle(Axis_Point first, Axis_Point second);
[[nodiscard]] static std::string range_label(const Abs_Axis* axis, Axis_Range range);
[[nodiscard]] bool has_visible_drag() const;
/* CRTP 覆盖:无需 Prepare 子图,直接绘制选择矩形。 */
template <Attached Object> void paint(Object* object);
/* CRTP 覆盖:处理拖拽并更新权威选择区域状态。 */
@@ -81,7 +83,11 @@ void Selection_Rectangle_Overlay::Private::paint(Object* object) {
painter.text({label_position.x, label_position.y + std::max(12.0, state.label_font.size * 1.35)}, "Y " + range_label(vertical_axis, region.vertical), state.label_font, state.label_pen);
};
for (const auto& region : state.selected_regions) paint_region(region);
if (dragging) paint_region(axis_rectangle(drag_origin, drag_current));
if (dragging && has_visible_drag()) paint_region(axis_rectangle(drag_origin, drag_current));
}
inline bool Selection_Rectangle_Overlay::Private::has_visible_drag() const {
const Point_F current_position{horizontal_axis->coordinate_to_pixel(drag_current.horizontal), vertical_axis->coordinate_to_pixel(drag_current.vertical)};
return std::hypot(current_position.x - press_position.x, current_position.y - press_position.y) >= minimum_drag_distance_pixels;
}
template <Attached Object>
void Selection_Rectangle_Overlay::Private::handle_event(Object* object, const Event& event) {
@@ -98,7 +104,7 @@ void Selection_Rectangle_Overlay::Private::handle_event(Object* object, const Ev
if (event.type == Event_Type::pointer_move && dragging) { drag_current = axis_point; object->template mark_dirty<Paint_Tag>(); event.accept(); return; }
if (event.type != Event_Type::pointer_release || !dragging) return;
dragging = false; drag_current = axis_point;
if (std::hypot(point.x - press_position.x, point.y - press_position.y) < 3.0) {
if (std::hypot(point.x - press_position.x, point.y - press_position.y) < minimum_drag_distance_pixels) {
object->template mark_dirty<Paint_Tag>(); event.accept(); return;
}
const Axis_Rectangle selected = axis_rectangle(drag_origin, drag_current);
@@ -15,7 +15,8 @@ struct Sweep_Spectrum : Def<Sweep_Spectrum, Renderable_2D<Renderable_2D_Cache::e
using Power_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {
std::size_t bins_per_block{}; /* 每个扫频块期望的功率点数;零值接受首块尺寸。 */
std::size_t block_count{1}; /* 最多保留的扫频块数;零值按 1 处理。 */
std::size_t block_count{1}; /* 一个完整扫频周期包含的块数;零值按 1 处理。 */
std::size_t next_block_index{}; /* 下一块覆盖的频率段下标;由 append_block() 推进。 */
Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */
bool visible_range_only{true}; /* 是否裁掉频率轴可见范围外的线段。 */
Axis_Range frequency_range{}; /* 全部扫描块覆盖的频率范围。 */
@@ -23,13 +24,13 @@ struct Sweep_Spectrum : Def<Sweep_Spectrum, Renderable_2D<Renderable_2D_Cache::e
Pen pen{Color::yellow()}; /* 扫频折线样式。 */
Pen current_frequency_pen{Color::red_color(), 2.0}; /* 当前扫频位置垂线样式。 */
Line_Interpolation_Mode interpolation_mode{Line_Interpolation_Mode::linear_value}; /* 相邻功率点插值方式。 */
std::vector<std::vector<Plot_Value>> blocks{}; /* 已提交扫描块的唯一权威集合。 */
std::vector<std::vector<Plot_Value>> blocks{}; /* 各频率段最新数据的唯一权威槽位;下标即频率段。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
std::size_t stored_block_count{}; /* 当前发布的扫描块数。 */
std::size_t stored_point_count{}; /* 当前发布的扫描点总数。 */
std::size_t rendered_point_count{}; /* 最近一次 Prepare 生成的曲线点数。 */
std::size_t stored_block_count{}; /* 当前已有最新数据的频率段数量。 */
std::size_t stored_point_count{}; /* 当前单条组合折线保留的总点数。 */
std::size_t rendered_point_count{}; /* 最近一次 Prepare 为单条折线生成的点数。 */
bool operator==(const State&) const;
};
struct Private;
@@ -5,13 +5,13 @@
namespace aethera::render_2d {
struct Sweep_Spectrum::Private : Prev_Private {
struct Prepared {
std::vector<detail::Curve_Prepared> partitions{}; /* Prepare 子图各分块的曲线输出。 */
std::vector<Plot_Value> values{}; /* 已提交扫描块拼接后的连续功率值。 */
Axis_Range domain{}; /* 当前已到达数据对应的频率区间。 */
Point_F marker_first{}; /* 当前扫描位置线的首端点。 */
Point_F marker_second{}; /* 当前扫描位置线的末端点。 */
std::vector<detail::Curve_Prepared> partitions{}; /* 单条组合折线各 Prepare 分块的曲线输出。 */
std::vector<Plot_Value> values{}; /* 各频率段最新数据按槽位顺序拼接的功率值。 */
Axis_Range domain{}; /* 当前已有频率段共同覆盖的业务坐标区间。 */
Point_F marker_first{}; /* 最新写入段末端扫描位置线的首端点。 */
Point_F marker_second{}; /* 最新写入段末端扫描位置线的末端点。 */
Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */
bool valid{}; /* 两根轴是否正交画布有效。 */
bool valid{}; /* 两根轴正交画布及组合折线均有效。 */
};
using Append_Run = void (*)(Root*, std::span<const Plot_Value>); using Count_Run = std::size_t (*)(const Root*);
struct Dispatch {
@@ -58,16 +58,20 @@ template <Attached Object>
tf::Taskflow Sweep_Spectrum::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("sweep_spectrum.paint.frame"); 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.partitions.resize(graph_partition_count); prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport;
if (prepared.canvas.empty() || frequency_layout.orientation == power_layout.orientation) return; for (const auto& block : state.blocks) prepared.values.insert(prepared.values.end(), block.begin(), block.end()); if (prepared.values.empty()) return;
const std::size_t bins_per_block = state.bins_per_block > 0
? state.bins_per_block : state.blocks.front().size();
const std::size_t expected_points = std::max<std::size_t>(1, bins_per_block * std::max<std::size_t>(1, state.block_count));
const double progress = expected_points > 1
? std::clamp(static_cast<double>(prepared.values.size() - 1) / static_cast<double>(expected_points - 1), 0.0, 1.0)
: 1.0;
const auto latest_frequency = state.frequency_range.origin + state.frequency_range.length() * progress;
prepared.domain = {state.frequency_range.origin, latest_frequency};
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;
if (prepared.canvas.empty() || frequency_layout.orientation == power_layout.orientation || state.blocks.empty()) return;
const std::size_t block_count = std::max<std::size_t>(1, state.block_count);
const std::size_t stored_block_count = std::min(block_count, state.blocks.size());
prepared.partitions.resize(graph_partition_count);
for (std::size_t index = 0; index < stored_block_count; ++index) prepared.values.insert(prepared.values.end(), state.blocks[index].begin(), state.blocks[index].end());
if (prepared.values.empty()) return;
const bool complete = stored_block_count == block_count;
const std::size_t latest_block_index = complete ? (state.next_block_index + block_count - 1) % block_count : stored_block_count - 1;
const double domain_progress = static_cast<double>(stored_block_count) / static_cast<double>(block_count);
const double marker_progress = static_cast<double>(latest_block_index + 1) / static_cast<double>(block_count);
const auto domain_target = state.frequency_range.origin + state.frequency_range.length() * domain_progress;
const auto latest_frequency = state.frequency_range.origin + state.frequency_range.length() * marker_progress;
prepared.domain = {state.frequency_range.origin, domain_target};
prepared.marker_first = detail::map_plot_point(frequency_axis, latest_frequency, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation); prepared.marker_second = detail::map_plot_point(frequency_axis, latest_frequency, power_axis, power_state.coordinate_range.target, frequency_layout.orientation); prepared.valid = true;
}
template <Attached Object>
@@ -78,13 +82,13 @@ void Sweep_Spectrum::Private::prepare_partition(Object* object, Plot_Partition_C
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); }
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>(); }
void Sweep_Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type> props) { if constexpr (std::same_as<Owner, Prop>) { auto& state = props.template get<Sweep_Spectrum::Base_Tag>(); const std::size_t block_count = std::max<std::size_t>(1, state.block_count); if (state.blocks.size() > block_count) state.blocks.resize(block_count); state.next_block_index = state.blocks.size() < block_count ? state.blocks.size() : state.next_block_index % block_count; object->template mark_dirty<Prepare_Data_Tag>(); } }
template <typename Object, typename Prop_Type, typename State_Type>
void Sweep_Spectrum::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { auto& state = pending_states.template get<Sweep_Spectrum::Base_Tag>(); const auto& prop = static_cast<const Prop&>(*current_prop); state.stored_block_count = prop.blocks.size(); state.stored_point_count = 0; for (const auto& block : prop.blocks) state.stored_point_count += block.size(); state.rendered_point_count = 0; for (const auto& partition : prepared.partitions) state.rendered_point_count += partition.points.size(); }
template <Attached Object>
const Sweep_Spectrum::Private::Dispatch& Sweep_Spectrum::Private::dispatch_for() {
static const Dispatch value{
[](Root* root, std::span<const Plot_Value> values) { auto* object = static_cast<Object*>(root); object->template update_prop<&Prop::blocks>([values](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Sweep_Spectrum::Base_Tag>(); state.blocks.emplace_back(values.begin(), values.end()); const std::size_t limit = std::max<std::size_t>(1, state.block_count); while (state.blocks.size() > limit) state.blocks.erase(state.blocks.begin()); }); },
[](Root* root, std::span<const Plot_Value> values) { auto* object = static_cast<Object*>(root); object->template update_prop<&Prop::blocks, &Prop::next_block_index>([values](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Sweep_Spectrum::Base_Tag>(); const std::size_t block_count = std::max<std::size_t>(1, state.block_count); if (state.blocks.size() > block_count) state.blocks.resize(block_count); if (state.blocks.size() < block_count) { state.next_block_index = state.blocks.size(); state.blocks.emplace_back(values.begin(), values.end()); } else { state.next_block_index %= block_count; state.blocks[state.next_block_index].assign(values.begin(), values.end()); } state.next_block_index = (state.next_block_index + 1) % block_count; }); },
[](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Sweep_Spectrum::Base_Tag>().blocks.size(); },
[](const Root* root) { const auto& blocks = static_cast<const Object*>(root)->template read_prop<Sweep_Spectrum::Base_Tag>().blocks; std::size_t count{}; for (const auto& block : blocks) count += block.size(); return count; },
[](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); std::size_t count{}; for (const auto& curve : data.prepared.partitions) count += curve.points.size(); return count; }
@@ -3,7 +3,7 @@ namespace aethera::render_2d {
bool Render_Scene_2D::State::operator==(const State&) const = default;
bool Render_Scene_2D::Prop::operator==(const Prop&) const = default;
void Render_Scene_2D::render() { static_cast<Private&>(*d).dispatch->render(this); }
Render_Scene_2D::Render_Result Render_Scene_2D::render(Frame_2D* frame) { return static_cast<Private&>(*d).dispatch->render(this, frame); }
void Render_Scene_2D::set_frame_callback(Frame_Callback callback) { static_cast<Private&>(*d).dispatch->set_frame_callback(this, std::move(callback)); }
void Render_Scene_2D::dispatch_event(std::unique_ptr<Event> event) {
static_cast<Private&>(*d).dispatch->push_event(this, std::move(event));
@@ -14,7 +14,4 @@ void Render_Scene_2D::activate_view() {
void Render_Scene_2D::deactivate_view() {
static_cast<Private&>(*d).dispatch->set_active(this, false);
}
Image_View Render_Scene_2D::frame_view() const {
return static_cast<const Private&>(*d).dispatch->frame_view(this);
}
}
@@ -1,21 +1,20 @@
#pragma once
#include "../base/Frame_2D.hpp"
#include "../base/Renderable_2D.hpp"
#include "../render/Blend2D_Cache.hpp"
#include <scene.hpp>
#include <functional>
#include <memory>
namespace aethera::render_2d {
struct Scene_Color_Cache_Tag {};
/* 执行二维 Renderable 图、合成颜色层并发布最终像素帧。 */
struct Render_Scene_2D : Def<Render_Scene_2D, Scene,
Tagged_Buffer<Scene_Color_Cache_Tag, Blend2D_Cache>,
Dependency_Graph_Type<Paint_Tag, Renderable_2D_Base>,
Dependency_Graph_Type<Paint_Cache_Tag, Renderable_2D_Base>
> {
struct Prop : Prev_Prop {
Size viewport{}; /* 最终帧的像素尺寸;空尺寸不执行渲染。 */
Color background{Color::black()}; /* 每帧合成前写入的背景颜色。 */
bool view_active{}; /* 视图是否接受 render() 产生新的完成帧。 */
bool view_active{}; /* 视图是否接受 render(frame*) 产生新的完成帧。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
@@ -34,10 +33,11 @@ struct Render_Scene_2D : Def<Render_Scene_2D, Scene,
private:
std::vector<std::function<std::expected<void, Dependency_Graph_Error>(Object*)>> attachments{}; /* 仅在 build 期间绑定已构造 Renderable。 */
};
using Frame_Callback = std::function<void(Image_View)>;
/* 合成一帧;执行期间的重复调用合并为一次后继帧。 */
void render();
/* 安装完成帧回调;Image_View 仅在回调执行期间有效。 */
enum class Render_Result { completed, view_inactive, empty_viewport };
using Frame_Callback = std::function<void(Frame_2D*)>;
/* 向调用方拥有的帧合成一次;frame 必须存活到完成回调返回。 */
[[nodiscard]] Render_Result render(Frame_2D* frame);
/* 安装完成帧回调;回调收到的就是对应 render(frame) 传入的对象。 */
void set_frame_callback(Frame_Callback callback);
/* 无锁提交事件所有权;下一次 Prepare 开始时按事件区域和 Paint 逆序派发,首个接受者终止传播。 */
void dispatch_event(std::unique_ptr<Event> event);
@@ -45,8 +45,6 @@ struct Render_Scene_2D : Def<Render_Scene_2D, Scene,
void activate_view();
/* 停止后续 render 调用,不清除最后一帧。 */
void deactivate_view();
/* 返回最后一次成功合成的只读像素视图;下次渲染后失效。 */
[[nodiscard]] Image_View frame_view() const;
};
}
#include "Render_Scene_2D.ipp"
+48 -35
View File
@@ -32,11 +32,10 @@ std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Render_Scene_2D::
return scene;
}
struct Render_Scene_2D::Private : Prev_Private {
using Render_Run = void (*)(Root*);
using Render_Run = Render_Result (*)(Root*, Frame_2D*);
using Callback_Run = void (*)(Root*, Frame_Callback);
using Event_Run = void (*)(Root*, std::unique_ptr<Event>);
using Active_Run = void (*)(Root*, bool);
using Frame_View_Run = Image_View (*)(const Root*);
struct Paint_Node {
Root* object{}; /* Paint 拓扑位置对应的最终对象;Scene 不拥有。 */
Renderable_2D_Base::Private* private_data{}; /* 对象的二维能力层;对象存活期间有效。 */
@@ -54,17 +53,16 @@ struct Render_Scene_2D::Private : Prev_Private {
Renderable_2D_Base::Private* private_data{}; /* 候选对象的二维能力层;仅在本次 Prepare 分发期间有效。 */
};
struct Dispatch {
Render_Run render; /* 执行最终 Scene 并合成颜色层。 */
Render_Run render; /* 向外部帧执行最终 Scene 并合成颜色层。 */
Callback_Run set_frame_callback; /* 安装最终完成帧回调。 */
Event_Run push_event; /* 向最终 Scene 的无锁待处理批次转移事件所有权。 */
Active_Run set_active; /* 修改最终 Scene 的视图活动状态。 */
Frame_View_Run frame_view; /* 访问最终 Scene 已合成的写侧帧。 */
};
const Dispatch* dispatch{}; /* Builder 绑定最终 Scene 类型后的静态分派表。 */
Frame_Callback frame_callback{}; /* 合成完成后的唯一像素发布出口。 */
std::unique_ptr<tf::Taskflow> paint_taskflow{}; /* 仅由二维 Paint 图构建的执行图。 */
bool rendering{}; /* 是否正在同步合成当前帧。 */
bool render_pending{}; /* 合成期间是否又收到 render();多个调用合并。 */
Frame_2D* active_frame{}; /* 当前同步 process 借用的外部帧;render 返回前清空。 */
Blend2D_Cache* frame_target{}; /* 当前 render(frame) 所属外部颜色层;调用返回后清空。 */
/* Impl CRTP 实现:在对象锁内执行 Kernel Scene,再按 Paint 图拓扑顺序合成颜色层。 */
std::vector<Paint_Node> paint_order{}; /* Paint 图当前拓扑序及 Scene 缓存分组结果。 */
std::vector<Cache_Group> cache_groups{}; /* 仅保存显式缓存根对应的执行分组。 */
@@ -77,7 +75,7 @@ struct Render_Scene_2D::Private : Prev_Private {
template <Attached Object> void prepare_paint_targets(Object* object, Blend2D_Cache& frame, Size viewport);
/* Prepare 开始时交换 Scene 事件批次,按区域、Paint 逆序和最近目标规则逐事件完成接受链。 */
template <Attached Object> void dispatch_events(Object* object, Size viewport);
template <Attached Object> void render(Object* object);
template <Attached Object> [[nodiscard]] Render_Result render(Object* object, Frame_2D* frame);
template <Attached Object>
[[nodiscard]] static const Dispatch& dispatch_for();
/* CRTP 覆盖:Builder 挂接最终 Private 后安装二维 Scene 的无虚函数业务分派。 */
@@ -174,8 +172,10 @@ void Render_Scene_2D::Private::after_advance(Object* object, Prop*, State_Access
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)
object->template pending_buffer<Scene_Color_Cache_Tag>().composite(*owner_node->private_data->valid_cache);
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);
}
}).name("render_2d.paint.cache.composite");
paint_done.precede(composite);
previous = composite;
@@ -228,14 +228,14 @@ void Render_Scene_2D::Private::prepare_paint_targets(Object*, Blend2D_Cache& fra
template <Attached Object, typename Callback>
void Render_Scene_2D::Private::process(Object* object, Callback&& callback)
requires std::invocable<Callback> {
Frame_2D* frame_object = active_frame;
if (!frame_object) throw std::logic_error("2D Scene process has no external frame");
const auto& state = static_cast<const Prop&>(*static_cast<typename Object::Private&>(*this).current);
if (!state.view_active) {
return;
}
if (state.viewport.empty()) {
return;
}
if (!state.view_active || state.viewport.empty()) return;
frame_object->mark(Frame_Trace_Marker::scene_render_started);
frame_object->mark(Frame_Trace_Marker::event_dispatch_started);
dispatch_events(object, state.viewport);
frame_object->mark(Frame_Trace_Marker::event_dispatch_finished);
object->template current_dependency_graph<Paint_Cache_Tag>().for_each([](const Dependency_Graph::Node& node) {
if (!node.object->template take_dirty<Paint_Cache_Tag>()) return;
node.object->template mark_dirty<Prepare_Data_Tag>();
@@ -248,9 +248,16 @@ void Render_Scene_2D::Private::process(Object* object, Callback&& callback)
execute = execute || !data.prepare_graph_built || data.dispatch->prepare.rebuild_predicate(renderable);
if (execute) prepare_executions.push_back(renderable);
});
Prev_Private::process(object, [&](const Scene::Private::Result&) {
Prev_Private::process(object, frame_object, [&](const Scene::Private::Result&) {
frame_object->mark(Frame_Trace_Marker::paint_started);
for (auto* renderable : prepare_executions) renderable->template mark_dirty<Paint_Tag>();
auto& frame = object->template pending_buffer<Scene_Color_Cache_Tag>();
auto& frame = detail::Frame_2D_Access::render_target(frame_object);
struct Frame_Target_Scope {
Blend2D_Cache*& target; /* Scene 当前帧目标槽位。 */
Blend2D_Cache* previous{}; /* 嵌套调用前的目标;析构时恢复。 */
~Frame_Target_Scope() { target = previous; }
} frame_target_scope{frame_target, frame_target};
frame_target = &frame;
frame.ensure_size(state.viewport);
frame.clear();
{
@@ -261,18 +268,31 @@ void Render_Scene_2D::Private::process(Object* object, Callback&& callback)
}
prepare_paint_targets(object, frame, state.viewport);
if (paint_taskflow && !paint_taskflow->empty()) aethera::detail::run_taskflow(*paint_taskflow);
frame_object->mark(Frame_Trace_Marker::paint_finished);
});
std::invoke(std::forward<Callback>(callback));
}
template <Attached Object>
void Render_Scene_2D::Private::render(Object* object) {
if (rendering) { render_pending = true; return; }
rendering = true;
do {
render_pending = false;
object->process([&] { if (frame_callback) frame_callback(object->template pending_buffer<Scene_Color_Cache_Tag>().view()); });
} while (render_pending);
rendering = false;
Render_Scene_2D::Render_Result Render_Scene_2D::Private::render(Object* object, Frame_2D* frame) {
static_cast<void>(detail::Frame_2D_Access::render_target(frame));
frame->mark(Frame_Trace_Marker::scene_render_requested);
struct Active_Frame_Scope {
Frame_2D*& target; /* 最终 Private 的同步 process 帧槽位。 */
Frame_2D* previous{}; /* 嵌套调用前的帧;析构时恢复。 */
~Active_Frame_Scope() { target = previous; }
} active_frame_scope{active_frame, active_frame};
active_frame = frame;
bool completed{};
object->process([&] {
completed = true;
frame->mark(Frame_Trace_Marker::scene_render_finished);
frame->mark(Frame_Trace_Marker::callback_started);
if (frame_callback) frame_callback(frame);
frame->mark(Frame_Trace_Marker::callback_finished);
});
if (completed) return Render_Result::completed;
const auto& prop = object->template read_prop<Render_Scene_2D::Base_Tag>();
return prop.view_active ? Render_Result::empty_viewport : Render_Result::view_inactive;
}
template <Attached Object>
void Render_Scene_2D::Private::dispatch_events(Object* object, Size viewport) {
@@ -303,9 +323,9 @@ void Render_Scene_2D::Private::dispatch_events(Object* object, Size viewport) {
template <Attached Object>
const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for() {
static const Dispatch value{
[](Root* root) {
[](Root* root, Frame_2D* frame) {
auto* object = static_cast<Object*>(root);
static_cast<typename Object::Private&>(*object->d).render(object);
return static_cast<typename Object::Private&>(*object->d).render(object, frame);
},
[](Root* root, Frame_Callback callback) {
auto* object = static_cast<Object*>(root);
@@ -315,14 +335,7 @@ const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for
auto* object = static_cast<Object*>(root);
static_cast<typename Object::Private&>(*object->d).push_event(std::move(event));
},
[](Root* root, bool active) {
static_cast<Object*>(root)->template set<&Prop::view_active>(active);
},
[](const Root* root) {
const auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
return private_data.buffer_storage.template get<Scene_Color_Cache_Tag>().pending->view();
}
[](Root* root, bool active) { static_cast<Object*>(root)->template set<&Prop::view_active>(active); }
};
return value;
}
+18 -8
View File
@@ -1,4 +1,5 @@
#include <render_2D/axis/Axis.hpp>
#include <render_2D/base/Frame_2D.hpp>
#include <render_2D/event/Event.hpp>
#include <render_2D/event/Event_Routing_Rules.hpp>
#include <render_2D/scene/Render_Scene_2D.hpp>
@@ -12,6 +13,15 @@ std::unique_ptr<Object> build_axis() {
if (!result) std::terminate();
return std::move(result).value();
}
template <typename Scene>
std::unique_ptr<Frame_2D> render_frame(Scene* scene) {
static std::uint64_t sequence{1};
auto frame = std::make_unique<Frame_2D>(Frame_Identity{sequence++, 0});
EXPECT_EQ(scene->render(frame.get()), Render_Scene_2D::Render_Result::completed);
return frame;
}
template <typename Scene>
void render_once(Scene* scene) { static_cast<void>(render_frame(scene)); }
}
TEST(event_routing_rule, keeps_layered_targets_before_nearest_scored_targets) {
std::vector<int> targets{1, 2, 3, 4};
@@ -82,8 +92,8 @@ TEST(axis_render, scene_prepares_and_paints_uncached_axis_into_frame) {
prepare.add(axis.get());
paint.add(axis.get());
}).has_value()));
scene->render();
const Image_View view = scene->frame_view();
const auto frame = render_frame(scene.get());
const Image_View view = frame->image();
ASSERT_FALSE(view.empty());
bool contains_color{};
for (int y = 0; y < view.height && !contains_color; ++y) {
@@ -106,12 +116,12 @@ TEST(axis_event, numeric_axis_wheel_zoom_and_drag_update_authoritative_range) {
prepare.add(axis.get()); paint.add(axis.get());
}).has_value()));
scene->activate_view();
scene->render();
render_once(scene.get());
auto wheel = std::make_unique<Wheel_Event>();
wheel->position = {50.0, 0.0};
wheel->angle_delta_y = 120.0;
scene->dispatch_event(std::move(wheel));
scene->render();
render_once(scene.get());
EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.5, 9.5}));
auto press = std::make_unique<Pointer_Event>(Event_Type::pointer_press);
press->position = {50.0, 0.0};
@@ -120,7 +130,7 @@ TEST(axis_event, numeric_axis_wheel_zoom_and_drag_update_authoritative_range) {
auto move = std::make_unique<Pointer_Event>(Event_Type::pointer_move);
move->position = {60.0, 0.0};
scene->dispatch_event(std::move(move));
scene->render();
render_once(scene.get());
EXPECT_EQ(axis->coordinate_range(), (Axis_Range{-0.4, 8.6}));
}
TEST(axis_event, scene_routes_pointer_interaction_to_the_nearest_axis_segment) {
@@ -143,13 +153,13 @@ TEST(axis_event, scene_routes_pointer_interaction_to_the_nearest_axis_segment) {
paint.add(horizontal.get()); paint.add(vertical.get());
}).has_value()));
scene->activate_view();
scene->render();
render_once(scene.get());
auto near_vertical = std::make_unique<Wheel_Event>();
near_vertical->position = {22.0, 80.0};
near_vertical->angle_delta_y = 120.0;
scene->dispatch_event(std::move(near_vertical));
scene->render();
render_once(scene.get());
EXPECT_DOUBLE_EQ(horizontal->coordinate_range().size(), 10.0);
EXPECT_DOUBLE_EQ(vertical->coordinate_range().size(), 9.0);
@@ -157,7 +167,7 @@ TEST(axis_event, scene_routes_pointer_interaction_to_the_nearest_axis_segment) {
near_horizontal->position = {120.0, 178.0};
near_horizontal->angle_delta_y = 120.0;
scene->dispatch_event(std::move(near_horizontal));
scene->render();
render_once(scene.get());
EXPECT_DOUBLE_EQ(horizontal->coordinate_range().size(), 9.0);
EXPECT_DOUBLE_EQ(vertical->coordinate_range().size(), 9.0);
}
+31 -11
View File
@@ -1,4 +1,5 @@
#include <render_2D/plottable/Plottables.hpp>
#include <render_2D/base/Frame_2D.hpp>
#include <render_2D/scene/Render_Scene_2D.hpp>
#include <gtest/gtest.h>
#include <array>
@@ -30,6 +31,12 @@ void configure_axis(Axis* axis, Axis_Orientation orientation, Point_F position,
axis->template set<&Abs_Axis::Prop::position>(position);
axis->template set<&Abs_Axis::Prop::pixel_length>(length);
}
template <typename Scene>
void render_once(Scene* scene) {
static std::uint64_t sequence{1};
Frame_2D frame{Frame_Identity{sequence++, 0}};
EXPECT_EQ(scene->render(&frame), Render_Scene_2D::Render_Result::completed);
}
}
TEST(plottable_migration, curve_plots_share_partitioned_rendering) {
@@ -60,18 +67,31 @@ TEST(plottable_migration, curve_plots_share_partitioned_rendering) {
trace->append_sample(time->append_time({2'000}), 50.0);
trace->append_sample(time->append_time({3'000}), 90.0);
sweep->set<&Sweep_Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0});
sweep->set<&Sweep_Spectrum::Prop::block_count>(2u);
sweep->set<&Sweep_Spectrum::Prop::partition_mode>(Plot_Partition_Mode::fixed);
sweep->set<&Sweep_Spectrum::Prop::partition_count>(2u);
const std::array<Plot_Value, 4> block{-90.0, -60.0, -30.0, -10.0};
sweep->append_block(block);
const std::array<Plot_Value, 4> first_block{-90.0, -60.0, -30.0, -10.0};
const std::array<Plot_Value, 4> second_block{-80.0, -50.0, -20.0, -5.0};
const std::array<Plot_Value, 4> replacement_block{-70.0, -40.0, -15.0, -2.0};
sweep->append_block(first_block);
sweep->append_block(second_block);
auto scene = build_scene<Scene>(trace.get(), sweep.get());
scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->activate_view();
scene->render();
render_once(scene.get());
EXPECT_EQ(trace->sample_count(), 3u);
EXPECT_GT(trace->rendered_point_count(), 0u);
EXPECT_EQ(sweep->stored_block_count(), 1u);
EXPECT_EQ(sweep->stored_block_count(), 2u);
EXPECT_GT(sweep->rendered_point_count(), 0u);
const auto completed_sweep_points = sweep->rendered_point_count();
sweep->append_block(replacement_block);
render_once(scene.get());
EXPECT_EQ(sweep->stored_block_count(), 2u);
EXPECT_EQ(sweep->rendered_point_count(), completed_sweep_points);
const auto& sweep_prop = sweep->read_prop<Sweep_Spectrum::Base_Tag>();
EXPECT_EQ(sweep_prop.blocks[0], std::vector<Plot_Value>(replacement_block.begin(), replacement_block.end()));
EXPECT_EQ(sweep_prop.blocks[1], std::vector<Plot_Value>(second_block.begin(), second_block.end()));
EXPECT_EQ(sweep_prop.next_block_index, 1u);
}
TEST(plottable_migration, raster_plots_share_partitioned_color_blocks) {
@@ -105,7 +125,7 @@ TEST(plottable_migration, raster_plots_share_partitioned_color_blocks) {
auto scene = build_scene<Scene>(glow.get(), waterfall.get());
scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->activate_view();
scene->render();
render_once(scene.get());
EXPECT_EQ(glow->history_count(), 1u);
EXPECT_GT(glow->rendered_cell_count(), 0u);
EXPECT_EQ(waterfall->row_count(), 2u);
@@ -133,7 +153,7 @@ TEST(plottable_migration, direct_overlay_and_constellation_build_and_render) {
auto scene = build_scene<Scene>(diagram.get(), overlay.get());
scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->activate_view();
scene->render();
render_once(scene.get());
EXPECT_EQ(diagram->point_count(), 1u);
EXPECT_EQ(overlay->read_state<Renderable::Base_Tag>().prepare_task_count, 0u);
EXPECT_EQ(overlay->read_state<Renderable::Base_Tag>().paint_task_count, 1u);
@@ -155,7 +175,7 @@ TEST(selection_overlay, control_extends_selection_and_plain_click_clears_it) {
auto scene = build_scene<Scene>(overlay.get());
scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->activate_view();
scene->render();
render_once(scene.get());
const auto drag = [&](Point_F first, Point_F second, Keyboard_Modifier modifiers) {
auto press = std::make_unique<Pointer_Event>(Event_Type::pointer_press);
@@ -172,7 +192,7 @@ TEST(selection_overlay, control_extends_selection_and_plain_click_clears_it) {
release->button = Mouse_Button::left;
release->modifiers = modifiers;
scene->dispatch_event(std::move(release));
scene->render();
render_once(scene.get());
};
drag({30.0, 90.0}, {60.0, 60.0}, Keyboard_Modifier::none);
EXPECT_EQ(overlay->selected_regions().size(), 1u);
@@ -202,7 +222,7 @@ TEST(selection_overlay, time_axis_selection_keeps_axis_coordinates_while_window_
auto scene = build_scene<Scene>(overlay.get());
scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->activate_view();
scene->render();
render_once(scene.get());
auto press = std::make_unique<Pointer_Event>(Event_Type::pointer_press);
press->position = {92.0, 88.0};
press->button = Mouse_Button::left;
@@ -214,12 +234,12 @@ TEST(selection_overlay, time_axis_selection_keeps_axis_coordinates_while_window_
release->position = {116.0, 56.0};
release->button = Mouse_Button::left;
scene->dispatch_event(std::move(release));
scene->render();
render_once(scene.get());
ASSERT_EQ(overlay->selected_regions().size(), 1u);
const Axis_Rectangle selected = overlay->selected_regions().front();
const auto pixel_before = time->coordinate_to_pixel(selected.horizontal.center());
time->append_time({4'000});
scene->render();
render_once(scene.get());
ASSERT_EQ(overlay->selected_regions().size(), 1u);
EXPECT_EQ(overlay->selected_regions().front(), selected);
EXPECT_NE(time->coordinate_to_pixel(selected.horizontal.center()), pixel_before);
+35 -20
View File
@@ -1,4 +1,5 @@
#include <render_2D/plottable/Spectrum.hpp>
#include <render_2D/base/Frame_2D.hpp>
#include <render_2D/scene/Render_Scene_2D.hpp>
#include <gtest/gtest.h>
namespace {
@@ -11,6 +12,14 @@ std::unique_ptr<Object> build_object(Args&&... args) {
if (!result) std::terminate();
return std::move(result).value();
}
template <typename Scene, typename... Renderables>
std::unique_ptr<Scene> build_scene(Renderables*... renderables) {
typename Scene::Builder builder;
(builder.add_renderable(renderables), ...);
auto result = builder.build();
if (!result) std::terminate();
return std::move(result).value();
}
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);
@@ -19,16 +28,22 @@ bool contains_color(Image_View view) {
}
return false;
}
template <typename Scene>
std::unique_ptr<Frame_2D> render_frame(Scene* scene) {
static std::uint64_t sequence{1};
auto frame = std::make_unique<Frame_2D>(Frame_Identity{sequence++, 0});
EXPECT_EQ(scene->render(frame.get()), Render_Scene_2D::Render_Result::completed);
return frame;
}
}
TEST(spectrum_data, publishes_samples_and_interpolates_power) {
using Frequency = Impl<Frequency_Axis>;
using Power = Impl<Numeric_Axis>;
using Object = Impl<Spectrum>;
using Scene_Object = Impl<Render_Scene_2D>;
auto scene = build_object<Scene_Object>();
auto frequency = build_object<Frequency>();
auto power = build_object<Power>();
auto spectrum = build_object<Object>(scene.get(), frequency.get(), power.get());
auto spectrum = build_object<Object>(frequency.get(), power.get());
const auto missing = spectrum->power_at(25.0);
ASSERT_FALSE(missing.has_value());
EXPECT_EQ(missing.error(), Spectrum::Power_At_Result::no_samples);
@@ -49,10 +64,9 @@ TEST(spectrum_markers, supports_selection_frequency_changes_and_known_results) {
using Power = Impl<Numeric_Axis>;
using Object = Impl<Spectrum>;
using Scene_Object = Impl<Render_Scene_2D>;
auto scene = build_object<Scene_Object>();
auto frequency = build_object<Frequency>();
auto power = build_object<Power>();
auto spectrum = build_object<Object>(scene.get(), frequency.get(), power.get());
auto spectrum = build_object<Object>(frequency.get(), power.get());
spectrum->add_custom_marker(10.0);
spectrum->add_custom_line_marker(20.0);
spectrum->advance();
@@ -79,10 +93,10 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
using Spectrum_Object = Impl<Spectrum>;
using Scene_Object = Impl<Render_Scene_2D>;
initialize_runtime(2);
auto scene = build_object<Scene_Object>();
auto frequency = build_object<Frequency>();
auto power = build_object<Power>();
auto spectrum = build_object<Spectrum_Object>(scene.get(), frequency.get(), power.get());
auto spectrum = build_object<Spectrum_Object>(frequency.get(), power.get());
auto scene = build_scene<Scene_Object>(spectrum.get());
const Size canvas{160, 120};
frequency->set<&Abs_Axis::Prop::position>(Point_F{20.0, 100.0});
frequency->set<&Abs_Axis::Prop::pixel_length>(120.0);
@@ -112,48 +126,49 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
EXPECT_TRUE(cache_graph.depends_on(spectrum.get(), scene.get()));
EXPECT_TRUE(cache_graph.depends_on(spectrum.get(), frequency.get()));
EXPECT_TRUE(cache_graph.depends_on(spectrum.get(), power.get()));
scene->render();
auto output_frame = render_frame(scene.get());
EXPECT_GT(spectrum->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);
const Image_View frame = scene->frame_view();
const Image_View frame = output_frame->image();
ASSERT_FALSE(frame.empty());
EXPECT_TRUE(contains_color(frame));
EXPECT_FALSE(spectrum->dirty<Paint_Cache_Tag>());
EXPECT_FALSE(spectrum->dirty<Paint_Tag>());
EXPECT_FALSE(frequency->dirty<Paint_Tag>());
EXPECT_FALSE(power->dirty<Paint_Tag>());
scene->render();
EXPECT_TRUE(contains_color(scene->frame_view()));
output_frame = render_frame(scene.get());
EXPECT_TRUE(contains_color(output_frame->image()));
frequency->set<&Numeric_Axis::Prop::precision>(3);
EXPECT_FALSE(spectrum->dirty<Paint_Cache_Tag>());
scene->render();
output_frame = render_frame(scene.get());
EXPECT_TRUE(spectrum->read_state<Renderable::Base_Tag>().paint_executed);
EXPECT_TRUE(frequency->read_state<Renderable::Base_Tag>().paint_executed);
EXPECT_TRUE(power->read_state<Renderable::Base_Tag>().paint_executed);
EXPECT_TRUE(contains_color(scene->frame_view()));
EXPECT_TRUE(contains_color(output_frame->image()));
frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 120.0});
EXPECT_TRUE(spectrum->dirty<Paint_Cache_Tag>());
scene->render();
output_frame = render_frame(scene.get());
EXPECT_FALSE(spectrum->dirty<Paint_Cache_Tag>());
EXPECT_TRUE(spectrum->read_state<Renderable::Base_Tag>().prepare_executed);
spectrum->set<&Spectrum::Prop::partition_count>(2u);
spectrum->update_samples(samples);
scene->render();
output_frame = render_frame(scene.get());
const auto& rebuilt_state = spectrum->read_state<Renderable::Base_Tag>();
EXPECT_TRUE(rebuilt_state.prepare_graph_rebuilt);
EXPECT_EQ(rebuilt_state.prepare_task_count, 3u);
EXPECT_TRUE(contains_color(scene->frame_view()));
EXPECT_TRUE(contains_color(output_frame->image()));
const Size resized_canvas{200, 140};
scene->set<&Render_Scene_2D::Prop::viewport>(resized_canvas);
EXPECT_TRUE(frequency->dirty<Prepare_Data_Tag>());
EXPECT_TRUE(power->dirty<Prepare_Data_Tag>());
scene->render();
EXPECT_TRUE(spectrum->dirty<Paint_Cache_Tag>());
EXPECT_FALSE(frequency->dirty<Prepare_Data_Tag>());
EXPECT_FALSE(power->dirty<Prepare_Data_Tag>());
output_frame = render_frame(scene.get());
EXPECT_EQ(scene->read_prop<Render_Scene_2D::Base_Tag>().viewport, resized_canvas);
EXPECT_TRUE(frequency->read_state<Renderable::Base_Tag>().prepare_executed);
EXPECT_FALSE(frequency->read_state<Renderable::Base_Tag>().prepare_executed);
EXPECT_TRUE(frequency->read_state<Renderable::Base_Tag>().paint_executed);
const Image_View resized_frame = scene->frame_view();
const Image_View resized_frame = output_frame->image();
EXPECT_EQ(resized_frame.width, resized_canvas.width);
EXPECT_EQ(resized_frame.height, resized_canvas.height);
EXPECT_TRUE(contains_color(resized_frame));