From d6c1db9d06107fdce162e21dee2210d83b5a6450 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Mon, 24 Aug 2026 08:12:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=89=E7=BC=93=E5=86=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Project_detail_specification.md | 2 +- .../double_buffer/Mpmc_Triple_Buffer.hpp | 140 ++++++++ kernel/src/kernel/double_buffer/mechanism.hpp | 25 +- kernel/src/kernel/double_buffer/model.hpp | 27 ++ kernel/src/kernel/render_common.hpp | 1 + kernel/src/kernel/scene.cpp | 25 +- kernel/src/kernel/scene.hpp | 5 +- kernel/src/kernel/scene.ipp | 46 ++- kernel/src/test/object_test.cpp | 47 +++ render_2D/render_2D/axis/Time_Axis.hpp | 6 +- render_2D/render_2D/axis/Time_Axis.ipp | 61 ++-- render_2D/render_2D/plottable/Afterglow.cpp | 19 +- render_2D/render_2D/plottable/Afterglow.hpp | 6 +- render_2D/render_2D/plottable/Afterglow.ipp | 290 ++++++++++++++--- .../plottable/Constellation_Diagram.cpp | 15 +- .../plottable/Constellation_Diagram.hpp | 40 ++- .../plottable/Constellation_Diagram.ipp | 175 ++++++++-- .../render_2D/plottable/Frequency_Trace.hpp | 5 +- .../render_2D/plottable/Frequency_Trace.ipp | 31 +- .../render_2D/plottable/Sweep_Spectrum.cpp | 4 +- .../render_2D/plottable/Sweep_Spectrum.hpp | 7 +- .../render_2D/plottable/Sweep_Spectrum.ipp | 33 +- render_2D/render_2D/plottable/Waterfall.cpp | 22 +- render_2D/render_2D/plottable/Waterfall.hpp | 5 +- render_2D/render_2D/plottable/Waterfall.ipp | 306 +++++++++++++++--- web_server/src/Gallery_Plots_2D.cpp | 15 +- 26 files changed, 1099 insertions(+), 259 deletions(-) create mode 100644 kernel/src/kernel/double_buffer/Mpmc_Triple_Buffer.hpp diff --git a/Project_detail_specification.md b/Project_detail_specification.md index 3a755c3..3a2319b 100644 --- a/Project_detail_specification.md +++ b/Project_detail_specification.md @@ -16,7 +16,7 @@ * 依赖可以选择 Prop/State 的单字段或整个 `Base_Tag` 层;字段写入必须同时发出字段级和所属层级变更,使用方按实际重建粒度选择一种依赖。 * 整体对象依赖只表达依赖图中的拓扑顺序,不传播 dirty;准备顺序、绘图顺序等业务含义由各自 Tag 解释。具体字段依赖才用于对应 Tag 的 dirty 传播,例如绘图缓存失效。 * Kernel `Scene` 只负责 2D/3D 共有的 Prepare 数据阶段;Paint、缓存失效、像素合成和异步后端提交由对应渲染模块自己的 Scene、Tag 与 Taskflow 负责。 -* `Scene::Private` 是输入事件流的唯一所有者:外部转移事件对象所有权,无锁提交到双缓冲队列,事件不得独立触发帧,只在下一次正常渲染的 Prepare 入口交换并按 FIFO 消费。2D 按 Renderable 区域与 Paint 顺序形成接受链,区域默认整个 viewport;3D 无等待提交渲染域,满载时保留当前事件供下一次 Prepare 重试。 +* `Scene::Private` 是输入事件流的唯一所有者:外部转移事件对象所有权,并发提交到 Def 注册的 MPMC 三缓冲;收集、内部渲染、外部查询分别占用一份队列,Prepare 入口只在交换锁下轮换三者并清空过期查询队列。事件不得独立触发帧。2D 按 Renderable 区域与 Paint 顺序形成接受链,区域默认整个 viewport;3D 无等待提交渲染域,满载时保留当前事件供下一次 Prepare 重试。 * 帧由 Scene 外部创建和持有;每次 `render(frame*)` 只借用该帧并在完成回调返回同一地址。Scene、异步后端和 Web 层只向帧写入固定语义的单调时间点与原始耗时,不保存平均值、分位数或波动等衍生统计。 * Plot 使用服务端帧时钟调用 `Scene::render(frame*)`;帧策略只决定调用频率,完成回调不得安排下一帧,也不存在浏览器逐帧请求或 request/ack。Scene 回调返回完成帧后,Web 层在 Render Domain 之外把 2D 原生 BGRA 或 3D 原生 RGBA 编码为 H.264,经 LibDataChannel WebRTC 视频轨直接发布;WebSocket 只承载 SDP/ICE、输入事件和诊断 JSON。多个订阅者共享同一编码结果,关闭视频时 3D 必须使用 diagnostics 输出并跳过 GPU 像素回读。 * 相机是 3D Scene 组件,只允许定义在 `render_3D/camera`;Kernel 和 2D 不得依赖相机类型。Web 层只为已有 `Camera_3D` 增加协议描述,不复制相机配置。Gallery 服务同一时刻只允许一个页面实例持有;该页面的所有 Plot 连接共享页面令牌,其他标签页或浏览器实例必须被拒绝。 diff --git a/kernel/src/kernel/double_buffer/Mpmc_Triple_Buffer.hpp b/kernel/src/kernel/double_buffer/Mpmc_Triple_Buffer.hpp new file mode 100644 index 0000000..8488d09 --- /dev/null +++ b/kernel/src/kernel/double_buffer/Mpmc_Triple_Buffer.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace double_buffer { + +/* Def mechanism declaration. Tag selects one independently exchanged MPMC stream. */ +template +struct Mpmc_Triple_Buffer { + using Tag_Type = Tag; + using Value_Type = Value; +}; + +namespace detail { + +/* + * The three buffers are queues, not snapshots. Producers append to collecting while + * the renderer owns rendering and observers own query. advance() rotates roles as: + * completed rendering -> query, collecting -> rendering, expired query -> collecting. + */ +template +class Mpmc_Triple_Buffer_Storage { +public: + using Value = typename Declaration::Value_Type; + + Mpmc_Triple_Buffer_Storage() = default; + Mpmc_Triple_Buffer_Storage(const Mpmc_Triple_Buffer_Storage&) = delete; + Mpmc_Triple_Buffer_Storage& operator=(const Mpmc_Triple_Buffer_Storage&) = delete; + + void submit(Value value) { + std::shared_lock lock(exchange_lock_); + if (!collecting_->enqueue(std::move(value))) throw std::bad_alloc{}; + } + + void advance() { + std::unique_lock lock(exchange_lock_); + auto* expired_query = query_; + query_ = rendering_; + rendering_ = collecting_; + collecting_ = expired_query; + clear(*collecting_); + } + + template + requires std::invocable> + decltype(auto) access_rendering(Callback&& callback) { + std::unique_lock lock(exchange_lock_); + return access(*rendering_, std::forward(callback)); + } + + template + requires std::invocable> + decltype(auto) access_query(Callback&& callback) const { + std::unique_lock lock(exchange_lock_); + auto adapter = [&](std::span values) -> decltype(auto) { + return std::invoke(callback, std::span{values}); + }; + return access(*query_, adapter); + } + +private: + using Queue = moodycamel::ConcurrentQueue; + + static void clear(Queue& queue) { + Value value; + while (queue.try_dequeue(value)) {} + } + + static void restore(Queue& queue, std::vector& values) { + for (auto& value : values) + if (!queue.enqueue(std::move(value))) throw std::bad_alloc{}; + } + + template + static decltype(auto) access(Queue& queue, Callback&& callback) { + std::vector values; + values.reserve(queue.size_approx()); + Value value; + while (queue.try_dequeue(value)) values.push_back(std::move(value)); + if constexpr (std::is_void_v>>) { + std::invoke(callback, std::span{values}); + restore(queue, values); + } + else { + static_assert(!std::is_reference_v>>, + "triple-buffer access results must not outlive the locked queue view"); + auto result = std::invoke(callback, std::span{values}); + restore(queue, values); + return result; + } + } + + mutable std::shared_mutex exchange_lock_; /* Serializes role exchange and stable role inspection. */ + mutable Queue first_{}; /* First physical MPMC queue; role changes only under exchange_lock_. */ + mutable Queue second_{}; /* Second physical MPMC queue; role changes only under exchange_lock_. */ + mutable Queue third_{}; /* Third physical MPMC queue; role changes only under exchange_lock_. */ + Queue* collecting_{&first_}; /* Concurrent producer destination until the next exchange. */ + Queue* rendering_{&second_}; /* Immutable-to-producers queue consumed by the renderer. */ + mutable Queue* query_{&third_}; /* Last completed render input exposed to observers. */ +}; + +template +class Mpmc_Triple_Buffer_Storage_Set; + +template +class Mpmc_Triple_Buffer_Storage_Set> { + template + static consteval std::size_t index_of() { + if constexpr (std::same_as) return 0; + else return 1 + index_of(); + } +public: + template + auto& get() noexcept { + constexpr std::size_t index = index_of(); + return std::get(storage_); + } + + template + const auto& get() const noexcept { + return const_cast(this)->template get(); + } + +private: + std::tuple...> storage_; +}; + +} +} diff --git a/kernel/src/kernel/double_buffer/mechanism.hpp b/kernel/src/kernel/double_buffer/mechanism.hpp index dbaf101..637b269 100644 --- a/kernel/src/kernel/double_buffer/mechanism.hpp +++ b/kernel/src/kernel/double_buffer/mechanism.hpp @@ -1,4 +1,5 @@ #pragma once +#include "Mpmc_Triple_Buffer.hpp" #include #include #include @@ -212,6 +213,10 @@ struct Is_Dependency_Graph_Type : std::false_type {}; template struct Is_Dependency_Graph_Type> : std::true_type {}; template +struct Is_Mpmc_Triple_Buffer : std::false_type {}; +template +struct Is_Mpmc_Triple_Buffer> : std::true_type {}; +template struct Is_State_Type : std::false_type {}; template struct Is_State_Type> : std::true_type {}; @@ -224,7 +229,9 @@ concept Buffer_Type = Is_Tagged_Buffer::value; template concept Dependency_Graph_Mechanism = Is_Dependency_Graph_Type::value; template -concept Mechanism_Type = Buffer_Type || Dependency_Graph_Mechanism; +concept Mpmc_Triple_Buffer_Mechanism = Is_Mpmc_Triple_Buffer::value; +template +concept Mechanism_Type = Buffer_Type || Dependency_Graph_Mechanism || Mpmc_Triple_Buffer_Mechanism; template concept Tagged_State = Prop_State && requires { typename Value::Tag_Type; @@ -298,6 +305,8 @@ template using Mechanism_Buffer_Tuple = std::conditional_t, std::tuple, std::tuple<>>; template using Mechanism_Dependency_Graph_Tuple = std::conditional_t, std::tuple, std::tuple<>>; +template +using Mechanism_Mpmc_Triple_Buffer_Tuple = std::conditional_t, std::tuple, std::tuple<>>; template struct Has_Tag : std::false_type {}; template @@ -328,6 +337,18 @@ template concept Buffer_List = Is_Buffer_List::value; template concept Dependency_Graph_List = Is_Dependency_Graph_List::value; +template +struct Is_Mpmc_Triple_Buffer_List : std::false_type {}; +template +struct Is_Mpmc_Triple_Buffer_List> : Tagged_List_Check< + (Mpmc_Triple_Buffer_Mechanism && ...), Declarations...> {}; +template +concept Mpmc_Triple_Buffer_List = Is_Mpmc_Triple_Buffer_List::value; +template +concept Mpmc_Triple_Buffer_Tag_In = requires { + requires Mpmc_Triple_Buffer_List; + requires Has_Tag::value; +}; template concept Buffer_Tag_In = requires { requires Buffer_List; @@ -655,6 +676,7 @@ concept Object_Core = requires { requires detail::State_Chain_Matches; requires detail::Buffer_List; requires detail::Dependency_Graph_List; + requires detail::Mpmc_Triple_Buffer_List; }; namespace detail { template @@ -678,6 +700,7 @@ struct Root { }; using Buffers = std::tuple<>; using Dependency_Graph_Types = std::tuple<>; + using Mpmc_Triple_Buffers = std::tuple<>; using Base_Tag = Root; using States = std::tuple>; struct Prop : Prop_Type { diff --git a/kernel/src/kernel/double_buffer/model.hpp b/kernel/src/kernel/double_buffer/model.hpp index 1ee946f..23210d9 100644 --- a/kernel/src/kernel/double_buffer/model.hpp +++ b/kernel/src/kernel/double_buffer/model.hpp @@ -35,6 +35,11 @@ using Impl_Dependency_Graph_Types = decltype(std::tuple_cat( std::declval(), std::declval>()... )); +template +using Impl_Mpmc_Triple_Buffers = decltype(std::tuple_cat( + std::declval(), + std::declval>()... +)); template using Impl_States = decltype(std::tuple_cat( std::declval(), @@ -44,6 +49,7 @@ template concept Impl_Mechanisms = Object_Root && (Mechanism_Type && ...) && requires { requires Buffer_List>; requires Dependency_Graph_List>; + requires Mpmc_Triple_Buffer_List>; requires State_List>; }; template @@ -65,6 +71,7 @@ struct Def : Base { using Base_Private = typename Base::Private; using Buffers = detail::Impl_Buffers; using Dependency_Graph_Types = detail::Impl_Dependency_Graph_Types; + using Mpmc_Triple_Buffers = detail::Impl_Mpmc_Triple_Buffers; using States = detail::Impl_States; struct Private : Base_Private { using Tag_Type = Base_Tag; @@ -118,6 +125,7 @@ struct Impl : Obj { using State = typename Obj::State; using Buffers = typename Obj::Buffers; using Dependency_Graph_Types = typename Obj::Dependency_Graph_Types; + using Mpmc_Triple_Buffers = typename Obj::Mpmc_Triple_Buffers; using States = typename Obj::States; /* CRTP 最终 Builder:所有基类流式接口都返回本类型,确保 build() 分派到最派生业务 Builder。 */ struct Builder : Obj::template Builder { @@ -131,6 +139,7 @@ struct Impl : Obj { detail::State_Callback_Storage state_callbacks; detail::Buffer_Storage buffer_storage; detail::Dependency_Graph_Storage dependency_graph_storage; + detail::Mpmc_Triple_Buffer_Storage_Set mpmc_triple_buffer_storage; explicit Private(std::pmr::memory_resource* resource) : Publish_Double_Buffer(std::allocator_arg, Allocator{resource}), state(std::allocator_arg, Allocator{resource}), buffer_storage(std::allocator_arg, Allocator{resource}), @@ -244,6 +253,24 @@ public: this->emit_dependency_source(detail::dependency_id>()); return *data().buffer_storage.template get().pending; } + template Tag, typename Input> + void submit_stream(Input&& input) { + data().mpmc_triple_buffer_storage.template get().submit(std::forward(input)); + } + template Tag> + void exchange_stream() { + data().mpmc_triple_buffer_storage.template get().advance(); + } + template Tag, typename Callback> + decltype(auto) access_rendering_stream(Callback&& callback) { + return data().mpmc_triple_buffer_storage.template get().access_rendering( + std::forward(callback)); + } + template Tag, typename Callback> + decltype(auto) access_query_stream(Callback&& callback) const { + return data().mpmc_triple_buffer_storage.template get().access_query( + std::forward(callback)); + } template Tag> const auto& current_buffer() const { return *data().buffer_storage.template get().current; diff --git a/kernel/src/kernel/render_common.hpp b/kernel/src/kernel/render_common.hpp index e352866..3a5ac94 100644 --- a/kernel/src/kernel/render_common.hpp +++ b/kernel/src/kernel/render_common.hpp @@ -21,6 +21,7 @@ using double_buffer::Prop_Access; using double_buffer::Pmr; using double_buffer::Root; using double_buffer::Tagged_Buffer; +using double_buffer::Mpmc_Triple_Buffer; using double_buffer::Attached; using double_buffer::Dependency_Graph_Type; using double_buffer::Dependency_Graph; diff --git a/kernel/src/kernel/scene.cpp b/kernel/src/kernel/scene.cpp index abfb1fe..a07fa91 100644 --- a/kernel/src/kernel/scene.cpp +++ b/kernel/src/kernel/scene.cpp @@ -7,32 +7,13 @@ Scene::Private::~Private() { } void Scene::Private::push_event(Event_Pointer event) { if (!event) throw std::invalid_argument("scene event ownership must not be empty"); - std::lock_guard lock(runtime->event_mutex); - runtime->events.pending->push_back(std::move(event)); + runtime->submit_event(runtime->object, std::move(event)); } Scene::Event_Batch Scene::Private::take_events(std::uint64_t frame_sequence) { - Event_Batch result{runtime->events.current->get_allocator()}; - { - std::lock_guard lock(runtime->event_mutex); - runtime->events.advance(); - runtime->events.pending->clear(); - result = std::move(*runtime->events.current); - } - for (const auto& event : result) event->mark_dispatch_started(frame_sequence); - if (!result.empty()) { - std::lock_guard lock(runtime->report_mutex); - runtime->reports.pending->insert(runtime->reports.pending->end(), - result.begin(), result.end()); - } - return result; + return runtime->take_events(runtime->object, runtime->resource, frame_sequence); } Scene::Event_Report_Batch Scene::Private::take_event_reports() { - Event_Report_Batch result{runtime->reports.current->get_allocator()}; - std::lock_guard lock(runtime->report_mutex); - runtime->reports.advance(); - runtime->reports.pending->clear(); - result = std::move(*runtime->reports.current); - return result; + return runtime->take_reports(runtime->object, runtime->resource); } void Scene::dispatch_event(Event_Pointer event) { static_cast(*d).push_event(std::move(event)); diff --git a/kernel/src/kernel/scene.hpp b/kernel/src/kernel/scene.hpp index 405b191..52c672d 100644 --- a/kernel/src/kernel/scene.hpp +++ b/kernel/src/kernel/scene.hpp @@ -5,12 +5,15 @@ #include #include namespace aethera { +struct Scene_Event_Stream_Tag {}; /* Scene 状态标签,用于访问和订阅 Scene::State。 */ /* * Scene 只汇总跨渲染后端共有的 Prepare 数据依赖图并构建 Taskflow。 * 用户最终通过 Impl 创建可使用实例;编辑 Dependency_Graph 后调用 advance() 提交结构变化,再调用 process(...) 执行当前场景。 */ -struct Scene : Def> { +struct Scene : Def, + Mpmc_Triple_Buffer>> { using Event_Pointer = std::shared_ptr; using Event_Batch = std::pmr::vector; using Event_Report_Pointer = std::shared_ptr; diff --git a/kernel/src/kernel/scene.ipp b/kernel/src/kernel/scene.ipp index 5a918ca..7a7866d 100644 --- a/kernel/src/kernel/scene.ipp +++ b/kernel/src/kernel/scene.ipp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include #include #include #include @@ -32,18 +32,48 @@ struct Scene::Private : Prev_Private { }; struct Scene::Private::Runtime { std::unique_ptr taskflow; /* 当前已构建的总 Taskflow;为空表示尚未构建。 */ - std::mutex event_mutex; - double_buffer::Double_Buffer events; - std::mutex report_mutex; - double_buffer::Double_Buffer reports; - explicit Runtime(std::pmr::memory_resource* resource) - : events(std::allocator_arg, typename decltype(events)::allocator_type{resource}), - reports(std::allocator_arg, typename decltype(reports)::allocator_type{resource}) {} + using Submit_Event_Call = void (*)(Root*, Event_Pointer); + using Take_Events_Call = Event_Batch (*)(Root*, std::pmr::memory_resource*, std::uint64_t); + using Take_Reports_Call = Event_Report_Batch (*)(const Root*, std::pmr::memory_resource*); + std::pmr::memory_resource* resource; + Root* object{}; + Submit_Event_Call submit_event{}; + Take_Events_Call take_events{}; + Take_Reports_Call take_reports{}; + explicit Runtime(std::pmr::memory_resource* resource) : resource(resource) {} }; template void Scene::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); runtime = std::make_unique(object->memory_resource()); + runtime->object = object; + runtime->submit_event = [](Root* root, Event_Pointer event) { + static_cast(root)->template submit_stream(std::move(event)); + }; + runtime->take_events = [](Root* root, std::pmr::memory_resource* resource, + std::uint64_t frame_sequence) { + auto* scene = static_cast(root); + scene->template exchange_stream(); + return scene->template access_rendering_stream( + [&](std::span events) { + Event_Batch result{resource}; + result.reserve(events.size()); + for (const auto& event : events) { + event->mark_dispatch_started(frame_sequence); + result.push_back(event); + } + return result; + }); + }; + runtime->take_reports = [](const Root* root, std::pmr::memory_resource* resource) { + return static_cast(root)->template access_query_stream( + [&](std::span events) { + Event_Report_Batch result{resource}; + result.reserve(events.size()); + for (const auto& event : events) result.push_back(event); + return result; + }); + }; } template std::shared_ptr Scene::make_event(Arguments&&... arguments) { diff --git a/kernel/src/test/object_test.cpp b/kernel/src/test/object_test.cpp index 0f37b29..8734db5 100644 --- a/kernel/src/test/object_test.cpp +++ b/kernel/src/test/object_test.cpp @@ -1,7 +1,17 @@ #include "double_buffer/model.hpp" #include +#include namespace { struct Object_Buffer_Tag {}; +struct Stream_Buffer_Tag {}; +struct Stream_Object : double_buffer::Def< + Stream_Object, double_buffer::Root, + double_buffer::Mpmc_Triple_Buffer> { + struct Prop : Prev_Prop {}; + struct State : Prev_State { bool operator==(const State&) const = default; }; + struct Private : Prev_Private {}; +}; +using Stream = double_buffer::Impl; struct Test_Object : double_buffer::Def> { struct Prop : Prev_Prop { int first{}; @@ -92,6 +102,43 @@ TEST(object_buffer, prop_publishes_and_keeps_incremental_baseline) { EXPECT_EQ(object->data_for_test().pending->first, 17); EXPECT_EQ(object->data_for_test().pending->second, 19); } +TEST(object_buffer, mpmc_triple_buffer_rotates_three_queue_roles) { + auto object = build_object(); + object->submit_stream(11); + object->submit_stream(13); + object->exchange_stream(); + object->access_rendering_stream([](std::span rendering) { + ASSERT_EQ(rendering.size(), 2); + EXPECT_EQ(rendering[0], 11); + EXPECT_EQ(rendering[1], 13); + }); + EXPECT_EQ(object->access_query_stream( + [](std::span query) { return query.size(); }), 0); + object->exchange_stream(); + object->access_query_stream([](std::span query) { + ASSERT_EQ(query.size(), 2); + EXPECT_EQ(query[0], 11); + EXPECT_EQ(query[1], 13); + }); +} +TEST(object_buffer, mpmc_triple_buffer_accepts_multiple_producers) { + auto object = build_object(); + std::vector producers; + for (int producer = 0; producer < 4; ++producer) + producers.emplace_back([&, producer] { + for (int index = 0; index < 100; ++index) + object->submit_stream(producer * 100 + index); + }); + for (auto& producer : producers) producer.join(); + object->exchange_stream(); + EXPECT_EQ(object->access_rendering_stream( + [](std::span rendering) { return rendering.size(); }), 400); + EXPECT_EQ(object->access_query_stream( + [](std::span query) { return query.size(); }), 0); + object->exchange_stream(); + EXPECT_EQ(object->access_query_stream( + [](std::span query) { return query.size(); }), 400); +} TEST(state_tag, callback_publishes_only_requested_layer) { auto object = build_object(); int calls = 0; diff --git a/render_2D/render_2D/axis/Time_Axis.hpp b/render_2D/render_2D/axis/Time_Axis.hpp index eabbb74..b47f0df 100644 --- a/render_2D/render_2D/axis/Time_Axis.hpp +++ b/render_2D/render_2D/axis/Time_Axis.hpp @@ -5,8 +5,11 @@ #include #include namespace aethera::render_2d { +struct Time_Axis_Stream_Tag {}; /* 将连续样本序号显示为一天内时间文本的坐标轴。 */ -struct Time_Axis : Def { +struct Time_Axis : Def>> { struct Prop : Prev_Prop { Axis_Visible_Count visible_count{100}; /* 当前坐标区间最多覆盖的样本数量;小于 2 时按 2 计算。 */ Axis_Pixel_Length tick_label_spacing_px{8.0}; /* 相邻标签之间预留的像素间距;负值按 0 计算。 */ @@ -18,7 +21,6 @@ struct Time_Axis : Def { /* 时间样本是由 append_time 发布给观察方的运行状态。 */ struct State : Prev_State { Axis_Time_Tick next_tick{}; /* 下一次 append_time 分配的单调样本序号。 */ - std::deque> samples{}; /* tick 到时间的保留窗口;最多保留 max(512, visible_count*4) 项。 */ bool operator==(const State&) const; }; /* 完整声明、时间轴 CRTP 能力及公开薄壳分派见 Time_Axis.ipp。 */ diff --git a/render_2D/render_2D/axis/Time_Axis.ipp b/render_2D/render_2D/axis/Time_Axis.ipp index 946778e..9aeb94e 100644 --- a/render_2D/render_2D/axis/Time_Axis.ipp +++ b/render_2D/render_2D/axis/Time_Axis.ipp @@ -1,5 +1,6 @@ #pragma once #include +#include #include namespace aethera::render_2d { struct Time_Axis::Private : Prev_Private { @@ -18,6 +19,8 @@ struct Time_Axis::Private : Prev_Private { Lookup_Call tick_to_time; /* 查询当前 tick 时间的最终类型分派。 */ }; const Time_Dispatch* time_dispatch{}; /* Builder 绑定最终时间轴类型后指向静态分派表。 */ + std::atomic next_tick{}; + std::span> active_samples{}; /* 多生产者分配 tick 的唯一权威来源。 */ [[nodiscard]] std::size_t time_point_count(const Root* object) const; int append_time(Root* object, Time_Of_Day time); [[nodiscard]] Time_Of_Day tick_to_time(const Root* object, int tick) const; @@ -31,14 +34,15 @@ struct Time_Axis::Private : Prev_Private { /* CRTP 覆盖:绑定通用轴机制和最终时间轴公开薄壳分派;派生 Private 必须先调用此实现。 */ template void bind_private_crtp(Object* object); + void prepare_data(Attached auto* object); + void paint(Attached auto* object); }; inline Axis_Range Time_Axis::Private::coordinate_range(const Attached auto* object) const { using Object = std::remove_cv_t>; const auto& private_data = static_cast(*this); - const auto& state = static_cast(*private_data.state.current); const auto& prop = static_cast(*private_data.current); const int visible_count = std::max(2, prop.visible_count); - const int latest = std::max(0, state.next_tick - 1); + const int latest = std::max(0, next_tick.load(std::memory_order_relaxed) - 1); if (prop.newest_at_start) return {static_cast(latest) + 0.5, static_cast(latest - visible_count) + 0.5}; return {static_cast(latest - visible_count) + 0.5, static_cast(latest) + 0.5}; } @@ -54,44 +58,39 @@ inline double Time_Axis::Private::tick_step(const Attached auto* object, Axis_Ra inline std::string Time_Axis::Private::tick_label(const Attached auto* object, double tick) const { using Object = std::remove_cv_t>; const auto& private_data = static_cast(*this); - const auto& state = static_cast(*private_data.state.current); const auto& prop = static_cast(*private_data.current); + const auto samples = active_samples; const int target = static_cast(std::llround(tick)); - const auto current = std::find_if(state.samples.begin(), state.samples.end(), [target](const auto& sample) { + const auto current = std::find_if(samples.begin(), samples.end(), [target](const auto& sample) { return sample.first == target; }); - return current == state.samples.end() ? std::string{} : formatted_time(current->second, prop.format); + return current == samples.end() ? std::string{} : formatted_time(current->second, prop.format); } template const Time_Axis::Private::Time_Dispatch& Time_Axis::Private::time_dispatch_for() { static const Time_Dispatch result{ [](const Root* root) { auto* object = static_cast(root); - const auto& private_data = static_cast(*object->d); - const auto& state = static_cast(*private_data.state.current); - return state.samples.size(); + return object->template access_query_stream( + [](std::span> samples) { return samples.size(); }); }, [](Root* root, Time_Of_Day time) { auto* object = static_cast(root); - const auto visible_count = object->template read_prop().visible_count; - int tick{}; - object->template update_state<&State::next_tick, &State::samples>([&](State_Access states) { - auto& state = states.template get(); - tick = state.next_tick++; - state.samples.emplace_back(tick, time); - const auto limit = static_cast(std::max(512, std::max(2, visible_count) * 4)); - while (state.samples.size() > limit) state.samples.pop_front(); - }); + auto& data = static_cast(*object->d); + const int tick = data.next_tick.fetch_add(1, std::memory_order_relaxed); + object->template submit_stream(std::pair{tick, time}); + object->template mark_dirty(); return tick; }, [](const Root* root, int tick) { auto* object = static_cast(root); - const auto& private_data = static_cast(*object->d); - const auto& state = static_cast(*private_data.state.current); - const auto current = std::find_if(state.samples.begin(), state.samples.end(), [tick](const auto& sample) { - return sample.first == tick; - }); - return current == state.samples.end() ? Time_Of_Day{} : current->second; + return object->template access_query_stream( + [tick](std::span> samples) { + const auto current = std::find_if(samples.begin(), samples.end(), [tick](const auto& sample) { + return sample.first == tick; + }); + return current == samples.end() ? Time_Of_Day{} : current->second; + }); } }; return result; @@ -101,6 +100,22 @@ void Time_Axis::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); time_dispatch = &Private::time_dispatch_for(); } +inline void Time_Axis::Private::prepare_data(Attached auto* object) { + using Object = std::remove_pointer_t; + object->template exchange_stream(); + auto& private_data = static_cast(*this); + auto& state = static_cast(*private_data.state.current); + state.next_tick = next_tick.load(std::memory_order_relaxed); + object->template access_rendering_stream( + [&](std::span> samples) { + active_samples = samples; + Prev_Private::prepare_data(object); + active_samples = {}; + }); +} +inline void Time_Axis::Private::paint(Attached auto* object) { + Prev_Private::paint(object); +} template void Time_Axis::Private::after_state_set(Object* object, Member Owner::*, State_Access) { if constexpr (std::same_as) object->template mark_dirty(); diff --git a/render_2D/render_2D/plottable/Afterglow.cpp b/render_2D/render_2D/plottable/Afterglow.cpp index e4fc3b0..0b54033 100644 --- a/render_2D/render_2D/plottable/Afterglow.cpp +++ b/render_2D/render_2D/plottable/Afterglow.cpp @@ -1,4 +1,21 @@ #include "Afterglow.hpp" /* Afterglow 最终实例及三类依赖实现。 */ namespace aethera::render_2d { bool Afterglow::Prop::operator==(const Prop&) const = default; -bool Afterglow::State::operator==(const State&) const = default; void Afterglow::append_spectrum(std::span values) { static_cast(*d).dispatch->append(this, values); } void Afterglow::append_spectrum(std::pmr::vector&& values) { append_spectrum(std::span(values.data(), values.size())); } std::size_t Afterglow::history_count() const { return static_cast(*d).dispatch->history_count(this); } std::size_t Afterglow::latest_spectrum_point_count() const { return static_cast(*d).dispatch->latest_count(this); } std::size_t Afterglow::rendered_cell_count() const { return static_cast(*d).dispatch->rendered_count(this); } } +bool Afterglow::State::operator==(const State&) const = default; +void Afterglow::append_spectrum(std::span values) { + static_cast(*d).dispatch->append(this, {values.begin(), values.end()}); +} +void Afterglow::append_spectrum(std::pmr::vector&& values) { + static_cast(*d).dispatch->append(this, + {std::make_move_iterator(values.begin()), std::make_move_iterator(values.end())}); +} +std::size_t Afterglow::history_count() const { + return static_cast(*d).dispatch->history_count(this); +} +std::size_t Afterglow::latest_spectrum_point_count() const { + return static_cast(*d).dispatch->latest_count(this); +} +std::size_t Afterglow::rendered_cell_count() const { + return static_cast(*d).dispatch->rendered_count(this); +} +} diff --git a/render_2D/render_2D/plottable/Afterglow.hpp b/render_2D/render_2D/plottable/Afterglow.hpp index 013af81..f96f666 100644 --- a/render_2D/render_2D/plottable/Afterglow.hpp +++ b/render_2D/render_2D/plottable/Afterglow.hpp @@ -9,7 +9,10 @@ #include #include namespace aethera::render_2d { -struct Afterglow : Def> { +struct Afterglow_Stream_Tag {}; +struct Afterglow : Def, + Mpmc_Triple_Buffer>>> { using Scene_Object = Impl; using Frequency_Object = Impl; using Power_Object = Impl; struct Prop : Prev_Prop { std::size_t frequency_point_size{}; /* 栅格频率列数;零值使用最新频谱尺寸。 */ @@ -21,7 +24,6 @@ struct Afterglow : Def> { Axis_Range power_range{0.0, 10.0}; /* 色块纵向覆盖的功率范围。 */ Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ Color_Map color_map{}; /* 强度到颜色的映射。 */ - std::vector> spectra{}; /* 从旧到新的历史频谱唯一权威集合。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Afterglow.ipp b/render_2D/render_2D/plottable/Afterglow.ipp index 6b62cc7..51a586c 100644 --- a/render_2D/render_2D/plottable/Afterglow.ipp +++ b/render_2D/render_2D/plottable/Afterglow.ipp @@ -5,68 +5,258 @@ namespace aethera::render_2d { struct Afterglow::Private : Prev_Private { struct Prepared { - detail::Raster_Layout layout{}; /* 两根轴决定的色块矩阵布局。 */ - std::vector intensity{}; /* 历史频谱衰减累加后的未归一化强度。 */ - std::vector pixels{}; /* 归一化强度经色图转换后的像素矩阵。 */ - Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */ - Plot_Ratio maximum{1.0}; /* 本帧强度归一化分母,最小为 1。 */ - bool valid{}; /* 轴布局和输入数据是否足以生成色块。 */ + std::vector>> source_spectra{}; /* Rendering-role ownership retained across Prepare tasks. */ + detail::Raster_Layout layout{}; /* 两根轴决定的色块矩阵布局。 */ + std::vector intensity{}; /* 历史频谱衰减累加后的未归一化强度。 */ + std::vector pixels{}; /* 归一化强度经色图转换后的像素矩阵。 */ + Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */ + Plot_Ratio maximum{1.0}; /* 本帧强度归一化分母,最小为 1。 */ + bool valid{}; /* 轴布局和输入数据是否足以生成色块。 */ }; - using Append_Run = void (*)(Root*, std::span); using Count_Run = std::size_t (*)(const Root*); + using Append_Run = void (*)(Root*, std::vector); + using Count_Run = std::size_t (*)(const Root*); struct Dispatch { - Append_Run append; /* 向最终对象提交一帧频谱。 */ - Count_Run history_count; /* 查询权威历史帧数。 */ - Count_Run latest_count; /* 查询最新一帧的点数。 */ - Count_Run rendered_count; /* 查询已准备的色块数。 */ + Append_Run append; /* 向最终对象提交一帧频谱。 */ + Count_Run history_count; /* 查询权威历史帧数。 */ + Count_Run latest_count; /* 查询最新一帧的点数。 */ + Count_Run rendered_count; /* 查询已准备的色块数。 */ }; - Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ - Frequency_Object* frequency_axis{}; /* 不拥有的频率轴。 */ - Power_Object* power_axis{}; /* 不拥有的功率轴。 */ - Prepared prepared{}; /* 当前权威状态推导出的 Paint 输入。 */ - Plot_Partition_Count graph_partition_count{}; /* Prepare 子图当前固化的分块数。 */ - const Dispatch* dispatch{}; /* 最终类型的公开薄壳分派表。 */ + Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ + Frequency_Object* frequency_axis{}; /* 不拥有的频率轴。 */ + Power_Object* power_axis{}; /* 不拥有的功率轴。 */ + Prepared prepared{}; /* 当前权威状态推导出的 Paint 输入。 */ + Plot_Partition_Count graph_partition_count{}; /* Prepare 子图当前固化的分块数。 */ + const Dispatch* dispatch{}; /* 最终类型的公开薄壳分派表。 */ /* CRTP 覆盖:绑定 Renderable 能力和最终 Afterglow 分派表。 */ - template void bind_private_crtp(Object* object); + template + void bind_private_crtp(Object* object); void bind_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value); - template [[nodiscard]] static const Dispatch& dispatch_for(); + template + [[nodiscard]] static const Dispatch& dispatch_for(); /* CRTP 覆盖:构建累加、归一化和着色三阶段 Prepare 子图。 */ - template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state); + template + [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state); /* CRTP 覆盖:构建消费色块矩阵的 Paint 子图。 */ - template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state); + template + [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state); /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ - template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); - template void prepare_frame(Object* object); template void accumulate_partition(Object* object, Plot_Partition_Count index); void normalize_frame(); template void color_partition(Object* object, Plot_Partition_Count index); template void paint_frame(Object* object); + template + [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); + template + void prepare_frame(Object* object); + template + void accumulate_partition(Object* object, Plot_Partition_Count index); + void normalize_frame(); + template + void color_partition(Object* object, Plot_Partition_Count index); + template + void paint_frame(Object* object); /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ - template void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); - template void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); + template + void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); + template + void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); }; -template Afterglow::Builder::Builder(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {} +template +Afterglow::Builder::Builder(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {} template std::expected, Dependency_Graph_Error> Afterglow::Builder::build() { - auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); - auto& private_data = static_cast(*plot->d); private_data.bind_sources(frequency_axis, power_axis); + auto result = Base::build(); + if (!result) return std::unexpected(result.error()); + auto plot = std::move(result).value(); + auto& private_data = static_cast(*plot->d); + private_data.bind_sources(frequency_axis, power_axis); private_data.scene_attach = [object = plot.get()](Root* root) -> std::expected { - auto* scene = static_cast(root); auto& data = static_cast(*object->d); data.scene = scene; auto* frequency_axis = data.frequency_axis; auto* power_axis = data.power_axis; - return scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { prepare.add_dependency(object, scene); prepare.add_dependency(object, frequency_axis); prepare.add_dependency(object, power_axis); paint.add_dependency(frequency_axis, object); paint.add_dependency(power_axis, object); cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(object, scene); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, frequency_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, power_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, power_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, power_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, power_axis); }); - }; return plot; + auto* scene = static_cast(root); + auto& data = static_cast(*object->d); + data.scene = scene; + auto* frequency_axis = data.frequency_axis; + auto* power_axis = data.power_axis; + return scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { + prepare.add_dependency(object, scene); + prepare.add_dependency(object, frequency_axis); + prepare.add_dependency(object, power_axis); + paint.add_dependency(frequency_axis, object); + paint.add_dependency(power_axis, object); + cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(object, scene); + cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, frequency_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, frequency_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, frequency_axis); + cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, frequency_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, power_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, power_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, power_axis); + cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, power_axis); + }); + }; + return plot; +} +template +void Afterglow::append_spectrum(const Values& values) { + append_spectrum(std::span(std::data(values), std::size(values))); +} +template +bool Afterglow::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { + const std::size_t available = object->template access_query_stream( + [](std::span>> spectra) { + return spectra.empty() ? 0 : spectra.back()->size(); + }); + const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; + return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, std::max(1, columns)); +} +template +tf::Taskflow Afterglow::Private::build_prepare_graph(Object* object, const Prop& state) { + const std::size_t available = object->template access_query_stream( + [](std::span>> spectra) { + return spectra.empty() ? 0 : spectra.back()->size(); + }); + const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; + graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, std::max(1, columns)); + tf::Taskflow graph; + auto begin = graph.emplace([this, object] { + prepare_frame(object); + }).name("afterglow.prepare.frame"); + auto normalize = graph.emplace([this] { + normalize_frame(); + }).name("afterglow.prepare.normalize"); + for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { + auto accumulate = graph.emplace([this, object, index] { + accumulate_partition(object, index); + }).name("afterglow.prepare.accumulate"); + auto color = graph.emplace([this, object, index] { + color_partition(object, index); + }).name("afterglow.prepare.color"); + begin.precede(accumulate); + accumulate.precede(normalize); + normalize.precede(color); + } + return graph; +} +template +tf::Taskflow Afterglow::Private::build_paint_graph(Object* object, const Prop&) { + tf::Taskflow graph; + graph.emplace([this, object] { + paint_frame(object); + }).name("afterglow.paint.frame"); + return graph; +} +template +void Afterglow::Private::prepare_frame(Object* object) { + const auto& state = object->template read_prop(); + object->template exchange_stream(); + prepared = {}; + object->template access_rendering_stream( + [&](std::span>> spectra) { + prepared.source_spectra.assign(spectra.begin(), spectra.end()); + }); + const auto& frequency_layout = frequency_axis->template read_prop(); + const auto& power_layout = power_axis->template read_prop(); + const std::size_t available = prepared.source_spectra.empty() ? 0 : prepared.source_spectra.back()->size(); + const int columns = static_cast(state.frequency_point_size ? std::min(state.frequency_point_size, available) : available); + const int rows = static_cast(state.power_point_size ? state.power_point_size : std::max(1.0, std::abs(power_layout.pixel_length))); + prepared = {}; + prepared.canvas = scene->template read_prop().viewport; + prepared.layout = detail::raster_layout(frequency_axis, state.frequency_range, columns, power_axis, state.power_range, rows, frequency_layout.orientation, power_layout.orientation); + if (prepared.canvas.empty() || !prepared.layout.valid()) { + return; + } + const std::size_t cells = static_cast(columns) * rows; + prepared.intensity.assign(cells, 0.0); + prepared.pixels.assign(cells, 0); + prepared.valid = true; +} +template +void Afterglow::Private::accumulate_partition(Object* object, Plot_Partition_Count index) { + if (!prepared.valid) return; + const auto& state = object->template read_prop(); + const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; + const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; + const auto [first, last] = detail::raster_partition_range(static_cast(columns), index, graph_partition_count); + Plot_Ratio attenuation{1.0}; + const Plot_Ratio decay = 1.0 - std::clamp(state.attenuation_rate, 0.0, 1.0); + const auto& spectra = prepared.source_spectra; + for (auto spectrum = spectra.rbegin(); spectrum != spectra.rend() && attenuation >= 0.01; ++spectrum, attenuation *= decay) { + const std::size_t count = std::min(columns, (*spectrum)->size()); + for (std::size_t column = first; column < std::min(last, count); ++column) { + const int row = std::clamp(static_cast(detail::normalized_plot_value((**spectrum)[column], state.power_range) * (rows - 1)), 0, rows - 1); + prepared.intensity[static_cast(row) * columns + column] += attenuation; + if (state.interpolate && row + 1 < rows) prepared.intensity[static_cast(row + 1) * columns + column] += attenuation * 0.35; + } + } +} +inline void Afterglow::Private::normalize_frame() { + if (prepared.valid && !prepared.intensity.empty()) prepared.maximum = std::max(1.0, *std::max_element(prepared.intensity.begin(), prepared.intensity.end())); +} +template +void Afterglow::Private::color_partition(Object* object, Plot_Partition_Count index) { + if (!prepared.valid) return; + const auto& state = object->template read_prop(); + const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; + const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; + const auto [first, last] = detail::raster_partition_range(static_cast(columns), index, graph_partition_count); + for (std::size_t column = first; column < last; ++column) + for (int row = 0; row < rows; ++row) { + const std::size_t cell = static_cast(row) * columns + column; + prepared.pixels[prepared.layout.index(static_cast(column), row)] = premultiply(state.color_map.sample(prepared.intensity[cell] / prepared.maximum)); + } +} +template +void Afterglow::Private::paint_frame(Object* object) { + auto& cache = this->paint_surface(); + if (!prepared.valid) { + return; + } + detail::Painter painter(cache, prepared.canvas); + detail::paint_raster(painter, prepared.layout, prepared.pixels, Image_Interpolation_Mode::bilinear); +} +template +void Afterglow::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { + if constexpr (std::same_as) object->template mark_dirty(); +} +template +void Afterglow::Private::before_advance(Object* object, Prop_Type*, State_Access pending_states, const Prop_Type*, State_Access) { + auto& state = pending_states.template get(); + object->template access_query_stream([&](std::span>> spectra) { + state.history_count = spectra.size(); + state.latest_spectrum_point_count = spectra.empty() ? 0 : spectra.back()->size(); + state.rendered_cell_count = state.latest_spectrum_point_count; + }); +} +template +const Afterglow::Private::Dispatch& Afterglow::Private::dispatch_for() { + static const Dispatch value{ + [](Root* root, std::vector values) { + auto* object = static_cast(root); + object->template submit_stream( + std::make_shared>(std::move(values))); + object->template mark_dirty(); + }, + [](const Root* root) { + return static_cast(root)->template access_query_stream( + [](std::span>> spectra) { return spectra.size(); }); + }, + [](const Root* root) { + return static_cast(root)->template access_query_stream( + [](std::span>> spectra) { + return spectra.empty() ? 0 : spectra.back()->size(); + }); + }, + [](const Root* root) { + return static_cast(root)->template access_query_stream( + [](std::span>> spectra) { + return spectra.empty() ? 0 : spectra.back()->size(); + }); + } + }; + return value; +} +template +void Afterglow::Private::bind_private_crtp(Object* object) { + Prev_Private::bind_private_crtp(object); + dispatch = &dispatch_for(); +} +inline void Afterglow::Private::bind_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) { + frequency_axis = frequency_axis_value; + power_axis = power_axis_value; } -template void Afterglow::append_spectrum(const Values& values) { append_spectrum(std::span(std::data(values), std::size(values))); } -template bool Afterglow::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, std::max(1, columns)); } -template -tf::Taskflow Afterglow::Private::build_prepare_graph(Object* object, const Prop& state) { const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, std::max(1, columns)); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("afterglow.prepare.frame"); auto normalize = graph.emplace([this] { normalize_frame(); }).name("afterglow.prepare.normalize"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto accumulate = graph.emplace([this, object, index] { accumulate_partition(object, index); }).name("afterglow.prepare.accumulate"); auto color = graph.emplace([this, object, index] { color_partition(object, index); }).name("afterglow.prepare.color"); begin.precede(accumulate); accumulate.precede(normalize); normalize.precede(color); } return graph; } -template tf::Taskflow Afterglow::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("afterglow.paint.frame"); return graph; } -template -void Afterglow::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop(); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& power_layout = power_axis->template read_prop(); const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const int columns = static_cast(state.frequency_point_size ? std::min(state.frequency_point_size, available) : available); const int rows = static_cast(state.power_point_size ? state.power_point_size : std::max(1.0, std::abs(power_layout.pixel_length))); prepared = {}; prepared.canvas = scene->template read_prop().viewport; prepared.layout = detail::raster_layout(frequency_axis, state.frequency_range, columns, power_axis, state.power_range, rows, frequency_layout.orientation, power_layout.orientation); if (prepared.canvas.empty() || !prepared.layout.valid()) return; const std::size_t cells = static_cast(columns) * rows; prepared.intensity.assign(cells, 0.0); prepared.pixels.assign(cells, 0); prepared.valid = true; } -template -void Afterglow::Private::accumulate_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; const auto [first, last] = detail::raster_partition_range(static_cast(columns), index, graph_partition_count); Plot_Ratio attenuation{1.0}; const Plot_Ratio decay = 1.0 - std::clamp(state.attenuation_rate, 0.0, 1.0); for (auto spectrum = state.spectra.rbegin(); spectrum != state.spectra.rend() && attenuation >= 0.01; ++spectrum, attenuation *= decay) { const std::size_t count = std::min(columns, spectrum->size()); for (std::size_t column = first; column < std::min(last, count); ++column) { const int row = std::clamp(static_cast(detail::normalized_plot_value((*spectrum)[column], state.power_range) * (rows - 1)), 0, rows - 1); prepared.intensity[static_cast(row) * columns + column] += attenuation; if (state.interpolate && row + 1 < rows) prepared.intensity[static_cast(row + 1) * columns + column] += attenuation * 0.35; } } } -inline void Afterglow::Private::normalize_frame() { if (prepared.valid && !prepared.intensity.empty()) prepared.maximum = std::max(1.0, *std::max_element(prepared.intensity.begin(), prepared.intensity.end())); } -template -void Afterglow::Private::color_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; const auto [first, last] = detail::raster_partition_range(static_cast(columns), index, graph_partition_count); for (std::size_t column = first; column < last; ++column) for (int row = 0; row < rows; ++row) { const std::size_t cell = static_cast(row) * columns + column; prepared.pixels[prepared.layout.index(static_cast(column), row)] = premultiply(state.color_map.sample(prepared.intensity[cell] / prepared.maximum)); } } -template void Afterglow::Private::paint_frame(Object*) { auto& cache = this->paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); detail::paint_raster(painter, prepared.layout, prepared.pixels, Image_Interpolation_Mode::bilinear); } -template void Afterglow::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } -template void Afterglow::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { auto& state = pending_states.template get(); const auto& prop = static_cast(*current_prop); state.history_count = prop.spectra.size(); state.latest_spectrum_point_count = prop.spectra.empty() ? 0 : prop.spectra.back().size(); state.rendered_cell_count = prepared.valid ? prepared.pixels.size() : 0; } -template -const Afterglow::Private::Dispatch& Afterglow::Private::dispatch_for() { static const Dispatch value{[](Root* root, std::span values) { auto* object = static_cast(root); object->template update_prop<&Prop::spectra>([values](Prop_Access props) { auto& state = props.template get(); state.spectra.emplace_back(values.begin(), values.end()); constexpr std::size_t history_limit = 64; while (state.spectra.size() > history_limit) state.spectra.erase(state.spectra.begin()); }); }, [](const Root* root) { return static_cast(root)->template read_prop().spectra.size(); }, [](const Root* root) { const auto& spectra = static_cast(root)->template read_prop().spectra; return spectra.empty() ? 0 : spectra.back().size(); }, [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; } -template void Afterglow::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } -inline void Afterglow::Private::bind_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) { frequency_axis = frequency_axis_value; power_axis = power_axis_value; } } diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.cpp b/render_2D/render_2D/plottable/Constellation_Diagram.cpp index de27bca..77cafc1 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.cpp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.cpp @@ -1,4 +1,15 @@ #include "Constellation_Diagram.hpp" /* Constellation 最终实例及三类依赖实现。 */ namespace aethera::render_2d { -bool Constellation_Point::operator==(const Constellation_Point&) const = default; bool Constellation_Diagram::Prop::operator==(const Prop&) const = default; -bool Constellation_Diagram::State::operator==(const State&) const = default; void Constellation_Diagram::append_point(Point_F point) { static_cast(*d).dispatch->append(this, point); } std::size_t Constellation_Diagram::point_count() const { return static_cast(*d).dispatch->count(this); } void Constellation_Diagram::fit_square_to_axes() { static_cast(*d).dispatch->fit(this); } } +bool Constellation_Point::operator==(const Constellation_Point&) const = default; +bool Constellation_Diagram::Prop::operator==(const Prop&) const = default; +bool Constellation_Diagram::State::operator==(const State&) const = default; +void Constellation_Diagram::append_point(Point_F point) { + static_cast(*d).dispatch->append(this, {point, monotonic_milliseconds()}); +} +std::size_t Constellation_Diagram::point_count() const { + return static_cast(*d).dispatch->count(this); +} +void Constellation_Diagram::fit_square_to_axes() { + static_cast(*d).dispatch->fit(this); +} +} diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.hpp b/render_2D/render_2D/plottable/Constellation_Diagram.hpp index f88b5b7..c01b8e0 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.hpp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.hpp @@ -8,34 +8,44 @@ #include namespace aethera::render_2d { enum class Constellation_Diagram_Type : std::uint8_t { psk4 = 4, psk8 = 8, psk16 = 16 }; -struct Constellation_Point { Point_F point{}; Plot_Duration_Milliseconds submitted_at_ms{}; bool operator==(const Constellation_Point&) const; }; -struct Constellation_Diagram : Def> { - using Scene_Object = Impl; using Axis_Object = Impl; +struct Constellation_Point { + Point_F point{}; + Plot_Duration_Milliseconds submitted_at_ms{}; + bool operator==(const Constellation_Point&) const; +}; +struct Constellation_Stream_Tag {}; +struct Constellation_Diagram : Def, + Mpmc_Triple_Buffer> { + using Scene_Object = Impl; + using Axis_Object = Impl; struct Prop : Prev_Prop { - Plot_Duration_Milliseconds point_lifetime_ms{1000}; /* 接收点保留时间,单位为毫秒。 */ + Plot_Duration_Milliseconds point_lifetime_ms{1000}; /* 接收点保留时间,单位为毫秒。 */ Constellation_Diagram_Type type{Constellation_Diagram_Type::psk8}; /* 理想 PSK 锚点数量。 */ - Plot_Ratio phase_offset_radians{}; /* 理想锚点相位偏移,单位为弧度。 */ - Axis_Range i_range{0.0, 100.0}; /* 同相分量显示范围。 */ - Axis_Range q_range{0.0, 100.0}; /* 正交分量显示范围。 */ - Color point_color{Color::red_color()}; /* 接收点颜色。 */ - Color anchor_color{Color::yellow()}; /* 理想星座锚点颜色。 */ - std::vector points{}; /* 已提交且尚未过期的点。 */ + Plot_Ratio phase_offset_radians{}; /* 理想锚点相位偏移,单位为弧度。 */ + Axis_Range i_range{0.0, 100.0}; /* 同相分量显示范围。 */ + Axis_Range q_range{0.0, 100.0}; /* 正交分量显示范围。 */ + Color point_color{Color::red_color()}; /* 接收点颜色。 */ + Color anchor_color{Color::yellow()}; /* 理想星座锚点颜色。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { - std::size_t point_count{}; /* 当前发布且尚未过期的点数。 */ + std::size_t point_count{}; /* 当前发布且尚未过期的点数。 */ bool operator==(const State&) const; }; struct Private; - template struct Builder : Prev_Builder { + template + struct Builder : Prev_Builder { using Base = Prev_Builder; Builder(Axis_Object* i_axis, Axis_Object* q_axis); [[nodiscard]] std::expected, Dependency_Graph_Error> build(); private: - Axis_Object* i_axis{}; /* 不拥有的同相分量轴;生命周期必须覆盖星座图。 */ - Axis_Object* q_axis{}; /* 不拥有的正交分量轴;生命周期必须覆盖星座图。 */ + Axis_Object* i_axis{}; /* 不拥有的同相分量轴;生命周期必须覆盖星座图。 */ + Axis_Object* q_axis{}; /* 不拥有的正交分量轴;生命周期必须覆盖星座图。 */ }; - void append_point(Point_F point); [[nodiscard]] std::size_t point_count() const; void fit_square_to_axes(); + void append_point(Point_F point); + [[nodiscard]] std::size_t point_count() const; + void fit_square_to_axes(); }; } #include "Constellation_Diagram.ipp" diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.ipp b/render_2D/render_2D/plottable/Constellation_Diagram.ipp index 29ecf7c..1ce4f41 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.ipp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.ipp @@ -5,44 +5,159 @@ namespace aethera::render_2d { struct Constellation_Diagram::Private : Prev_Private { struct Prepared { - std::vector points{}; /* 尚未过期的接收点画布坐标。 */ - std::vector anchors{}; /* 当前调制类型的理想锚点画布坐标。 */ - Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */ - bool valid{}; /* 两根轴是否正交且画布有效。 */ + std::vector points{}; /* 尚未过期的接收点画布坐标。 */ + std::vector anchors{}; /* 当前调制类型的理想锚点画布坐标。 */ + Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */ + bool valid{}; /* 两根轴是否正交且画布有效。 */ }; - using Point_Run = void (*)(Root*, Point_F); using Count_Run = std::size_t (*)(const Root*); using Void_Run = void (*)(Root*); + using Point_Run = void (*)(Root*, Constellation_Point); + using Count_Run = std::size_t (*)(const Root*); + using Void_Run = void (*)(Root*); struct Dispatch { - Point_Run append; /* 向最终对象提交接收点。 */ - Count_Run count; /* 查询权威接收点数。 */ - Void_Run fit; /* 将两根轴调整为等跨度。 */ + Point_Run append; /* 向最终对象提交接收点。 */ + Count_Run count; /* 查询权威接收点数。 */ + Void_Run fit; /* 将两根轴调整为等跨度。 */ }; - Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ - Axis_Object* i_axis{}; /* 不拥有的同相分量轴。 */ - Axis_Object* q_axis{}; /* 不拥有的正交分量轴。 */ - Prepared prepared{}; /* 当前权威状态推导出的 Paint 输入。 */ - const Dispatch* dispatch{}; /* 最终类型的公开薄壳分派表。 */ + Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ + Axis_Object* i_axis{}; /* 不拥有的同相分量轴。 */ + Axis_Object* q_axis{}; /* 不拥有的正交分量轴。 */ + Prepared prepared{}; /* 当前权威状态推导出的 Paint 输入。 */ + const Dispatch* dispatch{}; /* 最终类型的公开薄壳分派表。 */ /* CRTP 覆盖:绑定 Renderable 能力和最终 Constellation 分派表。 */ - template void bind_private_crtp(Object* object); + template + void bind_private_crtp(Object* object); void bind_sources(Axis_Object* i_axis_value, Axis_Object* q_axis_value); - template [[nodiscard]] static const Dispatch& dispatch_for(); + template + [[nodiscard]] static const Dispatch& dispatch_for(); /* CRTP 覆盖:从当前点集和轴状态准备画布坐标。 */ - template void prepare_data(Object* object); + template + void prepare_data(Object* object); /* CRTP 覆盖:直接绘制已准备的星座点与锚点。 */ - template void paint(Object* object); + template + void paint(Object* object); /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ - template void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); - template void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); + template + void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); + template + void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); }; -template Constellation_Diagram::Builder::Builder(Axis_Object* i_axis_value, Axis_Object* q_axis_value) : Base(), i_axis(i_axis_value), q_axis(q_axis_value) {} template -std::expected, Dependency_Graph_Error> Constellation_Diagram::Builder::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); auto& private_data = static_cast(*plot->d); private_data.bind_sources(i_axis, q_axis); private_data.scene_attach = [object = plot.get()](Root* root) -> std::expected { auto* scene = static_cast(root); auto& data = static_cast(*object->d); data.scene = scene; auto* i_axis = data.i_axis; auto* q_axis = data.q_axis; return scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { prepare.add_dependency(object, scene); prepare.add_dependency(object, i_axis); prepare.add_dependency(object, q_axis); paint.add_dependency(i_axis, object); paint.add_dependency(q_axis, object); cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(object, scene); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, i_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, i_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, i_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, i_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, q_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, q_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, q_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, q_axis); }); }; return plot; } -template -void Constellation_Diagram::Private::prepare_data(Object* object) { const auto& state = object->template read_prop(); const auto& i_layout = i_axis->template read_prop(); const auto& q_layout = q_axis->template read_prop(); prepared = {}; prepared.canvas = scene->template read_prop().viewport; if (prepared.canvas.empty() || i_layout.orientation == q_layout.orientation) return; const auto current = monotonic_milliseconds(); for (const auto& value : state.points) if (current - value.submitted_at_ms <= state.point_lifetime_ms) prepared.points.push_back(detail::map_plot_point(i_axis, value.point.x, q_axis, value.point.y, i_layout.orientation)); const int count = static_cast(state.type); const Plot_Coordinate center_i = state.i_range.center(); const Plot_Coordinate center_q = state.q_range.center(); const Plot_Coordinate radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; for (int index = 0; index < count; ++index) { const Plot_Ratio angle = state.phase_offset_radians + 2.0 * std::numbers::pi * index / count; prepared.anchors.push_back(detail::map_plot_point(i_axis, center_i + std::cos(angle) * radius, q_axis, center_q + std::sin(angle) * radius, i_layout.orientation)); } prepared.valid = true; object->template mark_dirty(); } -template void Constellation_Diagram::Private::paint(Object* object) { const auto& state = object->template read_prop(); auto& cache = this->paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& anchor : prepared.anchors) painter.circle(anchor, 4.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::solid}); for (const auto& point : prepared.points) painter.circle(point, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::solid}); } -template void Constellation_Diagram::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } -template void Constellation_Diagram::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { pending_states.template get().point_count = static_cast(*current_prop).points.size(); } -template -const Constellation_Diagram::Private::Dispatch& Constellation_Diagram::Private::dispatch_for() { static const Dispatch value{[](Root* root, Point_F point) { auto* object = static_cast(root); const auto submitted = monotonic_milliseconds(); object->template update_prop<&Prop::points>([=](Prop_Access props) { auto& state = props.template get(); state.points.erase(std::remove_if(state.points.begin(), state.points.end(), [=](const auto& value) { return submitted - value.submitted_at_ms > state.point_lifetime_ms; }), state.points.end()); state.points.push_back({point, submitted}); }); }, [](const Root* root) { return static_cast(root)->template read_prop().points.size(); }, [](Root* root) { auto* object = static_cast(root); auto& data = static_cast(*object->d); const auto& state = object->template read_prop(); const Plot_Coordinate size = std::max(state.i_range.size(), state.q_range.size()); const Plot_Coordinate i_center = state.i_range.center(); const Plot_Coordinate q_center = state.q_range.center(); data.i_axis->template set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{i_center - size * 0.5, i_center + size * 0.5}); data.q_axis->template set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{q_center - size * 0.5, q_center + size * 0.5}); }}; return value; } -template void Constellation_Diagram::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } -inline void Constellation_Diagram::Private::bind_sources(Axis_Object* i_axis_value, Axis_Object* q_axis_value) { i_axis = i_axis_value; q_axis = q_axis_value; } +Constellation_Diagram::Builder::Builder(Axis_Object* i_axis_value, Axis_Object* q_axis_value) : Base(), i_axis(i_axis_value), q_axis(q_axis_value) {} +template +std::expected, Dependency_Graph_Error> Constellation_Diagram::Builder::build() { + auto result = Base::build(); + if (!result) return std::unexpected(result.error()); + auto plot = std::move(result).value(); + auto& private_data = static_cast(*plot->d); + private_data.bind_sources(i_axis, q_axis); + private_data.scene_attach = [object = plot.get()](Root* root) -> std::expected { + auto* scene = static_cast(root); + auto& data = static_cast(*object->d); + data.scene = scene; + auto* i_axis = data.i_axis; + auto* q_axis = data.q_axis; + return scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { + prepare.add_dependency(object, scene); + prepare.add_dependency(object, i_axis); + prepare.add_dependency(object, q_axis); + paint.add_dependency(i_axis, object); + paint.add_dependency(q_axis, object); + cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(object, scene); + cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, i_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, i_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, i_axis); + cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, i_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, q_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, q_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, q_axis); + cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, q_axis); + }); + }; + return plot; +} +template +void Constellation_Diagram::Private::prepare_data(Object* object) { + const auto& state = object->template read_prop(); + object->template exchange_stream(); + const auto& i_layout = i_axis->template read_prop(); + const auto& q_layout = q_axis->template read_prop(); + prepared = {}; + prepared.canvas = scene->template read_prop().viewport; + if (prepared.canvas.empty() || i_layout.orientation == q_layout.orientation) { + return; + } + const auto current = monotonic_milliseconds(); + object->template access_rendering_stream( + [&](std::span points) { + for (const auto& value : points) + if (current - value.submitted_at_ms <= state.point_lifetime_ms) + prepared.points.push_back(detail::map_plot_point( + i_axis, value.point.x, q_axis, value.point.y, i_layout.orientation)); + }); + const int count = static_cast(state.type); + const Plot_Coordinate center_i = state.i_range.center(); + const Plot_Coordinate center_q = state.q_range.center(); + const Plot_Coordinate radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; + for (int index = 0; index < count; ++index) { + const Plot_Ratio angle = state.phase_offset_radians + 2.0 * std::numbers::pi * index / count; + prepared.anchors.push_back(detail::map_plot_point(i_axis, center_i + std::cos(angle) * radius, q_axis, center_q + std::sin(angle) * radius, i_layout.orientation)); + } + prepared.valid = true; + object->template mark_dirty(); +} +template +void Constellation_Diagram::Private::paint(Object* object) { + const auto& state = object->template read_prop(); + auto& cache = this->paint_surface(); + if (!prepared.valid) { + return; + } + detail::Painter painter(cache, prepared.canvas); + for (const auto& anchor : prepared.anchors) painter.circle(anchor, 4.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::solid}); + for (const auto& point : prepared.points) painter.circle(point, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::solid}); +} +template +void Constellation_Diagram::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { + if constexpr (std::same_as) object->template mark_dirty(); +} +template +void Constellation_Diagram::Private::before_advance(Object* object, Prop_Type*, State_Access pending_states, const Prop_Type*, State_Access) { + pending_states.template get().point_count = + object->template access_query_stream( + [](std::span points) { return points.size(); }); +} +template +const Constellation_Diagram::Private::Dispatch& Constellation_Diagram::Private::dispatch_for() { + static const Dispatch value{ + [](Root* root, Constellation_Point point) { + auto* object = static_cast(root); + object->template submit_stream(std::move(point)); + object->template mark_dirty(); + }, + [](const Root* root) { + return static_cast(root)->template access_query_stream( + [](std::span points) { return points.size(); }); + }, + [](Root* root) { + auto* object = static_cast(root); + auto& data = static_cast(*object->d); + const auto& state = object->template read_prop(); + const Plot_Coordinate size = std::max(state.i_range.size(), state.q_range.size()); + const Plot_Coordinate i_center = state.i_range.center(); + const Plot_Coordinate q_center = state.q_range.center(); + data.i_axis->template set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{i_center - size * 0.5, i_center + size * 0.5}); + data.q_axis->template set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{q_center - size * 0.5, q_center + size * 0.5}); + } + }; + return value; +} +template +void Constellation_Diagram::Private::bind_private_crtp(Object* object) { + Prev_Private::bind_private_crtp(object); + dispatch = &dispatch_for(); +} +inline void Constellation_Diagram::Private::bind_sources(Axis_Object* i_axis_value, Axis_Object* q_axis_value) { + i_axis = i_axis_value; + q_axis = q_axis_value; +} } diff --git a/render_2D/render_2D/plottable/Frequency_Trace.hpp b/render_2D/render_2D/plottable/Frequency_Trace.hpp index 2c7c33a..b3f9fe6 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.hpp +++ b/render_2D/render_2D/plottable/Frequency_Trace.hpp @@ -12,7 +12,9 @@ struct Frequency_Trace_Sample { Plot_Value value{}; /* 该时间点对应的频率值。 */ bool operator==(const Frequency_Trace_Sample&) const; }; -struct Frequency_Trace : Def> { +struct Frequency_Trace_Stream_Tag {}; +struct Frequency_Trace : Def, + Mpmc_Triple_Buffer> { using Scene_Object = Impl; using Time_Object = Impl; using Value_Object = Impl; @@ -20,7 +22,6 @@ struct Frequency_Trace : Def samples{}; /* 已提交轨迹样本的唯一权威集合。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Frequency_Trace.ipp b/render_2D/render_2D/plottable/Frequency_Trace.ipp index a5108f0..ff4daa2 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.ipp +++ b/render_2D/render_2D/plottable/Frequency_Trace.ipp @@ -46,10 +46,10 @@ std::expected, Dependency_Graph_Error> Frequency_Trace:: private_data.scene_attach = [object = trace.get()](Root* root) -> std::expected { auto* scene = static_cast(root); auto& data = static_cast(*object->d); data.scene = scene; auto* time_axis = data.time_axis; auto* value_axis = data.value_axis; return scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { prepare.add_dependency(object, scene); prepare.add_dependency(object, time_axis); prepare.add_dependency(object, value_axis); paint.add_dependency(time_axis, object); paint.add_dependency(value_axis, object); cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(object, scene); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, time_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, time_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, time_axis); cache.template add_prop_dependency<&Time_Axis::Prop::visible_count>(object, time_axis); cache.template add_prop_dependency<&Time_Axis::Prop::newest_at_start>(object, time_axis); cache.template add_dependency<&Time_Axis::State::next_tick>(object, time_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, value_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, value_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, value_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, value_axis); }); }; return trace; } template -bool Frequency_Trace::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, state.samples.size()); } +bool Frequency_Trace::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, object->template access_query_stream([](std::span samples) { return samples.size(); })); } template tf::Taskflow Frequency_Trace::Private::build_prepare_graph(Object* object, const Prop& state) { - graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, state.samples.size()); tf::Taskflow graph; + graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, object->template access_query_stream([](std::span samples) { return samples.size(); })); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object, graph_partition_count); }).name("frequency_trace.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("frequency_trace.prepare.partition"); begin.precede(task); } return graph; @@ -59,9 +59,15 @@ tf::Taskflow Frequency_Trace::Private::build_paint_graph(Object* object, const P template void Frequency_Trace::Private::prepare_frame(Object* object, Plot_Partition_Count partition_count) { const auto& state = object->template read_prop(); const auto& time_layout = time_axis->template read_prop(); const auto& value_layout = value_axis->template read_prop(); + const auto visible_count = static_cast(std::max(2, time_axis->template read_prop().visible_count)); + object->template exchange_stream(); prepared = {}; prepared.partitions.resize(partition_count); prepared.canvas = scene->template read_prop().viewport; - if (prepared.canvas.empty() || time_layout.orientation == value_layout.orientation || state.samples.empty()) return; - prepared.samples.reserve(state.samples.size()); for (const auto& sample : state.samples) prepared.samples.push_back({static_cast(sample.tick), sample.value}); prepared.valid = true; + if (prepared.canvas.empty() || time_layout.orientation == value_layout.orientation || prepared.samples.empty()) { return; } + object->template access_rendering_stream([&](std::span samples) { + prepared.samples.reserve(samples.size()); + for (const auto& sample : samples) prepared.samples.push_back({static_cast(sample.tick), sample.value}); + }); + prepared.valid = !prepared.samples.empty(); } template void Frequency_Trace::Private::prepare_partition(Object* object, Plot_Partition_Count partition_index) { @@ -76,24 +82,17 @@ void Frequency_Trace::Private::paint_frame(Object* object) { template void Frequency_Trace::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } template -void Frequency_Trace::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { auto& state = pending_states.template get(); const auto& prop = static_cast(*current_prop); state.sample_count = prop.samples.size(); state.rendered_point_count = 0; for (const auto& partition : prepared.partitions) state.rendered_point_count += partition.points.size(); } +void Frequency_Trace::Private::before_advance(Object* object, Prop_Type*, State_Access pending_states, const Prop_Type*, State_Access) { auto& state = pending_states.template get(); state.sample_count = object->template access_query_stream([](std::span samples) { return samples.size(); }); state.rendered_point_count = state.sample_count; } template const Frequency_Trace::Private::Dispatch& Frequency_Trace::Private::dispatch_for() { static const Dispatch value{ [](Root* root, Plot_Time_Tick tick, Plot_Value sample_value) { auto* object = static_cast(root); - const auto& data = static_cast(*object->d); - const auto visible_count = static_cast(std::max( - 2, data.time_axis->template read_prop().visible_count)); - object->template update_prop<&Prop::samples>([=](Prop_Access props) { - auto& samples = props.template get().samples; - samples.push_back({tick, sample_value}); - if (samples.size() > visible_count) - samples.erase(samples.begin(), samples.begin() + static_cast(samples.size() - visible_count)); - }); + object->template submit_stream(Frequency_Trace_Sample{tick, sample_value}); + object->template mark_dirty(); }, - [](const Root* root) { return static_cast(root)->template read_prop().samples.size(); }, - [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); std::size_t result{}; for (const auto& curve : data.prepared.partitions) result += curve.points.size(); return result; } + [](const Root* root) { return static_cast(root)->template access_query_stream([](std::span samples) { return samples.size(); }); }, + [](const Root* root) { return static_cast(root)->template access_query_stream([](std::span samples) { return samples.size(); }); } }; return value; } template diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.cpp b/render_2D/render_2D/plottable/Sweep_Spectrum.cpp index a27764c..6fea207 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.cpp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.cpp @@ -2,8 +2,8 @@ namespace aethera::render_2d { bool Sweep_Spectrum::Prop::operator==(const Prop&) const = default; bool Sweep_Spectrum::State::operator==(const State&) const = default; -void Sweep_Spectrum::append_block(std::span values) { static_cast(*d).dispatch->append(this, values); } -void Sweep_Spectrum::append_block(std::pmr::vector&& values) { append_block(std::span(values.data(), values.size())); } +void Sweep_Spectrum::append_block(std::span values) { static_cast(*d).dispatch->append(this, {values.begin(), values.end()}); } +void Sweep_Spectrum::append_block(std::pmr::vector&& values) { static_cast(*d).dispatch->append(this, {std::make_move_iterator(values.begin()), std::make_move_iterator(values.end())}); } std::size_t Sweep_Spectrum::stored_block_count() const { return static_cast(*d).dispatch->block_count(this); } std::size_t Sweep_Spectrum::stored_point_count() const { return static_cast(*d).dispatch->point_count(this); } std::size_t Sweep_Spectrum::rendered_point_count() const { return static_cast(*d).dispatch->rendered_point_count(this); } diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.hpp b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp index 4978d28..db1cbe8 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.hpp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp @@ -9,14 +9,16 @@ #include #include namespace aethera::render_2d { -struct Sweep_Spectrum : Def> { +struct Sweep_Spectrum_Stream_Tag {}; +struct Sweep_Spectrum : Def, + Mpmc_Triple_Buffer>>> { using Scene_Object = Impl; using Frequency_Object = Impl; using Power_Object = Impl; struct Prop : Prev_Prop { std::size_t bins_per_block{}; /* 每个扫频块期望的功率点数;零值接受首块尺寸。 */ 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{}; /* 全部扫描块覆盖的频率范围。 */ @@ -24,7 +26,6 @@ struct Sweep_Spectrum : Def> blocks{}; /* 各频率段最新数据的唯一权威槽位;下标即频率段。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.ipp b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp index 9f71a9d..a38e4b3 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.ipp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp @@ -13,7 +13,7 @@ struct Sweep_Spectrum::Private : Prev_Private { Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */ bool valid{}; /* 两根轴正交、画布及组合折线均有效。 */ }; - using Append_Run = void (*)(Root*, std::span); using Count_Run = std::size_t (*)(const Root*); + using Append_Run = void (*)(Root*, std::vector); using Count_Run = std::size_t (*)(const Root*); struct Dispatch { Append_Run append; /* 向最终对象提交一个扫描块。 */ Count_Run block_count; /* 查询权威扫描块数。 */ @@ -51,22 +51,29 @@ std::expected, Dependency_Graph_Error> Sweep_Spectrum::B } template void Sweep_Spectrum::append_block(const Values& values) { append_block(std::span(std::data(values), std::size(values))); } template -bool Sweep_Spectrum::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { std::size_t points{}; for (const auto& block : state.blocks) points += block.size(); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, points); } +bool Sweep_Spectrum::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { const auto points = object->template access_query_stream([](std::span>> blocks) { std::size_t count{}; for (const auto& block : blocks) count += block->size(); return count; }); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, points); } template -tf::Taskflow Sweep_Spectrum::Private::build_prepare_graph(Object* object, const Prop& state) { std::size_t points{}; for (const auto& block : state.blocks) points += block.size(); graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, points); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("sweep_spectrum.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("sweep_spectrum.prepare.partition"); begin.precede(task); } return graph; } +tf::Taskflow Sweep_Spectrum::Private::build_prepare_graph(Object* object, const Prop& state) { const auto points = object->template access_query_stream([](std::span>> blocks) { std::size_t count{}; for (const auto& block : blocks) count += block->size(); return count; }); graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, points); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("sweep_spectrum.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("sweep_spectrum.prepare.partition"); begin.precede(task); } return graph; } template 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 void Sweep_Spectrum::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop(); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& power_layout = power_axis->template read_prop(); const auto& power_state = power_axis->template read_prop(); prepared = {}; prepared.canvas = scene->template read_prop().viewport; - if (prepared.canvas.empty() || frequency_layout.orientation == power_layout.orientation || state.blocks.empty()) return; const std::size_t block_count = std::max(1, state.block_count); - const std::size_t stored_block_count = std::min(block_count, state.blocks.size()); + object->template exchange_stream(); + std::size_t available_blocks{}; + object->template access_rendering_stream([&](std::span>> blocks) { + available_blocks = blocks.size(); + const auto first = blocks.size() > block_count ? blocks.size() - block_count : 0; + for (std::size_t index = first; index < blocks.size(); ++index) + prepared.values.insert(prepared.values.end(), blocks[index]->begin(), blocks[index]->end()); + }); + if (prepared.canvas.empty() || frequency_layout.orientation == power_layout.orientation || available_blocks == 0) return; + const std::size_t stored_block_count = std::min(block_count, available_blocks); 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 std::size_t latest_block_index = stored_block_count - 1; const double domain_progress = static_cast(stored_block_count) / static_cast(block_count); const double marker_progress = static_cast(latest_block_index + 1) / static_cast(block_count); const auto domain_target = state.frequency_range.origin + state.frequency_range.length() * domain_progress; @@ -82,16 +89,16 @@ void Sweep_Spectrum::Private::prepare_partition(Object* object, Plot_Partition_C template void Sweep_Spectrum::Private::paint_frame(Object* object) { const auto& state = object->template read_prop(); auto& cache = this->paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); painter.line(prepared.marker_first, prepared.marker_second, state.current_frequency_pen); } template -void Sweep_Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access props) { if constexpr (std::same_as) { auto& state = props.template get(); const std::size_t block_count = std::max(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(); } } +void Sweep_Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } template -void Sweep_Spectrum::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { auto& state = pending_states.template get(); const auto& prop = static_cast(*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(); } +void Sweep_Spectrum::Private::before_advance(Object* object, Prop_Type*, State_Access pending_states, const Prop_Type*, State_Access) { auto& state = pending_states.template get(); object->template access_query_stream([&](std::span>> blocks) { state.stored_block_count = blocks.size(); state.stored_point_count = 0; for (const auto& block : blocks) state.stored_point_count += block->size(); state.rendered_point_count = state.stored_point_count; }); } template const Sweep_Spectrum::Private::Dispatch& Sweep_Spectrum::Private::dispatch_for() { static const Dispatch value{ - [](Root* root, std::span values) { auto* object = static_cast(root); object->template update_prop<&Prop::blocks, &Prop::next_block_index>([values](Prop_Access props) { auto& state = props.template get(); const std::size_t block_count = std::max(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(root)->template read_prop().blocks.size(); }, - [](const Root* root) { const auto& blocks = static_cast(root)->template read_prop().blocks; std::size_t count{}; for (const auto& block : blocks) count += block.size(); return count; }, - [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); std::size_t count{}; for (const auto& curve : data.prepared.partitions) count += curve.points.size(); return count; } + [](Root* root, std::vector values) { auto* object = static_cast(root); object->template submit_stream(std::make_shared>(std::move(values))); object->template mark_dirty(); }, + [](const Root* root) { return static_cast(root)->template access_query_stream([](std::span>> blocks) { return blocks.size(); }); }, + [](const Root* root) { return static_cast(root)->template access_query_stream([](std::span>> blocks) { std::size_t count{}; for (const auto& block : blocks) count += block->size(); return count; }); }, + [](const Root* root) { return static_cast(root)->template access_query_stream([](std::span>> blocks) { std::size_t count{}; for (const auto& block : blocks) count += block->size(); return count; }); } }; return value; } template void Sweep_Spectrum::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } diff --git a/render_2D/render_2D/plottable/Waterfall.cpp b/render_2D/render_2D/plottable/Waterfall.cpp index 31b645b..f792291 100644 --- a/render_2D/render_2D/plottable/Waterfall.cpp +++ b/render_2D/render_2D/plottable/Waterfall.cpp @@ -1,4 +1,22 @@ #include "Waterfall.hpp" /* Waterfall 最终实例及三类依赖实现。 */ namespace aethera::render_2d { -bool Waterfall_Row::operator==(const Waterfall_Row&) const = default; bool Waterfall::Prop::operator==(const Prop&) const = default; -bool Waterfall::State::operator==(const State&) const = default; void Waterfall::append_row(Plot_Time_Tick tick, std::span values) { static_cast(*d).dispatch->append(this, tick, values); } void Waterfall::append_row(Plot_Time_Tick tick, std::pmr::vector&& values) { append_row(tick, std::span(values.data(), values.size())); } std::size_t Waterfall::row_count() const { return static_cast(*d).dispatch->row_count(this); } std::size_t Waterfall::stored_point_count() const { return static_cast(*d).dispatch->point_count(this); } std::size_t Waterfall::rendered_cell_count() const { return static_cast(*d).dispatch->rendered_count(this); } } +bool Waterfall_Row::operator==(const Waterfall_Row&) const = default; +bool Waterfall::Prop::operator==(const Prop&) const = default; +bool Waterfall::State::operator==(const State&) const = default; +void Waterfall::append_row(Plot_Time_Tick tick, std::span values) { + static_cast(*d).dispatch->append(this, Waterfall_Row{tick, {values.begin(), values.end()}}); +} +void Waterfall::append_row(Plot_Time_Tick tick, std::pmr::vector&& values) { + static_cast(*d).dispatch->append(this, Waterfall_Row{ + tick, {std::make_move_iterator(values.begin()), std::make_move_iterator(values.end())}}); +} +std::size_t Waterfall::row_count() const { + return static_cast(*d).dispatch->row_count(this); +} +std::size_t Waterfall::stored_point_count() const { + return static_cast(*d).dispatch->point_count(this); +} +std::size_t Waterfall::rendered_cell_count() const { + return static_cast(*d).dispatch->rendered_count(this); +} +} diff --git a/render_2D/render_2D/plottable/Waterfall.hpp b/render_2D/render_2D/plottable/Waterfall.hpp index e5b03cb..8a278ae 100644 --- a/render_2D/render_2D/plottable/Waterfall.hpp +++ b/render_2D/render_2D/plottable/Waterfall.hpp @@ -15,7 +15,9 @@ struct Waterfall_Row { std::vector values{}; /* 该时间槽从低频到高频排列的功率值。 */ bool operator==(const Waterfall_Row&) const; }; -struct Waterfall : Def> { +struct Waterfall_Stream_Tag {}; +struct Waterfall : Def, + Mpmc_Triple_Buffer>> { using Scene_Object = Impl; using Frequency_Object = Impl; using Time_Object = Impl; @@ -32,7 +34,6 @@ struct Waterfall : Def> { Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ Image_Interpolation_Mode interpolation_mode{Image_Interpolation_Mode::nearest}; /* 栅格放大时的图像插值方式。 */ Color_Map color_map{}; /* 功率到颜色的映射。 */ - std::vector rows{}; /* 从旧到新的瀑布行唯一权威集合。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Waterfall.ipp b/render_2D/render_2D/plottable/Waterfall.ipp index 8b84a4d..812390b 100644 --- a/render_2D/render_2D/plottable/Waterfall.ipp +++ b/render_2D/render_2D/plottable/Waterfall.ipp @@ -9,74 +9,274 @@ namespace aethera::render_2d { struct Waterfall::Private : Prev_Private { struct Prepared_Row { - std::size_t source{}; /* Prop::rows 中提供该可见行功率值的下标。 */ - int slot{}; /* 该 tick 在当前固定时间窗口中的离散槽位。 */ + std::size_t source{}; /* 本帧冻结 Waterfall_Stream 中可见行的下标。 */ + int slot{}; /* 该 tick 在当前固定时间窗口中的离散槽位。 */ }; struct Prepared { - detail::Raster_Layout layout{}; /* 可视频段与完整时间窗口组成的色块布局。 */ - std::vector pixels{}; /* 固定时间窗口的像素矩阵;无数据槽保持透明。 */ - std::vector rows{}; /* 可见源行到固定时间槽位的映射。 */ - Rect_F tooltip_box{}; /* 当前 hover 提示框的画布矩形。 */ - std::string tooltip_text{}; /* 当前 hover 频率文本;空值表示不绘制。 */ - Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */ - int source_first{}; /* 可视频段在源频谱行中的首列。 */ - bool valid{}; /* 轴布局和行数据是否足以生成色块。 */ + std::vector> source_rows{}; /* Rendering-role row ownership retained through parallel Prepare tasks. */ + detail::Raster_Layout layout{}; /* 可视频段与完整时间窗口组成的色块布局。 */ + std::vector pixels{}; /* 固定时间窗口的像素矩阵;无数据槽保持透明。 */ + std::vector rows{}; /* 可见源行到固定时间槽位的映射。 */ + Rect_F tooltip_box{}; /* 当前 hover 提示框的画布矩形。 */ + std::string tooltip_text{}; /* 当前 hover 频率文本;空值表示不绘制。 */ + Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */ + int source_first{}; /* 可视频段在源频谱行中的首列。 */ + bool valid{}; /* 轴布局和行数据是否足以生成色块。 */ }; - using Append_Run = void (*)(Root*, Plot_Time_Tick, std::span); using Count_Run = std::size_t (*)(const Root*); + using Append_Run = void (*)(Root*, Waterfall_Row); + using Count_Run = std::size_t (*)(const Root*); struct Dispatch { - Append_Run append; /* 按 tick 向最终对象提交一行。 */ - Count_Run row_count; /* 查询权威行数。 */ - Count_Run point_count; /* 查询权威样本总数。 */ - Count_Run rendered_count; /* 查询已准备的色块数。 */ + Append_Run append; /* 按 tick 向最终对象提交一行。 */ + Count_Run row_count; /* 查询权威行数。 */ + Count_Run point_count; /* 查询权威样本总数。 */ + Count_Run rendered_count; /* 查询已准备的色块数。 */ }; - Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ - Frequency_Object* frequency_axis{}; /* 不拥有的频率轴。 */ - Time_Object* time_axis{}; /* 不拥有的时间轴,同时权威决定保留行数。 */ - Prepared prepared{}; /* 当前权威状态推导出的 Paint 输入。 */ - detail::Hover_Tooltip_Runtime tooltip{}; /* 事件侧当前 hover 位置。 */ - Plot_Partition_Count graph_partition_count{}; /* Prepare 子图当前固化的分块数。 */ - const Dispatch* dispatch{}; /* 最终类型的公开薄壳分派表。 */ + Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ + Frequency_Object* frequency_axis{}; /* 不拥有的频率轴。 */ + Time_Object* time_axis{}; /* 不拥有的时间轴,同时权威决定保留行数。 */ + Prepared prepared{}; /* 当前权威状态推导出的 Paint 输入。 */ + detail::Hover_Tooltip_Runtime tooltip{}; /* 事件侧当前 hover 位置。 */ + Plot_Partition_Count graph_partition_count{}; /* Prepare 子图当前固化的分块数。 */ + const Dispatch* dispatch{}; /* 最终类型的公开薄壳分派表。 */ /* CRTP 覆盖:绑定 Renderable、事件能力和最终 Waterfall 分派表。 */ - template void bind_private_crtp(Object* object); + template + void bind_private_crtp(Object* object); void bind_sources(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value); - template [[nodiscard]] static const Dispatch& dispatch_for(); + template + [[nodiscard]] static const Dispatch& dispatch_for(); /* CRTP 覆盖:按当前色块工作量构建分块 Prepare 子图。 */ - template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state); + template + [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state); /* CRTP 覆盖:构建消费色块矩阵和提示信息的 Paint 子图。 */ - template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state); + template + [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state); /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ - template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); - template void prepare_frame(Object* object); template void prepare_partition(Object* object, Plot_Partition_Count index); template void paint_frame(Object* object); + template + [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); + template + void prepare_frame(Object* object); + template + void prepare_partition(Object* object, Plot_Partition_Count index); + template + void paint_frame(Object* object); /* CRTP 覆盖:更新 hover 位置并请求重绘。 */ - template void handle_event(Object* object, const Event& event); + template + void handle_event(Object* object, const Event& event); /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ - template void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); - template void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); + template + void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); + template + void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); }; -template Waterfall::Builder::Builder(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) : Base(), frequency_axis(frequency_axis_value), time_axis(time_axis_value) {} +template +Waterfall::Builder::Builder(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) : Base(), frequency_axis(frequency_axis_value), time_axis(time_axis_value) {} template std::expected, Dependency_Graph_Error> Waterfall::Builder::build() { - auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); - auto& private_data = static_cast(*plot->d); private_data.bind_sources(frequency_axis, time_axis); + auto result = Base::build(); + if (!result) return std::unexpected(result.error()); + auto plot = std::move(result).value(); + auto& private_data = static_cast(*plot->d); + private_data.bind_sources(frequency_axis, time_axis); private_data.scene_attach = [object = plot.get()](Root* root) -> std::expected { - auto* scene = static_cast(root); auto& data = static_cast(*object->d); data.scene = scene; auto* frequency_axis = data.frequency_axis; auto* time_axis = data.time_axis; - return scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { prepare.add_dependency(object, scene); prepare.add_dependency(object, frequency_axis); prepare.add_dependency(object, time_axis); paint.add_dependency(frequency_axis, object); paint.add_dependency(time_axis, object); cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(object, scene); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, frequency_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, time_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, time_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, time_axis); cache.template add_prop_dependency<&Time_Axis::Prop::visible_count>(object, time_axis); cache.template add_prop_dependency<&Time_Axis::Prop::newest_at_start>(object, time_axis); cache.template add_dependency<&Time_Axis::State::next_tick>(object, time_axis); }); - }; return plot; + auto* scene = static_cast(root); + auto& data = static_cast(*object->d); + data.scene = scene; + auto* frequency_axis = data.frequency_axis; + auto* time_axis = data.time_axis; + return scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { + prepare.add_dependency(object, scene); + prepare.add_dependency(object, frequency_axis); + prepare.add_dependency(object, time_axis); + paint.add_dependency(frequency_axis, object); + paint.add_dependency(time_axis, object); + cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(object, scene); + cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, frequency_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, frequency_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, frequency_axis); + cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, frequency_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, time_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, time_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, time_axis); + cache.template add_prop_dependency<&Time_Axis::Prop::visible_count>(object, time_axis); + cache.template add_prop_dependency<&Time_Axis::Prop::newest_at_start>(object, time_axis); + cache.template add_dependency<&Time_Axis::State::next_tick>(object, time_axis); + }); + }; + return plot; +} +template +void Waterfall::append_row(Plot_Time_Tick tick, const Values& values) { + append_row(tick, std::span(std::data(values), std::size(values))); +} +template +bool Waterfall::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { + const std::size_t cells = object->template access_query_stream([&](std::span> rows) { + return rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : rows.empty() ? 1 : rows.back()->values.size()); + }); + return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, cells); +} +template +tf::Taskflow Waterfall::Private::build_prepare_graph(Object* object, const Prop& state) { + const std::size_t cells = object->template access_query_stream([&](std::span> rows) { + return rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : rows.empty() ? 1 : rows.back()->values.size()); + }); + graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, cells); + tf::Taskflow graph; + auto begin = graph.emplace([this, object] { + prepare_frame(object); + }).name("waterfall.prepare.frame"); + for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { + auto task = graph.emplace([this, object, index] { + prepare_partition(object, index); + }).name("waterfall.prepare.partition"); + begin.precede(task); + } + return graph; +} +template +tf::Taskflow Waterfall::Private::build_paint_graph(Object* object, const Prop&) { + tf::Taskflow graph; + graph.emplace([this, object] { + paint_frame(object); + }).name("waterfall.paint.frame"); + return graph; +} +template +void Waterfall::Private::prepare_frame(Object* object) { + const auto& state = object->template read_prop(); + object->template exchange_stream(); + prepared = {}; + object->template access_rendering_stream([&](std::span> rows) { + prepared.source_rows.assign(rows.begin(), rows.end()); + }); + const auto& rows = prepared.source_rows; + const auto& frequency_layout = frequency_axis->template read_prop(); + const auto& time_layout = time_axis->template read_prop(); + prepared = {}; + prepared.canvas = scene->template read_prop().viewport; + if (rows.empty()) { + return; + } + const auto shortest = std::min_element(rows.begin(), rows.end(), [](const auto& left, const auto& right) { + return left->values.size() < right->values.size(); + }); + const std::size_t available = (*shortest)->values.size(); + const int source_columns = static_cast(state.frequency_bin_count ? std::min(state.frequency_bin_count, available) : available); + const auto selection = detail::raster_axis_selection(state.frequency_range, frequency_axis->coordinate_range(), source_columns, state.visible_range_only); + if (!selection) { + return; + } + const int time_slots = std::max(2, time_axis->template read_prop().visible_count); + const Axis_Range time_range = time_axis->coordinate_range(); + prepared.layout = detail::raster_layout(frequency_axis, selection->range, selection->count(), time_axis, time_range, time_slots, frequency_layout.orientation, time_layout.orientation); + if (prepared.canvas.empty() || !prepared.layout.valid()) { + return; + } + prepared.source_first = selection->first; + prepared.pixels.assign(static_cast(prepared.layout.width) * prepared.layout.height, 0); + const Axis_Coordinate direction = time_range.length() < 0.0 ? -1.0 : 1.0; + const Axis_Coordinate first_center = time_range.origin + direction * 0.5; + for (std::size_t source = 0; source < rows.size(); ++source) { + const int slot = static_cast(std::llround((static_cast(rows[source]->tick) - first_center) / direction)); + if (slot >= 0 && slot < time_slots) prepared.rows.push_back({source, slot}); + } + if (prepared.rows.empty()) { + return; + } + if (state.tooltip_enabled && tooltip.active && prepared.layout.target.contains(tooltip.position)) { + std::ostringstream text; + text << std::fixed << std::setprecision(2) << frequency_axis->point_to_coordinate(tooltip.position) << " Hz"; + prepared.tooltip_text = text.str(); + prepared.tooltip_box = {tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; + } + prepared.valid = true; +} +template +void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) { + if (!prepared.valid) return; + const auto& state = object->template read_prop(); + const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; + const std::size_t cells = static_cast(columns) * prepared.rows.size(); + const auto [first, last] = detail::raster_partition_range(cells, index, graph_partition_count); + for (std::size_t cell = first; cell < last; ++cell) { + const auto& row = prepared.rows[cell / static_cast(columns)]; + const int column = static_cast(cell % static_cast(columns)); + const auto& values = prepared.source_rows[row.source]->values; + const std::size_t source = static_cast(prepared.source_first + column); + prepared.pixels[prepared.layout.index(column, row.slot)] = premultiply(state.color_map.sample(detail::normalized_plot_value(values[source], state.power_range))); + } +} +template +void Waterfall::Private::paint_frame(Object* object) { + const auto& state = object->template read_prop(); + auto& cache = this->paint_surface(); + if (!prepared.valid) { + return; + } + detail::Painter painter(cache, prepared.canvas); + detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode); + if (!prepared.tooltip_text.empty()) { + painter.rect(prepared.tooltip_box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); + painter.text({prepared.tooltip_box.x + 4.0, prepared.tooltip_box.y + 3.0}, prepared.tooltip_text, state.tooltip_font, state.tooltip_text_pen); + } +} +template +void Waterfall::Private::handle_event(Object* object, const Event& event) { + if (detail::update_hover_tooltip(tooltip, event)) object->template mark_dirty(); +} +template +void Waterfall::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { + if constexpr (std::same_as) object->template mark_dirty(); +} +template +void Waterfall::Private::before_advance(Object* object, Prop_Type*, State_Access pending_states, const Prop_Type*, State_Access) { + auto& state = pending_states.template get(); + object->template access_query_stream([&](std::span> rows) { + state.row_count = rows.size(); + state.stored_point_count = 0; + for (const auto& row : rows) state.stored_point_count += row->values.size(); + state.rendered_cell_count = state.stored_point_count; + }); +} +template +const Waterfall::Private::Dispatch& Waterfall::Private::dispatch_for() { + static const Dispatch value{ + [](Root* root, Waterfall_Row row) { + auto* object = static_cast(root); + object->template submit_stream( + std::make_shared(std::move(row))); + object->template mark_dirty(); + }, + [](const Root* root) { + return static_cast(root)->template access_query_stream( + [](std::span> rows) { return rows.size(); }); + }, + [](const Root* root) { + return static_cast(root)->template access_query_stream( + [](std::span> rows) { + std::size_t count{}; + for (const auto& row : rows) count += row->values.size(); + return count; + }); + }, + [](const Root* root) { + return static_cast(root)->template access_query_stream( + [](std::span> rows) { + std::size_t count{}; + for (const auto& row : rows) count += row->values.size(); + return count; + }); + } + }; + return value; +} +template +void Waterfall::Private::bind_private_crtp(Object* object) { + Prev_Private::bind_private_crtp(object); + dispatch = &dispatch_for(); +} +inline void Waterfall::Private::bind_sources(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) { + frequency_axis = frequency_axis_value; + time_axis = time_axis_value; } -template void Waterfall::append_row(Plot_Time_Tick tick, const Values& values) { append_row(tick, std::span(std::data(values), std::size(values))); } -template bool Waterfall::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { const std::size_t cells = state.rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : state.rows.empty() ? 1 : state.rows.back().values.size()); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, cells); } -template tf::Taskflow Waterfall::Private::build_prepare_graph(Object* object, const Prop& state) { const std::size_t cells = state.rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : state.rows.empty() ? 1 : state.rows.back().values.size()); graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, cells); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("waterfall.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("waterfall.prepare.partition"); begin.precede(task); } return graph; } -template tf::Taskflow Waterfall::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("waterfall.paint.frame"); return graph; } -template -void Waterfall::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop(); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& time_layout = time_axis->template read_prop(); prepared = {}; prepared.canvas = scene->template read_prop().viewport; if (state.rows.empty()) return; const auto shortest = std::min_element(state.rows.begin(), state.rows.end(), [](const Waterfall_Row& left, const Waterfall_Row& right) { return left.values.size() < right.values.size(); }); const std::size_t available = shortest->values.size(); const int source_columns = static_cast(state.frequency_bin_count ? std::min(state.frequency_bin_count, available) : available); const auto selection = detail::raster_axis_selection(state.frequency_range, frequency_axis->coordinate_range(), source_columns, state.visible_range_only); if (!selection) return; const int time_slots = std::max(2, time_axis->template read_prop().visible_count); const Axis_Range time_range = time_axis->coordinate_range(); prepared.layout = detail::raster_layout(frequency_axis, selection->range, selection->count(), time_axis, time_range, time_slots, frequency_layout.orientation, time_layout.orientation); if (prepared.canvas.empty() || !prepared.layout.valid()) return; prepared.source_first = selection->first; prepared.pixels.assign(static_cast(prepared.layout.width) * prepared.layout.height, 0); const Axis_Coordinate direction = time_range.length() < 0.0 ? -1.0 : 1.0; const Axis_Coordinate first_center = time_range.origin + direction * 0.5; for (std::size_t source = 0; source < state.rows.size(); ++source) { const int slot = static_cast(std::llround((static_cast(state.rows[source].tick) - first_center) / direction)); if (slot >= 0 && slot < time_slots) prepared.rows.push_back({source, slot}); } if (prepared.rows.empty()) return; if (state.tooltip_enabled && tooltip.active && prepared.layout.target.contains(tooltip.position)) { std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency_axis->point_to_coordinate(tooltip.position) << " Hz"; prepared.tooltip_text = text.str(); prepared.tooltip_box = {tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; } prepared.valid = true; } -template -void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const std::size_t cells = static_cast(columns) * prepared.rows.size(); const auto [first, last] = detail::raster_partition_range(cells, index, graph_partition_count); for (std::size_t cell = first; cell < last; ++cell) { const auto& row = prepared.rows[cell / static_cast(columns)]; const int column = static_cast(cell % static_cast(columns)); const auto& values = state.rows[row.source].values; const std::size_t source = static_cast(prepared.source_first + column); prepared.pixels[prepared.layout.index(column, row.slot)] = premultiply(state.color_map.sample(detail::normalized_plot_value(values[source], state.power_range))); } } -template void Waterfall::Private::paint_frame(Object* object) { const auto& state = object->template read_prop(); auto& cache = this->paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode); if (!prepared.tooltip_text.empty()) { painter.rect(prepared.tooltip_box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); painter.text({prepared.tooltip_box.x + 4.0, prepared.tooltip_box.y + 3.0}, prepared.tooltip_text, state.tooltip_font, state.tooltip_text_pen); } } -template void Waterfall::Private::handle_event(Object* object, const Event& event) { if (detail::update_hover_tooltip(tooltip, event)) object->template mark_dirty(); } -template void Waterfall::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } -template void Waterfall::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { auto& state = pending_states.template get(); const auto& prop = static_cast(*current_prop); state.row_count = prop.rows.size(); state.stored_point_count = 0; for (const auto& row : prop.rows) state.stored_point_count += row.values.size(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; state.rendered_cell_count = prepared.valid ? prepared.rows.size() * static_cast(columns) : 0; } -template -const Waterfall::Private::Dispatch& Waterfall::Private::dispatch_for() { static const Dispatch value{[](Root* root, Plot_Time_Tick tick, std::span values) { auto* object = static_cast(root); auto& data = static_cast(*object->d); const std::size_t row_limit = static_cast(std::max(2, data.time_axis->template read_prop().visible_count)); object->template update_prop<&Prop::rows>([=](Prop_Access props) { auto& state = props.template get(); state.rows.push_back({tick, {values.begin(), values.end()}}); while (state.rows.size() > row_limit) state.rows.erase(state.rows.begin()); }); }, [](const Root* root) { return static_cast(root)->template read_prop().rows.size(); }, [](const Root* root) { const auto& rows = static_cast(root)->template read_prop().rows; std::size_t count{}; for (const auto& row : rows) count += row.values.size(); return count; }, [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; } -template void Waterfall::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } -inline void Waterfall::Private::bind_sources(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) { frequency_axis = frequency_axis_value; time_axis = time_axis_value; } } diff --git a/web_server/src/Gallery_Plots_2D.cpp b/web_server/src/Gallery_Plots_2D.cpp index 6c8c165..7d8a4fa 100644 --- a/web_server/src/Gallery_Plots_2D.cpp +++ b/web_server/src/Gallery_Plots_2D.cpp @@ -152,8 +152,7 @@ std::unique_ptr make_axis_component, Prop_Field<&Time_Axis::Prop::format, "format", "Time label format.">, Prop_Field<&Time_Axis::Prop::newest_at_start, "newest_at_start", "Places the newest time at the range origin.">, - State_Field, - State_Field>( + State_Field>( std::move(id), std::move(label), "axis", axis); } using Json = nlohmann::json; @@ -292,7 +291,7 @@ nlohmann::json generate_2d_data(Object& object, const Json& input) { std::vector samples(count); for (std::size_t index = 0; index < count; ++index) samples[index] = {static_cast(index * tick_step), distribution(engine)}; - object.template set<&Frequency_Trace::Prop::samples>(std::move(samples)); + for (const auto& sample : samples) object.append_sample(sample.tick, sample.value); generated_count = count; } else if constexpr (std::same_as) { const auto block_count = generator_count(input, "block_count", 65'536); @@ -306,7 +305,7 @@ nlohmann::json generate_2d_data(Object& object, const Json& input) { std::ranges::copy_n(complete.begin() + block * width, width, blocks[block].begin()); object.template set<&Sweep_Spectrum::Prop::bins_per_block>(width); object.template set<&Sweep_Spectrum::Prop::block_count>(block_count); - object.template set<&Sweep_Spectrum::Prop::blocks>(std::move(blocks)); + for (auto& block : blocks) object.append_block(block); generated_count = block_count * width; } else if constexpr (std::same_as) { const auto row_count = generator_count(input, "history_count", 4096); @@ -318,7 +317,7 @@ nlohmann::json generate_2d_data(Object& object, const Json& input) { const auto noise_stddev = generator_number(input, "noise_stddev"); for (std::size_t row = 0; row < row_count; ++row) generate_spectral_row(spectra[row], row, signal_count, minimum, maximum, noise_stddev, engine); - object.template set<&Afterglow::Prop::spectra>(std::move(spectra)); + for (auto& spectrum : spectra) object.append_spectrum(spectrum); generated_count = row_count * width; } else if constexpr (std::same_as) { const auto row_count = generator_count(input, "row_count", 4096); @@ -335,7 +334,7 @@ nlohmann::json generate_2d_data(Object& object, const Json& input) { rows.push_back({static_cast(row), std::move(row_values)}); } object.template set<&Waterfall::Prop::frequency_bin_count>(width); - object.template set<&Waterfall::Prop::rows>(std::move(rows)); + for (auto& row : rows) object.append_row(row.tick, row.values); generated_count = row_count * width; } else if constexpr (std::same_as) { const auto count = generator_count(input, "point_count"); @@ -346,7 +345,7 @@ nlohmann::json generate_2d_data(Object& object, const Json& input) { const auto submitted = monotonic_milliseconds(); for (std::size_t index = 0; index < count; ++index) points[index] = {{i_distribution(engine), q_distribution(engine)}, submitted}; - object.template set<&Constellation_Diagram::Prop::points>(std::move(points)); + for (const auto& point : points) object.append_point(point.point); generated_count = count; } else if constexpr (std::same_as) { const auto count = generator_count(input, "region_count"); @@ -653,7 +652,7 @@ std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor) { const auto& state = raw->template read_prop(); const std::size_t block_count = std::max(1, state.block_count); const std::size_t bins_per_block = std::max(1, state.bins_per_block); - const std::size_t block_index = state.blocks.size() < block_count ? state.blocks.size() : state.next_block_index % block_count; + const std::size_t block_index = static_cast(event.sequence % block_count); std::vector values(bins_per_block); for (std::size_t i = 0; i < values.size(); ++i) { const auto sweep_index = block_index * values.size() + i;