From cb9822074b1ea73ae8b4fc519fd2774056cbb0ff Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Mon, 24 Aug 2026 10:45:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dependency_Graph_Storage.hpp | 3 +- .../double_buffer/Mpmc_Triple_Buffer.hpp | 43 +++++ kernel/src/kernel/double_buffer/model.hpp | 10 ++ kernel/src/test/object_test.cpp | 22 +++ render_2D/render_2D/axis/Time_Axis.ipp | 16 +- render_2D/render_2D/plottable/Afterglow.ipp | 29 +++- .../plottable/Constellation_Diagram.ipp | 6 +- .../render_2D/plottable/Frequency_Trace.ipp | 4 +- render_2D/render_2D/plottable/Spectrum.cpp | 45 ------ render_2D/render_2D/plottable/Spectrum.hpp | 46 ------ render_2D/render_2D/plottable/Spectrum.ipp | 151 +----------------- .../render_2D/plottable/Sweep_Spectrum.hpp | 10 +- .../render_2D/plottable/Sweep_Spectrum.ipp | 52 ++++-- render_2D/render_2D/plottable/Waterfall.ipp | 6 +- render_2D/render_2D/scene/Render_Scene_2D.cpp | 3 + render_2D/render_2D/scene/Render_Scene_2D.ipp | 1 + render_2D/tests/Axis_Test.cpp | 4 + render_2D/tests/Frame_Pixel_Format_Test.cpp | 4 +- render_2D/tests/Plottable_Migration_Test.cpp | 29 +++- render_2D/tests/Spectrum_Test.cpp | 56 +++---- .../detail/Datoviz_Visual_Backend.cpp | 98 ++++++------ render_3D/render_3D/visual/Basic_Visual.hpp | 4 +- render_3D/render_3D/visual/Basic_Visual.ipp | 21 +-- .../render_3D/visual/Prepared_Visual.hpp | 3 +- render_3D/tests/Visual_Tests.cpp | 12 +- web_server/src/Gallery_Plots_2D.cpp | 8 +- web_server/src/Graph_WebSocket.cpp | 29 +--- web_server/src/Graph_WebSocket.hpp | 1 - web_server/src/Plot.cpp | 81 ++++------ web_server/src/Plot.hpp | 22 +-- webapp_gallery/src/app.tsx | 80 +++------- 31 files changed, 363 insertions(+), 536 deletions(-) diff --git a/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp b/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp index fbf0bff..c5fd6dd 100644 --- a/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp +++ b/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp @@ -244,8 +244,7 @@ struct Root::Builder { Buffer_Build buffer_storage; /* build() 前暂存的普通 Buffer 初值。 */ Dependency_Graph_Build dependency_graph_storage; /* build() 前独立编辑并验证的依赖图。 */ template - explicit Builder(Args&&... args) : object(new Object(std::forward(args)...)), - dependency_graph_storage(object->memory_resource()) {} + explicit Builder(Args&&... args) : object(new Object(std::forward(args)...)) {} Final_Builder& set_pmr(Pmr pmr) { object->set_pmr_resource(pmr); return static_cast(*this); diff --git a/kernel/src/kernel/double_buffer/Mpmc_Triple_Buffer.hpp b/kernel/src/kernel/double_buffer/Mpmc_Triple_Buffer.hpp index 8488d09..53e6cda 100644 --- a/kernel/src/kernel/double_buffer/Mpmc_Triple_Buffer.hpp +++ b/kernel/src/kernel/double_buffer/Mpmc_Triple_Buffer.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -52,6 +53,31 @@ public: clear(*collecting_); } + /* + * Accumulating streams promote the last published version into the current + * rendering version before appending the newly collected batch. Query keeps + * the previous complete version and producers remain isolated in collecting. + */ + void accumulate(std::size_t capacity) + requires std::copy_constructible { + accumulate_with([capacity](std::vector& values) { + if (values.size() <= capacity) return; + values.erase(values.begin(), values.end() - + static_cast(capacity)); + }); + } + + template + requires std::copy_constructible && + std::predicate + void accumulate(Predicate&& retain) { + accumulate_with([&](std::vector& values) { + std::erase_if(values, [&](const Value& value) { + return !std::invoke(retain, value); + }); + }); + } + template requires std::invocable> decltype(auto) access_rendering(Callback&& callback) { @@ -82,6 +108,23 @@ private: if (!queue.enqueue(std::move(value))) throw std::bad_alloc{}; } + template + void accumulate_with(Mutation&& mutate) { + std::unique_lock lock(exchange_lock_); + std::vector published; + published.reserve(query_->size_approx()); + Value value; + while (query_->try_dequeue(value)) + published.push_back(std::move(value)); + std::vector accumulated{published.begin(), published.end()}; + restore(*query_, published); + accumulated.reserve(accumulated.size() + rendering_->size_approx()); + while (rendering_->try_dequeue(value)) + accumulated.push_back(std::move(value)); + std::invoke(std::forward(mutate), accumulated); + restore(*rendering_, accumulated); + } + template static decltype(auto) access(Queue& queue, Callback&& callback) { std::vector values; diff --git a/kernel/src/kernel/double_buffer/model.hpp b/kernel/src/kernel/double_buffer/model.hpp index 23210d9..2e5c09d 100644 --- a/kernel/src/kernel/double_buffer/model.hpp +++ b/kernel/src/kernel/double_buffer/model.hpp @@ -261,6 +261,16 @@ public: void exchange_stream() { data().mpmc_triple_buffer_storage.template get().advance(); } + template Tag> + void accumulate_stream(std::size_t capacity) { + data().mpmc_triple_buffer_storage.template get().accumulate(capacity); + } + template Tag, + typename Predicate> + void accumulate_stream(Predicate&& retain) { + data().mpmc_triple_buffer_storage.template get().accumulate( + std::forward(retain)); + } template Tag, typename Callback> decltype(auto) access_rendering_stream(Callback&& callback) { return data().mpmc_triple_buffer_storage.template get().access_rendering( diff --git a/kernel/src/test/object_test.cpp b/kernel/src/test/object_test.cpp index 8734db5..57ae200 100644 --- a/kernel/src/test/object_test.cpp +++ b/kernel/src/test/object_test.cpp @@ -139,6 +139,28 @@ TEST(object_buffer, mpmc_triple_buffer_accepts_multiple_producers) { EXPECT_EQ(object->access_query_stream( [](std::span query) { return query.size(); }), 400); } +TEST(object_buffer, mpmc_triple_buffer_accumulates_published_history_with_capacity) { + auto object = build_object(); + object->submit_stream(11); + object->submit_stream(13); + object->exchange_stream(); + object->accumulate_stream(3); + object->submit_stream(17); + object->submit_stream(19); + object->exchange_stream(); + object->accumulate_stream(3); + object->access_query_stream([](std::span query) { + ASSERT_EQ(query.size(), 2); + EXPECT_EQ(query[0], 11); + EXPECT_EQ(query[1], 13); + }); + object->access_rendering_stream([](std::span rendering) { + ASSERT_EQ(rendering.size(), 3); + EXPECT_EQ(rendering[0], 13); + EXPECT_EQ(rendering[1], 17); + EXPECT_EQ(rendering[2], 19); + }); +} TEST(state_tag, callback_publishes_only_requested_layer) { auto object = build_object(); int calls = 0; diff --git a/render_2D/render_2D/axis/Time_Axis.ipp b/render_2D/render_2D/axis/Time_Axis.ipp index 9aeb94e..f538564 100644 --- a/render_2D/render_2D/axis/Time_Axis.ipp +++ b/render_2D/render_2D/axis/Time_Axis.ipp @@ -59,12 +59,15 @@ inline std::string Time_Axis::Private::tick_label(const Attached auto* object, d using Object = std::remove_cv_t>; const auto& private_data = static_cast(*this); const auto& prop = static_cast(*private_data.current); - const auto samples = active_samples; const int target = static_cast(std::llround(tick)); - const auto current = std::find_if(samples.begin(), samples.end(), [target](const auto& sample) { - return sample.first == target; - }); - return current == samples.end() ? std::string{} : formatted_time(current->second, prop.format); + const auto find_label = [&](std::span> samples) { + const auto current = std::find_if(samples.begin(), samples.end(), [target](const auto& sample) { + return sample.first == target; + }); + return current == samples.end() ? std::string{} : formatted_time(current->second, prop.format); + }; + if (!active_samples.empty()) return find_label(active_samples); + return object->template access_query_stream(find_label); } template const Time_Axis::Private::Time_Dispatch& Time_Axis::Private::time_dispatch_for() { @@ -104,6 +107,9 @@ inline void Time_Axis::Private::prepare_data(Attached auto* object) { using Object = std::remove_pointer_t; object->template exchange_stream(); auto& private_data = static_cast(*this); + const auto& prop = static_cast(*private_data.current); + object->template accumulate_stream( + static_cast(std::max(2, prop.visible_count))); auto& state = static_cast(*private_data.state.current); state.next_tick = next_tick.load(std::memory_order_relaxed); object->template access_rendering_stream( diff --git a/render_2D/render_2D/plottable/Afterglow.ipp b/render_2D/render_2D/plottable/Afterglow.ipp index 4c33aaa..5fdeeec 100644 --- a/render_2D/render_2D/plottable/Afterglow.ipp +++ b/render_2D/render_2D/plottable/Afterglow.ipp @@ -124,25 +124,36 @@ template void Afterglow::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop(); object->template exchange_stream(); - prepared = {}; + prepared.source_spectra.clear(); object->template access_rendering_stream( [&](std::span>> spectra) { prepared.source_spectra.assign(spectra.begin(), spectra.end()); }); + object->template accumulate_stream(std::size_t{64}); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& power_layout = power_axis->template read_prop(); const std::size_t available = prepared.source_spectra.empty() ? 0 : prepared.source_spectra.back()->size(); const int columns = static_cast(state.frequency_point_size ? std::min(state.frequency_point_size, available) : available); const int rows = static_cast(state.power_point_size ? state.power_point_size : std::max(1.0, std::abs(power_layout.pixel_length))); - prepared = {}; - prepared.canvas = scene->template read_prop().viewport; - prepared.layout = detail::raster_layout(frequency_axis, state.frequency_range, columns, power_axis, state.power_range, rows, frequency_layout.orientation, power_layout.orientation); - if (prepared.canvas.empty() || !prepared.layout.valid()) { + const Size canvas = scene->template read_prop().viewport; + const auto layout = detail::raster_layout(frequency_axis, state.frequency_range, columns, power_axis, state.power_range, rows, frequency_layout.orientation, power_layout.orientation); + if (canvas.empty() || !layout.valid()) { + prepared.valid = false; return; } const std::size_t cells = static_cast(columns) * rows; - prepared.intensity.assign(cells, 0.0); - prepared.pixels.assign(cells, 0); + const bool layout_changed = prepared.canvas != canvas || + prepared.layout.width != layout.width || prepared.layout.height != layout.height || + prepared.layout.target != layout.target || + prepared.layout.first_reversed != layout.first_reversed || + prepared.layout.second_reversed != layout.second_reversed || + prepared.layout.first_horizontal != layout.first_horizontal; + prepared.canvas = canvas; + prepared.layout = layout; + if (layout_changed || prepared.intensity.size() != cells) { + prepared.intensity.assign(cells, 0.0); + prepared.pixels.assign(cells, 0); + } prepared.valid = true; } template @@ -155,6 +166,10 @@ void Afterglow::Private::accumulate_partition(Object* object, Plot_Partition_Cou 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; + const Plot_Ratio retained = std::pow(decay, static_cast(spectra.size())); + for (std::size_t column = first; column < last; ++column) + for (int row = 0; row < rows; ++row) + prepared.intensity[static_cast(row) * columns + column] *= retained; for (auto spectrum = spectra.rbegin(); spectrum != spectra.rend() && attenuation >= 0.01; ++spectrum, attenuation *= decay) { const std::size_t count = std::min(columns, (*spectrum)->size()); for (std::size_t column = first; column < std::min(last, count); ++column) { diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.ipp b/render_2D/render_2D/plottable/Constellation_Diagram.ipp index f3ec1e1..04eabed 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.ipp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.ipp @@ -63,6 +63,11 @@ template void Constellation_Diagram::Private::prepare_data(Object* object) { const auto& state = object->template read_prop(); object->template exchange_stream(); + const auto current = monotonic_milliseconds(); + object->template accumulate_stream( + [&](const Constellation_Point& point) { + return current - point.submitted_at_ms <= state.point_lifetime_ms; + }); const auto& i_layout = i_axis->template read_prop(); const auto& q_layout = q_axis->template read_prop(); prepared = {}; @@ -70,7 +75,6 @@ void Constellation_Diagram::Private::prepare_data(Object* object) { if (prepared.canvas.empty() || i_layout.orientation == q_layout.orientation) { return; } - const auto current = monotonic_milliseconds(); object->template access_rendering_stream( [&](std::span points) { for (const auto& value : points) diff --git a/render_2D/render_2D/plottable/Frequency_Trace.ipp b/render_2D/render_2D/plottable/Frequency_Trace.ipp index 4282ec2..2da407c 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.ipp +++ b/render_2D/render_2D/plottable/Frequency_Trace.ipp @@ -49,12 +49,14 @@ void Frequency_Trace::Private::prepare_frame(Object* object, Plot_Partition_Coun const auto& state = object->template read_prop(); const auto& time_layout = time_axis->template read_prop(); const auto& value_layout = value_axis->template read_prop(); const auto visible_count = static_cast(std::max(2, time_axis->template read_prop().visible_count)); object->template exchange_stream(); + object->template accumulate_stream(visible_count); prepared = {}; prepared.partitions.resize(partition_count); prepared.canvas = scene->template read_prop().viewport; - if (prepared.canvas.empty() || time_layout.orientation == value_layout.orientation || prepared.samples.empty()) { return; } + if (prepared.canvas.empty() || time_layout.orientation == value_layout.orientation) return; object->template access_rendering_stream([&](std::span samples) { prepared.samples.reserve(samples.size()); for (const auto& sample : samples) prepared.samples.push_back({static_cast(sample.tick), sample.value}); }); + if (prepared.samples.empty()) return; prepared.valid = !prepared.samples.empty(); } template diff --git a/render_2D/render_2D/plottable/Spectrum.cpp b/render_2D/render_2D/plottable/Spectrum.cpp index 4bd62cd..72cc4b2 100644 --- a/render_2D/render_2D/plottable/Spectrum.cpp +++ b/render_2D/render_2D/plottable/Spectrum.cpp @@ -3,49 +3,4 @@ namespace aethera::render_2d { bool Spectrum_Frame::operator==(const Spectrum_Frame&) const = default; bool Spectrum::Prop::operator==(const Prop&) const = default; bool Spectrum::State::operator==(const State&) const = default; -std::expected Spectrum::power_at(Spectrum_Frequency frequency) const { - return static_cast(*d).dispatch->power_at(this, frequency); -} -void Spectrum::add_custom_marker(Spectrum_Frequency frequency) { - static_cast(*d).dispatch->add_marker(this, frequency); -} -void Spectrum::add_custom_line_marker(Spectrum_Frequency frequency) { - static_cast(*d).dispatch->add_marker(this, frequency); -} -void Spectrum::remove_custom_marker(Spectrum_Frequency frequency) { - static_cast(*d).dispatch->remove_marker(this, frequency); -} -void Spectrum::remove_selected_marker() { - static_cast(*d).dispatch->remove_selected_marker(this); -} -void Spectrum::clear_custom_markers() { - static_cast(*d).dispatch->clear_markers(this); -} -std::size_t Spectrum::selectable_line_marker_count() const { - return static_cast(*d).dispatch->marker_count(this); -} -Spectrum_Marker_Index Spectrum::selected_marker_index() const { - return static_cast(*d).dispatch->selected_marker(this); -} -void Spectrum::set_selected_marker_index(Spectrum_Marker_Index index) { - static_cast(*d).dispatch->set_selected_marker(this, index); -} -void Spectrum::select_next_marker() { - static_cast(*d).dispatch->select_next_marker(this); -} -void Spectrum::select_previous_marker() { - static_cast(*d).dispatch->select_previous_marker(this); -} -void Spectrum::clear_marker_selection() { - static_cast(*d).dispatch->set_selected_marker(this, -1); -} -std::expected Spectrum::marker_frequency(Spectrum_Marker_Index index) const { - return static_cast(*d).dispatch->marker_frequency(this, index); -} -Spectrum::Set_Marker_Frequency_Result Spectrum::set_marker_frequency(Spectrum_Marker_Index index, Spectrum_Frequency frequency) { - return static_cast(*d).dispatch->set_marker_frequency(this, index, frequency); -} -Spectrum::Set_Current_Marker_Frequency_Result Spectrum::set_current_marker_frequency(Spectrum_Frequency frequency) { - return static_cast(*d).dispatch->set_current_marker_frequency(this, frequency); -} } diff --git a/render_2D/render_2D/plottable/Spectrum.hpp b/render_2D/render_2D/plottable/Spectrum.hpp index d4a662c..d2fc4c4 100644 --- a/render_2D/render_2D/plottable/Spectrum.hpp +++ b/render_2D/render_2D/plottable/Spectrum.hpp @@ -69,52 +69,6 @@ struct Spectrum : Def power_at(Spectrum_Frequency frequency) const; - /* 添加一个自定义频率标记。 */ - void add_custom_marker(Spectrum_Frequency frequency); - /* 添加一个自定义垂直线标记。 */ - void add_custom_line_marker(Spectrum_Frequency frequency); - /* 删除与给定频率距离最近的自定义标记。 */ - void remove_custom_marker(Spectrum_Frequency frequency); - /* 删除当前选中的自定义标记;未选择时不处理。 */ - void remove_selected_marker(); - /* 删除全部自定义标记并清除选择。 */ - void clear_custom_markers(); - /* 返回可选择的自定义线标记数量。 */ - [[nodiscard]] std::size_t selectable_line_marker_count() const; - /* 返回当前选中标记下标;-1 表示未选择。 */ - [[nodiscard]] Spectrum_Marker_Index selected_marker_index() const; - /* 选择指定下标;越界值清除选择。 */ - void set_selected_marker_index(Spectrum_Marker_Index index); - /* 循环选择下一个标记;无标记时清除选择。 */ - void select_next_marker(); - /* 循环选择上一个标记;无标记时清除选择。 */ - void select_previous_marker(); - /* 清除当前标记选择。 */ - void clear_marker_selection(); - enum class Marker_Frequency_Result { - index_out_of_range - }; - /* 查询指定标记的频率。 */ - [[nodiscard]] std::expected marker_frequency(Spectrum_Marker_Index index) const; - enum class Set_Marker_Frequency_Result { - updated, - index_out_of_range - }; - /* 修改指定标记频率。 */ - [[nodiscard]] Set_Marker_Frequency_Result set_marker_frequency(Spectrum_Marker_Index index, Spectrum_Frequency frequency); - enum class Set_Current_Marker_Frequency_Result { - updated, - no_selection - }; - /* 修改当前选中标记频率。 */ - [[nodiscard]] Set_Current_Marker_Frequency_Result set_current_marker_frequency(Spectrum_Frequency frequency); }; } #include "Spectrum.ipp" diff --git a/render_2D/render_2D/plottable/Spectrum.ipp b/render_2D/render_2D/plottable/Spectrum.ipp index ff208e0..e93391f 100644 --- a/render_2D/render_2D/plottable/Spectrum.ipp +++ b/render_2D/render_2D/plottable/Spectrum.ipp @@ -30,30 +30,6 @@ struct Spectrum::Private : Prev_Private { Size canvas_size{}; /* 所属 Scene viewport 决定的颜色层尺寸。 */ bool valid{}; /* 两根轴与画布是否足以生成绘制数据。 */ }; - using Power_Run = std::expected (*)(const Root*, Spectrum_Frequency); - using Count_Run = std::size_t (*)(const Root*); - using Frequency_Run = void (*)(Root*, Spectrum_Frequency); - using Void_Run = void (*)(Root*); - using Index_Get_Run = Spectrum_Marker_Index (*)(const Root*); - using Index_Set_Run = void (*)(Root*, Spectrum_Marker_Index); - using Marker_Frequency_Run = std::expected (*)(const Root*, Spectrum_Marker_Index); - using Set_Marker_Frequency_Run = Set_Marker_Frequency_Result (*)(Root*, Spectrum_Marker_Index, Spectrum_Frequency); - using Set_Current_Marker_Frequency_Run = Set_Current_Marker_Frequency_Result (*)(Root*, Spectrum_Frequency); - struct Dispatch { - Power_Run power_at; /* 查询最终对象已发布帧的插值功率。 */ - Frequency_Run add_marker; /* 添加自定义标记。 */ - Frequency_Run remove_marker; /* 删除最接近指定频率的标记。 */ - Void_Run remove_selected_marker; /* 删除当前选中标记。 */ - Void_Run clear_markers; /* 清空全部标记。 */ - Count_Run marker_count; /* 查询自定义标记数量。 */ - Index_Get_Run selected_marker; /* 查询当前选中下标。 */ - Index_Set_Run set_selected_marker; /* 修改当前选中下标。 */ - Void_Run select_next_marker; /* 循环选择下一个标记。 */ - Void_Run select_previous_marker; /* 循环选择上一个标记。 */ - Marker_Frequency_Run marker_frequency; /* 查询指定标记频率。 */ - Set_Marker_Frequency_Run set_marker_frequency; /* 修改指定标记频率。 */ - Set_Current_Marker_Frequency_Run set_current_marker_frequency; /* 修改当前选中标记频率。 */ - }; Scene_Object* scene{}; /* 不拥有的所属二维 Scene;Builder 已登记状态层依赖。 */ Frequency_Object* frequency_axis{}; /* 不拥有的频率轴;Prepare 直接读取其当前状态。 */ Power_Object* power_axis{}; /* 不拥有的功率轴;Prepare 直接读取其当前状态。 */ @@ -61,24 +37,10 @@ struct Spectrum::Private : Prev_Private { std::vector maxima{}; /* 当前样本历史逐点最大值;只由 Prepare 更新。 */ std::vector minima{}; /* 当前样本历史逐点最小值;只由 Prepare 更新。 */ std::size_t prepare_graph_partition_count{}; /* 当前 Prepare 子图实际固化的任务分块数。 */ - const Dispatch* dispatch{}; /* Builder 绑定最终 Spectrum 类型后的静态分派表。 */ - /* CRTP 覆盖:绑定 Renderable 机制和 Spectrum 公开薄壳分派;派生 Private 必须先调用此实现。 */ - template void bind_private_crtp(Object* object); /* Builder 内部绑定 Scene 与两根轴;三个来源都必须比 Spectrum 生命周期更长。 */ void bind_render_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value); - template [[nodiscard]] static const Dispatch& dispatch_for(); - template void add_marker(Object* object, Spectrum_Frequency frequency); - template void remove_marker(Object* object, Spectrum_Frequency frequency); - template void remove_selected_marker(Object* object); - template void clear_markers(Object* object); - template void set_selected_marker(Object* object, Spectrum_Marker_Index index); - template void select_next_marker(Object* object); - template void select_previous_marker(Object* object); - template [[nodiscard]] Set_Marker_Frequency_Result set_marker_frequency(Object* object, Spectrum_Marker_Index index, Spectrum_Frequency frequency); - template [[nodiscard]] Set_Current_Marker_Frequency_Result set_current_marker_frequency(Object* object, Spectrum_Frequency frequency); /* CRTP State 钩子:Spectrum 业务状态写入后标记自身 Prepare;其他继承层状态由各自 Private 负责。 */ template void after_prop_set(Object* object, Member Owner::* member, Prop_Access pending_states); - template void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); /* CRTP 子图能力:按当前样本数和 State 分块策略构建并行 Prepare 图。 */ template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state); /* CRTP 子图能力:构建背景、分块曲线和覆盖标记的 Paint 图。 */ @@ -89,7 +51,6 @@ struct Spectrum::Private : Prev_Private { template void prepare_frame(Object* object, std::size_t partition_count); template void prepare_partition(Object* object, std::size_t partition_index); template void paint_frame(Object* object); - [[nodiscard]] static std::expected power_at(const Prop& state, const Spectrum_Frame& frame, Spectrum_Frequency frequency); }; template Spectrum::Builder::Builder(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {} @@ -125,17 +86,6 @@ std::expected, Dependency_Graph_Error> Spectrum::Builder }; return spectrum; } -inline std::expected Spectrum::Private::power_at(const Prop& state, const Spectrum_Frame& frame, Spectrum_Frequency frequency) { - if (frame.samples.empty()) return std::unexpected(Power_At_Result::no_samples); - if (state.frequency_range.length() == 0.0) return std::unexpected(Power_At_Result::invalid_frequency_range); - if (!state.frequency_range.contains(frequency)) return std::unexpected(Power_At_Result::frequency_out_of_range); - const Spectrum_Interpolation_Ratio normalized = (frequency - state.frequency_range.origin) / state.frequency_range.length(); - const Spectrum_Interpolation_Ratio position = std::clamp(normalized, 0.0, 1.0) * static_cast(frame.samples.size() - 1); - const auto lower = static_cast(std::floor(position)); - const auto upper = std::min(lower + 1, frame.samples.size() - 1); - const Spectrum_Interpolation_Ratio fraction = position - static_cast(lower); - return frame.samples[lower] * (1.0 - fraction) + frame.samples[upper] * fraction; -} template std::size_t Spectrum::Private::desired_partition_count(const Object* object, const Prop& state) const { return detail::curve_partition_count(state.partition_mode, state.partition_count, object->template current_buffer().samples.size()); @@ -227,6 +177,13 @@ template void Spectrum::Private::paint_frame(Object* object) { auto& private_data = static_cast(*this); const auto& state = static_cast(*private_data.current); + auto& published = static_cast(*private_data.state.current); + const auto& frame = object->template current_buffer(); + published.sample_count = frame.samples.size(); + published.rendered_point_count = 0; + for (const auto& partition : prepared.partitions) + published.rendered_point_count += partition.current.points.size(); + published.selectable_marker_count = state.custom_markers.size(); auto& cache = private_data.paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas_size); @@ -247,104 +204,10 @@ void Spectrum::Private::paint_frame(Object* object) { painter.circle(extreme.point, 3.0, pen, Brush{pen.color, Brush_Style::solid}); } } -template -void Spectrum::Private::add_marker(Object* object, Spectrum_Frequency frequency) { - object->template update_prop<&Prop::custom_markers>([frequency](Prop_Access props) { props.template get().custom_markers.push_back(frequency); }); - object->template mark_dirty(); -} -template -void Spectrum::Private::remove_marker(Object* object, Spectrum_Frequency frequency) { - object->template update_prop<&Prop::custom_markers, &Prop::selected_marker>([frequency](Prop_Access props) { - auto& state = props.template get(); - if (state.custom_markers.empty()) return; - const auto closest = std::min_element(state.custom_markers.begin(), state.custom_markers.end(), [frequency](Spectrum_Frequency left, Spectrum_Frequency right) { return std::abs(left - frequency) < std::abs(right - frequency); }); - const Spectrum_Marker_Index removed = std::distance(state.custom_markers.begin(), closest); - state.custom_markers.erase(closest); - if (state.selected_marker == removed) state.selected_marker = -1; - else if (state.selected_marker > removed) --state.selected_marker; - }); - object->template mark_dirty(); -} -template -void Spectrum::Private::remove_selected_marker(Object* object) { - object->template update_prop<&Prop::custom_markers, &Prop::selected_marker>([](Prop_Access props) { - auto& state = props.template get(); - if (state.selected_marker < 0 || static_cast(state.selected_marker) >= state.custom_markers.size()) return; - state.custom_markers.erase(state.custom_markers.begin() + state.selected_marker); - state.selected_marker = -1; - }); - object->template mark_dirty(); -} -template -void Spectrum::Private::clear_markers(Object* object) { - object->template update_prop<&Prop::custom_markers, &Prop::selected_marker>([](Prop_Access props) { auto& state = props.template get(); state.custom_markers.clear(); state.selected_marker = -1; }); - object->template mark_dirty(); -} -template -void Spectrum::Private::set_selected_marker(Object* object, Spectrum_Marker_Index index) { - object->template update_prop<&Prop::selected_marker>([index](Prop_Access props) { auto& state = props.template get(); state.selected_marker = index >= 0 && static_cast(index) < state.custom_markers.size() ? index : -1; }); - object->template mark_dirty(); -} -template -void Spectrum::Private::select_next_marker(Object* object) { - object->template update_prop<&Prop::selected_marker>([](Prop_Access props) { auto& state = props.template get(); state.selected_marker = state.custom_markers.empty() ? -1 : (state.selected_marker + 1) % static_cast(state.custom_markers.size()); }); - object->template mark_dirty(); -} -template -void Spectrum::Private::select_previous_marker(Object* object) { - object->template update_prop<&Prop::selected_marker>([](Prop_Access props) { auto& state = props.template get(); state.selected_marker = state.custom_markers.empty() ? -1 : (state.selected_marker <= 0 ? static_cast(state.custom_markers.size()) : state.selected_marker) - 1; }); - object->template mark_dirty(); -} -template -Spectrum::Set_Marker_Frequency_Result Spectrum::Private::set_marker_frequency(Object* object, Spectrum_Marker_Index index, Spectrum_Frequency frequency) { - if (index < 0 || static_cast(index) >= object->template read_prop().custom_markers.size()) return Set_Marker_Frequency_Result::index_out_of_range; - object->template update_prop<&Prop::custom_markers>([index, frequency](Prop_Access props) { props.template get().custom_markers[static_cast(index)] = frequency; }); - object->template mark_dirty(); - return Set_Marker_Frequency_Result::updated; -} -template -Spectrum::Set_Current_Marker_Frequency_Result Spectrum::Private::set_current_marker_frequency(Object* object, Spectrum_Frequency frequency) { - const Spectrum_Marker_Index index = object->template read_prop().selected_marker; - if (index < 0) return Set_Current_Marker_Frequency_Result::no_selection; - return set_marker_frequency(object, index, frequency) == Set_Marker_Frequency_Result::updated ? Set_Current_Marker_Frequency_Result::updated : Set_Current_Marker_Frequency_Result::no_selection; -} template void Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } -template -void Spectrum::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { - const auto& private_data = static_cast(*this); - auto& state = pending_states.template get(); - state.sample_count = private_data.buffer_storage.template get().pending->samples.size(); - state.rendered_point_count = 0; - for (const auto& partition : prepared.partitions) state.rendered_point_count += partition.current.points.size(); - state.selectable_marker_count = static_cast(*current_prop).custom_markers.size(); -} -template -const Spectrum::Private::Dispatch& Spectrum::Private::dispatch_for() { - static const Dispatch value{ - [](const Root* root, Spectrum_Frequency frequency) { const auto* object = static_cast(root); const auto& data = static_cast(*object->d); return Private::power_at(static_cast(*data.current), object->template current_buffer(), frequency); }, - [](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast(root); static_cast(*object->d).add_marker(object, frequency); }, - [](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast(root); static_cast(*object->d).remove_marker(object, frequency); }, - [](Root* root) { auto* object = static_cast(root); static_cast(*object->d).remove_selected_marker(object); }, - [](Root* root) { auto* object = static_cast(root); static_cast(*object->d).clear_markers(object); }, - [](const Root* root) { return static_cast(root)->template read_prop().custom_markers.size(); }, - [](const Root* root) { return static_cast(root)->template read_prop().selected_marker; }, - [](Root* root, Spectrum_Marker_Index index) { auto* object = static_cast(root); static_cast(*object->d).set_selected_marker(object, index); }, - [](Root* root) { auto* object = static_cast(root); static_cast(*object->d).select_next_marker(object); }, - [](Root* root) { auto* object = static_cast(root); static_cast(*object->d).select_previous_marker(object); }, - [](const Root* root, Spectrum_Marker_Index index) -> std::expected { const auto& markers = static_cast(root)->template read_prop().custom_markers; if (index < 0 || static_cast(index) >= markers.size()) return std::unexpected(Marker_Frequency_Result::index_out_of_range); return markers[static_cast(index)]; }, - [](Root* root, Spectrum_Marker_Index index, Spectrum_Frequency frequency) { auto* object = static_cast(root); return static_cast(*object->d).set_marker_frequency(object, index, frequency); }, - [](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast(root); return static_cast(*object->d).set_current_marker_frequency(object, frequency); } - }; - return value; -} -template -void Spectrum::Private::bind_private_crtp(Object* object) { - Prev_Private::bind_private_crtp(object); - dispatch = &Private::dispatch_for(); -} inline void Spectrum::Private::bind_render_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) { frequency_axis = frequency_axis_value; power_axis = power_axis_value; diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.hpp b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp index b61fcae..a3ef884 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.hpp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp @@ -10,9 +10,17 @@ #include namespace aethera::render_2d { struct Sweep_Spectrum_Stream_Tag {}; +/* + * 一次输入交换可以包含同一扫频周期的一组块更新。index 是块在完整扫频中的 + * 固定槽位,而不是历史序号;未出现在本组中的槽位必须保留原值。 + */ +struct Sweep_Spectrum_Block { + std::size_t index{}; /* 当前完整扫频周期内的块位置。 */ + std::vector values{}; /* 该频率块按频率递增排列的功率值。 */ +}; struct Sweep_Spectrum : Def>>> { + std::shared_ptr>> { using Scene_Object = Impl; using Frequency_Object = Impl; using Power_Object = Impl; diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.ipp b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp index 9029c43..91299f4 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.ipp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp @@ -16,6 +16,13 @@ struct Sweep_Spectrum::Private : Prev_Private { Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ Frequency_Object* frequency_axis{}; /* 不拥有的频率轴。 */ Power_Object* power_axis{}; /* 不拥有的功率轴。 */ + /* + * 完整扫频的唯一持久状态。每次交换得到一组块更新并原位覆盖相应槽位; + * 三缓冲只负责跨线程交付输入组,不承担扫频历史保存职责。 + */ + std::vector> sweep_blocks{}; + std::size_t latest_block_index{}; /* 最近输入组最后更新的块,决定红色扫描线。 */ + bool has_latest_block{}; Prepared prepared{}; /* 当前块集合推导出的绘制输入。 */ Plot_Partition_Count graph_partition_count{}; /* 当前 Prepare 子图分块数。 */ void bind_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value); @@ -28,6 +35,7 @@ struct Sweep_Spectrum::Private : Prev_Private { template void prepare_frame(Object* object); template void prepare_partition(Object* object, Plot_Partition_Count index); template void paint_frame(Object* object); + [[nodiscard]] std::size_t stored_point_count() const; template void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); }; template @@ -38,31 +46,43 @@ std::expected, Dependency_Graph_Error> Sweep_Spectrum::B private_data.scene_attach = [object = sweep.get()](Root* root) -> std::expected { auto* scene = static_cast(root); auto& data = static_cast(*object->d); data.scene = scene; auto* frequency_axis = data.frequency_axis; auto* power_axis = data.power_axis; return scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { prepare.add_dependency(object, scene); prepare.add_dependency(object, frequency_axis); prepare.add_dependency(object, power_axis); paint.add_dependency(frequency_axis, object); paint.add_dependency(power_axis, object); cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(object, scene); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, frequency_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, power_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, power_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, power_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, power_axis); }); }; return sweep; } template -bool Sweep_Spectrum::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { const auto points = object->template access_query_stream([](std::span>> blocks) { std::size_t count{}; for (const auto& block : blocks) count += block->size(); return count; }); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, points); } +bool Sweep_Spectrum::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, stored_point_count()); } template -tf::Taskflow Sweep_Spectrum::Private::build_prepare_graph(Object* object, const Prop& state) { const auto points = object->template access_query_stream([](std::span>> blocks) { std::size_t count{}; for (const auto& block : blocks) count += block->size(); return count; }); graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, points); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("sweep_spectrum.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("sweep_spectrum.prepare.partition"); begin.precede(task); } return graph; } +tf::Taskflow Sweep_Spectrum::Private::build_prepare_graph(Object* object, const Prop& state) { graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, stored_point_count()); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("sweep_spectrum.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("sweep_spectrum.prepare.partition"); begin.precede(task); } return graph; } template tf::Taskflow Sweep_Spectrum::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("sweep_spectrum.paint.frame"); return graph; } template void Sweep_Spectrum::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop(); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& power_layout = power_axis->template read_prop(); const auto& power_state = power_axis->template read_prop(); prepared = {}; prepared.canvas = scene->template read_prop().viewport; const std::size_t block_count = std::max(1, state.block_count); + if (sweep_blocks.size() != block_count) { + sweep_blocks.assign(block_count, {}); + latest_block_index = 0; + has_latest_block = false; + } object->template exchange_stream(); - std::size_t available_blocks{}; - object->template access_rendering_stream([&](std::span>> blocks) { - available_blocks = blocks.size(); - const auto first = blocks.size() > block_count ? blocks.size() - block_count : 0; - for (std::size_t index = first; index < blocks.size(); ++index) - prepared.values.insert(prepared.values.end(), blocks[index]->begin(), blocks[index]->end()); + /* 当前 rendering 队列就是本次输入组;组内所有合法块依次覆盖持久槽位。 */ + object->template access_rendering_stream([&](std::span> blocks) { + for (const auto& block : blocks) { + if (!block || block->index >= block_count) continue; + sweep_blocks[block->index] = block; + latest_block_index = block->index; + has_latest_block = true; + } }); + std::size_t available_blocks{}; + for (const auto& block : sweep_blocks) { + if (!block) continue; + ++available_blocks; + prepared.values.insert(prepared.values.end(), block->values.begin(), block->values.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); if (prepared.values.empty()) return; - const bool complete = stored_block_count == block_count; - const std::size_t latest_block_index = stored_block_count - 1; - const double domain_progress = static_cast(stored_block_count) / static_cast(block_count); - const double marker_progress = static_cast(latest_block_index + 1) / static_cast(block_count); + const double domain_progress = available_blocks == block_count ? 1.0 : + static_cast(latest_block_index + 1) / static_cast(block_count); + const double marker_progress = has_latest_block ? + static_cast(latest_block_index + 1) / static_cast(block_count) : 0.0; const auto domain_target = state.frequency_range.origin + state.frequency_range.length() * domain_progress; const auto latest_frequency = state.frequency_range.origin + state.frequency_range.length() * marker_progress; prepared.domain = {state.frequency_range.origin, domain_target}; @@ -78,4 +98,10 @@ void Sweep_Spectrum::Private::paint_frame(Object* object) { const auto& state = template void Sweep_Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } inline void Sweep_Spectrum::Private::bind_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) { frequency_axis = frequency_axis_value; power_axis = power_axis_value; } +inline std::size_t Sweep_Spectrum::Private::stored_point_count() const { + std::size_t count{}; + for (const auto& block : sweep_blocks) + if (block) count += block->values.size(); + return count; +} } diff --git a/render_2D/render_2D/plottable/Waterfall.ipp b/render_2D/render_2D/plottable/Waterfall.ipp index f1c9ea0..908cf05 100644 --- a/render_2D/render_2D/plottable/Waterfall.ipp +++ b/render_2D/render_2D/plottable/Waterfall.ipp @@ -125,15 +125,17 @@ template void Waterfall::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop(); object->template exchange_stream(); + const auto row_limit = static_cast(std::max( + 2, time_axis->template read_prop().visible_count)); + object->template accumulate_stream(row_limit); prepared = {}; object->template access_rendering_stream([&](std::span> rows) { prepared.source_rows.assign(rows.begin(), rows.end()); }); - const auto& rows = prepared.source_rows; const auto& frequency_layout = frequency_axis->template read_prop(); const auto& time_layout = time_axis->template read_prop(); - prepared = {}; prepared.canvas = scene->template read_prop().viewport; + const auto& rows = prepared.source_rows; if (rows.empty()) { return; } diff --git a/render_2D/render_2D/scene/Render_Scene_2D.cpp b/render_2D/render_2D/scene/Render_Scene_2D.cpp index 766ed72..13bb0fb 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.cpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.cpp @@ -1,5 +1,8 @@ #include "Render_Scene_2D.hpp" /* 二维 Paint Taskflow、缓存失效与合成实现。 */ namespace aethera::render_2d { +Render_Scene_2D::Private::~Private() { + if (paint_taskflow) aethera::detail::clear_stage_observers(paint_taskflow.get()); +} bool Render_Scene_2D::State::operator==(const State&) const = default; bool Render_Scene_2D::Prop::operator==(const Prop&) const = default; diff --git a/render_2D/render_2D/scene/Render_Scene_2D.ipp b/render_2D/render_2D/scene/Render_Scene_2D.ipp index a31587c..a4a5516 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.ipp +++ b/render_2D/render_2D/scene/Render_Scene_2D.ipp @@ -59,6 +59,7 @@ struct Render_Scene_2D::Private : Prev_Private { const Dispatch* dispatch{}; /* Builder 绑定最终 Scene 类型后的静态分派表。 */ Frame_Callback frame_callback{}; /* 合成完成后的唯一像素发布出口。 */ std::unique_ptr paint_taskflow{}; /* 仅由二维 Paint 图构建的执行图。 */ + ~Private(); Frame_2D* active_frame{}; /* 当前同步 process 借用的外部帧;render 返回前清空。 */ Blend2D_Cache* frame_target{}; /* 当前 render(frame) 所属外部颜色层;调用返回后清空。 */ /* Impl CRTP 实现:在对象锁内执行 Kernel Scene,再按 Paint 图拓扑顺序合成颜色层。 */ diff --git a/render_2D/tests/Axis_Test.cpp b/render_2D/tests/Axis_Test.cpp index 76af147..32e3b96 100644 --- a/render_2D/tests/Axis_Test.cpp +++ b/render_2D/tests/Axis_Test.cpp @@ -191,6 +191,10 @@ TEST(axis_time, sample_state_drives_range_lookup_and_label_without_snapshot) { auto axis = build_axis(); EXPECT_EQ(axis->append_time({3'723'004}), 0); EXPECT_EQ(axis->time_point_count(), 0u); + axis->exchange_stream(); + axis->accumulate_stream(100); + axis->exchange_stream(); + axis->accumulate_stream(100); axis->advance(); EXPECT_EQ(axis->time_point_count(), 1u); EXPECT_EQ(axis->coordinate_range(), (Axis_Range{-99.5, 0.5})); diff --git a/render_2D/tests/Frame_Pixel_Format_Test.cpp b/render_2D/tests/Frame_Pixel_Format_Test.cpp index 824ed54..e737f9e 100644 --- a/render_2D/tests/Frame_Pixel_Format_Test.cpp +++ b/render_2D/tests/Frame_Pixel_Format_Test.cpp @@ -4,8 +4,8 @@ namespace { using namespace aethera; using namespace aethera::render_2d; void paint_translucent_red(Frame_2D& frame) { - auto& target = detail::Frame_2D_Access::render_target(&frame); - detail::Painter painter(target, Size{1, 1}); + auto& target = aethera::render_2d::detail::Frame_2D_Access::render_target(&frame); + aethera::render_2d::detail::Painter painter(target, Size{1, 1}); painter.rect(Rect_F{0.0, 0.0, 1.0, 1.0}, Pen{Color::transparent(), 0.0, Line_Style::none}, Brush{Color{255, 0, 0, 128}, Brush_Style::solid}); diff --git a/render_2D/tests/Plottable_Migration_Test.cpp b/render_2D/tests/Plottable_Migration_Test.cpp index a8c80b1..f10b5dd 100644 --- a/render_2D/tests/Plottable_Migration_Test.cpp +++ b/render_2D/tests/Plottable_Migration_Test.cpp @@ -78,9 +78,11 @@ TEST(plottable_migration, curve_plots_share_partitioned_rendering) { const std::array second_block{-80.0, -50.0, -20.0, -5.0}; const std::array replacement_block{-70.0, -40.0, -15.0, -2.0}; sweep->submit_stream( - std::make_shared>(first_block.begin(), first_block.end())); + std::make_shared(Sweep_Spectrum_Block{ + 0, {first_block.begin(), first_block.end()}})); sweep->submit_stream( - std::make_shared>(second_block.begin(), second_block.end())); + std::make_shared(Sweep_Spectrum_Block{ + 1, {second_block.begin(), second_block.end()}})); sweep->mark_dirty(); auto scene = build_scene(trace.get(), sweep.get()); scene->set<&Render_Scene_2D::Prop::viewport>(canvas); @@ -89,13 +91,19 @@ TEST(plottable_migration, curve_plots_share_partitioned_rendering) { EXPECT_EQ(trace->access_rendering_stream( [](std::span samples) { return samples.size(); }), 3u); EXPECT_EQ(sweep->access_rendering_stream( - [](std::span>> blocks) { return blocks.size(); }), 2u); + [](std::span> blocks) { return blocks.size(); }), 2u); sweep->submit_stream( - std::make_shared>(replacement_block.begin(), replacement_block.end())); + std::make_shared(Sweep_Spectrum_Block{ + 0, {replacement_block.begin(), replacement_block.end()}})); + trace->submit_stream( + Frequency_Trace_Sample{time->append_time({4'000}), 60.0}); + trace->mark_dirty(); sweep->mark_dirty(); render_once(scene.get()); + EXPECT_EQ(trace->access_rendering_stream( + [](std::span samples) { return samples.size(); }), 4u); EXPECT_EQ(sweep->access_query_stream( - [](std::span>> blocks) { return blocks.size(); }), 2u); + [](std::span> blocks) { return blocks.size(); }), 2u); } TEST(plottable_migration, raster_plots_share_partitioned_color_blocks) { @@ -139,6 +147,17 @@ TEST(plottable_migration, raster_plots_share_partitioned_color_blocks) { [](std::span>> spectra) { return spectra.size(); }), 1u); EXPECT_EQ(waterfall->access_rendering_stream( [](std::span> rows) { return rows.size(); }), 2u); + glow->submit_stream( + std::make_shared>(row.begin(), row.end())); + glow->mark_dirty(); + waterfall->submit_stream(std::make_shared( + Waterfall_Row{time->append_time({3'000}), {row.begin(), row.end()}})); + waterfall->mark_dirty(); + render_once(scene.get()); + EXPECT_EQ(glow->access_rendering_stream( + [](std::span>> spectra) { return spectra.size(); }), 2u); + EXPECT_EQ(waterfall->access_rendering_stream( + [](std::span> rows) { return rows.size(); }), 3u); } TEST(plottable_migration, direct_overlay_and_constellation_build_and_render) { diff --git a/render_2D/tests/Spectrum_Test.cpp b/render_2D/tests/Spectrum_Test.cpp index ec286ac..de4abef 100644 --- a/render_2D/tests/Spectrum_Test.cpp +++ b/render_2D/tests/Spectrum_Test.cpp @@ -36,7 +36,7 @@ std::unique_ptr render_frame(Scene* scene) { return frame; } } -TEST(spectrum_data, publishes_samples_and_interpolates_power) { +TEST(spectrum_data, publishes_samples_through_tagged_frame_buffer) { using Frequency = Impl; using Power = Impl; using Object = Impl; @@ -44,24 +44,16 @@ TEST(spectrum_data, publishes_samples_and_interpolates_power) { auto frequency = build_object(); auto power = build_object(); auto spectrum = build_object(frequency.get(), power.get()); - const auto missing = spectrum->power_at(25.0); - ASSERT_FALSE(missing.has_value()); - EXPECT_EQ(missing.error(), Spectrum::Power_At_Result::no_samples); spectrum->set<&Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); const double samples[]{-100.0, -50.0, 0.0}; spectrum->pending_buffer() = Spectrum_Frame{std::vector(std::begin(samples), std::end(samples))}; spectrum->mark_dirty(); spectrum->advance(); - EXPECT_EQ(spectrum->current_buffer().samples.size(), 3u); - const auto value = spectrum->power_at(25.0); - ASSERT_TRUE(value.has_value()); - EXPECT_DOUBLE_EQ(*value, -75.0); - const auto outside = spectrum->power_at(101.0); - ASSERT_FALSE(outside.has_value()); - EXPECT_EQ(outside.error(), Spectrum::Power_At_Result::frequency_out_of_range); + EXPECT_EQ(spectrum->current_buffer().samples, + (std::vector{-100.0, -50.0, 0.0})); } -TEST(spectrum_markers, supports_selection_frequency_changes_and_known_results) { +TEST(spectrum_markers, uses_tagged_properties_as_the_only_marker_interface) { using Frequency = Impl; using Power = Impl; using Object = Impl; @@ -69,25 +61,31 @@ TEST(spectrum_markers, supports_selection_frequency_changes_and_known_results) { auto frequency = build_object(); auto power = build_object(); auto spectrum = build_object(frequency.get(), power.get()); - spectrum->add_custom_marker(10.0); - spectrum->add_custom_line_marker(20.0); + spectrum->set<&Spectrum::Prop::custom_markers>( + std::vector{10.0, 20.0}); spectrum->advance(); - EXPECT_EQ(spectrum->selectable_line_marker_count(), 2u); - spectrum->set_selected_marker_index(1); + EXPECT_EQ(spectrum->read_prop().custom_markers.size(), 2u); + spectrum->set<&Spectrum::Prop::selected_marker>(1); spectrum->advance(); - EXPECT_EQ(spectrum->selected_marker_index(), 1); - EXPECT_EQ(spectrum->set_current_marker_frequency(25.0), Spectrum::Set_Current_Marker_Frequency_Result::updated); + EXPECT_EQ(spectrum->read_prop().selected_marker, 1); + spectrum->update_prop<&Spectrum::Prop::custom_markers>( + [](Prop_Access props) { + props.get().custom_markers[1] = 25.0; + }); spectrum->advance(); - const auto frequency_value = spectrum->marker_frequency(1); - ASSERT_TRUE(frequency_value.has_value()); - EXPECT_DOUBLE_EQ(*frequency_value, 25.0); - const auto invalid = spectrum->marker_frequency(2); - ASSERT_FALSE(invalid.has_value()); - EXPECT_EQ(invalid.error(), Spectrum::Marker_Frequency_Result::index_out_of_range); - spectrum->remove_selected_marker(); + EXPECT_DOUBLE_EQ( + spectrum->read_prop().custom_markers[1], 25.0); + spectrum->update_prop<&Spectrum::Prop::custom_markers, + &Spectrum::Prop::selected_marker>( + [](Prop_Access props) { + auto& values = props.get(); + values.custom_markers.erase(values.custom_markers.begin() + + values.selected_marker); + values.selected_marker = -1; + }); spectrum->advance(); - EXPECT_EQ(spectrum->selectable_line_marker_count(), 1u); - EXPECT_EQ(spectrum->selected_marker_index(), -1); + EXPECT_EQ(spectrum->read_prop().custom_markers.size(), 1u); + EXPECT_EQ(spectrum->read_prop().selected_marker, -1); } TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) { using Frequency = Impl; @@ -142,6 +140,10 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) { EXPECT_FALSE(spectrum->dirty()); EXPECT_FALSE(frequency->dirty()); EXPECT_FALSE(power->dirty()); + spectrum->set<&Renderable_2D::Prop::cache_enabled>(false); + output_frame = render_frame(scene.get()); + EXPECT_TRUE(contains_color(output_frame->image())); + spectrum->set<&Renderable_2D::Prop::cache_enabled>(true); output_frame = render_frame(scene.get()); EXPECT_TRUE(contains_color(output_frame->image())); frequency->set<&Numeric_Axis::Prop::precision>(3); diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp index 877a740..f56b4f4 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp @@ -2172,58 +2172,62 @@ void Datoviz_Visual_Backend::destroy() { queue_lock = std::unique_lock(context->queue_mutex(), std::defer_lock); std::lock(api_lock, queue_lock); } - std::lock_guard target_lock(target_mutex_); - for (auto& slot : runtime_slots_) { - if (slot.runtime != nullptr) { - dvz_drp2_runtime_destroy(slot.runtime); - slot.runtime = nullptr; + { + std::lock_guard target_lock(target_mutex_); + for (auto& slot : runtime_slots_) { + if (slot.runtime != nullptr) { + dvz_drp2_runtime_destroy(slot.runtime); + slot.runtime = nullptr; + } + if (slot.emitter != nullptr) { + dvz_frame_plan_emitter_destroy(slot.emitter); + slot.emitter = nullptr; + } } - if (slot.emitter != nullptr) { - dvz_frame_plan_emitter_destroy(slot.emitter); - slot.emitter = nullptr; + for (auto& target : targets_->values) target.reset(); + for (auto* buffer : external_buffers_) { + if (buffer == nullptr) continue; + dvz_buffer_destroy(buffer); + dvz_buffer_free(buffer); + } + external_buffers_.clear(); + if (camera_controller_ != nullptr) { + dvz_controller_destroy(camera_controller_); + camera_controller_ = nullptr; + } + if (item_interaction_ != nullptr) { + dvz_item_interaction_destroy(item_interaction_); + item_interaction_ = nullptr; + } + if (hover_readout_ != nullptr) { + dvz_pinned_readout_destroy(hover_readout_); + hover_readout_ = nullptr; + } + if (panel_ != nullptr && input_router_ != nullptr) + (void)dvz_panel_connect_input(panel_, nullptr); + if (gesture_handler_ != nullptr) { + dvz_pointer_gesture_handler_destroy(gesture_handler_); + gesture_handler_ = nullptr; + } + if (input_router_ != nullptr) { + dvz_input_router_destroy(input_router_); + input_router_ = nullptr; + } + visuals_.clear(); + axes_visual_ = nullptr; + axes_text_ = nullptr; + applied_camera_.reset(); + applied_axes_.reset(); + panel_ = nullptr; + figure_ = nullptr; + if (scene_ != nullptr) { + dvz_scene_destroy(scene_); + scene_ = nullptr; } - } - for (auto& target : targets_->values) target.reset(); - for (auto* buffer : external_buffers_) { - if (buffer == nullptr) continue; - dvz_buffer_destroy(buffer); - dvz_buffer_free(buffer); - } - external_buffers_.clear(); - if (camera_controller_ != nullptr) { - dvz_controller_destroy(camera_controller_); - camera_controller_ = nullptr; - } - if (item_interaction_ != nullptr) { - dvz_item_interaction_destroy(item_interaction_); - item_interaction_ = nullptr; - } - if (hover_readout_ != nullptr) { - dvz_pinned_readout_destroy(hover_readout_); - hover_readout_ = nullptr; - } - if (panel_ != nullptr && input_router_ != nullptr) (void)dvz_panel_connect_input(panel_, nullptr); - if (gesture_handler_ != nullptr) { - dvz_pointer_gesture_handler_destroy(gesture_handler_); - gesture_handler_ = nullptr; - } - if (input_router_ != nullptr) { - dvz_input_router_destroy(input_router_); - input_router_ = nullptr; - } - visuals_.clear(); - axes_visual_ = nullptr; - axes_text_ = nullptr; - applied_camera_.reset(); - applied_axes_.reset(); - panel_ = nullptr; - figure_ = nullptr; - if (scene_ != nullptr) { - dvz_scene_destroy(scene_); - scene_ = nullptr; } render_context_.reset(); if (api_lock.owns_lock()) api_lock.unlock(); + if (queue_lock.owns_lock()) queue_lock.unlock(); context.reset(); } } // namespace aethera::render_3d::detail diff --git a/render_3D/render_3D/visual/Basic_Visual.hpp b/render_3D/render_3D/visual/Basic_Visual.hpp index 425aae5..b4e8e20 100644 --- a/render_3D/render_3D/visual/Basic_Visual.hpp +++ b/render_3D/render_3D/visual/Basic_Visual.hpp @@ -5,9 +5,7 @@ #include namespace aethera::render_3d { template -struct Basic_Visual : Def, Renderable, - Tagged_Buffer>> { +struct Basic_Visual : Def, Renderable> { using Specification = Spec; using Item = typename Spec::Item; using Prepared_Data = typename Spec::Prepared_Data; diff --git a/render_3D/render_3D/visual/Basic_Visual.ipp b/render_3D/render_3D/visual/Basic_Visual.ipp index 5e285ee..487b062 100644 --- a/render_3D/render_3D/visual/Basic_Visual.ipp +++ b/render_3D/render_3D/visual/Basic_Visual.ipp @@ -10,13 +10,14 @@ struct Basic_Visual::Private : Basic_Visual::Prev_Private { Enqueue_Paint enqueue{}; /* 只入队不等待 GPU 完成的 Paint 入口。 */ }; Paint_Target paint_target{}; /* Builder 绑定的所属 Scene 异步 Paint 目标。 */ + Prepared_Visual prepared{}; /* 最近一次 Prepare 生成的唯一提交产物。 */ std::uint64_t next_revision{1}; /* 下一份 Prepared_Visual 的单调版本。 */ /* Scene Builder 调用:绑定只入队的异步 Paint 目标,目标生命周期必须覆盖 Visual。 */ void bind_paint_target(std::shared_ptr context, Enqueue_Paint enqueue); /* CRTP 数据能力:从当前 Prop 构建后端字段,不访问其他缓冲角色。 */ template void prepare_data(Object* object); - /* CRTP 子图能力:每帧提交一次后端渲染;比较双槽 revision 后直接使用最新 Prepared 数据。 */ + /* CRTP 子图能力:提交本 Visual 最近一次 Prepare 生成的唯一产物。 */ template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& prop); /* CRTP 覆盖:绑定 Scene 后每帧执行 Paint;dirty 只表示 Prepared 数据是否更新,不控制帧生成。 */ @@ -25,9 +26,6 @@ struct Basic_Visual::Private : Basic_Visual::Prev_Private { /* CRTP Prop hook:本层字段改变时标记 Prepare。 */ template void after_prop_set(Object* object, Member Owner::* member, Prop_Access props); - /* CRTP 推进 hook:从 Prop 计算公开计数,不复制图元集合。 */ - template - void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); [[nodiscard]] static bool valid_items(const std::vector& items); }; template @@ -53,15 +51,16 @@ void Basic_Visual::Private::prepare_data(Object* object) { auto data = std::make_shared(); Spec::prepare(prop.items, *data); if constexpr (requires { data->style = prop.style; }) data->style = prop.style; - auto& output = object->template pending_buffer(); + auto& output = prepared; output = {}; output.transform = prop.transform; output.visible = prop.visible; output.depth_test = prop.depth_test; output.revision = next_revision++; output.data = std::move(data); - object->template update_state<&State::prepared_item_count, &State::prepared_revision>([&](State_Access states) { + object->template update_state<&State::item_count, &State::prepared_item_count, &State::prepared_revision>([&](State_Access states) { auto& state = states.template get(); + state.item_count = prop.items.size(); state.prepared_item_count = prop.items.size(); state.prepared_revision = output.revision; }); @@ -72,9 +71,7 @@ tf::Taskflow Basic_Visual::Private::build_paint_graph(Object* object, cons tf::Taskflow graph; graph.emplace([this, object] { if (auto context = paint_target.context.lock(); context && paint_target.enqueue) { - const auto& pending = object->template pending_buffer(); - const auto& current = object->template current_buffer(); - paint_target.enqueue(context.get(), object, pending.revision >= current.revision ? pending : current); + paint_target.enqueue(context.get(), object, prepared); } }).name("render_3d.paint.publish"); return graph; @@ -89,10 +86,4 @@ template void Basic_Visual::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } -template -template -void Basic_Visual::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { - using Visual_Tag = typename Basic_Visual::Base_Tag; - pending_states.template get().item_count = static_cast(*current_prop).items.size(); -} } diff --git a/render_3D/render_3D/visual/Prepared_Visual.hpp b/render_3D/render_3D/visual/Prepared_Visual.hpp index 47f757d..a118ad6 100644 --- a/render_3D/render_3D/visual/Prepared_Visual.hpp +++ b/render_3D/render_3D/visual/Prepared_Visual.hpp @@ -2,11 +2,10 @@ #include "../base/Types.hpp" #include namespace aethera::render_3d::detail { -struct Prepared_Visual_Tag {}; using Prepared_Position = std::array; using Prepared_Color = std::array; -/* 每个 Visual 的双缓冲只保存自己 Spec::Prepared_Data 的强类型结果。 */ +/* 每个 Visual 的 Private 只保存自己 Spec::Prepared_Data 的强类型结果。 */ template struct Prepared_Visual_For { Matrix4 transform{}; diff --git a/render_3D/tests/Visual_Tests.cpp b/render_3D/tests/Visual_Tests.cpp index 6688f28..a82d99f 100644 --- a/render_3D/tests/Visual_Tests.cpp +++ b/render_3D/tests/Visual_Tests.cpp @@ -15,7 +15,6 @@ void verify_visual(Item valid, Item) { object->template set<&Visual::Prop::items>(std::vector{valid}); object->advance(); EXPECT_EQ(object->template read_prop().items.size(), 1U); - EXPECT_EQ(object->template read_state().item_count, 1U); } } TEST(Render_3D_Visual, Migrated_Families_Build_And_Validate) { @@ -41,7 +40,10 @@ TEST(Render_3D_Scene, Builder_Instantiates_Async_Paint_Contract) { auto visual_result = typename Visual_Object::Builder{}.build(); ASSERT_TRUE(visual_result.has_value()); auto visual = std::move(visual_result).value(); - typename Scene_Object::Builder scene_builder(visual.get()); + auto camera = *Impl::Builder{}.build(); + auto axes = *Impl::Builder{}.build(); + typename Scene_Object::Builder scene_builder; + scene_builder.add_renderable(visual.get()).add_camera(camera.get()).add_axes(axes.get()); static_assert(std::same_as, Dependency_Graph_Error>>); } TEST(Render_3D_Scene, Paint_Submits_Without_Waiting_For_Gpu) { @@ -50,10 +52,14 @@ TEST(Render_3D_Scene, Paint_Submits_Without_Waiting_For_Gpu) { auto visual_result = typename Visual_Object::Builder{}.build(); ASSERT_TRUE(visual_result.has_value()); auto visual = std::move(visual_result).value(); + auto camera = *Impl::Builder{}.build(); + auto axes = *Impl::Builder{}.build(); visual->set<&Point_Visual::Prop::items>(std::vector{Point{.position = {0.0F, 0.0F, 0.0F}, .color = Color::red_color(), .diameter_px = 8.0F}}); visual->advance(); try { - auto scene_result = typename Scene_Object::Builder(visual.get()).build(); + typename Scene_Object::Builder scene_builder; + scene_builder.add_renderable(visual.get()).add_camera(camera.get()).add_axes(axes.get()); + auto scene_result = scene_builder.build(); ASSERT_TRUE(scene_result.has_value()); auto scene = std::move(scene_result).value(); std::atomic_bool frame_ready{}; diff --git a/web_server/src/Gallery_Plots_2D.cpp b/web_server/src/Gallery_Plots_2D.cpp index 6cbde5e..803defa 100644 --- a/web_server/src/Gallery_Plots_2D.cpp +++ b/web_server/src/Gallery_Plots_2D.cpp @@ -309,9 +309,10 @@ 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); - for (auto& block : blocks) + for (std::size_t block_index = 0; block_index < blocks.size(); ++block_index) object.template submit_stream( - std::make_shared>(std::move(block))); + std::make_shared( + Sweep_Spectrum_Block{block_index, std::move(blocks[block_index])})); object.template mark_dirty(); generated_count = block_count * width; } else if constexpr (std::same_as) { @@ -675,7 +676,8 @@ std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor) { values[i] = -90.0 + 35.0 * std::sin(sweep_index * 0.08 + event.time_milliseconds * 0.002); } raw->template submit_stream( - std::make_shared>(std::move(values))); + std::make_shared( + Sweep_Spectrum_Block{block_index, std::move(values)})); raw->template mark_dirty(); }; auto view = make_scene_view< diff --git a/web_server/src/Graph_WebSocket.cpp b/web_server/src/Graph_WebSocket.cpp index e764878..50d4e7b 100644 --- a/web_server/src/Graph_WebSocket.cpp +++ b/web_server/src/Graph_WebSocket.cpp @@ -69,25 +69,6 @@ void Graph_WebSocket::deliver_frame( } } -void Graph_WebSocket::deliver_input_completion( - Plot_Input_Observation observation) { - if (!d->attached.load(std::memory_order_acquire)) return; - const auto connection = d->connection.lock(); - if (!connection || !connection->connected()) return; - const nlohmann::json message{ - {"kind", "input_completion"}, - {"protocol", "aethera.input.latency"}, - {"version", 2}, - {"sequence", observation.sequence}, - {"rendered_frame_sequence", observation.rendered_frame_sequence}, - {"coalesced_event_count", observation.coalesced_event_count} - }; - try { - connection->send(message.dump(), drogon::WebSocketMessageType::Text); - } - catch (...) {} -} - void Graph_WebSocket::receive(std::string_view message) { const auto received_time = std::chrono::steady_clock::now(); const auto json = nlohmann::json::parse(message, nullptr, false); @@ -126,8 +107,6 @@ void Graph_WebSocket::receive(std::string_view message) { height = d->height; } Plot_Input_Event decoded; - decoded.sequence = input->value("sequence", std::uint64_t{}); - if (decoded.sequence == 0) return; decoded.coalesced_event_count = std::clamp( input->value("coalesced_event_count", std::size_t{1}), std::size_t{1}, std::size_t{1'000}); @@ -167,13 +146,7 @@ void Graph_WebSocket::receive(std::string_view message) { input->value("key", std::string{"unknown"})).value_or(Key::unknown); decoded.native_key = input->value("native_key", 0U); decoded.auto_repeat = input->value("auto_repeat", false); - const auto weak = weak_from_this(); - d->plot->submit_input( - std::move(decoded), - [weak](Plot_Input_Observation observation) { - if (const auto socket = weak.lock()) - socket->deliver_input_completion(std::move(observation)); - }); + d->plot->submit_input(std::move(decoded)); } catch (const nlohmann::json::exception&) {} catch (...) {} diff --git a/web_server/src/Graph_WebSocket.hpp b/web_server/src/Graph_WebSocket.hpp index c38e6f6..c5ee33e 100644 --- a/web_server/src/Graph_WebSocket.hpp +++ b/web_server/src/Graph_WebSocket.hpp @@ -19,7 +19,6 @@ public: void close() noexcept; private: void deliver_frame(std::shared_ptr frame); - void deliver_input_completion(Plot_Input_Observation observation); struct Private; std::unique_ptr d; }; diff --git a/web_server/src/Plot.cpp b/web_server/src/Plot.cpp index 5229b2a..861ca09 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -62,9 +62,15 @@ double elapsed_milliseconds(std::chrono::steady_clock::time_point start, struct Web_Input_Metadata { Plot_Input_Event input; - Plot::Input_Handler handler; std::chrono::steady_clock::time_point scene_dispatched; }; +struct Plot_Input_Observation { + double admission_ms{}; /* WebSocket 接收到提交 Scene 事件流的耗时。 */ + double scene_wait_ms{}; /* Scene 收到事件到 Prepare 消费事件的等待。 */ + double dispatch_ms{}; /* Scene 开始分发到 Renderable 完成消费的耗时。 */ + double server_consume_ms{}; /* WebSocket 接收到 Renderable 完成消费的服务端总耗时。 */ + std::size_t coalesced_event_count{}; /* 本次服务端事件代表的浏览器原始事件数量。 */ +}; struct Web_Input_Event { virtual ~Web_Input_Event() = default; [[nodiscard]] virtual const Web_Input_Metadata& web_input_metadata() const noexcept = 0; @@ -413,12 +419,11 @@ void Plot_Diagnostics::submit( } void Plot_Diagnostics::submit_input(const Plot_Input_Observation& observation) { - const std::array, 6> values{{ - {"input_plot_queue_ms", observation.plot_queue_ms}, - {"input_scene_queue_ms", observation.scene_queue_ms}, - {"input_backend_queue_ms", observation.backend_queue_ms}, - {"input_frame_completion_ms", observation.frame_completion_ms}, - {"input_server_total_ms", observation.server_total_ms}, + const std::array, 5> values{{ + {"input_admission_ms", observation.admission_ms}, + {"input_scene_wait_ms", observation.scene_wait_ms}, + {"input_dispatch_ms", observation.dispatch_ms}, + {"input_server_consume_ms", observation.server_consume_ms}, {"input_coalesced_event_count", static_cast(observation.coalesced_event_count)}}}; input_values.submit(values); } @@ -466,10 +471,8 @@ nlohmann::json Plot_Diagnostics::snapshot() const { } template -void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input, - Plot::Input_Handler handler) { - Web_Input_Metadata metadata{input, std::move(handler), - std::chrono::steady_clock::now()}; +void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { + Web_Input_Metadata metadata{input, std::chrono::steady_clock::now()}; const auto dispatch = [&](auto event) { scene.template submit_stream(std::move(event)); }; @@ -568,7 +571,6 @@ struct Plot::Private { bool tick_task_scheduled{}; /* Plot strand 上是否已有唯一消费任务。 */ double last_clock_render_time_ms{-std::numeric_limits::infinity()}; std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()}; - std::vector pending_event_reports; /* Scene 已处理、等待目标画面完成的报告。 */ template Private(asio::any_io_executor executor, std::unique_ptr value_scene, @@ -590,7 +592,7 @@ struct Plot::Private { void clock_tick(const Plot_Render_Tick& tick); void render_frame(Plot_Render_Tick tick); void queue_completed_frame(Render_Frame* frame); - void complete_event_reports(Frame_Identity rendered_identity); + void collect_consumed_input_statistics(); void fail(std::exception_ptr failure) noexcept; }; @@ -746,6 +748,7 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { (*scene_2d)->set<&Render_Scene_2D::Prop::viewport>( Size{static_cast(tick.width), static_cast(tick.height)}); const auto result = (*scene_2d)->render(&output); + collect_consumed_input_statistics(); if (result != Render_Scene_2D::Render_Result::completed) rollback_unsubmitted(); return; @@ -757,6 +760,7 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { auto& scene_3d = std::get>(scene); scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{tick.width, tick.height}); const auto result = scene_3d->render(&output); + collect_consumed_input_statistics(); if (result == Render_Scene_3D::Render_Result::submitted) return; rollback_unsubmitted(); if (result == Render_Scene_3D::Render_Result::backend_unavailable) @@ -768,7 +772,7 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { } } -void Plot::Private::complete_event_reports(Frame_Identity rendered_identity) { +void Plot::Private::collect_consumed_input_statistics() { aethera::Scene::Event_Report_Batch reports = std::visit( [](auto& value) { return value->template access_query_stream( @@ -779,50 +783,28 @@ void Plot::Private::complete_event_reports(Frame_Identity rendered_identity) { return result; }); }, scene); - pending_event_reports.insert(pending_event_reports.end(), - std::make_move_iterator(reports.begin()), - std::make_move_iterator(reports.end())); - const auto now = std::chrono::steady_clock::now(); - const auto first_complete = std::stable_partition( - pending_event_reports.begin(), pending_event_reports.end(), - [&](const aethera::Scene::Event_Report_Pointer& event) { - const auto timing = event->dispatch_timing(); - return timing.completed_steady_ns == 0 || - timing.frame_sequence > rendered_identity.sequence; - }); - for (auto iterator = first_complete; - iterator != pending_event_reports.end(); ++iterator) { - const auto* web_event = dynamic_cast(iterator->get()); + for (const auto& event : reports) { + const auto* web_event = dynamic_cast(event.get()); if (!web_event) continue; const auto& metadata = web_event->web_input_metadata(); - const auto timing = (*iterator)->dispatch_timing(); + const auto timing = event->dispatch_timing(); + if (timing.completed_steady_ns == 0) continue; const auto started = std::chrono::steady_clock::time_point{ std::chrono::nanoseconds(timing.started_steady_ns)}; const auto completed = std::chrono::steady_clock::time_point{ std::chrono::nanoseconds(timing.completed_steady_ns)}; Plot_Input_Observation observation{}; - observation.sequence = metadata.input.sequence; - observation.type = metadata.input.type; - observation.stage = Plot_Input_Stage::frame_completed; - observation.plot_queue_ms = elapsed_milliseconds( + observation.admission_ms = elapsed_milliseconds( metadata.input.received_time, metadata.scene_dispatched); - observation.scene_queue_ms = elapsed_milliseconds( + observation.scene_wait_ms = elapsed_milliseconds( metadata.scene_dispatched, started); - observation.backend_queue_ms = elapsed_milliseconds(started, completed); - observation.frame_completion_ms = elapsed_milliseconds(completed, now); - observation.server_total_ms = elapsed_milliseconds( - metadata.input.received_time, now); - observation.target_frame_sequence = timing.frame_sequence; - observation.rendered_frame_sequence = rendered_identity.sequence; + observation.dispatch_ms = elapsed_milliseconds(started, completed); + observation.server_consume_ms = elapsed_milliseconds( + metadata.input.received_time, completed); observation.coalesced_event_count = metadata.input.coalesced_event_count; diagnostics.submit_input(observation); - try { - if (metadata.handler) metadata.handler(std::move(observation)); - } - catch (...) {} } - pending_event_reports.erase(first_complete, pending_event_reports.end()); } void Plot::Private::queue_completed_frame(Render_Frame* frame) { @@ -906,8 +888,6 @@ void Plot::Private::queue_completed_frame(Render_Frame* frame) { pixel_storage = frame_3d->share_pixels(); } - complete_event_reports(rendered_identity); - auto pixels = std::make_shared(Plot_Pixel_Frame{ std::move(pixel_storage), managed->presentation_time, identity.sequence, identity.correlation_id, @@ -1066,7 +1046,7 @@ void Plot::render_once() { }); } -void Plot::submit_input(Plot_Input_Event event, Input_Handler handler) { +void Plot::submit_input(Plot_Input_Event event) { ensure_started(); if (d->terminal_failure.load(std::memory_order_acquire)) return; if (event.received_time == std::chrono::steady_clock::time_point{}) @@ -1078,10 +1058,9 @@ void Plot::submit_input(Plot_Input_Event event, Input_Handler handler) { */ try { if (auto* scene_2d = std::get_if>(&d->scene)) - dispatch_plot_input(**scene_2d, event, std::move(handler)); + dispatch_plot_input(**scene_2d, event); else - dispatch_plot_input(*std::get>(d->scene), - event, std::move(handler)); + dispatch_plot_input(*std::get>(d->scene), event); } catch (...) { d->fail(std::current_exception()); diff --git a/web_server/src/Plot.hpp b/web_server/src/Plot.hpp index 86fc069..bfc6678 100644 --- a/web_server/src/Plot.hpp +++ b/web_server/src/Plot.hpp @@ -15,7 +15,6 @@ namespace aethera::web { struct Plot_Input_Event { std::size_t coalesced_event_count{1}; /* 浏览器显示周期内合并的原始输入数量。 */ - std::uint64_t sequence{}; /* 页面为本次原始输入分配的单调身份。 */ Event_Type type{Event_Type::pointer_move}; /* 输入事件业务类型。 */ render_2d::Point_F position{}; /* Plot 像素坐标。 */ render_2d::Point_F global_position{}; /* 浏览器屏幕像素坐标。 */ @@ -32,24 +31,6 @@ struct Plot_Input_Event { std::chrono::steady_clock::time_point received_time{}; /* WebSocket 完成解析的服务端单调时刻。 */ }; -enum class Plot_Input_Stage : std::uint8_t { - frame_completed -}; - -struct Plot_Input_Observation { - std::uint64_t sequence{}; /* 对应页面原始输入身份。 */ - Event_Type type{Event_Type::pointer_move}; /* 对应输入事件业务类型。 */ - Plot_Input_Stage stage{Plot_Input_Stage::frame_completed}; /* 对外只发布完整画面闭环。 */ - double plot_queue_ms{}; /* WebSocket 接收到 Scene 接收的等待。 */ - double scene_queue_ms{}; /* Scene 接收事件到下一次 Render 消费事件的等待。 */ - double backend_queue_ms{}; /* 事件进入渲染后端到实际应用的等待。 */ - double frame_completion_ms{}; /* 后端应用事件到包含该状态的画面完成的等待。 */ - double server_total_ms{}; /* WebSocket 接收到画面完成或当前阶段的总耗时。 */ - std::uint64_t target_frame_sequence{}; /* 首个应包含本事件状态的逻辑帧。 */ - std::uint64_t rendered_frame_sequence{}; /* 实际包含本事件状态的物理画面帧。 */ - std::size_t coalesced_event_count{}; /* 合并到本事件的连续输入数量,包含自身。 */ -}; - struct Plot_Render_Tick { std::uint64_t sequence{}; /* 页面级帧时钟分配的关联序号。 */ double time_milliseconds{}; /* 页面级单调时间线,所有图共享同一个动画时刻。 */ @@ -78,7 +59,6 @@ public: using Stream_Id = std::uint64_t; using Stream_Handler = std::function)>; using Json_Handler = std::function; - using Input_Handler = std::function; class Scene_View { public: @@ -107,7 +87,7 @@ public: void configure_stream(Stream_Id stream, std::uint32_t width, std::uint32_t height); void schedule_render(Plot_Render_Tick tick); void render_once(); - void submit_input(Plot_Input_Event event, Input_Handler handler); + void submit_input(Plot_Input_Event event); void async_schema(Json_Handler handler); void async_write_prop(std::string component, std::string key, nlohmann::json value, Json_Handler handler); diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 87b29a4..1c392e0 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -59,9 +59,6 @@ type Frame_Diagnostics = {server: Plot_Diagnostics; samples: Frame_Sample[]; vid type Frame_Metrics = {sequence: number; generated_time_unix_ms: number; server_completion_ms: number; average_server_completion_ms: number; p95_server_completion_ms: number; p99_server_completion_ms: number; frame_rate_fps: number; p95_frame_interval_jitter_ms: number; pacing_mode: Frame_Pacing_Mode; fixed_rate_fps: number; delivery: Frame_Delivery; video_playback: Video_Playback_Metrics}; -type Input_Completion = {kind: "input_completion"; protocol: "aethera.input.latency"; version: 2; sequence: number; - rendered_frame_sequence: number; coalesced_event_count: number}; -type Input_Latency = Input_Completion & {browser_created_time_ms: number; browser_observation_ms: number}; type Stage_Statistic = "average" | "variability" | "p95" | "p99"; type Stage_Unit = "value" | "percentage"; @@ -96,13 +93,6 @@ function valid_plot_diagnostics(value: unknown): value is Plot_Diagnostics { Boolean(diagnostics.input_statistics) && Boolean(diagnostics.pacing); } -function valid_input_completion(value: unknown): value is Input_Completion { - if (!value || typeof value !== "object") return false; - const completion = value as Partial; - return completion.kind === "input_completion" && completion.protocol === "aethera.input.latency" && - completion.version === 2 && typeof completion.sequence === "number"; -} - function valid_gallery_layout(value: unknown): value is Gallery_Layout { if (!value || typeof value !== "object") return false; const layout = value as Partial; @@ -308,13 +298,10 @@ function use_plot_stream(plot: Plot, const [status, set_status] = useState("CONNECTING"); const [metrics, set_metrics] = useState(null); const [server_diagnostics, set_server_diagnostics] = useState(null); - const [input_latency, set_input_latency] = useState(null); const [error, set_error] = useState(null); const socket_ref = useRef(null); const pending_pointer_move = useRef | null>(null); const pointer_frame = useRef(0); - const next_input_sequence = useRef(1); - const input_origins = useRef(new Map()); const viewport_ref = useRef({width: tile_width, height: tile_height}); const playback_ref = useRef(shared_playback); useEffect(() => { playback_ref.current = shared_playback; }, [shared_playback]); @@ -323,10 +310,8 @@ function use_plot_stream(plot: Plot, const transmit = useCallback((kind: "input", event?: Record) => { const socket = socket_ref.current; if (socket?.readyState !== WebSocket.OPEN) return; - const sequence = next_input_sequence.current++; - input_origins.current.set(sequence, performance.now()); socket.send(JSON.stringify({kind, viewport: viewport_ref.current, - ...(event ? {event: {...event, sequence}} : {})})); + ...(event ? {event} : {})})); }, []); useEffect(() => { @@ -419,16 +404,6 @@ function use_plot_stream(plot: Plot, if (typeof event.data !== "string") return; let decoded: unknown; try { decoded = JSON.parse(event.data); } catch { return; } - if (valid_input_completion(decoded)) { - const created = input_origins.current.get(decoded.sequence); - if (created !== undefined) { - const now = performance.now(); - set_input_latency({...decoded, browser_created_time_ms: created, - browser_observation_ms: Math.max(0, now - created)}); - input_origins.current.delete(decoded.sequence); - } - return; - } const message = decoded as {kind?: string; message?: string}; if (message?.kind === "plot_error") { stopped = true; @@ -451,7 +426,6 @@ function use_plot_stream(plot: Plot, window.removeEventListener("aethera-reset-frame-diagnostics", on_diagnostics_reset); if (pointer_frame.current) cancelAnimationFrame(pointer_frame.current); pointer_frame.current = 0; - input_origins.current.clear(); socket_ref.current = null; socket.close(); }; @@ -614,7 +588,7 @@ function use_plot_stream(plot: Plot, surface.removeEventListener("contextmenu", on_context); }; }, [surface_ref, transmit]); - return {status, metrics, server_diagnostics, input_latency, error}; + return {status, metrics, server_diagnostics, error}; } const member_labels: Record = {x: "横坐标", y: "纵坐标", z: "深度", width: "宽度", height: "高度", origin: "起点", target: "终点", @@ -934,8 +908,8 @@ function Frame_Diagnostics_View({diagnostics, dimension, on_reset}: {diagnostics ["服务端完成 P99", `${(server_completion?.p99 ?? 0).toFixed(2)} ms`, "99% 样本不超过该服务端完成耗时,用于观察极端长尾。"], ["帧间隔平均", `${(frame_interval?.average ?? 0).toFixed(2)} ms`, `服务端相邻两次${completion}的滑动平均间隔。`], ["帧间隔波动", `${(frame_interval?.variability ?? 0).toFixed(2)} ms`, "服务端帧间隔窗口的总体标准差。"], - ["服务端事件闭环 P95", `${(input.input_server_total_ms?.p95 ?? 0).toFixed(2)} ms`, "浏览器事件抵达服务端到对应画面完成的服务端 P95;浏览器创建和显示阶段不在此值中。"], - ["Scene 等帧 P95", `${(input.input_scene_queue_ms?.p95 ?? 0).toFixed(2)} ms`, "事件进入 Scene 后等待下一次 Prepare 消费的服务端 P95。"], + ["服务端事件消费 P95", `${(input.input_server_consume_ms?.p95 ?? 0).toFixed(2)} ms`, "浏览器事件抵达服务端到 Renderable 完成消费的服务端 P95;不等待渲染、编码或画面显示。"], + ["Scene 等帧 P95", `${(input.input_scene_wait_ms?.p95 ?? 0).toFixed(2)} ms`, "事件进入 Scene 后等待下一次 Prepare 消费的服务端 P95。"], ["逻辑序列缺口", displayed.server.dropped_sequence_count.toLocaleString("zh-CN"), "服务端滑动统计期间未完成的逻辑帧序号数量。"] ]; const open_context_menu = (event: React.MouseEvent) => { @@ -1089,7 +1063,6 @@ const Plot_Card = memo(function Plot_Card({plot, selected, policy, gallery, on_p const tile_width = gallery.layout?.tile_width ?? 720; const tile_height = gallery.layout?.tile_height ?? 420; const graph = use_plot_stream(plot, surface_ref, gallery.playback, tile_width, tile_height); - const [input_visible_latency, set_input_visible_latency] = useState(null); const status: Stream_Status = graph.status === "OFFLINE" || gallery.status === "OFFLINE" ? "OFFLINE" : graph.status === "LIVE" && gallery.status === "LIVE" ? "LIVE" : "CONNECTING"; const metrics = graph.metrics; @@ -1104,17 +1077,6 @@ const Plot_Card = memo(function Plot_Card({plot, selected, policy, gallery, on_p video.srcObject = gallery.stream; if (gallery.stream) void video.play(); }, [gallery.stream]); - useEffect(() => { - const input = graph.input_latency; - const video = video_ref.current; - if (!input || !video || - typeof video.requestVideoFrameCallback !== "function") return; - const callback = video.requestVideoFrameCallback(() => { - set_input_visible_latency(Math.max( - 0, performance.now() - input.browser_created_time_ms)); - }); - return () => video.cancelVideoFrameCallback(callback); - }, [graph.input_latency]); const atlas_style: React.CSSProperties = gallery.layout && tile ? { width: `${gallery.layout.columns * 100}%`, height: `${gallery.layout.rows * 100}%`, @@ -1123,13 +1085,11 @@ const Plot_Card = memo(function Plot_Card({plot, selected, policy, gallery, on_p } : {}; const generated_time = metrics ? new Date(metrics.generated_time_unix_ms).toLocaleTimeString("zh-CN", {hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, fractionalSecondDigits: 3}) : "尚未生成帧"; return
on_select(plot)}>
绘图组件 · {plot.dimension}

{plot_labels[plot.id] ?? plot.title}

@@ -1144,23 +1104,21 @@ const Plot_Card = memo(function Plot_Card({plot, selected, policy, gallery, on_p {encoder_label} {gallery.transport ? gallery.transport.encode_average_ms.toFixed(1) : "--.-"} ms 落后 {source_transport?.has_rendered_frame ? source_transport.clock_lag_ticks : "--"} tick {plot.dimension === "3D" ? <> - 事件准入 P95 {server_input?.input_plot_queue_ms ? server_input.input_plot_queue_ms.p95.toFixed(1) : "--.-"} ms - Scene 等帧 P95 {server_input?.input_scene_queue_ms ? server_input.input_scene_queue_ms.p95.toFixed(1) : "--.-"} ms - 后端应用 P95 {server_input?.input_backend_queue_ms ? server_input.input_backend_queue_ms.p95.toFixed(1) : "--.-"} ms - 操作闭环 {graph.input_latency ? graph.input_latency.browser_observation_ms.toFixed(1) : "--.-"} ms - 可见画面 {input_visible_latency !== null ? input_visible_latency.toFixed(1) : "--.-"} ms - 合并 {graph.input_latency?.coalesced_event_count ?? 0} + 事件准入 P95 {server_input?.input_admission_ms ? server_input.input_admission_ms.p95.toFixed(1) : "--.-"} ms + Scene 等帧 P95 {server_input?.input_scene_wait_ms ? server_input.input_scene_wait_ms.p95.toFixed(1) : "--.-"} ms + 事件分发 P95 {server_input?.input_dispatch_ms ? server_input.input_dispatch_ms.p95.toFixed(1) : "--.-"} ms + 服务端消费 P95 {server_input?.input_server_consume_ms ? server_input.input_server_consume_ms.p95.toFixed(1) : "--.-"} ms + 平均合并 {server_input?.input_coalesced_event_count ? server_input.input_coalesced_event_count.average.toFixed(1) : "--.-"} : null}
{plot.dimension === "2D" ?
- 事件准入 P95 {server_input?.input_plot_queue_ms ? server_input.input_plot_queue_ms.p95.toFixed(1) : "--.-"} ms - Scene 等帧 P95 {server_input?.input_scene_queue_ms ? server_input.input_scene_queue_ms.p95.toFixed(1) : "--.-"} ms - 事件处理 P95 {server_input?.input_backend_queue_ms ? server_input.input_backend_queue_ms.p95.toFixed(1) : "--.-"} ms - 操作闭环 {graph.input_latency ? graph.input_latency.browser_observation_ms.toFixed(1) : "--.-"} ms - 可见画面 {input_visible_latency !== null ? input_visible_latency.toFixed(1) : "--.-"} ms - 合并 {graph.input_latency?.coalesced_event_count ?? 0} + 事件准入 P95 {server_input?.input_admission_ms ? server_input.input_admission_ms.p95.toFixed(1) : "--.-"} ms + Scene 等帧 P95 {server_input?.input_scene_wait_ms ? server_input.input_scene_wait_ms.p95.toFixed(1) : "--.-"} ms + 事件分发 P95 {server_input?.input_dispatch_ms ? server_input.input_dispatch_ms.p95.toFixed(1) : "--.-"} ms + 服务端消费 P95 {server_input?.input_server_consume_ms ? server_input.input_server_consume_ms.p95.toFixed(1) : "--.-"} ms + 平均合并 {server_input?.input_coalesced_event_count ? server_input.input_coalesced_event_count.average.toFixed(1) : "--.-"}
: null}
event.stopPropagation()}>