From db251b5abf270b7f938a93286ff1b084b8bce257 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Mon, 24 Aug 2026 00:55:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=A4=84=E7=90=86=E8=B5=B0?= =?UTF-8?q?=E5=8F=8C=E7=BC=93=E5=86=B2=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kernel/src/kernel/event.hpp | 12 ++ kernel/src/kernel/event.ipp | 20 +++ kernel/src/kernel/scene.cpp | 43 +++++- kernel/src/kernel/scene.hpp | 11 ++ kernel/src/kernel/scene.ipp | 46 +++--- render_2D/render_2D/scene/Render_Scene_2D.cpp | 3 - render_2D/render_2D/scene/Render_Scene_2D.hpp | 2 - render_2D/render_2D/scene/Render_Scene_2D.ipp | 22 ++- render_2D/tests/Axis_Test.cpp | 20 +-- render_2D/tests/Plottable_Migration_Test.cpp | 24 +-- .../render_3D/detail/Async_Render_Backend.cpp | 103 ++++++------- .../render_3D/detail/Async_Render_Backend.hpp | 12 +- render_3D/render_3D/detail/Render_Domain.cpp | 14 +- render_3D/render_3D/scene/Render_Scene_3D.cpp | 1 - render_3D/render_3D/scene/Render_Scene_3D.hpp | 2 - render_3D/render_3D/scene/Render_Scene_3D.ipp | 19 +-- web_server/src/Graph_WebSocket.cpp | 42 +++++- web_server/src/Graph_WebSocket.hpp | 1 + web_server/src/Plot.cpp | 140 +++++++++++++++--- web_server/src/Plot.hpp | 50 +++++-- webapp_gallery/src/app.tsx | 121 ++++++++++++++- 21 files changed, 497 insertions(+), 211 deletions(-) diff --git a/kernel/src/kernel/event.hpp b/kernel/src/kernel/event.hpp index 0b32b98..adef7ed 100644 --- a/kernel/src/kernel/event.hpp +++ b/kernel/src/kernel/event.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -17,13 +18,24 @@ enum class Event_Type : std::uint8_t { }; /* 所有输入事件的公共业务基类。 */ struct Event { + struct Dispatch_Timing { + std::uint64_t frame_sequence{}; + std::uint64_t started_steady_ns{}; + std::uint64_t completed_steady_ns{}; + }; explicit Event(Event_Type value); virtual ~Event(); void accept() const noexcept; [[nodiscard]] bool is_accepted() const noexcept; + void mark_dispatch_started(std::uint64_t frame_sequence) const noexcept; + void mark_dispatch_completed() const noexcept; + [[nodiscard]] Dispatch_Timing dispatch_timing() const noexcept; Event_Type type; /* 事件种类;构造后保持不变。 */ private: mutable bool accepted{}; /* 处理链是否已经消费事件。 */ + mutable std::atomic_uint64_t dispatch_frame_sequence_{}; + mutable std::atomic_uint64_t dispatch_started_steady_ns_{}; + mutable std::atomic_uint64_t dispatch_completed_steady_ns_{}; }; template concept Event_Object = std::derived_from, Event>; diff --git a/kernel/src/kernel/event.ipp b/kernel/src/kernel/event.ipp index 3ab19d7..29e38a0 100644 --- a/kernel/src/kernel/event.ipp +++ b/kernel/src/kernel/event.ipp @@ -1,9 +1,29 @@ #pragma once +#include namespace aethera { +namespace detail { +inline std::uint64_t event_steady_time_ns() noexcept { + return static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); +} +} inline Event::Event(Event_Type value) : type(value) {} inline Event::~Event() = default; inline void Event::accept() const noexcept { accepted = true; } inline bool Event::is_accepted() const noexcept { return accepted; } +inline void Event::mark_dispatch_started(std::uint64_t frame_sequence) const noexcept { + dispatch_frame_sequence_.store(frame_sequence, std::memory_order_relaxed); + dispatch_completed_steady_ns_.store(0, std::memory_order_relaxed); + dispatch_started_steady_ns_.store(detail::event_steady_time_ns(), std::memory_order_release); +} +inline void Event::mark_dispatch_completed() const noexcept { + dispatch_completed_steady_ns_.store(detail::event_steady_time_ns(), std::memory_order_release); +} +inline Event::Dispatch_Timing Event::dispatch_timing() const noexcept { + return {dispatch_frame_sequence_.load(std::memory_order_relaxed), + dispatch_started_steady_ns_.load(std::memory_order_acquire), + dispatch_completed_steady_ns_.load(std::memory_order_acquire)}; +} constexpr Keyboard_Modifier operator|(Keyboard_Modifier left, Keyboard_Modifier right) noexcept { return static_cast(static_cast(left) | static_cast(right)); } template Basic_Pointer_Event::Basic_Pointer_Event(Event_Type value) : Event(value) {} template double Basic_Pointer_Event::position_x() const noexcept { return static_cast(position.x); } diff --git a/kernel/src/kernel/scene.cpp b/kernel/src/kernel/scene.cpp index fdd13ea..abfb1fe 100644 --- a/kernel/src/kernel/scene.cpp +++ b/kernel/src/kernel/scene.cpp @@ -1,16 +1,43 @@ #include "scene.hpp" /* 后端共有 Prepare Scene 实现。 */ -#include #include namespace aethera { -void Scene::Private::Event_Queue::push(std::shared_ptr event) { - if (!pending.enqueue(std::move(event))) throw std::bad_alloc{}; -} -Scene::Private::Private() : runtime(std::make_unique()) {} +Scene::Private::Private() = default; Scene::Private::~Private() { - if (runtime->taskflow) detail::clear_stage_observers(runtime->taskflow.get()); + if (runtime && runtime->taskflow) detail::clear_stage_observers(runtime->taskflow.get()); } -void Scene::Private::push_event(std::unique_ptr event) { +void Scene::Private::push_event(Event_Pointer event) { if (!event) throw std::invalid_argument("scene event ownership must not be empty"); - event_queue.push(std::shared_ptr{std::move(event)}); + std::lock_guard lock(runtime->event_mutex); + runtime->events.pending->push_back(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; +} +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; +} +void Scene::dispatch_event(Event_Pointer event) { + static_cast(*d).push_event(std::move(event)); +} +Scene::Event_Report_Batch Scene::take_event_reports() { + return static_cast(*d).take_event_reports(); } } diff --git a/kernel/src/kernel/scene.hpp b/kernel/src/kernel/scene.hpp index 9e0470c..405b191 100644 --- a/kernel/src/kernel/scene.hpp +++ b/kernel/src/kernel/scene.hpp @@ -1,6 +1,9 @@ #pragma once #include "frame.hpp" #include "renderable.hpp" +#include +#include +#include namespace aethera { /* Scene 状态标签,用于访问和订阅 Scene::State。 */ /* @@ -8,6 +11,10 @@ namespace aethera { * 用户最终通过 Impl 创建可使用实例;编辑 Dependency_Graph 后调用 advance() 提交结构变化,再调用 process(...) 执行当前场景。 */ struct Scene : Def> { + using Event_Pointer = std::shared_ptr; + using Event_Batch = std::pmr::vector; + using Event_Report_Pointer = std::shared_ptr; + using Event_Report_Batch = std::pmr::vector; /* Scene 当前没有额外发布属性;派生定义可在自己的 Prop 中继续追加字段。 */ struct Prop : Prev_Prop {}; /* Scene 每次 process(...) 后发布的总图结构与执行统计。 */ @@ -24,6 +31,10 @@ struct Scene : Def + [[nodiscard]] std::shared_ptr make_event(Arguments&&... arguments); + void dispatch_event(Event_Pointer event); + [[nodiscard]] Event_Report_Batch take_event_reports(); }; } #include "scene.ipp" diff --git a/kernel/src/kernel/scene.ipp b/kernel/src/kernel/scene.ipp index ecec2bc..5a918ca 100644 --- a/kernel/src/kernel/scene.ipp +++ b/kernel/src/kernel/scene.ipp @@ -1,6 +1,6 @@ #pragma once -#include #include +#include #include #include #include @@ -8,23 +8,13 @@ namespace aethera { struct Scene::Private : Prev_Private { struct Result {}; /* process(...) 完成回调的结果类型;当前仅表示完成。 */ struct Runtime; /* Scene 的 Taskflow 构建产物;完整定义位于本文件下方。 */ - /* Scene 唯一事件流:第三方 MPMC 队列负责跨线程所有权转移,当前事件仅供单消费者重试。 */ - struct Event_Queue : Pinned { - moodycamel::ConcurrentQueue> pending{}; /* 外部线程提交、Prepare 线程消费的事件队列。 */ - std::shared_ptr current{}; /* 后端暂不可用时留到下一帧重试的事件。 */ - void push(std::shared_ptr event); - template - void consume(Callback&& callback) requires std::predicate&>; - }; std::unique_ptr runtime; /* Scene 唯一运行时构建产物的所有权。 */ - Event_Queue event_queue; /* 跨输入线程与 Scene Prepare 边界交换的唯一事件队列。 */ Private(); ~Private(); - /* 将事件所有权无锁提交到外部待处理批次;空所有权违反调用契约。 */ - void push_event(std::unique_ptr event); - /* 在 Scene 执行线程交换并按提交顺序消费一个完整批次。 */ - template - void consume_events(Callback&& callback) requires std::predicate&>; + void push_event(Event_Pointer event); + [[nodiscard]] Event_Batch take_events(std::uint64_t frame_sequence); + [[nodiscard]] Event_Report_Batch take_event_reports(); + template void bind_private_crtp(Object* object); /* Def CRTP hook:所有缓冲推进后重建必要的总 Taskflow,并更新 Scene_State_Tag 状态层。 */ template void after_advance(Object* object, @@ -42,19 +32,23 @@ struct Scene::Private : Prev_Private { }; struct Scene::Private::Runtime { std::unique_ptr taskflow; /* 当前已构建的总 Taskflow;为空表示尚未构建。 */ + std::mutex event_mutex; + double_buffer::Double_Buffer events; + std::mutex report_mutex; + double_buffer::Double_Buffer reports; + explicit Runtime(std::pmr::memory_resource* resource) + : events(std::allocator_arg, typename decltype(events)::allocator_type{resource}), + reports(std::allocator_arg, typename decltype(reports)::allocator_type{resource}) {} }; -template -void Scene::Private::Event_Queue::consume(Callback&& callback) requires std::predicate&> { - if (current && !std::invoke(callback, std::as_const(current))) return; - current.reset(); - while (pending.try_dequeue(current)) { - if (!std::invoke(callback, std::as_const(current))) return; - current.reset(); - } +template +void Scene::Private::bind_private_crtp(Object* object) { + Prev_Private::bind_private_crtp(object); + runtime = std::make_unique(object->memory_resource()); } -template -void Scene::Private::consume_events(Callback&& callback) requires std::predicate&> { - event_queue.consume(std::forward(callback)); +template +std::shared_ptr Scene::make_event(Arguments&&... arguments) { + return std::allocate_shared( + allocator(), std::forward(arguments)...); } template void Scene::Private::process(Object* object, Callback&& callback) requires std::invocable { diff --git a/render_2D/render_2D/scene/Render_Scene_2D.cpp b/render_2D/render_2D/scene/Render_Scene_2D.cpp index ee71b83..766ed72 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.cpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.cpp @@ -5,9 +5,6 @@ bool Render_Scene_2D::Prop::operator==(const Prop&) const = default; Render_Scene_2D::Render_Result Render_Scene_2D::render(Frame_2D* frame) { return static_cast(*d).dispatch->render(this, frame); } void Render_Scene_2D::set_frame_callback(Frame_Callback callback) { static_cast(*d).dispatch->set_frame_callback(this, std::move(callback)); } -void Render_Scene_2D::dispatch_event(std::unique_ptr event) { - static_cast(*d).dispatch->push_event(this, std::move(event)); -} void Render_Scene_2D::activate_view() { static_cast(*d).dispatch->set_active(this, true); } diff --git a/render_2D/render_2D/scene/Render_Scene_2D.hpp b/render_2D/render_2D/scene/Render_Scene_2D.hpp index f811091..8633470 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.hpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.hpp @@ -39,8 +39,6 @@ struct Render_Scene_2D : Def event); /* 激活后 render 才会执行。 */ void activate_view(); /* 停止后续 render 调用,不清除最后一帧。 */ diff --git a/render_2D/render_2D/scene/Render_Scene_2D.ipp b/render_2D/render_2D/scene/Render_Scene_2D.ipp index 726bcf7..428faa4 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.ipp +++ b/render_2D/render_2D/scene/Render_Scene_2D.ipp @@ -34,7 +34,6 @@ std::expected, Dependency_Graph_Error> Render_Scene_2D:: struct Render_Scene_2D::Private : Prev_Private { using Render_Run = Render_Result (*)(Root*, Frame_2D*); using Callback_Run = void (*)(Root*, Frame_Callback); - using Event_Run = void (*)(Root*, std::unique_ptr); using Active_Run = void (*)(Root*, bool); struct Paint_Node { Root* object{}; /* Paint 拓扑位置对应的最终对象;Scene 不拥有。 */ @@ -55,7 +54,6 @@ struct Render_Scene_2D::Private : Prev_Private { struct Dispatch { Render_Run render; /* 向外部帧执行最终 Scene 并合成颜色层。 */ Callback_Run set_frame_callback; /* 安装最终完成帧回调。 */ - Event_Run push_event; /* 向最终 Scene 的无锁待处理批次转移事件所有权。 */ Active_Run set_active; /* 修改最终 Scene 的视图活动状态。 */ }; const Dispatch* dispatch{}; /* Builder 绑定最终 Scene 类型后的静态分派表。 */ @@ -74,7 +72,9 @@ struct Render_Scene_2D::Private : Prev_Private { /* 每帧在 Prepare 完成后计算组级 dirty,并为所有二维节点指定唯一绘制目标。 */ template void prepare_paint_targets(Object* object, Blend2D_Cache& frame, Size viewport); /* Prepare 开始时交换 Scene 事件批次,按区域、Paint 逆序和最近目标规则逐事件完成接受链。 */ - template void dispatch_events(Object* object, Size viewport); + template + void dispatch_events(Object* object, Size viewport, + std::uint64_t frame_sequence); template [[nodiscard]] Render_Result render(Object* object, Frame_2D* frame); template [[nodiscard]] static const Dispatch& dispatch_for(); @@ -234,7 +234,7 @@ void Render_Scene_2D::Private::process(Object* object, Callback&& callback) if (!state.view_active || state.viewport.empty()) return; frame_object->mark(Frame_Trace_Marker::scene_render_started); frame_object->mark(Frame_Trace_Marker::event_dispatch_started); - dispatch_events(object, state.viewport); + dispatch_events(object, state.viewport, frame_object->identity().sequence); frame_object->mark(Frame_Trace_Marker::event_dispatch_finished); object->template current_dependency_graph().for_each([](const Dependency_Graph::Node& node) { if (!node.object->template take_dirty()) return; @@ -295,8 +295,10 @@ Render_Scene_2D::Render_Result Render_Scene_2D::Private::render(Object* object, return prop.view_active ? Render_Result::empty_viewport : Render_Result::view_inactive; } template -void Render_Scene_2D::Private::dispatch_events(Object* object, Size viewport) { - this->consume_events([&](const std::shared_ptr& owned_event) { +void Render_Scene_2D::Private::dispatch_events(Object* object, Size viewport, + std::uint64_t frame_sequence) { + auto events = this->take_events(frame_sequence); + for (const auto& owned_event : events) { const Event& event = *owned_event; const auto* pointer = dynamic_cast(&event); std::vector order; @@ -317,8 +319,8 @@ void Render_Scene_2D::Private::dispatch_events(Object* object, Size viewport) { if (event.is_accepted()) break; target.private_data->event_run(target.object, event); } - return true; - }); + event.mark_dispatch_completed(); + } } template const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for() { @@ -331,10 +333,6 @@ const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for auto* object = static_cast(root); static_cast(*object->d).frame_callback = std::move(callback); }, - [](Root* root, std::unique_ptr event) { - auto* object = static_cast(root); - static_cast(*object->d).push_event(std::move(event)); - }, [](Root* root, bool active) { static_cast(root)->template set<&Prop::view_active>(active); } }; return value; diff --git a/render_2D/tests/Axis_Test.cpp b/render_2D/tests/Axis_Test.cpp index 3275c32..86f97ba 100644 --- a/render_2D/tests/Axis_Test.cpp +++ b/render_2D/tests/Axis_Test.cpp @@ -117,19 +117,19 @@ TEST(axis_event, numeric_axis_wheel_zoom_and_drag_update_authoritative_range) { }).has_value())); scene->activate_view(); render_once(scene.get()); - auto wheel = std::make_unique(); + auto wheel = scene->make_event(); wheel->position = {50.0, 0.0}; wheel->angle_delta_y = 120.0; - scene->dispatch_event(std::move(wheel)); + scene->dispatch_event(wheel); render_once(scene.get()); EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.5, 9.5})); - auto press = std::make_unique(Event_Type::pointer_press); + auto press = scene->make_event(Event_Type::pointer_press); press->position = {50.0, 0.0}; press->button = Mouse_Button::left; - scene->dispatch_event(std::move(press)); - auto move = std::make_unique(Event_Type::pointer_move); + scene->dispatch_event(press); + auto move = scene->make_event(Event_Type::pointer_move); move->position = {60.0, 0.0}; - scene->dispatch_event(std::move(move)); + scene->dispatch_event(move); render_once(scene.get()); EXPECT_EQ(axis->coordinate_range(), (Axis_Range{-0.4, 8.6})); } @@ -155,18 +155,18 @@ TEST(axis_event, scene_routes_pointer_interaction_to_the_nearest_axis_segment) { scene->activate_view(); render_once(scene.get()); - auto near_vertical = std::make_unique(); + auto near_vertical = scene->make_event(); near_vertical->position = {22.0, 80.0}; near_vertical->angle_delta_y = 120.0; - scene->dispatch_event(std::move(near_vertical)); + scene->dispatch_event(near_vertical); render_once(scene.get()); EXPECT_DOUBLE_EQ(horizontal->coordinate_range().size(), 10.0); EXPECT_DOUBLE_EQ(vertical->coordinate_range().size(), 9.0); - auto near_horizontal = std::make_unique(); + auto near_horizontal = scene->make_event(); near_horizontal->position = {120.0, 178.0}; near_horizontal->angle_delta_y = 120.0; - scene->dispatch_event(std::move(near_horizontal)); + scene->dispatch_event(near_horizontal); render_once(scene.get()); EXPECT_DOUBLE_EQ(horizontal->coordinate_range().size(), 9.0); EXPECT_DOUBLE_EQ(vertical->coordinate_range().size(), 9.0); diff --git a/render_2D/tests/Plottable_Migration_Test.cpp b/render_2D/tests/Plottable_Migration_Test.cpp index 3dddf32..128cd36 100644 --- a/render_2D/tests/Plottable_Migration_Test.cpp +++ b/render_2D/tests/Plottable_Migration_Test.cpp @@ -178,20 +178,20 @@ TEST(selection_overlay, control_extends_selection_and_plain_click_clears_it) { render_once(scene.get()); const auto drag = [&](Point_F first, Point_F second, Keyboard_Modifier modifiers) { - auto press = std::make_unique(Event_Type::pointer_press); + auto press = scene->make_event(Event_Type::pointer_press); press->position = first; press->button = Mouse_Button::left; press->modifiers = modifiers; - scene->dispatch_event(std::move(press)); - auto move = std::make_unique(Event_Type::pointer_move); + scene->dispatch_event(press); + auto move = scene->make_event(Event_Type::pointer_move); move->position = second; move->modifiers = modifiers; - scene->dispatch_event(std::move(move)); - auto release = std::make_unique(Event_Type::pointer_release); + scene->dispatch_event(move); + auto release = scene->make_event(Event_Type::pointer_release); release->position = second; release->button = Mouse_Button::left; release->modifiers = modifiers; - scene->dispatch_event(std::move(release)); + scene->dispatch_event(release); render_once(scene.get()); }; drag({30.0, 90.0}, {60.0, 60.0}, Keyboard_Modifier::none); @@ -223,17 +223,17 @@ TEST(selection_overlay, time_axis_selection_keeps_axis_coordinates_while_window_ scene->set<&Render_Scene_2D::Prop::viewport>(canvas); scene->activate_view(); render_once(scene.get()); - auto press = std::make_unique(Event_Type::pointer_press); + auto press = scene->make_event(Event_Type::pointer_press); press->position = {92.0, 88.0}; press->button = Mouse_Button::left; - scene->dispatch_event(std::move(press)); - auto move = std::make_unique(Event_Type::pointer_move); + scene->dispatch_event(press); + auto move = scene->make_event(Event_Type::pointer_move); move->position = {116.0, 56.0}; - scene->dispatch_event(std::move(move)); - auto release = std::make_unique(Event_Type::pointer_release); + scene->dispatch_event(move); + auto release = scene->make_event(Event_Type::pointer_release); release->position = {116.0, 56.0}; release->button = Mouse_Button::left; - scene->dispatch_event(std::move(release)); + scene->dispatch_event(release); render_once(scene.get()); ASSERT_EQ(overlay->selected_regions().size(), 1u); const Axis_Rectangle selected = overlay->selected_regions().front(); diff --git a/render_3D/render_3D/detail/Async_Render_Backend.cpp b/render_3D/render_3D/detail/Async_Render_Backend.cpp index 878dffa..fbb3efd 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.cpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.cpp @@ -72,6 +72,7 @@ struct Async_Render_Backend::Implementation Shared_Prepared_Visual_Batch visuals{}; /* Scene CPU Prepare 发布的大批量不可变快照。 */ Scene_3D_Parameters parameters{}; /* 本帧 Scene、Camera 与轴参数快照。 */ std::vector completions{}; /* 合并到同一实际画面的逻辑帧。 */ + Scene::Event_Batch events{}; /* Prepare 边界已交换的完整事件批次。 */ }; struct Pending { Datoviz_Visual_Backend::Pending_Frame backend_frame{}; /* 三缓冲目标对应的已录制提交。 */ @@ -84,11 +85,7 @@ struct Async_Render_Backend::Implementation std::optional result{}; /* fence 正常交付时的结果。 */ std::exception_ptr failure{}; /* 提交域或完成服务的 Unknown Failure。 */ }; - struct Event_Command { - std::shared_ptr event{}; /* Scene 转移给准备域的输入事件。 */ - Extent viewport{}; /* 事件产生时的物理像素视口。 */ - }; - using Command = std::variant; + using Command = std::variant; static constexpr std::size_t command_capacity = 64; std::shared_ptr render_domain; /* 同 GPU 唯一的轻量 Queue Submit 域。 */ @@ -124,7 +121,7 @@ struct Async_Render_Backend::Implementation void initialize(const Scene_3D_Parameters& parameters); [[nodiscard]] Async_Render_Backend::Submit_Result render( Shared_Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, - Frame_3D* frame); + Frame_3D* frame, Scene::Event_Batch events); void accept(Submission submission); void merge_deferred(Submission submission); void prepare(Submission submission); @@ -133,7 +130,7 @@ struct Async_Render_Backend::Implementation void resolve(std::shared_ptr pending, std::optional completed); void complete(std::vector completions); - void dispatch(Event_Command command); + void dispatch(const std::shared_ptr& event, Extent viewport); }; Async_Render_Backend::Async_Render_Backend( @@ -198,7 +195,7 @@ void Async_Render_Backend::Implementation::initialize( Async_Render_Backend::Submit_Result Async_Render_Backend::Implementation::render( Shared_Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, - Frame_3D* frame) { + Frame_3D* frame, Scene::Event_Batch events) { if (!frame) throw std::invalid_argument("3D backend requires a non-null external frame"); if (!visuals || visuals->empty()) @@ -215,7 +212,8 @@ Async_Render_Backend::Implementation::render( frame->mark(Frame_Trace_Marker::backend_queue_entered); try { enqueue(Submission{std::move(visuals), std::move(parameters), - {Completion{frame, std::move(callback)}}}); + {Completion{frame, std::move(callback)}}, + std::move(events)}); } catch (...) { fail(std::current_exception()); @@ -226,6 +224,10 @@ Async_Render_Backend::Implementation::render( void Async_Render_Backend::Implementation::merge_deferred( Submission submission) { if (deferred_submission) { + submission.events.insert( + submission.events.begin(), + std::make_move_iterator(deferred_submission->events.begin()), + std::make_move_iterator(deferred_submission->events.end())); submission.completions.insert( submission.completions.begin(), std::make_move_iterator(deferred_submission->completions.begin()), @@ -253,6 +255,9 @@ void Async_Render_Backend::Implementation::prepare(Submission submission) { std::optional prepared; try { initialize(submission.parameters); + for (const auto& event : submission.events) + dispatch(event, submission.parameters.viewport); + submission.events.clear(); for (const auto& completion : submission.completions) completion.output->mark(Frame_Trace_Marker::backend_prepare_started); const auto sequence = @@ -446,14 +451,21 @@ void Async_Render_Backend::Implementation::finish(Gpu_Completion completion) { resolve(std::move(pending), std::nullopt); } } -void Async_Render_Backend::Implementation::dispatch(Event_Command command) { - if (!backend || !command.event) return; +void Async_Render_Backend::Implementation::dispatch( + const std::shared_ptr& event, Extent viewport) { + if (!event) return; const auto* pointer = - dynamic_cast(command.event.get()); + dynamic_cast(event.get()); const auto* wheel = - dynamic_cast(command.event.get()); - const auto* key = dynamic_cast(command.event.get()); - if (wheel) { + dynamic_cast(event.get()); + const auto* key = dynamic_cast(event.get()); + const bool pointer_valid = pointer && + std::isfinite(pointer->position_x()) && + std::isfinite(pointer->position_y()) && + pointer->position_x() >= 0.0 && pointer->position_y() >= 0.0 && + pointer->position_x() <= viewport.width && + pointer->position_y() <= viewport.height; + if (wheel && pointer_valid) { backend->dispatch_wheel( static_cast(pointer->position_x()), static_cast(pointer->position_y()), @@ -461,20 +473,24 @@ void Async_Render_Backend::Implementation::dispatch(Event_Command command) { wheel->angle_delta_x_value()), wheel_step(wheel->pixel_delta_y_value(), wheel->angle_delta_y_value()), - pointer->keyboard_modifiers(), command.viewport); + pointer->keyboard_modifiers(), viewport); } - else if (pointer) { + else if (pointer_valid && + (event->type == Event_Type::pointer_move || + event->type == Event_Type::pointer_press || + event->type == Event_Type::pointer_release)) { backend->dispatch_pointer( - command.event->type, + event->type, static_cast(pointer->position_x()), static_cast(pointer->position_y()), - command.event->type == Event_Type::pointer_move + event->type == Event_Type::pointer_move ? held_button(pointer->pointer_buttons()) : pointer->pointer_button(), - pointer->keyboard_modifiers(), command.viewport); + pointer->keyboard_modifiers(), viewport); } else if (key) backend->dispatch_key(*key); - command.event->accept(); + if ((wheel && pointer_valid) || pointer_valid || key) event->accept(); + event->mark_dispatch_completed(); } void Async_Render_Backend::Implementation::run() noexcept { for (;;) { @@ -483,12 +499,11 @@ void Async_Render_Backend::Implementation::run() noexcept { command, std::chrono::milliseconds(10)); if (received) { try { - if (auto* submission = std::get_if(&command)) + if (auto* submission = std::get_if(&command)) { accept(std::move(*submission)); + } else if (auto* completion = std::get_if(&command)) finish(std::move(*completion)); - else if (auto* event = std::get_if(&command)) - dispatch(std::move(*event)); } catch (...) { fail(std::current_exception()); @@ -527,8 +542,9 @@ void Async_Render_Backend::Implementation::run() noexcept { Async_Render_Backend::Submit_Result Async_Render_Backend::render( Shared_Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, - Frame_3D* frame) { - return implementation_->render(std::move(visuals), parameters, frame); + Frame_3D* frame, Scene::Event_Batch events) { + return implementation_->render(std::move(visuals), parameters, frame, + std::move(events)); } void Async_Render_Backend::set_frame_callback(Frame_Callback callback) { std::lock_guard lock(implementation_->callback_mutex); @@ -537,39 +553,4 @@ void Async_Render_Backend::set_frame_callback(Frame_Callback callback) { bool Async_Render_Backend::available() const noexcept { return implementation_->available.load(std::memory_order_acquire); } -Async_Render_Backend::Dispatch_Event_Result -Async_Render_Backend::dispatch_event(std::shared_ptr event, - Extent viewport) { - auto self = implementation_; - const auto* pointer = dynamic_cast(event.get()); - const auto* wheel = dynamic_cast(event.get()); - const auto* key = dynamic_cast(event.get()); - if (wheel && !pointer) return Dispatch_Event_Result::invalid_event; - if (pointer) { - if (!std::isfinite(pointer->position_x()) || - !std::isfinite(pointer->position_y())) - return Dispatch_Event_Result::invalid_event; - if (pointer->position_x() < 0.0 || pointer->position_y() < 0.0 || - pointer->position_x() > viewport.width || - pointer->position_y() > viewport.height) - return Dispatch_Event_Result::ignored; - if (!wheel && event->type != Event_Type::pointer_move && - event->type != Event_Type::pointer_press && - event->type != Event_Type::pointer_release) - return Dispatch_Event_Result::invalid_event; - } - else if (!key) return Dispatch_Event_Result::ignored; - std::lock_guard admission_lock(self->admission_mutex); - if (!self->available.load(std::memory_order_acquire) || - self->stop_requested.load(std::memory_order_acquire)) - return Dispatch_Event_Result::backend_unavailable; - try { - self->enqueue(Implementation::Event_Command{std::move(event), viewport}); - } - catch (...) { - self->fail(std::current_exception()); - return Dispatch_Event_Result::backend_unavailable; - } - return Dispatch_Event_Result::queued; -} } diff --git a/render_3D/render_3D/detail/Async_Render_Backend.hpp b/render_3D/render_3D/detail/Async_Render_Backend.hpp index f86c183..d289fec 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.hpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.hpp @@ -1,6 +1,7 @@ #pragma once #include "../base/Frame_3D.hpp" #include "Backend_Types.hpp" +#include #include #include namespace aethera::render_3d::detail { @@ -19,19 +20,10 @@ public: /* 将 Visual 快照所有权无等待转移给所属 GPU Render Domain。 */ [[nodiscard]] Submit_Result render( Shared_Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, - Frame_3D* frame); + Frame_3D* frame, Scene::Event_Batch events); /* 设置后续提交捕获的逻辑帧完成出口。 */ void set_frame_callback(Frame_Callback callback); [[nodiscard]] bool available() const noexcept; - enum class Dispatch_Event_Result : std::uint8_t { - queued, - ignored, - invalid_event, - backend_unavailable - }; - /* 将事件所有权无等待转移到 Render Domain;停止时由 Scene 在下一帧重试。 */ - [[nodiscard]] Dispatch_Event_Result dispatch_event( - std::shared_ptr event, Extent viewport); private: struct Implementation; std::shared_ptr implementation_; /* 已入队闭包共享实现生命周期。 */ diff --git a/render_3D/render_3D/detail/Render_Domain.cpp b/render_3D/render_3D/detail/Render_Domain.cpp index abc4d78..a1ec043 100644 --- a/render_3D/render_3D/detail/Render_Domain.cpp +++ b/render_3D/render_3D/detail/Render_Domain.cpp @@ -64,7 +64,8 @@ Render_Domain::Post_Result Render_Domain::post( active_posters_.fetch_sub(1, std::memory_order_release); return Post_Result::stopping; } - if (!tasks_.enqueue(std::move(task))) { + const bool enqueued = tasks_.enqueue(std::move(task)); + if (!enqueued) { active_posters_.fetch_sub(1, std::memory_order_release); throw std::bad_alloc{}; } @@ -75,8 +76,14 @@ void Render_Domain::run() noexcept { current_domain_ = this; for (;;) { std::unique_ptr task; - const bool received = tasks_.wait_dequeue_timed( + bool received = tasks_.wait_dequeue_timed( task, std::chrono::milliseconds(1)); + if (!received && stopping_.load(std::memory_order_acquire) && + active_posters_.load(std::memory_order_acquire) == 0) { + /* stop 边界关闭生产者后再真实探测队列,不能用 + * size_approx 决定生命周期。 */ + received = tasks_.try_dequeue(task); + } if (received && task) { try { task->function(); @@ -95,8 +102,7 @@ void Render_Domain::run() noexcept { // 已经消费完所有先于停止边界进入的任务。size_approx() 只能诊断, // 不能参与线程退出正确性。 if (!received && stopping_.load(std::memory_order_acquire) && - active_posters_.load(std::memory_order_acquire) == 0 && - !task) { + active_posters_.load(std::memory_order_acquire) == 0) { current_domain_ = nullptr; if (destroy_on_exit_.load(std::memory_order_acquire)) { if (thread_.joinable()) thread_.detach(); diff --git a/render_3D/render_3D/scene/Render_Scene_3D.cpp b/render_3D/render_3D/scene/Render_Scene_3D.cpp index cfdd484..c6972ef 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.cpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.cpp @@ -4,7 +4,6 @@ bool Render_Scene_3D::Prop::operator==(const Prop&) const = default; bool Render_Scene_3D::State::operator==(const State&) const = default; Render_Scene_3D::Render_Result Render_Scene_3D::render(Frame_3D* frame) { return static_cast(*d).dispatch->render(this, frame); } void Render_Scene_3D::set_frame_callback(Frame_Callback callback) { static_cast(*d).dispatch->set_frame_callback(this, std::move(callback)); } -void Render_Scene_3D::dispatch_event(std::unique_ptr event) { static_cast(*d).dispatch->push_event(this, std::move(event)); } void Render_Scene_3D::activate_view() { static_cast(*d).dispatch->set_active(this, true); } void Render_Scene_3D::deactivate_view() { static_cast(*d).dispatch->set_active(this, false); } } diff --git a/render_3D/render_3D/scene/Render_Scene_3D.hpp b/render_3D/render_3D/scene/Render_Scene_3D.hpp index 5b6a1e1..6bdb6ac 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.hpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.hpp @@ -62,8 +62,6 @@ struct Render_Scene_3D : Def event); void activate_view(); void deactivate_view(); }; diff --git a/render_3D/render_3D/scene/Render_Scene_3D.ipp b/render_3D/render_3D/scene/Render_Scene_3D.ipp index 7d45a19..2544310 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.ipp +++ b/render_3D/render_3D/scene/Render_Scene_3D.ipp @@ -17,12 +17,10 @@ struct Scene_Paint_Context { struct Render_Scene_3D::Private : Prev_Private { using Render_Run = Render_Result (*)(Root*, Frame_3D*); using Callback_Run = void (*)(Root*, Frame_Callback); - using Event_Run = void (*)(Root*, std::unique_ptr); using Active_Run = void (*)(Root*, bool); struct Dispatch { Render_Run render; /* 执行 CPU 图并异步提交 Paint。 */ Callback_Run set_frame_callback; /* 安装最终完成帧回调。 */ - Event_Run push_event; /* 向 Kernel Scene 无锁待处理批次转移事件所有权。 */ Active_Run set_active; /* 修改最终 Scene 的活动属性。 */ }; std::shared_ptr backend{}; /* Scene 拥有的异步后端;已入队命令自行延长实现寿命。 */ @@ -44,8 +42,6 @@ struct Render_Scene_3D::Private : Prev_Private { template void bind_private_crtp(Object* object); /* CRTP 覆盖:先执行 Kernel Prepare Taskflow,再运行只负责异步入队的三维 Submit 阶段。 */ template void process(Object* object, Callback&& callback) requires std::invocable; - /* Prepare 开始时交换公共 Scene 事件批次,并无等待提交到 Datoviz 渲染域。 */ - void dispatch_events(Extent viewport); template [[nodiscard]] Render_Result render(Object* object, Frame_3D* frame); template [[nodiscard]] static const Dispatch& dispatch_for(); }; @@ -164,12 +160,6 @@ void Render_Scene_3D::Private::initialize_backend( backend, object, nullptr, initial, std::make_shared()}); } -inline void Render_Scene_3D::Private::dispatch_events(Extent viewport) { - this->consume_events([&](const std::shared_ptr& event) { - return backend->dispatch_event(event, viewport) != - detail::Async_Render_Backend::Dispatch_Event_Result::backend_unavailable; - }); -} template void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requires std::invocable { Frame_3D* frame = active_frame; @@ -178,7 +168,7 @@ void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requ if (!prop.view_active || prop.viewport.empty() || !backend || !backend->available()) return; frame->mark(Frame_Trace_Marker::scene_render_started); frame->mark(Frame_Trace_Marker::event_dispatch_started); - dispatch_events(prop.viewport); + auto events = this->take_events(frame->identity().sequence); frame->mark(Frame_Trace_Marker::event_dispatch_finished); auto context = std::static_pointer_cast>(paint_context); context->parameters = parameters(object); @@ -202,7 +192,7 @@ void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requ frame->mark(Frame_Trace_Marker::prepare_finished); frame->mark(Frame_Trace_Marker::paint_started); const auto result = context->backend->render( - context->visuals, context->parameters, frame); + context->visuals, context->parameters, frame, std::move(events)); frame->mark(Frame_Trace_Marker::paint_finished); if (result == detail::Async_Render_Backend::Submit_Result::queued) std::invoke(std::forward(callback)); @@ -254,7 +244,8 @@ void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requ if (context->visuals->empty()) throw std::logic_error("render scene submit graph published no Visual snapshots"); backend_submitted = context->backend->render( - context->visuals, context->parameters, context->frame) == + context->visuals, context->parameters, context->frame, + std::move(events)) == detail::Async_Render_Backend::Submit_Result::queued; frame->mark(Frame_Trace_Marker::paint_finished); }); @@ -278,6 +269,6 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object, if (prop.viewport.empty()) return Render_Result::empty_viewport; return Render_Result::backend_unavailable; } -template const Render_Scene_3D::Private::Dispatch& Render_Scene_3D::Private::dispatch_for() { static const Dispatch value{[](Root* root, Frame_3D* frame) { auto* object = static_cast(root); return static_cast(*object->d).render(object, frame); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast(root); auto& data = static_cast(*object->d); if (data.backend) data.backend->set_frame_callback(std::move(callback)); }, [](Root* root, std::unique_ptr event) { auto* object = static_cast(root); static_cast(*object->d).push_event(std::move(event)); }, [](Root* root, bool active) { static_cast(root)->template set<&Prop::view_active>(active); }}; return value; } +template const Render_Scene_3D::Private::Dispatch& Render_Scene_3D::Private::dispatch_for() { static const Dispatch value{[](Root* root, Frame_3D* frame) { auto* object = static_cast(root); return static_cast(*object->d).render(object, frame); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast(root); auto& data = static_cast(*object->d); if (data.backend) data.backend->set_frame_callback(std::move(callback)); }, [](Root* root, bool active) { static_cast(root)->template set<&Prop::view_active>(active); }}; return value; } template void Render_Scene_3D::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } } diff --git a/web_server/src/Graph_WebSocket.cpp b/web_server/src/Graph_WebSocket.cpp index 10334c8..f5c4a9e 100644 --- a/web_server/src/Graph_WebSocket.cpp +++ b/web_server/src/Graph_WebSocket.cpp @@ -69,7 +69,35 @@ void Graph_WebSocket::deliver_frame( } } +void Graph_WebSocket::deliver_input_observation( + 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_observation"}, + {"protocol", "aethera.input.latency"}, + {"version", 1}, + {"sequence", observation.sequence}, + {"event_type", magic_enum::enum_name(observation.type)}, + {"stage", magic_enum::enum_name(observation.stage)}, + {"plot_queue_ms", observation.plot_queue_ms}, + {"scene_queue_ms", observation.scene_queue_ms}, + {"backend_queue_ms", observation.backend_queue_ms}, + {"frame_completion_ms", observation.frame_completion_ms}, + {"server_total_ms", observation.server_total_ms}, + {"target_frame_sequence", observation.target_frame_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); if (json.is_discarded() || !json.is_object()) return; const auto kind = json.value("kind", std::string{}); @@ -106,7 +134,13 @@ 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}); decoded.type = *type; + decoded.received_time = received_time; const auto read_point = [&](std::string_view key, render_2d::Point_F& point, bool viewport_relative) { @@ -141,7 +175,13 @@ 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); - d->plot->submit_input(std::move(decoded)); + 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_observation(std::move(observation)); + }); } catch (const nlohmann::json::exception&) {} catch (...) {} diff --git a/web_server/src/Graph_WebSocket.hpp b/web_server/src/Graph_WebSocket.hpp index c5ee33e..f752a0a 100644 --- a/web_server/src/Graph_WebSocket.hpp +++ b/web_server/src/Graph_WebSocket.hpp @@ -19,6 +19,7 @@ public: void close() noexcept; private: void deliver_frame(std::shared_ptr frame); + void deliver_input_observation(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 26dc1d0..5922546 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -32,6 +32,35 @@ using Scene_3D = Impl; constexpr std::uint16_t frame_protocol_version{9}; constexpr std::uint64_t diagnostic_sample_period{100}; +double elapsed_milliseconds(std::chrono::steady_clock::time_point start, + std::chrono::steady_clock::time_point finish) { + if (start == std::chrono::steady_clock::time_point{} || finish < start) + return 0.0; + return std::chrono::duration(finish - start).count(); +} + +struct Web_Input_Metadata { + Plot_Input_Event input; + Plot::Input_Handler handler; + std::chrono::steady_clock::time_point scene_dispatched; +}; +struct Web_Input_Event { + virtual ~Web_Input_Event() = default; + [[nodiscard]] virtual const Web_Input_Metadata& web_input_metadata() const noexcept = 0; +}; +template +struct Observed_Input_Event final : Event_Object_Type, Web_Input_Event { + template + Observed_Input_Event(Web_Input_Metadata value_metadata, + Arguments&&... arguments) + : Event_Object_Type(std::forward(arguments)...), + metadata(std::move(value_metadata)) {} + [[nodiscard]] const Web_Input_Metadata& web_input_metadata() const noexcept override { + return metadata; + } + Web_Input_Metadata metadata; +}; + std::string exception_description(const std::exception_ptr& failure) { try { if (failure) std::rethrow_exception(failure); @@ -209,7 +238,10 @@ nlohmann::json frame_metadata(Render_Frame& frame, std::uint32_t width, } template -void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { +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()}; const auto dispatch = [&](auto event) { scene.dispatch_event(std::move(event)); }; const auto apply_pointer = [&](auto& event) { event.position = input.position; @@ -222,13 +254,15 @@ void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { case Event_Type::pointer_move: case Event_Type::pointer_press: case Event_Type::pointer_release: { - auto event = std::make_unique>(input.type); + auto event = scene.template make_event>>( + std::move(metadata), input.type); apply_pointer(*event); dispatch(std::move(event)); break; } case Event_Type::wheel: { - auto event = std::make_unique>(); + auto event = scene.template make_event>>( + std::move(metadata)); apply_pointer(*event); event->pixel_delta_x = input.pixel_delta_x; event->pixel_delta_y = input.pixel_delta_y; @@ -239,7 +273,8 @@ void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { } case Event_Type::key_press: case Event_Type::key_release: { - auto event = std::make_unique(input.type); + auto event = scene.template make_event>( + std::move(metadata), input.type); event->key = input.key; event->native_key = input.native_key; event->modifiers = input.modifiers; @@ -248,7 +283,8 @@ void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { break; } default: - dispatch(std::make_unique(input.type)); + dispatch(scene.template make_event>( + std::move(metadata), input.type)); break; } } @@ -301,6 +337,7 @@ 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, @@ -322,6 +359,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 fail(std::exception_ptr failure) noexcept; }; @@ -422,6 +460,12 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { if (!pacing.render_enabled || streams.consumers.empty()) return; { std::lock_guard lock(frame_mutex); + const auto in_flight = std::ranges::count_if( + frame_slots, [](const Managed_Frame& slot) { + return slot.state == Frame_State::in_flight; + }); + if (std::holds_alternative>(scene) && + in_flight >= scene_frame_capacity - 1) return; if (std::ranges::none_of(frame_slots, [](const Managed_Frame& slot) { return slot.state == Frame_State::available; })) return; @@ -493,6 +537,54 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { } } +void Plot::Private::complete_event_reports(Frame_Identity rendered_identity) { + aethera::Scene::Event_Report_Batch reports = std::visit( + [](auto& value) { return value->take_event_reports(); }, 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()); + if (!web_event) continue; + const auto& metadata = web_event->web_input_metadata(); + const auto timing = (*iterator)->dispatch_timing(); + 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( + metadata.input.received_time, metadata.scene_dispatched); + observation.scene_queue_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.coalesced_event_count = + metadata.input.coalesced_event_count; + 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) { if (!frame) throw std::invalid_argument("Plot received a null completed frame"); @@ -574,6 +666,8 @@ 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, @@ -734,26 +828,26 @@ void Plot::render_once() { }); } -void Plot::submit_input(Plot_Input_Event event) { +void Plot::submit_input(Plot_Input_Event event, Input_Handler handler) { ensure_started(); if (d->terminal_failure.load(std::memory_order_acquire)) return; - const auto weak = weak_from_this(); - asio::post(d->strand, [weak, event = std::move(event)] { - const auto owner = weak.lock(); - if (!owner || owner->d->terminal_failure.load(std::memory_order_acquire)) - return; - try { - if (auto* scene = std::get_if>( - &owner->d->scene)) - dispatch_plot_input(**scene, event); - else - dispatch_plot_input( - *std::get>(owner->d->scene), event); - } - catch (...) { - owner->d->fail(std::current_exception()); - } - }); + if (event.received_time == std::chrono::steady_clock::time_point{}) + event.received_time = std::chrono::steady_clock::now(); + /* + * WebSocket 线程只向 Scene 的当前事件缓冲追加一个由 Scene + * memory_resource 分配的基类指针。Prepare 边界交换完整批次, + * 处理结果再由反向双缓冲交给 Plot 统计;事件处理不执行 Web 回调。 + */ + try { + if (auto* scene_2d = std::get_if>(&d->scene)) + dispatch_plot_input(**scene_2d, event, std::move(handler)); + else + dispatch_plot_input(*std::get>(d->scene), + event, std::move(handler)); + } + catch (...) { + d->fail(std::current_exception()); + } } void Plot::async_schema(Json_Handler handler) { diff --git a/web_server/src/Plot.hpp b/web_server/src/Plot.hpp index a09a1d7..b6dd23e 100644 --- a/web_server/src/Plot.hpp +++ b/web_server/src/Plot.hpp @@ -14,19 +14,40 @@ namespace aethera::web { struct Plot_Input_Event { - Event_Type type{Event_Type::pointer_move}; - render_2d::Point_F position{}; - render_2d::Point_F global_position{}; - Mouse_Button button{Mouse_Button::none}; - Mouse_Button_Mask buttons{}; - Keyboard_Modifier modifiers{Keyboard_Modifier::none}; - double pixel_delta_x{}; - double pixel_delta_y{}; - double angle_delta_x{}; - double angle_delta_y{}; - Key key{Key::unknown}; - std::uint32_t native_key{}; - bool auto_repeat{}; + 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{}; /* 浏览器屏幕像素坐标。 */ + Mouse_Button button{Mouse_Button::none}; /* 本次变化涉及的鼠标按键。 */ + Mouse_Button_Mask buttons{}; /* 事件产生时保持按下的按键集合。 */ + Keyboard_Modifier modifiers{Keyboard_Modifier::none}; /* 事件产生时的修饰键集合。 */ + double pixel_delta_x{}; /* 水平高精度滚轮增量。 */ + double pixel_delta_y{}; /* 垂直高精度滚轮增量。 */ + double angle_delta_x{}; /* 水平离散滚轮增量。 */ + double angle_delta_y{}; /* 垂直离散滚轮增量。 */ + Key key{Key::unknown}; /* 标准化键盘按键。 */ + std::uint32_t native_key{}; /* 浏览器原生按键码。 */ + bool auto_repeat{}; /* 是否为系统重复按键。 */ + 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 { @@ -57,6 +78,7 @@ 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: @@ -85,7 +107,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); + void submit_input(Plot_Input_Event event, Input_Handler handler); 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 ccc3007..e317bad 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -60,6 +60,11 @@ type Frame_Diagnostics = {metadata: Frame_Metadata; samples: Frame_Sample[]; fra 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_Stage = "queued" | "scene_dispatched" | "backend_queued" | "backend_applied" | "frame_completed"; +type Input_Observation = {kind: "input_observation"; protocol: "aethera.input.latency"; version: 1; sequence: number; + event_type: string; stage: Input_Stage; plot_queue_ms: number; scene_queue_ms: number; backend_queue_ms: number; frame_completion_ms: number; + server_total_ms: number; target_frame_sequence: number; rendered_frame_sequence: number; coalesced_event_count: number}; +type Input_Latency = Input_Observation & {browser_created_time_ms: number; browser_observation_ms: number}; type Stage_Statistic = "average" | "variability" | "p95" | "p99"; type Stage_Unit = "value" | "percentage"; @@ -100,6 +105,14 @@ function valid_frame_metadata(value: unknown): value is Frame_Metadata { && Boolean(frame.trace); } +function valid_input_observation(value: unknown): value is Input_Observation { + if (!value || typeof value !== "object") return false; + const observation = value as Partial; + return observation.kind === "input_observation" && observation.protocol === "aethera.input.latency" && + observation.version === 1 && typeof observation.sequence === "number" && + typeof observation.server_total_ms === "number" && typeof observation.stage === "string"; +} + function valid_gallery_layout(value: unknown): value is Gallery_Layout { if (!value || typeof value !== "object") return false; const layout = value as Partial; @@ -442,10 +455,13 @@ function use_plot_stream(plot: Plot, tile_width = 720, tile_height = 420) { const [status, set_status] = useState("CONNECTING"); const [metrics, set_metrics] = 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]); @@ -454,8 +470,10 @@ 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 ? {event: {...event, sequence}} : {})})); }, []); useEffect(() => { @@ -575,6 +593,17 @@ function use_plot_stream(plot: Plot, complete_metadata(decoded); return; } + if (valid_input_observation(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)}); + if (decoded.stage === "frame_completed") + input_origins.current.delete(decoded.sequence); + } + return; + } const message = decoded as {kind?: string; message?: string}; if (message?.kind === "plot_error") { stopped = true; @@ -597,6 +626,7 @@ 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(); }; @@ -625,9 +655,11 @@ function use_plot_stream(plot: Plot, const pointer_payload = (type: string, event: PointerEvent) => ({ type, position: point(event), global_position: global_point(event), button: type === "pointer_move" ? active_button(event) : button(event.button), - buttons: event.buttons, modifiers: modifiers(event) + buttons: event.buttons, modifiers: modifiers(event), coalesced_event_count: 1 }); let drag_move_started = false; + let pending_wheel: Record | null = null; + let wheel_frame = 0; const on_pointer_move = (event: PointerEvent) => { event.stopPropagation(); const payload = pointer_payload("pointer_move", event); @@ -636,7 +668,11 @@ function use_plot_stream(plot: Plot, transmit("input", payload); return; } - pending_pointer_move.current = payload; + const previous_count = Number( + pending_pointer_move.current?.coalesced_event_count ?? 0); + pending_pointer_move.current = { + ...payload, coalesced_event_count: previous_count + 1 + }; if (!pointer_frame.current) pointer_frame.current = requestAnimationFrame(() => { pointer_frame.current = 0; @@ -682,12 +718,28 @@ function use_plot_stream(plot: Plot, event.stopPropagation(); const scale = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 16 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? surface.clientHeight : 1; - transmit("input", {type: "wheel", position: point(event), + const next = {type: "wheel", position: point(event), global_position: global_point(event), button: "none", buttons: 0, modifiers: modifiers(event), pixel_delta_x: -event.deltaX * scale, pixel_delta_y: -event.deltaY * scale, angle_delta_x: Math.max(-120, Math.min(120, -event.deltaX * scale)), - angle_delta_y: Math.max(-120, Math.min(120, -event.deltaY * scale))}); + angle_delta_y: Math.max(-120, Math.min(120, -event.deltaY * scale)), + coalesced_event_count: 1}; + if (pending_wheel) { + next.pixel_delta_x += Number(pending_wheel.pixel_delta_x ?? 0); + next.pixel_delta_y += Number(pending_wheel.pixel_delta_y ?? 0); + next.angle_delta_x += Number(pending_wheel.angle_delta_x ?? 0); + next.angle_delta_y += Number(pending_wheel.angle_delta_y ?? 0); + next.coalesced_event_count = + Number(pending_wheel.coalesced_event_count ?? 0) + 1; + } + pending_wheel = next; + if (!wheel_frame) + wheel_frame = requestAnimationFrame(() => { + wheel_frame = 0; + if (pending_wheel) transmit("input", pending_wheel); + pending_wheel = null; + }); }; const key_name = (key: string) => ({Escape: "escape", Enter: "enter", " ": "space", Delete: "delete_key", Backspace: "backspace", @@ -721,6 +773,9 @@ function use_plot_stream(plot: Plot, surface.addEventListener("contextmenu", on_context); return () => { pending_pointer_move.current = null; + pending_wheel = null; + if (wheel_frame) cancelAnimationFrame(wheel_frame); + wheel_frame = 0; surface.removeEventListener("pointermove", on_pointer_move); surface.removeEventListener("pointerdown", on_pointer_down); surface.removeEventListener("pointerup", on_pointer_up); @@ -734,7 +789,7 @@ function use_plot_stream(plot: Plot, surface.removeEventListener("contextmenu", on_context); }; }, [surface_ref, transmit]); - return {status, metrics, error}; + return {status, metrics, input_latency, error}; } const member_labels: Record = {x: "横坐标", y: "纵坐标", z: "深度", width: "宽度", height: "高度", origin: "起点", target: "终点", @@ -1201,6 +1256,7 @@ 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; @@ -1214,6 +1270,17 @@ 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 || input.stage !== "frame_completed" || !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}%`, @@ -1221,8 +1288,46 @@ const Plot_Card = memo(function Plot_Card({plot, selected, policy, gallery, on_p top: `${-tile.row * 100}%` } : {}; 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}

