事件处理走双缓冲了
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
@@ -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 <typename T>
|
||||
concept Event_Object = std::derived_from<std::remove_cvref_t<T>, Event>;
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
#pragma once
|
||||
#include <chrono>
|
||||
namespace aethera {
|
||||
namespace detail {
|
||||
inline std::uint64_t event_steady_time_ns() noexcept {
|
||||
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
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<Keyboard_Modifier>(static_cast<std::uint8_t>(left) | static_cast<std::uint8_t>(right)); }
|
||||
template <Event_Point Point> Basic_Pointer_Event<Point>::Basic_Pointer_Event(Event_Type value) : Event(value) {}
|
||||
template <Event_Point Point> double Basic_Pointer_Event<Point>::position_x() const noexcept { return static_cast<double>(position.x); }
|
||||
|
||||
@@ -1,16 +1,43 @@
|
||||
#include "scene.hpp" /* 后端共有 Prepare Scene 实现。 */
|
||||
#include <new>
|
||||
#include <stdexcept>
|
||||
namespace aethera {
|
||||
void Scene::Private::Event_Queue::push(std::shared_ptr<Event> event) {
|
||||
if (!pending.enqueue(std::move(event))) throw std::bad_alloc{};
|
||||
}
|
||||
Scene::Private::Private() : runtime(std::make_unique<Runtime>()) {}
|
||||
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> 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<Event>{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<Private&>(*d).push_event(std::move(event));
|
||||
}
|
||||
Scene::Event_Report_Batch Scene::take_event_reports() {
|
||||
return static_cast<Private&>(*d).take_event_reports();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
#include "frame.hpp"
|
||||
#include "renderable.hpp"
|
||||
#include <memory>
|
||||
#include <memory_resource>
|
||||
#include <vector>
|
||||
namespace aethera {
|
||||
/* Scene 状态标签,用于访问和订阅 Scene::State。 */
|
||||
/*
|
||||
@@ -8,6 +11,10 @@ namespace aethera {
|
||||
* 用户最终通过 Impl<Scene> 创建可使用实例;编辑 Dependency_Graph 后调用 advance() 提交结构变化,再调用 process(...) 执行当前场景。
|
||||
*/
|
||||
struct Scene : Def<Scene, Root, Dependency_Graph_Type<Prepare_Data_Tag, Renderable>> {
|
||||
using Event_Pointer = std::shared_ptr<Event>;
|
||||
using Event_Batch = std::pmr::vector<Event_Pointer>;
|
||||
using Event_Report_Pointer = std::shared_ptr<const Event>;
|
||||
using Event_Report_Batch = std::pmr::vector<Event_Report_Pointer>;
|
||||
/* Scene 当前没有额外发布属性;派生定义可在自己的 Prop 中继续追加字段。 */
|
||||
struct Prop : Prev_Prop {};
|
||||
/* Scene 每次 process(...) 后发布的总图结构与执行统计。 */
|
||||
@@ -24,6 +31,10 @@ struct Scene : Def<Scene, Root, Dependency_Graph_Type<Prepare_Data_Tag, Renderab
|
||||
};
|
||||
/* 完整声明、字段及可覆盖 CRTP hook 见 scene.ipp 中的 Scene::Private。 */
|
||||
struct Private;
|
||||
template <Event_Object Event_Object_Type, typename... Arguments>
|
||||
[[nodiscard]] std::shared_ptr<Event_Object_Type> make_event(Arguments&&... arguments);
|
||||
void dispatch_event(Event_Pointer event);
|
||||
[[nodiscard]] Event_Report_Batch take_event_reports();
|
||||
};
|
||||
}
|
||||
#include "scene.ipp"
|
||||
|
||||
+20
-26
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
#include <concurrentqueue-1.0.5/concurrentqueue.h>
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
@@ -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<std::shared_ptr<Event>> pending{}; /* 外部线程提交、Prepare 线程消费的事件队列。 */
|
||||
std::shared_ptr<Event> current{}; /* 后端暂不可用时留到下一帧重试的事件。 */
|
||||
void push(std::shared_ptr<Event> event);
|
||||
template <typename Callback>
|
||||
void consume(Callback&& callback) requires std::predicate<Callback, const std::shared_ptr<Event>&>;
|
||||
};
|
||||
std::unique_ptr<Runtime> runtime; /* Scene 唯一运行时构建产物的所有权。 */
|
||||
Event_Queue event_queue; /* 跨输入线程与 Scene Prepare 边界交换的唯一事件队列。 */
|
||||
Private();
|
||||
~Private();
|
||||
/* 将事件所有权无锁提交到外部待处理批次;空所有权违反调用契约。 */
|
||||
void push_event(std::unique_ptr<Event> event);
|
||||
/* 在 Scene 执行线程交换并按提交顺序消费一个完整批次。 */
|
||||
template <typename Callback>
|
||||
void consume_events(Callback&& callback) requires std::predicate<Callback, const std::shared_ptr<Event>&>;
|
||||
void push_event(Event_Pointer event);
|
||||
[[nodiscard]] Event_Batch take_events(std::uint64_t frame_sequence);
|
||||
[[nodiscard]] Event_Report_Batch take_event_reports();
|
||||
template <Attached Object> void bind_private_crtp(Object* object);
|
||||
/* Def CRTP hook:所有缓冲推进后重建必要的总 Taskflow,并更新 Scene_State_Tag 状态层。 */
|
||||
template <Attached Object>
|
||||
void after_advance(Object* object,
|
||||
@@ -42,19 +32,23 @@ struct Scene::Private : Prev_Private {
|
||||
};
|
||||
struct Scene::Private::Runtime {
|
||||
std::unique_ptr<tf::Taskflow> taskflow; /* 当前已构建的总 Taskflow;为空表示尚未构建。 */
|
||||
std::mutex event_mutex;
|
||||
double_buffer::Double_Buffer<Event_Batch> events;
|
||||
std::mutex report_mutex;
|
||||
double_buffer::Double_Buffer<Event_Report_Batch> reports;
|
||||
explicit Runtime(std::pmr::memory_resource* resource)
|
||||
: events(std::allocator_arg, typename decltype(events)::allocator_type{resource}),
|
||||
reports(std::allocator_arg, typename decltype(reports)::allocator_type{resource}) {}
|
||||
};
|
||||
template <typename Callback>
|
||||
void Scene::Private::Event_Queue::consume(Callback&& callback) requires std::predicate<Callback, const std::shared_ptr<Event>&> {
|
||||
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 <Attached Object>
|
||||
void Scene::Private::bind_private_crtp(Object* object) {
|
||||
Prev_Private::bind_private_crtp(object);
|
||||
runtime = std::make_unique<Runtime>(object->memory_resource());
|
||||
}
|
||||
template <typename Callback>
|
||||
void Scene::Private::consume_events(Callback&& callback) requires std::predicate<Callback, const std::shared_ptr<Event>&> {
|
||||
event_queue.consume(std::forward<Callback>(callback));
|
||||
template <Event_Object Event_Object_Type, typename... Arguments>
|
||||
std::shared_ptr<Event_Object_Type> Scene::make_event(Arguments&&... arguments) {
|
||||
return std::allocate_shared<Event_Object_Type>(
|
||||
allocator<Event_Object_Type>(), std::forward<Arguments>(arguments)...);
|
||||
}
|
||||
template <Attached Object, typename Callback>
|
||||
void Scene::Private::process(Object* object, Callback&& callback) requires std::invocable<Callback, const Result&> {
|
||||
|
||||
@@ -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<Private&>(*d).dispatch->render(this, frame); }
|
||||
void Render_Scene_2D::set_frame_callback(Frame_Callback callback) { static_cast<Private&>(*d).dispatch->set_frame_callback(this, std::move(callback)); }
|
||||
void Render_Scene_2D::dispatch_event(std::unique_ptr<Event> event) {
|
||||
static_cast<Private&>(*d).dispatch->push_event(this, std::move(event));
|
||||
}
|
||||
void Render_Scene_2D::activate_view() {
|
||||
static_cast<Private&>(*d).dispatch->set_active(this, true);
|
||||
}
|
||||
|
||||
@@ -39,8 +39,6 @@ struct Render_Scene_2D : Def<Render_Scene_2D, Scene,
|
||||
[[nodiscard]] Render_Result render(Frame_2D* frame);
|
||||
/* 安装完成帧回调;回调收到的就是对应 render(frame) 传入的对象。 */
|
||||
void set_frame_callback(Frame_Callback callback);
|
||||
/* 无锁提交事件所有权;下一次 Prepare 开始时按事件区域和 Paint 逆序派发,首个接受者终止传播。 */
|
||||
void dispatch_event(std::unique_ptr<Event> event);
|
||||
/* 激活后 render 才会执行。 */
|
||||
void activate_view();
|
||||
/* 停止后续 render 调用,不清除最后一帧。 */
|
||||
|
||||
@@ -34,7 +34,6 @@ std::expected<std::unique_ptr<Object>, 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<Event>);
|
||||
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 <Attached Object> void prepare_paint_targets(Object* object, Blend2D_Cache& frame, Size viewport);
|
||||
/* Prepare 开始时交换 Scene 事件批次,按区域、Paint 逆序和最近目标规则逐事件完成接受链。 */
|
||||
template <Attached Object> void dispatch_events(Object* object, Size viewport);
|
||||
template <Attached Object>
|
||||
void dispatch_events(Object* object, Size viewport,
|
||||
std::uint64_t frame_sequence);
|
||||
template <Attached Object> [[nodiscard]] Render_Result render(Object* object, Frame_2D* frame);
|
||||
template <Attached Object>
|
||||
[[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<Paint_Cache_Tag>().for_each([](const Dependency_Graph::Node& node) {
|
||||
if (!node.object->template take_dirty<Paint_Cache_Tag>()) 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 <Attached Object>
|
||||
void Render_Scene_2D::Private::dispatch_events(Object* object, Size viewport) {
|
||||
this->consume_events([&](const std::shared_ptr<Event>& 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<const Pointer_Event_Capability*>(&event);
|
||||
std::vector<Event_Target> 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 <Attached Object>
|
||||
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<Object*>(root);
|
||||
static_cast<typename Object::Private&>(*object->d).frame_callback = std::move(callback);
|
||||
},
|
||||
[](Root* root, std::unique_ptr<Event> event) {
|
||||
auto* object = static_cast<Object*>(root);
|
||||
static_cast<typename Object::Private&>(*object->d).push_event(std::move(event));
|
||||
},
|
||||
[](Root* root, bool active) { static_cast<Object*>(root)->template set<&Prop::view_active>(active); }
|
||||
};
|
||||
return value;
|
||||
|
||||
@@ -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<Wheel_Event>();
|
||||
auto wheel = scene->make_event<Wheel_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<Pointer_Event>(Event_Type::pointer_press);
|
||||
auto press = scene->make_event<Pointer_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<Pointer_Event>(Event_Type::pointer_move);
|
||||
scene->dispatch_event(press);
|
||||
auto move = scene->make_event<Pointer_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<Wheel_Event>();
|
||||
auto near_vertical = scene->make_event<Wheel_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<Wheel_Event>();
|
||||
auto near_horizontal = scene->make_event<Wheel_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);
|
||||
|
||||
@@ -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<Pointer_Event>(Event_Type::pointer_press);
|
||||
auto press = scene->make_event<Pointer_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<Pointer_Event>(Event_Type::pointer_move);
|
||||
scene->dispatch_event(press);
|
||||
auto move = scene->make_event<Pointer_Event>(Event_Type::pointer_move);
|
||||
move->position = second;
|
||||
move->modifiers = modifiers;
|
||||
scene->dispatch_event(std::move(move));
|
||||
auto release = std::make_unique<Pointer_Event>(Event_Type::pointer_release);
|
||||
scene->dispatch_event(move);
|
||||
auto release = scene->make_event<Pointer_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<Pointer_Event>(Event_Type::pointer_press);
|
||||
auto press = scene->make_event<Pointer_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<Pointer_Event>(Event_Type::pointer_move);
|
||||
scene->dispatch_event(press);
|
||||
auto move = scene->make_event<Pointer_Event>(Event_Type::pointer_move);
|
||||
move->position = {116.0, 56.0};
|
||||
scene->dispatch_event(std::move(move));
|
||||
auto release = std::make_unique<Pointer_Event>(Event_Type::pointer_release);
|
||||
scene->dispatch_event(move);
|
||||
auto release = scene->make_event<Pointer_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();
|
||||
|
||||
@@ -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<Completion> 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<Gpu_Completion_Service::Result> result{}; /* fence 正常交付时的结果。 */
|
||||
std::exception_ptr failure{}; /* 提交域或完成服务的 Unknown Failure。 */
|
||||
};
|
||||
struct Event_Command {
|
||||
std::shared_ptr<const Event> event{}; /* Scene 转移给准备域的输入事件。 */
|
||||
Extent viewport{}; /* 事件产生时的物理像素视口。 */
|
||||
};
|
||||
using Command = std::variant<Submission, Gpu_Completion, Event_Command>;
|
||||
using Command = std::variant<Submission, Gpu_Completion>;
|
||||
|
||||
static constexpr std::size_t command_capacity = 64;
|
||||
std::shared_ptr<Render_Domain> 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> pending,
|
||||
std::optional<Datoviz_Visual_Backend::Completed_Frame> completed);
|
||||
void complete(std::vector<Completion> completions);
|
||||
void dispatch(Event_Command command);
|
||||
void dispatch(const std::shared_ptr<Event>& 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<Datoviz_Visual_Backend::Pending_Frame> 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>& event, Extent viewport) {
|
||||
if (!event) return;
|
||||
const auto* pointer =
|
||||
dynamic_cast<const Pointer_Event_Capability*>(command.event.get());
|
||||
dynamic_cast<const Pointer_Event_Capability*>(event.get());
|
||||
const auto* wheel =
|
||||
dynamic_cast<const Wheel_Event_Capability*>(command.event.get());
|
||||
const auto* key = dynamic_cast<const Key_Event*>(command.event.get());
|
||||
if (wheel) {
|
||||
dynamic_cast<const Wheel_Event_Capability*>(event.get());
|
||||
const auto* key = dynamic_cast<const Key_Event*>(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<float>(pointer->position_x()),
|
||||
static_cast<float>(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<float>(pointer->position_x()),
|
||||
static_cast<float>(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<Submission>(&command))
|
||||
if (auto* submission = std::get_if<Submission>(&command)) {
|
||||
accept(std::move(*submission));
|
||||
}
|
||||
else if (auto* completion = std::get_if<Gpu_Completion>(&command))
|
||||
finish(std::move(*completion));
|
||||
else if (auto* event = std::get_if<Event_Command>(&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<const Event> event,
|
||||
Extent viewport) {
|
||||
auto self = implementation_;
|
||||
const auto* pointer = dynamic_cast<const Pointer_Event_Capability*>(event.get());
|
||||
const auto* wheel = dynamic_cast<const Wheel_Event_Capability*>(event.get());
|
||||
const auto* key = dynamic_cast<const Key_Event*>(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "../base/Frame_3D.hpp"
|
||||
#include "Backend_Types.hpp"
|
||||
#include <scene.hpp>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
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<const Event> event, Extent viewport);
|
||||
private:
|
||||
struct Implementation;
|
||||
std::shared_ptr<Implementation> implementation_; /* 已入队闭包共享实现生命周期。 */
|
||||
|
||||
@@ -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> 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();
|
||||
|
||||
@@ -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<Private&>(*d).dispatch->render(this, frame); }
|
||||
void Render_Scene_3D::set_frame_callback(Frame_Callback callback) { static_cast<Private&>(*d).dispatch->set_frame_callback(this, std::move(callback)); }
|
||||
void Render_Scene_3D::dispatch_event(std::unique_ptr<Event> event) { static_cast<Private&>(*d).dispatch->push_event(this, std::move(event)); }
|
||||
void Render_Scene_3D::activate_view() { static_cast<Private&>(*d).dispatch->set_active(this, true); }
|
||||
void Render_Scene_3D::deactivate_view() { static_cast<Private&>(*d).dispatch->set_active(this, false); }
|
||||
}
|
||||
|
||||
@@ -62,8 +62,6 @@ struct Render_Scene_3D : Def<Render_Scene_3D, Scene, Dependency_Graph_Type<Submi
|
||||
[[nodiscard]] Render_Result render(Frame_3D* frame);
|
||||
/* 安装完成帧回调;回调由 Datoviz Render Domain 线程传回同一外部帧。 */
|
||||
void set_frame_callback(Frame_Callback callback);
|
||||
/* 无锁提交事件所有权;下一次 Prepare 开始时无等待转交 Datoviz 渲染域。 */
|
||||
void dispatch_event(std::unique_ptr<Event> event);
|
||||
void activate_view();
|
||||
void deactivate_view();
|
||||
};
|
||||
|
||||
@@ -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<Event>);
|
||||
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<detail::Async_Render_Backend> backend{}; /* Scene 拥有的异步后端;已入队命令自行延长实现寿命。 */
|
||||
@@ -44,8 +42,6 @@ struct Render_Scene_3D::Private : Prev_Private {
|
||||
template <Attached Object> void bind_private_crtp(Object* object);
|
||||
/* CRTP 覆盖:先执行 Kernel Prepare Taskflow,再运行只负责异步入队的三维 Submit 阶段。 */
|
||||
template <Attached Object, typename Callback> void process(Object* object, Callback&& callback) requires std::invocable<Callback>;
|
||||
/* Prepare 开始时交换公共 Scene 事件批次,并无等待提交到 Datoviz 渲染域。 */
|
||||
void dispatch_events(Extent viewport);
|
||||
template <Attached Object> [[nodiscard]] Render_Result render(Object* object, Frame_3D* frame);
|
||||
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
|
||||
};
|
||||
@@ -164,12 +160,6 @@ void Render_Scene_3D::Private::initialize_backend(
|
||||
backend, object, nullptr, initial,
|
||||
std::make_shared<detail::Prepared_Visual_Batch>()});
|
||||
}
|
||||
inline void Render_Scene_3D::Private::dispatch_events(Extent viewport) {
|
||||
this->consume_events([&](const std::shared_ptr<Event>& event) {
|
||||
return backend->dispatch_event(event, viewport) !=
|
||||
detail::Async_Render_Backend::Dispatch_Event_Result::backend_unavailable;
|
||||
});
|
||||
}
|
||||
template <Attached Object, typename Callback>
|
||||
void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requires std::invocable<Callback> {
|
||||
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<detail::Scene_Paint_Context<Object>>(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>(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 <Attached Object> 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<Object*>(root); return static_cast<typename Object::Private&>(*object->d).render(object, frame); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); if (data.backend) data.backend->set_frame_callback(std::move(callback)); }, [](Root* root, std::unique_ptr<Event> event) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).push_event(std::move(event)); }, [](Root* root, bool active) { static_cast<Object*>(root)->template set<&Prop::view_active>(active); }}; return value; }
|
||||
template <Attached Object> 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<Object*>(root); return static_cast<typename Object::Private&>(*object->d).render(object, frame); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); if (data.backend) data.backend->set_frame_callback(std::move(callback)); }, [](Root* root, bool active) { static_cast<Object*>(root)->template set<&Prop::view_active>(active); }}; return value; }
|
||||
template <Attached Object> void Render_Scene_3D::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); }
|
||||
}
|
||||
|
||||
@@ -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 (...) {}
|
||||
|
||||
@@ -19,6 +19,7 @@ public:
|
||||
void close() noexcept;
|
||||
private:
|
||||
void deliver_frame(std::shared_ptr<const Plot_Stream_Frame> frame);
|
||||
void deliver_input_observation(Plot_Input_Observation observation);
|
||||
struct Private;
|
||||
std::unique_ptr<Private> d;
|
||||
};
|
||||
|
||||
+117
-23
@@ -32,6 +32,35 @@ using Scene_3D = Impl<Render_Scene_3D>;
|
||||
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<double, std::milli>(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 <typename Event_Object_Type>
|
||||
struct Observed_Input_Event final : Event_Object_Type, Web_Input_Event {
|
||||
template <typename... Arguments>
|
||||
Observed_Input_Event(Web_Input_Metadata value_metadata,
|
||||
Arguments&&... arguments)
|
||||
: Event_Object_Type(std::forward<Arguments>(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 <typename Scene_Object>
|
||||
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<Basic_Pointer_Event<Point_F>>(input.type);
|
||||
auto event = scene.template make_event<Observed_Input_Event<Basic_Pointer_Event<Point_F>>>(
|
||||
std::move(metadata), input.type);
|
||||
apply_pointer(*event);
|
||||
dispatch(std::move(event));
|
||||
break;
|
||||
}
|
||||
case Event_Type::wheel: {
|
||||
auto event = std::make_unique<Basic_Wheel_Event<Point_F>>();
|
||||
auto event = scene.template make_event<Observed_Input_Event<Basic_Wheel_Event<Point_F>>>(
|
||||
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<Key_Event>(input.type);
|
||||
auto event = scene.template make_event<Observed_Input_Event<Key_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<Event>(input.type));
|
||||
dispatch(scene.template make_event<Observed_Input_Event<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<double>::infinity()};
|
||||
std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()};
|
||||
std::vector<aethera::Scene::Event_Report_Pointer> pending_event_reports; /* Scene 已处理、等待目标画面完成的报告。 */
|
||||
|
||||
template <typename Scene_Object>
|
||||
Private(asio::any_io_executor executor, std::unique_ptr<Scene_Object> 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<std::unique_ptr<Scene_3D>>(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<const Web_Input_Event*>(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<const Plot_Pixel_Frame>(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<std::unique_ptr<Scene_2D>>(
|
||||
&owner->d->scene))
|
||||
dispatch_plot_input(**scene, event);
|
||||
else
|
||||
dispatch_plot_input(
|
||||
*std::get<std::unique_ptr<Scene_3D>>(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<std::unique_ptr<Scene_2D>>(&d->scene))
|
||||
dispatch_plot_input(**scene_2d, event, std::move(handler));
|
||||
else
|
||||
dispatch_plot_input(*std::get<std::unique_ptr<Scene_3D>>(d->scene),
|
||||
event, std::move(handler));
|
||||
}
|
||||
catch (...) {
|
||||
d->fail(std::current_exception());
|
||||
}
|
||||
}
|
||||
|
||||
void Plot::async_schema(Json_Handler handler) {
|
||||
|
||||
+36
-14
@@ -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<void(std::shared_ptr<const Plot_Stream_Frame>)>;
|
||||
using Json_Handler = std::function<void(nlohmann::json)>;
|
||||
using Input_Handler = std::function<void(Plot_Input_Observation)>;
|
||||
|
||||
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);
|
||||
|
||||
+113
-8
@@ -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<Input_Observation>;
|
||||
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<Gallery_Layout>;
|
||||
@@ -442,10 +455,13 @@ function use_plot_stream(plot: Plot,
|
||||
tile_width = 720, tile_height = 420) {
|
||||
const [status, set_status] = useState<Stream_Status>("CONNECTING");
|
||||
const [metrics, set_metrics] = useState<Frame_Metrics | null>(null);
|
||||
const [input_latency, set_input_latency] = useState<Input_Latency | null>(null);
|
||||
const [error, set_error] = useState<string | null>(null);
|
||||
const socket_ref = useRef<ReconnectingWebSocket | null>(null);
|
||||
const pending_pointer_move = useRef<Record<string, unknown> | null>(null);
|
||||
const pointer_frame = useRef(0);
|
||||
const next_input_sequence = useRef(1);
|
||||
const input_origins = useRef(new Map<number, number>());
|
||||
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<string, unknown>) => {
|
||||
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<string, unknown> | 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<string, string> = {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<number | null>(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 <article ref={card_ref} className={`card${selected ? " selected" : ""}`} data-plot-id={plot.id} aria-current={selected ? "true" : undefined}
|
||||
onClick={() => on_select(plot)}><header className="cardDragHandle"><div><span className="eyebrow">绘图组件 · {plot.dimension}</span><h2>{plot_labels[plot.id] ?? plot.title}</h2></div><div className="cardRuntime" title={`帧序号 ${metrics?.sequence ?? 0} · 创建于 ${generated_time}`}><div><span className="status">{{IDLE: "已停止", CONNECTING: "重连中", LIVE: "实时", OFFLINE: "已离线"}[status]}</span><span className="framePolicy">{metrics?.delivery === "diagnostics" ? "无像素传输" : metrics ? enum_label(metrics.pacing_mode) : "等待策略"}</span></div><div className="frameMetrics"><span>真实画面 {source_transport ? source_transport.rendered_frame_rate_fps.toFixed(1) : metrics ? metrics.frame_rate_fps.toFixed(1) : "--.-"} FPS</span><span>逻辑闭环 {source_transport ? source_transport.logical_completion_rate_fps.toFixed(1) : "--.-"} FPS</span><span>图集 {gallery.transport ? gallery.transport.encoded_frame_rate_fps.toFixed(1) : "--.-"} FPS</span><span>解码 {gallery.playback.frame_rate_fps.toFixed(1)} FPS</span><span>完成 {metrics ? metrics.server_completion_ms.toFixed(1) : "--.-"} ms</span><span title={gallery.transport ? `图集合成 ${gallery.transport.compose_average_ms.toFixed(2)} ms · WebRTC 发送 ${gallery.transport.publish_average_ms.toFixed(2)} ms` : "等待媒体流水线指标"}>{encoder_label} {gallery.transport ? gallery.transport.encode_average_ms.toFixed(1) : "--.-"} ms</span><span>落后 {source_transport?.has_rendered_frame ? source_transport.clock_lag_ticks : "--"} tick</span></div></div></header>
|
||||
return <article ref={card_ref} className={`card${selected ? " selected" : ""}`} data-plot-id={plot.id}
|
||||
data-input-stage={graph.input_latency?.stage ?? "none"} aria-current={selected ? "true" : undefined}
|
||||
data-input-plot-queue-ms={graph.input_latency?.plot_queue_ms ?? ""}
|
||||
data-input-scene-queue-ms={graph.input_latency?.scene_queue_ms ?? ""}
|
||||
data-input-backend-queue-ms={graph.input_latency?.backend_queue_ms ?? ""}
|
||||
data-input-server-total-ms={graph.input_latency?.server_total_ms ?? ""}
|
||||
data-input-browser-total-ms={graph.input_latency?.browser_observation_ms ?? ""}
|
||||
data-input-visible-ms={input_visible_latency ?? ""}
|
||||
onClick={() => on_select(plot)}>
|
||||
<header className="cardDragHandle">
|
||||
<div><span className="eyebrow">绘图组件 · {plot.dimension}</span><h2>{plot_labels[plot.id] ?? plot.title}</h2></div>
|
||||
<div className="cardRuntime" title={`帧序号 ${metrics?.sequence ?? 0} · 创建于 ${generated_time}`}>
|
||||
<div><span className="status">{{IDLE: "已停止", CONNECTING: "重连中", LIVE: "实时", OFFLINE: "已离线"}[status]}</span><span className="framePolicy">{metrics?.delivery === "diagnostics" ? "无像素传输" : metrics ? enum_label(metrics.pacing_mode) : "等待策略"}</span></div>
|
||||
<div className="frameMetrics">
|
||||
<span>真实画面 {source_transport ? source_transport.rendered_frame_rate_fps.toFixed(1) : metrics ? metrics.frame_rate_fps.toFixed(1) : "--.-"} FPS</span>
|
||||
<span>逻辑闭环 {source_transport ? source_transport.logical_completion_rate_fps.toFixed(1) : "--.-"} FPS</span>
|
||||
<span>图集 {gallery.transport ? gallery.transport.encoded_frame_rate_fps.toFixed(1) : "--.-"} FPS</span>
|
||||
<span>解码 {gallery.playback.frame_rate_fps.toFixed(1)} FPS</span>
|
||||
<span>完成 {metrics ? metrics.server_completion_ms.toFixed(1) : "--.-"} ms</span>
|
||||
<span title={gallery.transport ? `图集合成 ${gallery.transport.compose_average_ms.toFixed(2)} ms · WebRTC 发送 ${gallery.transport.publish_average_ms.toFixed(2)} ms` : "等待媒体流水线指标"}>{encoder_label} {gallery.transport ? gallery.transport.encode_average_ms.toFixed(1) : "--.-"} ms</span>
|
||||
<span>落后 {source_transport?.has_rendered_frame ? source_transport.clock_lag_ticks : "--"} tick</span>
|
||||
{plot.dimension === "3D" ? <>
|
||||
<span title="WebSocket 解码后直接转移到 Kernel Scene concurrentqueue 的耗时">事件准入 {graph.input_latency ? graph.input_latency.plot_queue_ms.toFixed(1) : "--.-"} ms</span>
|
||||
<span title="事件进入 Scene 后等待下一次正常 Render 在 Prepare 前消费的时间">Scene 等帧 {graph.input_latency ? graph.input_latency.scene_queue_ms.toFixed(1) : "--.-"} ms</span>
|
||||
<span title="Scene 消费事件后等待 Datoviz 控制器实际应用的时间">后端应用 {graph.input_latency ? graph.input_latency.backend_queue_ms.toFixed(1) : "--.-"} ms</span>
|
||||
<span title="从浏览器原始事件产生到服务端确认对应画面完成">操作闭环 {graph.input_latency ? graph.input_latency.browser_observation_ms.toFixed(1) : "--.-"} ms</span>
|
||||
<span title="从浏览器原始事件产生到下一张视频画面实际呈现">可见画面 {input_visible_latency !== null ? input_visible_latency.toFixed(1) : "--.-"} ms</span>
|
||||
<span title="浏览器显示周期内合并的连续输入数量">合并 {graph.input_latency?.coalesced_event_count ?? 0}</span>
|
||||
</> : null}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{plot.dimension === "2D" ? <div className="frameMetrics interactionMetrics">
|
||||
<span title="WebSocket 解码后直接转移到 Kernel Scene concurrentqueue 的耗时">事件准入 {graph.input_latency ? graph.input_latency.plot_queue_ms.toFixed(1) : "--.-"} ms</span>
|
||||
<span title="事件进入 Scene 后等待下一次正常 Render 在 Prepare 前消费的时间">Scene 等帧 {graph.input_latency ? graph.input_latency.scene_queue_ms.toFixed(1) : "--.-"} ms</span>
|
||||
<span title="Scene 内完成矩形选择、轴交互等事件路由的时间">事件处理 {graph.input_latency ? graph.input_latency.backend_queue_ms.toFixed(1) : "--.-"} ms</span>
|
||||
<span title="浏览器原始事件到对应画面服务端完成">操作闭环 {graph.input_latency ? graph.input_latency.browser_observation_ms.toFixed(1) : "--.-"} ms</span>
|
||||
<span title="浏览器原始事件到视频画面实际呈现">可见画面 {input_visible_latency !== null ? input_visible_latency.toFixed(1) : "--.-"} ms</span>
|
||||
<span title="浏览器显示周期内合并的连续输入数量">合并 {graph.input_latency?.coalesced_event_count ?? 0}</span>
|
||||
</div> : null}
|
||||
<div className="plotExecutionPolicy" onPointerDown={event => event.stopPropagation()}>
|
||||
<label title="控制这张图的画面是否显示;不改变 Scene 自身的 visible 属性。"><input type="checkbox" checked={policy.visible} onChange={event => on_policy(plot.id, {visible: event.target.checked})}/>显示画面</label>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user