更新三缓冲

This commit is contained in:
2026-08-24 08:12:20 +08:00
parent db251b5abf
commit d6c1db9d06
26 changed files with 1099 additions and 259 deletions
+1 -1
View File
@@ -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 连接共享页面令牌,其他标签页或浏览器实例必须被拒绝。
@@ -0,0 +1,140 @@
#pragma once
#include <concepts>
#include <concurrentqueue-1.0.5/concurrentqueue.h>
#include <cstddef>
#include <functional>
#include <mutex>
#include <new>
#include <shared_mutex>
#include <span>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>
namespace double_buffer {
/* Def mechanism declaration. Tag selects one independently exchanged MPMC stream. */
template <typename Tag, typename Value>
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 <typename Declaration>
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 <typename Callback>
requires std::invocable<Callback&, std::span<Value>>
decltype(auto) access_rendering(Callback&& callback) {
std::unique_lock lock(exchange_lock_);
return access(*rendering_, std::forward<Callback>(callback));
}
template <typename Callback>
requires std::invocable<Callback&, std::span<const Value>>
decltype(auto) access_query(Callback&& callback) const {
std::unique_lock lock(exchange_lock_);
auto adapter = [&](std::span<Value> values) -> decltype(auto) {
return std::invoke(callback, std::span<const Value>{values});
};
return access(*query_, adapter);
}
private:
using Queue = moodycamel::ConcurrentQueue<Value>;
static void clear(Queue& queue) {
Value value;
while (queue.try_dequeue(value)) {}
}
static void restore(Queue& queue, std::vector<Value>& values) {
for (auto& value : values)
if (!queue.enqueue(std::move(value))) throw std::bad_alloc{};
}
template <typename Callback>
static decltype(auto) access(Queue& queue, Callback&& callback) {
std::vector<Value> 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_result_t<Callback&, std::span<Value>>>) {
std::invoke(callback, std::span<Value>{values});
restore(queue, values);
}
else {
static_assert(!std::is_reference_v<std::invoke_result_t<Callback&, std::span<Value>>>,
"triple-buffer access results must not outlive the locked queue view");
auto result = std::invoke(callback, std::span<Value>{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 <typename Tuple>
class Mpmc_Triple_Buffer_Storage_Set;
template <typename... Declarations>
class Mpmc_Triple_Buffer_Storage_Set<std::tuple<Declarations...>> {
template <typename Tag, typename First, typename... Rest>
static consteval std::size_t index_of() {
if constexpr (std::same_as<Tag, typename First::Tag_Type>) return 0;
else return 1 + index_of<Tag, Rest...>();
}
public:
template <typename Tag>
auto& get() noexcept {
constexpr std::size_t index = index_of<Tag, Declarations...>();
return std::get<index>(storage_);
}
template <typename Tag>
const auto& get() const noexcept {
return const_cast<Mpmc_Triple_Buffer_Storage_Set*>(this)->template get<Tag>();
}
private:
std::tuple<Mpmc_Triple_Buffer_Storage<Declarations>...> storage_;
};
}
}
+24 -1
View File
@@ -1,4 +1,5 @@
#pragma once
#include "Mpmc_Triple_Buffer.hpp"
#include <algorithm>
#include <atomic>
#include <cstddef>
@@ -212,6 +213,10 @@ struct Is_Dependency_Graph_Type : std::false_type {};
template <typename Tag, typename Object>
struct Is_Dependency_Graph_Type<Dependency_Graph_Type<Tag, Object>> : std::true_type {};
template <typename Value>
struct Is_Mpmc_Triple_Buffer : std::false_type {};
template <typename Tag, typename Value>
struct Is_Mpmc_Triple_Buffer<Mpmc_Triple_Buffer<Tag, Value>> : std::true_type {};
template <typename Value>
struct Is_State_Type : std::false_type {};
template <typename Tag, typename Prev>
struct Is_State_Type<State_Type<Tag, Prev>> : std::true_type {};
@@ -224,7 +229,9 @@ concept Buffer_Type = Is_Tagged_Buffer<Value>::value;
template <typename Value>
concept Dependency_Graph_Mechanism = Is_Dependency_Graph_Type<Value>::value;
template <typename Value>
concept Mechanism_Type = Buffer_Type<Value> || Dependency_Graph_Mechanism<Value>;
concept Mpmc_Triple_Buffer_Mechanism = Is_Mpmc_Triple_Buffer<Value>::value;
template <typename Value>
concept Mechanism_Type = Buffer_Type<Value> || Dependency_Graph_Mechanism<Value> || Mpmc_Triple_Buffer_Mechanism<Value>;
template <typename Value>
concept Tagged_State = Prop_State<Value> && requires {
typename Value::Tag_Type;
@@ -298,6 +305,8 @@ template <typename T>
using Mechanism_Buffer_Tuple = std::conditional_t<Buffer_Type<T>, std::tuple<T>, std::tuple<>>;
template <typename T>
using Mechanism_Dependency_Graph_Tuple = std::conditional_t<Dependency_Graph_Mechanism<T>, std::tuple<T>, std::tuple<>>;
template <typename T>
using Mechanism_Mpmc_Triple_Buffer_Tuple = std::conditional_t<Mpmc_Triple_Buffer_Mechanism<T>, std::tuple<T>, std::tuple<>>;
template <typename Tag, typename Tuple>
struct Has_Tag : std::false_type {};
template <typename Tag, typename... Types>
@@ -328,6 +337,18 @@ template <typename Tuple>
concept Buffer_List = Is_Buffer_List<Tuple>::value;
template <typename Tuple>
concept Dependency_Graph_List = Is_Dependency_Graph_List<Tuple>::value;
template <typename Tuple>
struct Is_Mpmc_Triple_Buffer_List : std::false_type {};
template <typename... Declarations>
struct Is_Mpmc_Triple_Buffer_List<std::tuple<Declarations...>> : Tagged_List_Check<
(Mpmc_Triple_Buffer_Mechanism<Declarations> && ...), Declarations...> {};
template <typename Tuple>
concept Mpmc_Triple_Buffer_List = Is_Mpmc_Triple_Buffer_List<Tuple>::value;
template <typename Tag, typename Tuple>
concept Mpmc_Triple_Buffer_Tag_In = requires {
requires Mpmc_Triple_Buffer_List<Tuple>;
requires Has_Tag<Tag, Tuple>::value;
};
template <typename Tag, typename Tuple>
concept Buffer_Tag_In = requires {
requires Buffer_List<Tuple>;
@@ -655,6 +676,7 @@ concept Object_Core = requires {
requires detail::State_Chain_Matches<typename T::State, typename T::States>;
requires detail::Buffer_List<typename T::Buffers>;
requires detail::Dependency_Graph_List<typename T::Dependency_Graph_Types>;
requires detail::Mpmc_Triple_Buffer_List<typename T::Mpmc_Triple_Buffers>;
};
namespace detail {
template <typename Tuple>
@@ -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<State_Type<Base_Tag>>;
struct Prop : Prop_Type<Base_Tag> {
+27
View File
@@ -35,6 +35,11 @@ using Impl_Dependency_Graph_Types = decltype(std::tuple_cat(
std::declval<typename Base::Dependency_Graph_Types>(),
std::declval<Mechanism_Dependency_Graph_Tuple<Local_Mechanisms>>()...
));
template <typename Base, typename... Local_Mechanisms>
using Impl_Mpmc_Triple_Buffers = decltype(std::tuple_cat(
std::declval<typename Base::Mpmc_Triple_Buffers>(),
std::declval<Mechanism_Mpmc_Triple_Buffer_Tuple<Local_Mechanisms>>()...
));
template <typename Self, typename Base>
using Impl_States = decltype(std::tuple_cat(
std::declval<typename Base::States>(),
@@ -44,6 +49,7 @@ template <typename Self, typename Base, typename... Local_Mechanisms>
concept Impl_Mechanisms = Object_Root<Base> && (Mechanism_Type<Local_Mechanisms> && ...) && requires {
requires Buffer_List<Impl_Buffers<Base, Local_Mechanisms...>>;
requires Dependency_Graph_List<Impl_Dependency_Graph_Types<Base, Local_Mechanisms...>>;
requires Mpmc_Triple_Buffer_List<Impl_Mpmc_Triple_Buffers<Base, Local_Mechanisms...>>;
requires State_List<Impl_States<Self, Base>>;
};
template <typename Callback, typename Private, typename Object_T>
@@ -65,6 +71,7 @@ struct Def : Base {
using Base_Private = typename Base::Private;
using Buffers = detail::Impl_Buffers<Base, Local_Mechanisms...>;
using Dependency_Graph_Types = detail::Impl_Dependency_Graph_Types<Base, Local_Mechanisms...>;
using Mpmc_Triple_Buffers = detail::Impl_Mpmc_Triple_Buffers<Base, Local_Mechanisms...>;
using States = detail::Impl_States<Self, Base>;
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<Impl> {
@@ -131,6 +139,7 @@ struct Impl : Obj {
detail::State_Callback_Storage<typename Obj::State> state_callbacks;
detail::Buffer_Storage<Buffers> buffer_storage;
detail::Dependency_Graph_Storage<Dependency_Graph_Types> dependency_graph_storage;
detail::Mpmc_Triple_Buffer_Storage_Set<Mpmc_Triple_Buffers> mpmc_triple_buffer_storage;
explicit Private(std::pmr::memory_resource* resource) : Publish_Double_Buffer<Prop>(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<detail::Buffer_Dependency_Key<Tag>>());
return *data().buffer_storage.template get<Tag>().pending;
}
template <detail::Mpmc_Triple_Buffer_Tag_In<Mpmc_Triple_Buffers> Tag, typename Input>
void submit_stream(Input&& input) {
data().mpmc_triple_buffer_storage.template get<Tag>().submit(std::forward<Input>(input));
}
template <detail::Mpmc_Triple_Buffer_Tag_In<Mpmc_Triple_Buffers> Tag>
void exchange_stream() {
data().mpmc_triple_buffer_storage.template get<Tag>().advance();
}
template <detail::Mpmc_Triple_Buffer_Tag_In<Mpmc_Triple_Buffers> Tag, typename Callback>
decltype(auto) access_rendering_stream(Callback&& callback) {
return data().mpmc_triple_buffer_storage.template get<Tag>().access_rendering(
std::forward<Callback>(callback));
}
template <detail::Mpmc_Triple_Buffer_Tag_In<Mpmc_Triple_Buffers> Tag, typename Callback>
decltype(auto) access_query_stream(Callback&& callback) const {
return data().mpmc_triple_buffer_storage.template get<Tag>().access_query(
std::forward<Callback>(callback));
}
template <detail::Buffer_Tag_In<Buffers> Tag>
const auto& current_buffer() const {
return *data().buffer_storage.template get<Tag>().current;
+1
View File
@@ -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;
+3 -22
View File
@@ -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<Private&>(*d).push_event(std::move(event));
+4 -1
View File
@@ -5,12 +5,15 @@
#include <memory_resource>
#include <vector>
namespace aethera {
struct Scene_Event_Stream_Tag {};
/* Scene 状态标签,用于访问和订阅 Scene::State。 */
/*
* Scene 只汇总跨渲染后端共有的 Prepare 数据依赖图并构建 Taskflow。
* 用户最终通过 Impl<Scene> 创建可使用实例;编辑 Dependency_Graph 后调用 advance() 提交结构变化,再调用 process(...) 执行当前场景。
*/
struct Scene : Def<Scene, Root, Dependency_Graph_Type<Prepare_Data_Tag, Renderable>> {
struct Scene : Def<Scene, Root,
Dependency_Graph_Type<Prepare_Data_Tag, Renderable>,
Mpmc_Triple_Buffer<Scene_Event_Stream_Tag, std::shared_ptr<Event>>> {
using Event_Pointer = std::shared_ptr<Event>;
using Event_Batch = std::pmr::vector<Event_Pointer>;
using Event_Report_Pointer = std::shared_ptr<const Event>;
+38 -8
View File
@@ -1,6 +1,6 @@
#pragma once
#include <algorithm>
#include <mutex>
#include <span>
#include <unordered_map>
#include <unordered_set>
#include <vector>
@@ -32,18 +32,48 @@ struct Scene::Private : Prev_Private {
};
struct Scene::Private::Runtime {
std::unique_ptr<tf::Taskflow> taskflow; /* 当前已构建的总 Taskflow;为空表示尚未构建。 */
std::mutex event_mutex;
double_buffer::Double_Buffer<Event_Batch> events;
std::mutex report_mutex;
double_buffer::Double_Buffer<Event_Report_Batch> 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 <Attached Object>
void Scene::Private::bind_private_crtp(Object* object) {
Prev_Private::bind_private_crtp(object);
runtime = std::make_unique<Runtime>(object->memory_resource());
runtime->object = object;
runtime->submit_event = [](Root* root, Event_Pointer event) {
static_cast<Object*>(root)->template submit_stream<Scene_Event_Stream_Tag>(std::move(event));
};
runtime->take_events = [](Root* root, std::pmr::memory_resource* resource,
std::uint64_t frame_sequence) {
auto* scene = static_cast<Object*>(root);
scene->template exchange_stream<Scene_Event_Stream_Tag>();
return scene->template access_rendering_stream<Scene_Event_Stream_Tag>(
[&](std::span<Event_Pointer> 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<const Object*>(root)->template access_query_stream<Scene_Event_Stream_Tag>(
[&](std::span<const Event_Pointer> events) {
Event_Report_Batch result{resource};
result.reserve(events.size());
for (const auto& event : events) result.push_back(event);
return result;
});
};
}
template <Event_Object Event_Object_Type, typename... Arguments>
std::shared_ptr<Event_Object_Type> Scene::make_event(Arguments&&... arguments) {
+47
View File
@@ -1,7 +1,17 @@
#include "double_buffer/model.hpp"
#include <gtest/gtest.h>
#include <thread>
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<Stream_Buffer_Tag, int>> {
struct Prop : Prev_Prop {};
struct State : Prev_State { bool operator==(const State&) const = default; };
struct Private : Prev_Private {};
};
using Stream = double_buffer::Impl<Stream_Object>;
struct Test_Object : double_buffer::Def<Test_Object, double_buffer::Root, double_buffer::Tagged_Buffer<Object_Buffer_Tag, int>> {
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<Stream>();
object->submit_stream<Stream_Buffer_Tag>(11);
object->submit_stream<Stream_Buffer_Tag>(13);
object->exchange_stream<Stream_Buffer_Tag>();
object->access_rendering_stream<Stream_Buffer_Tag>([](std::span<int> rendering) {
ASSERT_EQ(rendering.size(), 2);
EXPECT_EQ(rendering[0], 11);
EXPECT_EQ(rendering[1], 13);
});
EXPECT_EQ(object->access_query_stream<Stream_Buffer_Tag>(
[](std::span<const int> query) { return query.size(); }), 0);
object->exchange_stream<Stream_Buffer_Tag>();
object->access_query_stream<Stream_Buffer_Tag>([](std::span<const int> 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<Stream>();
std::vector<std::thread> producers;
for (int producer = 0; producer < 4; ++producer)
producers.emplace_back([&, producer] {
for (int index = 0; index < 100; ++index)
object->submit_stream<Stream_Buffer_Tag>(producer * 100 + index);
});
for (auto& producer : producers) producer.join();
object->exchange_stream<Stream_Buffer_Tag>();
EXPECT_EQ(object->access_rendering_stream<Stream_Buffer_Tag>(
[](std::span<int> rendering) { return rendering.size(); }), 400);
EXPECT_EQ(object->access_query_stream<Stream_Buffer_Tag>(
[](std::span<const int> query) { return query.size(); }), 0);
object->exchange_stream<Stream_Buffer_Tag>();
EXPECT_EQ(object->access_query_stream<Stream_Buffer_Tag>(
[](std::span<const int> query) { return query.size(); }), 400);
}
TEST(state_tag, callback_publishes_only_requested_layer) {
auto object = build_object<Object>();
int calls = 0;
+4 -2
View File
@@ -5,8 +5,11 @@
#include <string>
#include <utility>
namespace aethera::render_2d {
struct Time_Axis_Stream_Tag {};
/* 将连续样本序号显示为一天内时间文本的坐标轴。 */
struct Time_Axis : Def<Time_Axis, Abs_Axis> {
struct Time_Axis : Def<Time_Axis, Abs_Axis,
Mpmc_Triple_Buffer<Time_Axis_Stream_Tag,
std::pair<Axis_Time_Tick, Time_Of_Day>>> {
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<Time_Axis, Abs_Axis> {
/* 时间样本是由 append_time 发布给观察方的运行状态。 */
struct State : Prev_State {
Axis_Time_Tick next_tick{}; /* 下一次 append_time 分配的单调样本序号。 */
std::deque<std::pair<Axis_Time_Tick, Time_Of_Day>> samples{}; /* tick 到时间的保留窗口;最多保留 max(512, visible_count*4) 项。 */
bool operator==(const State&) const;
};
/* 完整声明、时间轴 CRTP 能力及公开薄壳分派见 Time_Axis.ipp。 */
+38 -23
View File
@@ -1,5 +1,6 @@
#pragma once
#include <algorithm>
#include <atomic>
#include <cmath>
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<Axis_Time_Tick> next_tick{};
std::span<const std::pair<Axis_Time_Tick, Time_Of_Day>> 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 <Axis_Object Object>
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<std::remove_pointer_t<decltype(object)>>;
const auto& private_data = static_cast<const typename Object::Private&>(*this);
const auto& state = static_cast<const State&>(*private_data.state.current);
const auto& prop = static_cast<const Prop&>(*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<double>(latest) + 0.5, static_cast<double>(latest - visible_count) + 0.5};
return {static_cast<double>(latest - visible_count) + 0.5, static_cast<double>(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<std::remove_pointer_t<decltype(object)>>;
const auto& private_data = static_cast<const typename Object::Private&>(*this);
const auto& state = static_cast<const State&>(*private_data.state.current);
const auto& prop = static_cast<const Prop&>(*private_data.current);
const auto samples = active_samples;
const int target = static_cast<int>(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 <Axis_Object Object>
const Time_Axis::Private::Time_Dispatch& Time_Axis::Private::time_dispatch_for() {
static const Time_Dispatch result{
[](const Root* root) {
auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
const auto& state = static_cast<const State&>(*private_data.state.current);
return state.samples.size();
return object->template access_query_stream<Time_Axis_Stream_Tag>(
[](std::span<const std::pair<Axis_Time_Tick, Time_Of_Day>> samples) { return samples.size(); });
},
[](Root* root, Time_Of_Day time) {
auto* object = static_cast<Object*>(root);
const auto visible_count = object->template read_prop<Time_Axis::Base_Tag>().visible_count;
int tick{};
object->template update_state<&State::next_tick, &State::samples>([&](State_Access<typename Object::State> states) {
auto& state = states.template get<Time_Axis::Base_Tag>();
tick = state.next_tick++;
state.samples.emplace_back(tick, time);
const auto limit = static_cast<std::size_t>(std::max(512, std::max(2, visible_count) * 4));
while (state.samples.size() > limit) state.samples.pop_front();
});
auto& data = static_cast<typename Object::Private&>(*object->d);
const int tick = data.next_tick.fetch_add(1, std::memory_order_relaxed);
object->template submit_stream<Time_Axis_Stream_Tag>(std::pair{tick, time});
object->template mark_dirty<Prepare_Data_Tag>();
return tick;
},
[](const Root* root, int tick) {
auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
const auto& state = static_cast<const State&>(*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<Time_Axis_Stream_Tag>(
[tick](std::span<const std::pair<Axis_Time_Tick, Time_Of_Day>> 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<Object>();
}
inline void Time_Axis::Private::prepare_data(Attached auto* object) {
using Object = std::remove_pointer_t<decltype(object)>;
object->template exchange_stream<Time_Axis_Stream_Tag>();
auto& private_data = static_cast<typename Object::Private&>(*this);
auto& state = static_cast<State&>(*private_data.state.current);
state.next_tick = next_tick.load(std::memory_order_relaxed);
object->template access_rendering_stream<Time_Axis_Stream_Tag>(
[&](std::span<std::pair<Axis_Time_Tick, Time_Of_Day>> 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 <typename Object, typename Owner, typename Member, typename State_Type>
void Time_Axis::Private::after_state_set(Object* object, Member Owner::*, State_Access<State_Type>) {
if constexpr (std::same_as<Owner, State>) object->template mark_dirty<Prepare_Data_Tag>();
+18 -1
View File
@@ -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<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append(this, values); } void Afterglow::append_spectrum(std::pmr::vector<Plot_Value>&& values) { append_spectrum(std::span<const Plot_Value>(values.data(), values.size())); } std::size_t Afterglow::history_count() const { return static_cast<const Private&>(*d).dispatch->history_count(this); } std::size_t Afterglow::latest_spectrum_point_count() const { return static_cast<const Private&>(*d).dispatch->latest_count(this); } std::size_t Afterglow::rendered_cell_count() const { return static_cast<const Private&>(*d).dispatch->rendered_count(this); } }
bool Afterglow::State::operator==(const State&) const = default;
void Afterglow::append_spectrum(std::span<const Plot_Value> values) {
static_cast<Private&>(*d).dispatch->append(this, {values.begin(), values.end()});
}
void Afterglow::append_spectrum(std::pmr::vector<Plot_Value>&& values) {
static_cast<Private&>(*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<const Private&>(*d).dispatch->history_count(this);
}
std::size_t Afterglow::latest_spectrum_point_count() const {
return static_cast<const Private&>(*d).dispatch->latest_count(this);
}
std::size_t Afterglow::rendered_cell_count() const {
return static_cast<const Private&>(*d).dispatch->rendered_count(this);
}
}
+4 -2
View File
@@ -9,7 +9,10 @@
#include <span>
#include <vector>
namespace aethera::render_2d {
struct Afterglow : Def<Afterglow, Renderable_2D<Renderable_2D_Cache::enabled>> {
struct Afterglow_Stream_Tag {};
struct Afterglow : Def<Afterglow, Renderable_2D<Renderable_2D_Cache::enabled>,
Mpmc_Triple_Buffer<Afterglow_Stream_Tag,
std::shared_ptr<const std::vector<Plot_Value>>>> {
using Scene_Object = Impl<Render_Scene_2D>; using Frequency_Object = Impl<Frequency_Axis>; using Power_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {
std::size_t frequency_point_size{}; /* 栅格频率列数;零值使用最新频谱尺寸。 */
@@ -21,7 +24,6 @@ struct Afterglow : Def<Afterglow, Renderable_2D<Renderable_2D_Cache::enabled>> {
Axis_Range power_range{0.0, 10.0}; /* 色块纵向覆盖的功率范围。 */
Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */
Color_Map color_map{}; /* 强度到颜色的映射。 */
std::vector<std::vector<Plot_Value>> spectra{}; /* 从旧到新的历史频谱唯一权威集合。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
+240 -50
View File
@@ -5,68 +5,258 @@
namespace aethera::render_2d {
struct Afterglow::Private : Prev_Private {
struct Prepared {
detail::Raster_Layout layout{}; /* 两根轴决定的色块矩阵布局。 */
std::vector<Plot_Ratio> intensity{}; /* 历史频谱衰减累加后的未归一化强度。 */
std::vector<Pixel> pixels{}; /* 归一化强度经色图转换后的像素矩阵。 */
Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */
Plot_Ratio maximum{1.0}; /* 本帧强度归一化分母,最小为 1。 */
bool valid{}; /* 轴布局和输入数据是否足以生成色块。 */
std::vector<std::shared_ptr<const std::vector<Plot_Value>>> source_spectra{}; /* Rendering-role ownership retained across Prepare tasks. */
detail::Raster_Layout layout{}; /* 两根轴决定的色块矩阵布局。 */
std::vector<Plot_Ratio> intensity{}; /* 历史频谱衰减累加后的未归一化强度。 */
std::vector<Pixel> pixels{}; /* 归一化强度经色图转换后的像素矩阵。 */
Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */
Plot_Ratio maximum{1.0}; /* 本帧强度归一化分母,最小为 1。 */
bool valid{}; /* 轴布局和输入数据是否足以生成色块。 */
};
using Append_Run = void (*)(Root*, std::span<const Plot_Value>); using Count_Run = std::size_t (*)(const Root*);
using Append_Run = void (*)(Root*, std::vector<Plot_Value>);
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 <Attached Object> void bind_private_crtp(Object* object);
template <Attached Object>
void bind_private_crtp(Object* object);
void bind_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value);
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
template <Attached Object>
[[nodiscard]] static const Dispatch& dispatch_for();
/* CRTP 覆盖:构建累加、归一化和着色三阶段 Prepare 子图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state);
template <Attached Object>
[[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state);
/* CRTP 覆盖:构建消费色块矩阵的 Paint 子图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state);
template <Attached Object>
[[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state);
/* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */
template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object> void prepare_frame(Object* object); template <Attached Object> void accumulate_partition(Object* object, Plot_Partition_Count index); void normalize_frame(); template <Attached Object> void color_partition(Object* object, Plot_Partition_Count index); template <Attached Object> void paint_frame(Object* object);
template <Attached Object>
[[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object>
void prepare_frame(Object* object);
template <Attached Object>
void accumulate_partition(Object* object, Plot_Partition_Count index);
void normalize_frame();
template <Attached Object>
void color_partition(Object* object, Plot_Partition_Count index);
template <Attached Object>
void paint_frame(Object* object);
/* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */
template <typename Object, typename Owner, typename Member, typename Prop_Type> void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> states);
template <typename Object, typename Prop_Type, typename State_Type> void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
template <typename Object, typename Owner, typename Member, typename Prop_Type>
void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> states);
template <typename Object, typename Prop_Type, typename State_Type>
void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
};
template <typename Object> Afterglow::Builder<Object>::Builder(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {}
template <typename Object>
Afterglow::Builder<Object>::Builder(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {}
template <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Afterglow::Builder<Object>::build() {
auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value();
auto& private_data = static_cast<typename Object::Private&>(*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<typename Object::Private&>(*plot->d);
private_data.bind_sources(frequency_axis, power_axis);
private_data.scene_attach = [object = plot.get()](Root* root) -> std::expected<void, Dependency_Graph_Error> {
auto* scene = static_cast<Scene_Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); data.scene = scene; auto* frequency_axis = data.frequency_axis; auto* power_axis = data.power_axis;
return scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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<Scene_Object*>(root);
auto& data = static_cast<typename Object::Private&>(*object->d);
data.scene = scene;
auto* frequency_axis = data.frequency_axis;
auto* power_axis = data.power_axis;
return scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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 <typename Values>
void Afterglow::append_spectrum(const Values& values) {
append_spectrum(std::span<const Plot_Value>(std::data(values), std::size(values)));
}
template <Attached Object>
bool Afterglow::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) {
const std::size_t available = object->template access_query_stream<Afterglow_Stream_Tag>(
[](std::span<const std::shared_ptr<const std::vector<Plot_Value>>> spectra) {
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<std::size_t>(1, columns));
}
template <Attached Object>
tf::Taskflow Afterglow::Private::build_prepare_graph(Object* object, const Prop& state) {
const std::size_t available = object->template access_query_stream<Afterglow_Stream_Tag>(
[](std::span<const std::shared_ptr<const std::vector<Plot_Value>>> spectra) {
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<std::size_t>(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 <Attached Object>
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 <Attached Object>
void Afterglow::Private::prepare_frame(Object* object) {
const auto& state = object->template read_prop<Afterglow::Base_Tag>();
object->template exchange_stream<Afterglow_Stream_Tag>();
prepared = {};
object->template access_rendering_stream<Afterglow_Stream_Tag>(
[&](std::span<std::shared_ptr<const std::vector<Plot_Value>>> spectra) {
prepared.source_spectra.assign(spectra.begin(), spectra.end());
});
const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>();
const auto& power_layout = power_axis->template read_prop<Abs_Axis::Base_Tag>();
const std::size_t available = prepared.source_spectra.empty() ? 0 : prepared.source_spectra.back()->size();
const int columns = static_cast<int>(state.frequency_point_size ? std::min(state.frequency_point_size, available) : available);
const int rows = static_cast<int>(state.power_point_size ? state.power_point_size : std::max<Axis_Pixel_Length>(1.0, std::abs(power_layout.pixel_length)));
prepared = {};
prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().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<std::size_t>(columns) * rows;
prepared.intensity.assign(cells, 0.0);
prepared.pixels.assign(cells, 0);
prepared.valid = true;
}
template <Attached Object>
void Afterglow::Private::accumulate_partition(Object* object, Plot_Partition_Count index) {
if (!prepared.valid) return;
const auto& state = object->template read_prop<Afterglow::Base_Tag>();
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<std::size_t>(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<std::size_t>(columns, (*spectrum)->size());
for (std::size_t column = first; column < std::min(last, count); ++column) {
const int row = std::clamp(static_cast<int>(detail::normalized_plot_value((**spectrum)[column], state.power_range) * (rows - 1)), 0, rows - 1);
prepared.intensity[static_cast<std::size_t>(row) * columns + column] += attenuation;
if (state.interpolate && row + 1 < rows) prepared.intensity[static_cast<std::size_t>(row + 1) * columns + column] += attenuation * 0.35;
}
}
}
inline void Afterglow::Private::normalize_frame() {
if (prepared.valid && !prepared.intensity.empty()) prepared.maximum = std::max<Plot_Ratio>(1.0, *std::max_element(prepared.intensity.begin(), prepared.intensity.end()));
}
template <Attached Object>
void Afterglow::Private::color_partition(Object* object, Plot_Partition_Count index) {
if (!prepared.valid) return;
const auto& state = object->template read_prop<Afterglow::Base_Tag>();
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<std::size_t>(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<std::size_t>(row) * columns + column;
prepared.pixels[prepared.layout.index(static_cast<int>(column), row)] = premultiply(state.color_map.sample(prepared.intensity[cell] / prepared.maximum));
}
}
template <Attached Object>
void Afterglow::Private::paint_frame(Object* object) {
auto& cache = this->paint_surface();
if (!prepared.valid) {
return;
}
detail::Painter painter(cache, prepared.canvas);
detail::paint_raster(painter, prepared.layout, prepared.pixels, Image_Interpolation_Mode::bilinear);
}
template <typename Object, typename Owner, typename Member, typename Prop_Type>
void Afterglow::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) {
if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>();
}
template <typename Object, typename Prop_Type, typename State_Type>
void Afterglow::Private::before_advance(Object* object, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type*, State_Access<const State_Type>) {
auto& state = pending_states.template get<Afterglow::Base_Tag>();
object->template access_query_stream<Afterglow_Stream_Tag>([&](std::span<const std::shared_ptr<const std::vector<Plot_Value>>> 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 <Attached Object>
const Afterglow::Private::Dispatch& Afterglow::Private::dispatch_for() {
static const Dispatch value{
[](Root* root, std::vector<Plot_Value> values) {
auto* object = static_cast<Object*>(root);
object->template submit_stream<Afterglow_Stream_Tag>(
std::make_shared<const std::vector<Plot_Value>>(std::move(values)));
object->template mark_dirty<Prepare_Data_Tag>();
},
[](const Root* root) {
return static_cast<const Object*>(root)->template access_query_stream<Afterglow_Stream_Tag>(
[](std::span<const std::shared_ptr<const std::vector<Plot_Value>>> spectra) { return spectra.size(); });
},
[](const Root* root) {
return static_cast<const Object*>(root)->template access_query_stream<Afterglow_Stream_Tag>(
[](std::span<const std::shared_ptr<const std::vector<Plot_Value>>> spectra) {
return spectra.empty() ? 0 : spectra.back()->size();
});
},
[](const Root* root) {
return static_cast<const Object*>(root)->template access_query_stream<Afterglow_Stream_Tag>(
[](std::span<const std::shared_ptr<const std::vector<Plot_Value>>> spectra) {
return spectra.empty() ? 0 : spectra.back()->size();
});
}
};
return value;
}
template <Attached Object>
void Afterglow::Private::bind_private_crtp(Object* object) {
Prev_Private::bind_private_crtp(object);
dispatch = &dispatch_for<Object>();
}
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 <typename Values> void Afterglow::append_spectrum(const Values& values) { append_spectrum(std::span<const Plot_Value>(std::data(values), std::size(values))); }
template <Attached Object> 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<std::size_t>(1, columns)); }
template <Attached Object>
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<std::size_t>(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 <Attached Object> 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 <Attached Object>
void Afterglow::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop<Afterglow::Base_Tag>(); const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& power_layout = power_axis->template read_prop<Abs_Axis::Base_Tag>(); const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const int columns = static_cast<int>(state.frequency_point_size ? std::min(state.frequency_point_size, available) : available); const int rows = static_cast<int>(state.power_point_size ? state.power_point_size : std::max<Axis_Pixel_Length>(1.0, std::abs(power_layout.pixel_length))); prepared = {}; prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().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<std::size_t>(columns) * rows; prepared.intensity.assign(cells, 0.0); prepared.pixels.assign(cells, 0); prepared.valid = true; }
template <Attached Object>
void Afterglow::Private::accumulate_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop<Afterglow::Base_Tag>(); 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<std::size_t>(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<std::size_t>(columns, spectrum->size()); for (std::size_t column = first; column < std::min(last, count); ++column) { const int row = std::clamp(static_cast<int>(detail::normalized_plot_value((*spectrum)[column], state.power_range) * (rows - 1)), 0, rows - 1); prepared.intensity[static_cast<std::size_t>(row) * columns + column] += attenuation; if (state.interpolate && row + 1 < rows) prepared.intensity[static_cast<std::size_t>(row + 1) * columns + column] += attenuation * 0.35; } } }
inline void Afterglow::Private::normalize_frame() { if (prepared.valid && !prepared.intensity.empty()) prepared.maximum = std::max<Plot_Ratio>(1.0, *std::max_element(prepared.intensity.begin(), prepared.intensity.end())); }
template <Attached Object>
void Afterglow::Private::color_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop<Afterglow::Base_Tag>(); 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<std::size_t>(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<std::size_t>(row) * columns + column; prepared.pixels[prepared.layout.index(static_cast<int>(column), row)] = premultiply(state.color_map.sample(prepared.intensity[cell] / prepared.maximum)); } }
template <Attached Object> 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 <typename Object, typename Owner, typename Member, typename Prop_Type> void Afterglow::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type> void Afterglow::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { auto& state = pending_states.template get<Afterglow::Base_Tag>(); const auto& prop = static_cast<const Prop&>(*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 <Attached Object>
const Afterglow::Private::Dispatch& Afterglow::Private::dispatch_for() { static const Dispatch value{[](Root* root, std::span<const Plot_Value> values) { auto* object = static_cast<Object*>(root); object->template update_prop<&Prop::spectra>([values](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Afterglow::Base_Tag>(); 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<const Object*>(root)->template read_prop<Afterglow::Base_Tag>().spectra.size(); }, [](const Root* root) { const auto& spectra = static_cast<const Object*>(root)->template read_prop<Afterglow::Base_Tag>().spectra; return spectra.empty() ? 0 : spectra.back().size(); }, [](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; }
template <Attached Object> void Afterglow::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); }
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; }
}
@@ -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<Private&>(*d).dispatch->append(this, point); } std::size_t Constellation_Diagram::point_count() const { return static_cast<const Private&>(*d).dispatch->count(this); } void Constellation_Diagram::fit_square_to_axes() { static_cast<Private&>(*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<Private&>(*d).dispatch->append(this, {point, monotonic_milliseconds()});
}
std::size_t Constellation_Diagram::point_count() const {
return static_cast<const Private&>(*d).dispatch->count(this);
}
void Constellation_Diagram::fit_square_to_axes() {
static_cast<Private&>(*d).dispatch->fit(this);
}
}
@@ -8,34 +8,44 @@
#include <vector>
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<Constellation_Diagram, Renderable_2D<Renderable_2D_Cache::enabled>> {
using Scene_Object = Impl<Render_Scene_2D>; using Axis_Object = Impl<Numeric_Axis>;
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<Constellation_Diagram,
Renderable_2D<Renderable_2D_Cache::enabled>,
Mpmc_Triple_Buffer<Constellation_Stream_Tag, Constellation_Point>> {
using Scene_Object = Impl<Render_Scene_2D>;
using Axis_Object = Impl<Numeric_Axis>;
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<Constellation_Point> 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 <typename Object> struct Builder : Prev_Builder<Object> {
template <typename Object>
struct Builder : Prev_Builder<Object> {
using Base = Prev_Builder<Object>;
Builder(Axis_Object* i_axis, Axis_Object* q_axis);
[[nodiscard]] std::expected<std::unique_ptr<Object>, 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"
@@ -5,44 +5,159 @@
namespace aethera::render_2d {
struct Constellation_Diagram::Private : Prev_Private {
struct Prepared {
std::vector<Point_F> points{}; /* 尚未过期的接收点画布坐标。 */
std::vector<Point_F> anchors{}; /* 当前调制类型的理想锚点画布坐标。 */
Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */
bool valid{}; /* 两根轴是否正交且画布有效。 */
std::vector<Point_F> points{}; /* 尚未过期的接收点画布坐标。 */
std::vector<Point_F> 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 <Attached Object> void bind_private_crtp(Object* object);
template <Attached Object>
void bind_private_crtp(Object* object);
void bind_sources(Axis_Object* i_axis_value, Axis_Object* q_axis_value);
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
template <Attached Object>
[[nodiscard]] static const Dispatch& dispatch_for();
/* CRTP 覆盖:从当前点集和轴状态准备画布坐标。 */
template <Attached Object> void prepare_data(Object* object);
template <Attached Object>
void prepare_data(Object* object);
/* CRTP 覆盖:直接绘制已准备的星座点与锚点。 */
template <Attached Object> void paint(Object* object);
template <Attached Object>
void paint(Object* object);
/* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */
template <typename Object, typename Owner, typename Member, typename Prop_Type> void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> states);
template <typename Object, typename Prop_Type, typename State_Type> void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
template <typename Object, typename Owner, typename Member, typename Prop_Type>
void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> states);
template <typename Object, typename Prop_Type, typename State_Type>
void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
};
template <typename Object> Constellation_Diagram::Builder<Object>::Builder(Axis_Object* i_axis_value, Axis_Object* q_axis_value) : Base(), i_axis(i_axis_value), q_axis(q_axis_value) {}
template <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Constellation_Diagram::Builder<Object>::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); auto& private_data = static_cast<typename Object::Private&>(*plot->d); private_data.bind_sources(i_axis, q_axis); private_data.scene_attach = [object = plot.get()](Root* root) -> std::expected<void, Dependency_Graph_Error> { auto* scene = static_cast<Scene_Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); data.scene = scene; auto* i_axis = data.i_axis; auto* q_axis = data.q_axis; return scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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 <Attached Object>
void Constellation_Diagram::Private::prepare_data(Object* object) { const auto& state = object->template read_prop<Constellation_Diagram::Base_Tag>(); const auto& i_layout = i_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& q_layout = q_axis->template read_prop<Abs_Axis::Base_Tag>(); prepared = {}; prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().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<int>(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<Paint_Tag>(); }
template <Attached Object> void Constellation_Diagram::Private::paint(Object* object) { const auto& state = object->template read_prop<Constellation_Diagram::Base_Tag>(); 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 <typename Object, typename Owner, typename Member, typename Prop_Type> void Constellation_Diagram::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type> void Constellation_Diagram::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { pending_states.template get<Constellation_Diagram::Base_Tag>().point_count = static_cast<const Prop&>(*current_prop).points.size(); }
template <Attached Object>
const Constellation_Diagram::Private::Dispatch& Constellation_Diagram::Private::dispatch_for() { static const Dispatch value{[](Root* root, Point_F point) { auto* object = static_cast<Object*>(root); const auto submitted = monotonic_milliseconds(); object->template update_prop<&Prop::points>([=](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Constellation_Diagram::Base_Tag>(); 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<const Object*>(root)->template read_prop<Constellation_Diagram::Base_Tag>().points.size(); }, [](Root* root) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); const auto& state = object->template read_prop<Constellation_Diagram::Base_Tag>(); 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 <Attached Object> void Constellation_Diagram::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); }
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<Object>::Builder(Axis_Object* i_axis_value, Axis_Object* q_axis_value) : Base(), i_axis(i_axis_value), q_axis(q_axis_value) {}
template <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Constellation_Diagram::Builder<Object>::build() {
auto result = Base::build();
if (!result) return std::unexpected(result.error());
auto plot = std::move(result).value();
auto& private_data = static_cast<typename Object::Private&>(*plot->d);
private_data.bind_sources(i_axis, q_axis);
private_data.scene_attach = [object = plot.get()](Root* root) -> std::expected<void, Dependency_Graph_Error> {
auto* scene = static_cast<Scene_Object*>(root);
auto& data = static_cast<typename Object::Private&>(*object->d);
data.scene = scene;
auto* i_axis = data.i_axis;
auto* q_axis = data.q_axis;
return scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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 <Attached Object>
void Constellation_Diagram::Private::prepare_data(Object* object) {
const auto& state = object->template read_prop<Constellation_Diagram::Base_Tag>();
object->template exchange_stream<Constellation_Stream_Tag>();
const auto& i_layout = i_axis->template read_prop<Abs_Axis::Base_Tag>();
const auto& q_layout = q_axis->template read_prop<Abs_Axis::Base_Tag>();
prepared = {};
prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport;
if (prepared.canvas.empty() || i_layout.orientation == q_layout.orientation) {
return;
}
const auto current = monotonic_milliseconds();
object->template access_rendering_stream<Constellation_Stream_Tag>(
[&](std::span<Constellation_Point> 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<int>(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<Paint_Tag>();
}
template <Attached Object>
void Constellation_Diagram::Private::paint(Object* object) {
const auto& state = object->template read_prop<Constellation_Diagram::Base_Tag>();
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 <typename Object, typename Owner, typename Member, typename Prop_Type>
void Constellation_Diagram::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) {
if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>();
}
template <typename Object, typename Prop_Type, typename State_Type>
void Constellation_Diagram::Private::before_advance(Object* object, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type*, State_Access<const State_Type>) {
pending_states.template get<Constellation_Diagram::Base_Tag>().point_count =
object->template access_query_stream<Constellation_Stream_Tag>(
[](std::span<const Constellation_Point> points) { return points.size(); });
}
template <Attached Object>
const Constellation_Diagram::Private::Dispatch& Constellation_Diagram::Private::dispatch_for() {
static const Dispatch value{
[](Root* root, Constellation_Point point) {
auto* object = static_cast<Object*>(root);
object->template submit_stream<Constellation_Stream_Tag>(std::move(point));
object->template mark_dirty<Prepare_Data_Tag>();
},
[](const Root* root) {
return static_cast<const Object*>(root)->template access_query_stream<Constellation_Stream_Tag>(
[](std::span<const Constellation_Point> points) { return points.size(); });
},
[](Root* root) {
auto* object = static_cast<Object*>(root);
auto& data = static_cast<typename Object::Private&>(*object->d);
const auto& state = object->template read_prop<Constellation_Diagram::Base_Tag>();
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 <Attached Object>
void Constellation_Diagram::Private::bind_private_crtp(Object* object) {
Prev_Private::bind_private_crtp(object);
dispatch = &dispatch_for<Object>();
}
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;
}
}
@@ -12,7 +12,9 @@ struct Frequency_Trace_Sample {
Plot_Value value{}; /* 该时间点对应的频率值。 */
bool operator==(const Frequency_Trace_Sample&) const;
};
struct Frequency_Trace : Def<Frequency_Trace, Renderable_2D<Renderable_2D_Cache::enabled>> {
struct Frequency_Trace_Stream_Tag {};
struct Frequency_Trace : Def<Frequency_Trace, Renderable_2D<Renderable_2D_Cache::enabled>,
Mpmc_Triple_Buffer<Frequency_Trace_Stream_Tag, Frequency_Trace_Sample>> {
using Scene_Object = Impl<Render_Scene_2D>;
using Time_Object = Impl<Time_Axis>;
using Value_Object = Impl<Numeric_Axis>;
@@ -20,7 +22,6 @@ struct Frequency_Trace : Def<Frequency_Trace, Renderable_2D<Renderable_2D_Cache:
Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */
Pen pen{Color::yellow()}; /* 频率轨迹折线样式。 */
Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */
std::vector<Frequency_Trace_Sample> samples{}; /* 已提交轨迹样本的唯一权威集合。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
@@ -46,10 +46,10 @@ std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Frequency_Trace::
private_data.scene_attach = [object = trace.get()](Root* root) -> std::expected<void, Dependency_Graph_Error> { auto* scene = static_cast<Scene_Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); data.scene = scene; auto* time_axis = data.time_axis; auto* value_axis = data.value_axis; return scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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 <Attached Object>
bool Frequency_Trace::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, 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<Frequency_Trace_Stream_Tag>([](std::span<const Frequency_Trace_Sample> samples) { return samples.size(); })); }
template <Attached Object>
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<Frequency_Trace_Stream_Tag>([](std::span<const Frequency_Trace_Sample> 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 <Attached Object>
void Frequency_Trace::Private::prepare_frame(Object* object, Plot_Partition_Count partition_count) {
const auto& state = object->template read_prop<Frequency_Trace::Base_Tag>(); const auto& time_layout = time_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& value_layout = value_axis->template read_prop<Abs_Axis::Base_Tag>();
const auto visible_count = static_cast<std::size_t>(std::max<Axis_Visible_Count>(2, time_axis->template read_prop<Time_Axis::Base_Tag>().visible_count));
object->template exchange_stream<Frequency_Trace_Stream_Tag>();
prepared = {}; prepared.partitions.resize(partition_count); prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().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<Plot_Coordinate>(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<Frequency_Trace_Stream_Tag>([&](std::span<Frequency_Trace_Sample> samples) {
prepared.samples.reserve(samples.size());
for (const auto& sample : samples) prepared.samples.push_back({static_cast<Plot_Coordinate>(sample.tick), sample.value});
});
prepared.valid = !prepared.samples.empty();
}
template <Attached Object>
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 <typename Object, typename Owner, typename Member, typename Prop_Type>
void Frequency_Trace::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type>
void Frequency_Trace::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { auto& state = pending_states.template get<Frequency_Trace::Base_Tag>(); const auto& prop = static_cast<const Prop&>(*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<State_Type> pending_states, const Prop_Type*, State_Access<const State_Type>) { auto& state = pending_states.template get<Frequency_Trace::Base_Tag>(); state.sample_count = object->template access_query_stream<Frequency_Trace_Stream_Tag>([](std::span<const Frequency_Trace_Sample> samples) { return samples.size(); }); state.rendered_point_count = state.sample_count; }
template <Attached Object>
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<Object*>(root);
const auto& data = static_cast<const typename Object::Private&>(*object->d);
const auto visible_count = static_cast<std::size_t>(std::max<Axis_Visible_Count>(
2, data.time_axis->template read_prop<Time_Axis::Base_Tag>().visible_count));
object->template update_prop<&Prop::samples>([=](Prop_Access<typename Object::Prop> props) {
auto& samples = props.template get<Frequency_Trace::Base_Tag>().samples;
samples.push_back({tick, sample_value});
if (samples.size() > visible_count)
samples.erase(samples.begin(), samples.begin() + static_cast<std::ptrdiff_t>(samples.size() - visible_count));
});
object->template submit_stream<Frequency_Trace_Stream_Tag>(Frequency_Trace_Sample{tick, sample_value});
object->template mark_dirty<Prepare_Data_Tag>();
},
[](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Frequency_Trace::Base_Tag>().samples.size(); },
[](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(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<const Object*>(root)->template access_query_stream<Frequency_Trace_Stream_Tag>([](std::span<const Frequency_Trace_Sample> samples) { return samples.size(); }); },
[](const Root* root) { return static_cast<const Object*>(root)->template access_query_stream<Frequency_Trace_Stream_Tag>([](std::span<const Frequency_Trace_Sample> samples) { return samples.size(); }); }
}; return value;
}
template <Attached Object>
@@ -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<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append(this, values); }
void Sweep_Spectrum::append_block(std::pmr::vector<Plot_Value>&& values) { append_block(std::span<const Plot_Value>(values.data(), values.size())); }
void Sweep_Spectrum::append_block(std::span<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append(this, {values.begin(), values.end()}); }
void Sweep_Spectrum::append_block(std::pmr::vector<Plot_Value>&& values) { static_cast<Private&>(*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<const Private&>(*d).dispatch->block_count(this); }
std::size_t Sweep_Spectrum::stored_point_count() const { return static_cast<const Private&>(*d).dispatch->point_count(this); }
std::size_t Sweep_Spectrum::rendered_point_count() const { return static_cast<const Private&>(*d).dispatch->rendered_point_count(this); }
@@ -9,14 +9,16 @@
#include <span>
#include <vector>
namespace aethera::render_2d {
struct Sweep_Spectrum : Def<Sweep_Spectrum, Renderable_2D<Renderable_2D_Cache::enabled>> {
struct Sweep_Spectrum_Stream_Tag {};
struct Sweep_Spectrum : Def<Sweep_Spectrum, Renderable_2D<Renderable_2D_Cache::enabled>,
Mpmc_Triple_Buffer<Sweep_Spectrum_Stream_Tag,
std::shared_ptr<const std::vector<Plot_Value>>>> {
using Scene_Object = Impl<Render_Scene_2D>;
using Frequency_Object = Impl<Frequency_Axis>;
using Power_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {
std::size_t bins_per_block{}; /* 每个扫频块期望的功率点数;零值接受首块尺寸。 */
std::size_t block_count{1}; /* 一个完整扫频周期包含的块数;零值按 1 处理。 */
std::size_t 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<Sweep_Spectrum, Renderable_2D<Renderable_2D_Cache::e
Pen pen{Color::yellow()}; /* 扫频折线样式。 */
Pen current_frequency_pen{Color::red_color(), 2.0}; /* 当前扫频位置垂线样式。 */
Line_Interpolation_Mode interpolation_mode{Line_Interpolation_Mode::linear_value}; /* 相邻功率点插值方式。 */
std::vector<std::vector<Plot_Value>> blocks{}; /* 各频率段最新数据的唯一权威槽位;下标即频率段。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
@@ -13,7 +13,7 @@ struct Sweep_Spectrum::Private : Prev_Private {
Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */
bool valid{}; /* 两根轴正交、画布及组合折线均有效。 */
};
using Append_Run = void (*)(Root*, std::span<const Plot_Value>); using Count_Run = std::size_t (*)(const Root*);
using Append_Run = void (*)(Root*, std::vector<Plot_Value>); using Count_Run = std::size_t (*)(const Root*);
struct Dispatch {
Append_Run append; /* 向最终对象提交一个扫描块。 */
Count_Run block_count; /* 查询权威扫描块数。 */
@@ -51,22 +51,29 @@ std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Sweep_Spectrum::B
}
template <typename Values> void Sweep_Spectrum::append_block(const Values& values) { append_block(std::span<const Plot_Value>(std::data(values), std::size(values))); }
template <Attached Object>
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<Sweep_Spectrum_Stream_Tag>([](std::span<const std::shared_ptr<const std::vector<Plot_Value>>> 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 <Attached Object>
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<Sweep_Spectrum_Stream_Tag>([](std::span<const std::shared_ptr<const std::vector<Plot_Value>>> 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 <Attached Object>
tf::Taskflow Sweep_Spectrum::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("sweep_spectrum.paint.frame"); return graph; }
template <Attached Object>
void Sweep_Spectrum::Private::prepare_frame(Object* object) {
const auto& state = object->template read_prop<Sweep_Spectrum::Base_Tag>(); const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& power_layout = power_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& power_state = power_axis->template read_prop<Numeric_Axis::Base_Tag>(); prepared = {}; prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport;
if (prepared.canvas.empty() || frequency_layout.orientation == power_layout.orientation || state.blocks.empty()) return;
const std::size_t block_count = std::max<std::size_t>(1, state.block_count);
const std::size_t stored_block_count = std::min(block_count, state.blocks.size());
object->template exchange_stream<Sweep_Spectrum_Stream_Tag>();
std::size_t available_blocks{};
object->template access_rendering_stream<Sweep_Spectrum_Stream_Tag>([&](std::span<std::shared_ptr<const std::vector<Plot_Value>>> 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<double>(stored_block_count) / static_cast<double>(block_count);
const double marker_progress = static_cast<double>(latest_block_index + 1) / static_cast<double>(block_count);
const auto domain_target = state.frequency_range.origin + state.frequency_range.length() * domain_progress;
@@ -82,16 +89,16 @@ void Sweep_Spectrum::Private::prepare_partition(Object* object, Plot_Partition_C
template <Attached Object>
void Sweep_Spectrum::Private::paint_frame(Object* object) { const auto& state = object->template read_prop<Sweep_Spectrum::Base_Tag>(); auto& cache = this->paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); painter.line(prepared.marker_first, prepared.marker_second, state.current_frequency_pen); }
template <typename Object, typename Owner, typename Member, typename Prop_Type>
void Sweep_Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type> props) { if constexpr (std::same_as<Owner, Prop>) { auto& state = props.template get<Sweep_Spectrum::Base_Tag>(); const std::size_t block_count = std::max<std::size_t>(1, state.block_count); if (state.blocks.size() > block_count) state.blocks.resize(block_count); state.next_block_index = state.blocks.size() < block_count ? state.blocks.size() : state.next_block_index % block_count; object->template mark_dirty<Prepare_Data_Tag>(); } }
void Sweep_Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type>
void Sweep_Spectrum::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { auto& state = pending_states.template get<Sweep_Spectrum::Base_Tag>(); const auto& prop = static_cast<const Prop&>(*current_prop); state.stored_block_count = prop.blocks.size(); state.stored_point_count = 0; for (const auto& block : prop.blocks) state.stored_point_count += block.size(); state.rendered_point_count = 0; for (const auto& partition : prepared.partitions) state.rendered_point_count += partition.points.size(); }
void Sweep_Spectrum::Private::before_advance(Object* object, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type*, State_Access<const State_Type>) { auto& state = pending_states.template get<Sweep_Spectrum::Base_Tag>(); object->template access_query_stream<Sweep_Spectrum_Stream_Tag>([&](std::span<const std::shared_ptr<const std::vector<Plot_Value>>> 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 <Attached Object>
const Sweep_Spectrum::Private::Dispatch& Sweep_Spectrum::Private::dispatch_for() {
static const Dispatch value{
[](Root* root, std::span<const Plot_Value> values) { auto* object = static_cast<Object*>(root); object->template update_prop<&Prop::blocks, &Prop::next_block_index>([values](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Sweep_Spectrum::Base_Tag>(); const std::size_t block_count = std::max<std::size_t>(1, state.block_count); if (state.blocks.size() > block_count) state.blocks.resize(block_count); if (state.blocks.size() < block_count) { state.next_block_index = state.blocks.size(); state.blocks.emplace_back(values.begin(), values.end()); } else { state.next_block_index %= block_count; state.blocks[state.next_block_index].assign(values.begin(), values.end()); } state.next_block_index = (state.next_block_index + 1) % block_count; }); },
[](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Sweep_Spectrum::Base_Tag>().blocks.size(); },
[](const Root* root) { const auto& blocks = static_cast<const Object*>(root)->template read_prop<Sweep_Spectrum::Base_Tag>().blocks; std::size_t count{}; for (const auto& block : blocks) count += block.size(); return count; },
[](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); std::size_t count{}; for (const auto& curve : data.prepared.partitions) count += curve.points.size(); return count; }
[](Root* root, std::vector<Plot_Value> values) { auto* object = static_cast<Object*>(root); object->template submit_stream<Sweep_Spectrum_Stream_Tag>(std::make_shared<const std::vector<Plot_Value>>(std::move(values))); object->template mark_dirty<Prepare_Data_Tag>(); },
[](const Root* root) { return static_cast<const Object*>(root)->template access_query_stream<Sweep_Spectrum_Stream_Tag>([](std::span<const std::shared_ptr<const std::vector<Plot_Value>>> blocks) { return blocks.size(); }); },
[](const Root* root) { return static_cast<const Object*>(root)->template access_query_stream<Sweep_Spectrum_Stream_Tag>([](std::span<const std::shared_ptr<const std::vector<Plot_Value>>> blocks) { std::size_t count{}; for (const auto& block : blocks) count += block->size(); return count; }); },
[](const Root* root) { return static_cast<const Object*>(root)->template access_query_stream<Sweep_Spectrum_Stream_Tag>([](std::span<const std::shared_ptr<const std::vector<Plot_Value>>> blocks) { std::size_t count{}; for (const auto& block : blocks) count += block->size(); return count; }); }
}; return value;
}
template <Attached Object> void Sweep_Spectrum::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); }
+20 -2
View File
@@ -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<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append(this, tick, values); } void Waterfall::append_row(Plot_Time_Tick tick, std::pmr::vector<Plot_Value>&& values) { append_row(tick, std::span<const Plot_Value>(values.data(), values.size())); } std::size_t Waterfall::row_count() const { return static_cast<const Private&>(*d).dispatch->row_count(this); } std::size_t Waterfall::stored_point_count() const { return static_cast<const Private&>(*d).dispatch->point_count(this); } std::size_t Waterfall::rendered_cell_count() const { return static_cast<const Private&>(*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<const Plot_Value> values) {
static_cast<Private&>(*d).dispatch->append(this, Waterfall_Row{tick, {values.begin(), values.end()}});
}
void Waterfall::append_row(Plot_Time_Tick tick, std::pmr::vector<Plot_Value>&& values) {
static_cast<Private&>(*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<const Private&>(*d).dispatch->row_count(this);
}
std::size_t Waterfall::stored_point_count() const {
return static_cast<const Private&>(*d).dispatch->point_count(this);
}
std::size_t Waterfall::rendered_cell_count() const {
return static_cast<const Private&>(*d).dispatch->rendered_count(this);
}
}
+3 -2
View File
@@ -15,7 +15,9 @@ struct Waterfall_Row {
std::vector<Plot_Value> values{}; /* 该时间槽从低频到高频排列的功率值。 */
bool operator==(const Waterfall_Row&) const;
};
struct Waterfall : Def<Waterfall, Renderable_2D<Renderable_2D_Cache::enabled>> {
struct Waterfall_Stream_Tag {};
struct Waterfall : Def<Waterfall, Renderable_2D<Renderable_2D_Cache::enabled>,
Mpmc_Triple_Buffer<Waterfall_Stream_Tag, std::shared_ptr<const Waterfall_Row>>> {
using Scene_Object = Impl<Render_Scene_2D>;
using Frequency_Object = Impl<Frequency_Axis>;
using Time_Object = Impl<Time_Axis>;
@@ -32,7 +34,6 @@ struct Waterfall : Def<Waterfall, Renderable_2D<Renderable_2D_Cache::enabled>> {
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<Waterfall_Row> rows{}; /* 从旧到新的瀑布行唯一权威集合。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
+253 -53
View File
@@ -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<Pixel> pixels{}; /* 固定时间窗口的像素矩阵;无数据槽保持透明。 */
std::vector<Prepared_Row> rows{}; /* 可见源行到固定时间槽位的映射。 */
Rect_F tooltip_box{}; /* 当前 hover 提示框的画布矩形。 */
std::string tooltip_text{}; /* 当前 hover 频率文本;空值表示不绘制。 */
Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */
int source_first{}; /* 可视频段在源频谱行中的首列。 */
bool valid{}; /* 轴布局和行数据是否足以生成色块。 */
std::vector<std::shared_ptr<const Waterfall_Row>> source_rows{}; /* Rendering-role row ownership retained through parallel Prepare tasks. */
detail::Raster_Layout layout{}; /* 可视频段与完整时间窗口组成的色块布局。 */
std::vector<Pixel> pixels{}; /* 固定时间窗口的像素矩阵;无数据槽保持透明。 */
std::vector<Prepared_Row> 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<const Plot_Value>); 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 <Attached Object> void bind_private_crtp(Object* object);
template <Attached Object>
void bind_private_crtp(Object* object);
void bind_sources(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value);
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
template <Attached Object>
[[nodiscard]] static const Dispatch& dispatch_for();
/* CRTP 覆盖:按当前色块工作量构建分块 Prepare 子图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state);
template <Attached Object>
[[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state);
/* CRTP 覆盖:构建消费色块矩阵和提示信息的 Paint 子图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state);
template <Attached Object>
[[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state);
/* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */
template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object> void prepare_frame(Object* object); template <Attached Object> void prepare_partition(Object* object, Plot_Partition_Count index); template <Attached Object> void paint_frame(Object* object);
template <Attached Object>
[[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object>
void prepare_frame(Object* object);
template <Attached Object>
void prepare_partition(Object* object, Plot_Partition_Count index);
template <Attached Object>
void paint_frame(Object* object);
/* CRTP 覆盖:更新 hover 位置并请求重绘。 */
template <Attached Object> void handle_event(Object* object, const Event& event);
template <Attached Object>
void handle_event(Object* object, const Event& event);
/* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */
template <typename Object, typename Owner, typename Member, typename Prop_Type> void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> states);
template <typename Object, typename Prop_Type, typename State_Type> void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
template <typename Object, typename Owner, typename Member, typename Prop_Type>
void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> states);
template <typename Object, typename Prop_Type, typename State_Type>
void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
};
template <typename Object> Waterfall::Builder<Object>::Builder(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) : Base(), frequency_axis(frequency_axis_value), time_axis(time_axis_value) {}
template <typename Object>
Waterfall::Builder<Object>::Builder(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) : Base(), frequency_axis(frequency_axis_value), time_axis(time_axis_value) {}
template <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Waterfall::Builder<Object>::build() {
auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value();
auto& private_data = static_cast<typename Object::Private&>(*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<typename Object::Private&>(*plot->d);
private_data.bind_sources(frequency_axis, time_axis);
private_data.scene_attach = [object = plot.get()](Root* root) -> std::expected<void, Dependency_Graph_Error> {
auto* scene = static_cast<Scene_Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); data.scene = scene; auto* frequency_axis = data.frequency_axis; auto* time_axis = data.time_axis;
return scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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<Scene_Object*>(root);
auto& data = static_cast<typename Object::Private&>(*object->d);
data.scene = scene;
auto* frequency_axis = data.frequency_axis;
auto* time_axis = data.time_axis;
return scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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 <typename Values>
void Waterfall::append_row(Plot_Time_Tick tick, const Values& values) {
append_row(tick, std::span<const Plot_Value>(std::data(values), std::size(values)));
}
template <Attached Object>
bool Waterfall::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) {
const std::size_t cells = object->template access_query_stream<Waterfall_Stream_Tag>([&](std::span<const std::shared_ptr<const Waterfall_Row>> rows) {
return rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : rows.empty() ? 1 : rows.back()->values.size());
});
return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, cells);
}
template <Attached Object>
tf::Taskflow Waterfall::Private::build_prepare_graph(Object* object, const Prop& state) {
const std::size_t cells = object->template access_query_stream<Waterfall_Stream_Tag>([&](std::span<const std::shared_ptr<const Waterfall_Row>> rows) {
return rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : rows.empty() ? 1 : rows.back()->values.size());
});
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 <Attached Object>
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 <Attached Object>
void Waterfall::Private::prepare_frame(Object* object) {
const auto& state = object->template read_prop<Waterfall::Base_Tag>();
object->template exchange_stream<Waterfall_Stream_Tag>();
prepared = {};
object->template access_rendering_stream<Waterfall_Stream_Tag>([&](std::span<std::shared_ptr<const Waterfall_Row>> rows) {
prepared.source_rows.assign(rows.begin(), rows.end());
});
const auto& rows = prepared.source_rows;
const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>();
const auto& time_layout = time_axis->template read_prop<Abs_Axis::Base_Tag>();
prepared = {};
prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().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<int>(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<Axis_Visible_Count>(2, time_axis->template read_prop<Time_Axis::Base_Tag>().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<std::size_t>(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<int>(std::llround((static_cast<Axis_Coordinate>(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 <Attached Object>
void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) {
if (!prepared.valid) return;
const auto& state = object->template read_prop<Waterfall::Base_Tag>();
const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height;
const std::size_t cells = static_cast<std::size_t>(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<std::size_t>(columns)];
const int column = static_cast<int>(cell % static_cast<std::size_t>(columns));
const auto& values = prepared.source_rows[row.source]->values;
const std::size_t source = static_cast<std::size_t>(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 <Attached Object>
void Waterfall::Private::paint_frame(Object* object) {
const auto& state = object->template read_prop<Waterfall::Base_Tag>();
auto& cache = this->paint_surface();
if (!prepared.valid) {
return;
}
detail::Painter painter(cache, prepared.canvas);
detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode);
if (!prepared.tooltip_text.empty()) {
painter.rect(prepared.tooltip_box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush);
painter.text({prepared.tooltip_box.x + 4.0, prepared.tooltip_box.y + 3.0}, prepared.tooltip_text, state.tooltip_font, state.tooltip_text_pen);
}
}
template <Attached Object>
void Waterfall::Private::handle_event(Object* object, const Event& event) {
if (detail::update_hover_tooltip(tooltip, event)) object->template mark_dirty<Paint_Tag>();
}
template <typename Object, typename Owner, typename Member, typename Prop_Type>
void Waterfall::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) {
if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>();
}
template <typename Object, typename Prop_Type, typename State_Type>
void Waterfall::Private::before_advance(Object* object, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type*, State_Access<const State_Type>) {
auto& state = pending_states.template get<Waterfall::Base_Tag>();
object->template access_query_stream<Waterfall_Stream_Tag>([&](std::span<const std::shared_ptr<const Waterfall_Row>> 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 <Attached Object>
const Waterfall::Private::Dispatch& Waterfall::Private::dispatch_for() {
static const Dispatch value{
[](Root* root, Waterfall_Row row) {
auto* object = static_cast<Object*>(root);
object->template submit_stream<Waterfall_Stream_Tag>(
std::make_shared<const Waterfall_Row>(std::move(row)));
object->template mark_dirty<Prepare_Data_Tag>();
},
[](const Root* root) {
return static_cast<const Object*>(root)->template access_query_stream<Waterfall_Stream_Tag>(
[](std::span<const std::shared_ptr<const Waterfall_Row>> rows) { return rows.size(); });
},
[](const Root* root) {
return static_cast<const Object*>(root)->template access_query_stream<Waterfall_Stream_Tag>(
[](std::span<const std::shared_ptr<const Waterfall_Row>> rows) {
std::size_t count{};
for (const auto& row : rows) count += row->values.size();
return count;
});
},
[](const Root* root) {
return static_cast<const Object*>(root)->template access_query_stream<Waterfall_Stream_Tag>(
[](std::span<const std::shared_ptr<const Waterfall_Row>> rows) {
std::size_t count{};
for (const auto& row : rows) count += row->values.size();
return count;
});
}
};
return value;
}
template <Attached Object>
void Waterfall::Private::bind_private_crtp(Object* object) {
Prev_Private::bind_private_crtp(object);
dispatch = &dispatch_for<Object>();
}
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 <typename Values> void Waterfall::append_row(Plot_Time_Tick tick, const Values& values) { append_row(tick, std::span<const Plot_Value>(std::data(values), std::size(values))); }
template <Attached Object> 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 <Attached Object> 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 <Attached Object> 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 <Attached Object>
void Waterfall::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop<Waterfall::Base_Tag>(); const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& time_layout = time_axis->template read_prop<Abs_Axis::Base_Tag>(); prepared = {}; prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().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<int>(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<Axis_Visible_Count>(2, time_axis->template read_prop<Time_Axis::Base_Tag>().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<std::size_t>(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<int>(std::llround((static_cast<Axis_Coordinate>(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 <Attached Object>
void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop<Waterfall::Base_Tag>(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const std::size_t cells = static_cast<std::size_t>(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<std::size_t>(columns)]; const int column = static_cast<int>(cell % static_cast<std::size_t>(columns)); const auto& values = state.rows[row.source].values; const std::size_t source = static_cast<std::size_t>(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 <Attached Object> void Waterfall::Private::paint_frame(Object* object) { const auto& state = object->template read_prop<Waterfall::Base_Tag>(); auto& cache = this->paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode); if (!prepared.tooltip_text.empty()) { painter.rect(prepared.tooltip_box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); painter.text({prepared.tooltip_box.x + 4.0, prepared.tooltip_box.y + 3.0}, prepared.tooltip_text, state.tooltip_font, state.tooltip_text_pen); } }
template <Attached Object> void Waterfall::Private::handle_event(Object* object, const Event& event) { if (detail::update_hover_tooltip(tooltip, event)) object->template mark_dirty<Paint_Tag>(); }
template <typename Object, typename Owner, typename Member, typename Prop_Type> void Waterfall::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type> void Waterfall::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { auto& state = pending_states.template get<Waterfall::Base_Tag>(); const auto& prop = static_cast<const Prop&>(*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<std::size_t>(columns) : 0; }
template <Attached Object>
const Waterfall::Private::Dispatch& Waterfall::Private::dispatch_for() { static const Dispatch value{[](Root* root, Plot_Time_Tick tick, std::span<const Plot_Value> values) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); const std::size_t row_limit = static_cast<std::size_t>(std::max<Axis_Visible_Count>(2, data.time_axis->template read_prop<Time_Axis::Base_Tag>().visible_count)); object->template update_prop<&Prop::rows>([=](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Waterfall::Base_Tag>(); 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<const Object*>(root)->template read_prop<Waterfall::Base_Tag>().rows.size(); }, [](const Root* root) { const auto& rows = static_cast<const Object*>(root)->template read_prop<Waterfall::Base_Tag>().rows; std::size_t count{}; for (const auto& row : rows) count += row.values.size(); return count; }, [](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; }
template <Attached Object> void Waterfall::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); }
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; }
}
+7 -8
View File
@@ -152,8 +152,7 @@ std::unique_ptr<detail::Renderable_Descriptor> make_axis_component<Time_Axis_Obj
Prop_Field<&Time_Axis::Prop::estimated_label_width_px, "estimated_label_width_px", "Estimated time label width.">,
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<Time_Axis, &Time_Axis::State::next_tick, "next_tick", "Next allocated time tick.">,
State_Field<Time_Axis, &Time_Axis::State::samples, "samples", "Published time sample window.">>(
State_Field<Time_Axis, &Time_Axis::State::next_tick, "next_tick", "Next allocated time tick.">>(
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<Frequency_Trace_Sample> samples(count);
for (std::size_t index = 0; index < count; ++index)
samples[index] = {static_cast<Plot_Time_Tick>(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<Definition, Sweep_Spectrum>) {
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<Definition, Afterglow>) {
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<Definition, Waterfall>) {
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<Plot_Time_Tick>(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<Definition, Constellation_Diagram>) {
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<Definition, Selection_Rectangle_Overlay>) {
const auto count = generator_count(input, "region_count");
@@ -653,7 +652,7 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
const auto& state = raw->template read_prop<Sweep_Spectrum::Base_Tag>();
const std::size_t block_count = std::max<std::size_t>(1, state.block_count);
const std::size_t bins_per_block = std::max<std::size_t>(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<std::size_t>(event.sequence % block_count);
std::vector<double> values(bins_per_block);
for (std::size_t i = 0; i < values.size(); ++i) {
const auto sweep_index = block_index * values.size() + i;