{{IDLE: "已停止", CONNECTING: "重连中", LIVE: "实时", OFFLINE: "已离线"}[status]}{metrics?.delivery === "diagnostics" ? "无像素传输" : metrics ? enum_label(metrics.pacing_mode) : "等待策略"}
真实画面 {source_transport ? source_transport.rendered_frame_rate_fps.toFixed(1) : metrics ? metrics.frame_rate_fps.toFixed(1) : "--.-"} FPS逻辑闭环 {source_transport ? source_transport.logical_completion_rate_fps.toFixed(1) : "--.-"} FPS图集 {gallery.transport ? gallery.transport.encoded_frame_rate_fps.toFixed(1) : "--.-"} FPS解码 {gallery.playback.frame_rate_fps.toFixed(1)} FPS完成 {metrics ? metrics.server_completion_ms.toFixed(1) : "--.-"} ms{encoder_label} {gallery.transport ? gallery.transport.encode_average_ms.toFixed(1) : "--.-"} ms落后 {source_transport?.has_rendered_frame ? source_transport.clock_lag_ticks : "--"} tick
+ return
on_select(plot)}> +
+
绘图组件 · {plot.dimension}

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

+
+
{{IDLE: "已停止", CONNECTING: "重连中", LIVE: "实时", OFFLINE: "已离线"}[status]}{metrics?.delivery === "diagnostics" ? "无像素传输" : metrics ? enum_label(metrics.pacing_mode) : "等待策略"}
+
+ 真实画面 {source_transport ? source_transport.rendered_frame_rate_fps.toFixed(1) : metrics ? metrics.frame_rate_fps.toFixed(1) : "--.-"} FPS + 逻辑闭环 {source_transport ? source_transport.logical_completion_rate_fps.toFixed(1) : "--.-"} FPS + 图集 {gallery.transport ? gallery.transport.encoded_frame_rate_fps.toFixed(1) : "--.-"} FPS + 解码 {gallery.playback.frame_rate_fps.toFixed(1)} FPS + 完成 {metrics ? metrics.server_completion_ms.toFixed(1) : "--.-"} ms + {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" ? <> + 事件准入 {graph.input_latency ? graph.input_latency.plot_queue_ms.toFixed(1) : "--.-"} ms + Scene 等帧 {graph.input_latency ? graph.input_latency.scene_queue_ms.toFixed(1) : "--.-"} ms + 后端应用 {graph.input_latency ? graph.input_latency.backend_queue_ms.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} + : null} +
+
+
+ {plot.dimension === "2D" ?
+ 事件准入 {graph.input_latency ? graph.input_latency.plot_queue_ms.toFixed(1) : "--.-"} ms + Scene 等帧 {graph.input_latency ? graph.input_latency.scene_queue_ms.toFixed(1) : "--.-"} ms + 事件处理 {graph.input_latency ? graph.input_latency.backend_queue_ms.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} +
: null}
event.stopPropagation()}>