From 26a89335b4325b1951dae801dade2e95b61d3ccd Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Fri, 21 Aug 2026 01:30:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E5=90=8E=E7=AB=AF=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + kernel/src/kernel/double_buffer/mechanism.hpp | 8 +- render_2D/render_2D/plottable/Afterglow.cpp | 4 +- render_2D/render_2D/plottable/Afterglow.hpp | 31 +++-- .../plottable/Constellation_Diagram.cpp | 4 +- .../plottable/Constellation_Diagram.hpp | 27 ++-- .../render_2D/plottable/Frequency_Trace.cpp | 2 + .../render_2D/plottable/Frequency_Trace.hpp | 19 ++- .../plottable/Selection_Rectangle_Overlay.cpp | 1 + .../plottable/Selection_Rectangle_Overlay.hpp | 17 ++- render_2D/render_2D/plottable/Spectrum.cpp | 2 + render_2D/render_2D/plottable/Spectrum.hpp | 59 +++++---- .../render_2D/plottable/Sweep_Spectrum.cpp | 2 + .../render_2D/plottable/Sweep_Spectrum.hpp | 31 +++-- render_2D/render_2D/plottable/Waterfall.cpp | 4 +- render_2D/render_2D/plottable/Waterfall.hpp | 29 +++-- render_2D/render_2D/scene/Render_Scene_2D.cpp | 5 +- render_2D/render_2D/scene/Render_Scene_2D.hpp | 19 ++- render_2D/render_2D/scene/Render_Scene_2D.ipp | 40 ++++-- render_2D/tests/Plottable_Migration_Test.cpp | 6 +- render_2D/tests/Spectrum_Test.cpp | 6 +- .../render_3D/detail/Async_Render_Backend.cpp | 34 +++-- .../render_3D/detail/Async_Render_Backend.hpp | 18 +-- render_3D/render_3D/scene/Render_Scene_3D.cpp | 4 +- render_3D/render_3D/scene/Render_Scene_3D.hpp | 18 +-- render_3D/render_3D/scene/Render_Scene_3D.ipp | 19 ++- render_3D/tests/Visual_Tests.cpp | 10 +- .../src/scene/annotation/annotation_api.c | 10 +- .../datoviz/src/scene/annotation/axis.c | 2 +- .../datoviz/src/scene/annotation/colorbar.c | 2 +- .../datoviz/src/scene/annotation/guide.c | 8 +- .../datoviz/src/scene/annotation/legend.c | 2 +- .../datoviz/src/scene/annotation/scalebar.c | 14 +- .../datoviz/src/scene/annotation/text.c | 6 +- .../src/scene/annotation/text_glyph_realize.c | 2 +- .../datoviz/src/scene/core/_scene.h | 14 +- .../datoviz/src/scene/core/controllers.c | 6 +- .../src/scene/core/generated_visual_policy.h | 2 +- .../third_party/datoviz/src/scene/core/grid.c | 4 +- .../src/scene/core/orientation_gizmo.c | 6 +- .../datoviz/src/scene/core/panel_layout.c | 2 +- .../datoviz/src/scene/core/panel_view.c | 4 +- .../datoviz/src/scene/core/reference_grid.c | 6 +- .../datoviz/src/scene/core/scene.c | 20 +-- .../datoviz/src/scene/core/scene_notify.c | 34 ++--- .../src/scene/core/scene_notify_internal.h | 10 +- .../datoviz/src/scene/domain/compute.c | 6 +- .../src/scene/domain/polygon_composite.c | 2 +- .../datoviz/src/scene/interaction/core.c | 26 ++-- .../third_party/datoviz/src/scene/plot/band.c | 8 +- .../third_party/datoviz/src/scene/plot/bars.c | 6 +- .../datoviz/src/scene/query/queue.c | 2 +- .../datoviz/src/scene/tests/panzoom_arcball.c | 4 +- .../src/scene/tests/scene_interaction_graph.c | 8 +- .../datoviz/src/scene/visuals/bounds.c | 2 +- .../datoviz/src/scene/visuals/families.c | 8 +- web_server/src/Graph_Metadata.cpp | 43 ++++++ web_server/src/Graph_Metadata.hpp | 50 +++++++ web_server/src/Graph_Session.cpp | 122 ++++++++++++++++++ web_server/src/Graph_Session.hpp | 45 +++++++ web_server/src/Graph_WebSocket.cpp | 25 ++++ web_server/src/Graph_WebSocket.hpp | 31 +++++ web_server/src/Web_Server.cpp | 32 +++++ web_server/src/Web_Server.hpp | 6 + web_server/src/main.cpp | 12 +- webapp_gallery/index.html | 2 +- webapp_gallery/src/app.tsx | 12 ++ webapp_gallery/src/main.tsx | 5 + webapp_gallery/src/styles.css | 1 + webapp_gallery/tsconfig.app.tsbuildinfo | 2 +- 70 files changed, 752 insertions(+), 284 deletions(-) create mode 100644 web_server/src/Graph_Metadata.cpp create mode 100644 web_server/src/Graph_Metadata.hpp create mode 100644 web_server/src/Graph_Session.cpp create mode 100644 web_server/src/Graph_Session.hpp create mode 100644 web_server/src/Graph_WebSocket.cpp create mode 100644 web_server/src/Graph_WebSocket.hpp create mode 100644 web_server/src/Web_Server.cpp create mode 100644 web_server/src/Web_Server.hpp create mode 100644 webapp_gallery/src/app.tsx create mode 100644 webapp_gallery/src/main.tsx create mode 100644 webapp_gallery/src/styles.css diff --git a/.gitignore b/.gitignore index babc63e..915acc3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ /third_party/ /webapp_gallery/old/ /web_server/old/ +/third_party/Adminive/backend/ +/third_party/Adminive/backend/ +/output/ diff --git a/kernel/src/kernel/double_buffer/mechanism.hpp b/kernel/src/kernel/double_buffer/mechanism.hpp index a8397ec..dbaf101 100644 --- a/kernel/src/kernel/double_buffer/mechanism.hpp +++ b/kernel/src/kernel/double_buffer/mechanism.hpp @@ -281,10 +281,14 @@ template concept Prop_Member = requires { requires std::is_member_object_pointer_v; requires std::derived_from::Owner_Type>; - requires Tagged_Prop::Owner_Type>; + requires requires { typename Member_Pointer_Traits::Owner_Type::Prop_Tag; } || Tagged_Prop::Owner_Type>; }; +template +struct Prop_Owner_Tag { using Type = typename Owner::Tag_Type; }; +template +struct Prop_Owner_Tag> { using Type = typename Owner::Prop_Tag; }; template -using Prop_Member_Tag = typename Member_Pointer_Traits::Owner_Type::Tag_Type; +using Prop_Member_Tag = typename Prop_Owner_Tag::Owner_Type>::Type; template concept State_Member_Settable = requires(State& state, Value&& value) { requires State_Member; diff --git a/render_2D/render_2D/plottable/Afterglow.cpp b/render_2D/render_2D/plottable/Afterglow.cpp index 64253f8..68d5b64 100644 --- a/render_2D/render_2D/plottable/Afterglow.cpp +++ b/render_2D/render_2D/plottable/Afterglow.cpp @@ -1,3 +1,5 @@ #include "Afterglow.hpp" -namespace aethera::render_2d { bool Afterglow::Prop::operator==(const Prop&) const = default; +namespace aethera::render_2d { +bool Afterglow_Editable_Prop_Data::operator==(const Afterglow_Editable_Prop_Data&) const = default; +bool Afterglow_Prop_Data::operator==(const Afterglow_Prop_Data&) const = default; bool Afterglow::Prop::operator==(const Prop&) const = default; bool Afterglow::State::operator==(const State&) const = default; void Afterglow::append_spectrum(std::span values) { static_cast(*d).dispatch->append(this, values); } void Afterglow::append_spectrum(std::pmr::vector&& values) { append_spectrum(std::span(values.data(), values.size())); } std::size_t Afterglow::history_count() const { return static_cast(*d).dispatch->history_count(this); } std::size_t Afterglow::latest_spectrum_point_count() const { return static_cast(*d).dispatch->latest_count(this); } std::size_t Afterglow::rendered_cell_count() const { return static_cast(*d).dispatch->rendered_count(this); } } diff --git a/render_2D/render_2D/plottable/Afterglow.hpp b/render_2D/render_2D/plottable/Afterglow.hpp index de52c6e..5f53acf 100644 --- a/render_2D/render_2D/plottable/Afterglow.hpp +++ b/render_2D/render_2D/plottable/Afterglow.hpp @@ -9,19 +9,28 @@ #include #include namespace aethera::render_2d { +struct Afterglow; +struct Afterglow_Editable_Prop_Data { + using Prop_Tag = Afterglow; + std::size_t frequency_point_size{}; /* 栅格频率列数;零值使用最新频谱尺寸。 */ + std::size_t power_point_size{}; /* 栅格功率行数;零值使用默认尺寸。 */ + Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ + bool interpolate{true}; /* 是否对频谱列执行线性重采样。 */ + Plot_Ratio attenuation_rate{0.2}; /* 每增加一帧历史的强度衰减比例。 */ + bool operator==(const Afterglow_Editable_Prop_Data&) const; +}; +struct Afterglow_Prop_Data { + using Prop_Tag = Afterglow; + Axis_Range frequency_range{0.0, 10.0}; /* 输入频谱覆盖的频率范围。 */ + Axis_Range power_range{0.0, 10.0}; /* 色块纵向覆盖的功率范围。 */ + Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ + Color_Map color_map{}; /* 强度到颜色的映射。 */ + std::vector> spectra{}; /* 从旧到新的历史频谱唯一权威集合。 */ + bool operator==(const Afterglow_Prop_Data&) const; +}; struct Afterglow : Def> { using Scene_Object = Impl; using Frequency_Object = Impl; using Power_Object = Impl; - struct Prop : Prev_Prop { - Axis_Range frequency_range{0.0, 10.0}; /* 输入频谱覆盖的频率范围。 */ - Axis_Range power_range{0.0, 10.0}; /* 色块纵向覆盖的功率范围。 */ - std::size_t frequency_point_size{}; /* 栅格频率列数;零值使用最新频谱尺寸。 */ - std::size_t power_point_size{}; /* 栅格功率行数;零值使用 128。 */ - Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ - Plot_Partition_Count partition_count{1}; /* fixed 模式分块数。 */ - bool interpolate{true}; /* 是否对频谱列执行线性重采样。 */ - Plot_Ratio attenuation_rate{0.2}; /* 每增加一帧历史的强度衰减比例,限制到 0..1。 */ - Color_Map color_map{}; /* 强度到颜色的映射。 */ - std::vector> spectra{}; /* 从旧到新的历史频谱唯一权威集合。 */ + struct Prop : Prev_Prop, Afterglow_Editable_Prop_Data, Afterglow_Prop_Data { bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.cpp b/render_2D/render_2D/plottable/Constellation_Diagram.cpp index e8375a3..7e72042 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.cpp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.cpp @@ -1,3 +1,5 @@ #include "Constellation_Diagram.hpp" -namespace aethera::render_2d { bool Constellation_Point::operator==(const Constellation_Point&) const = default; bool Constellation_Diagram::Prop::operator==(const Prop&) const = default; +namespace aethera::render_2d { +bool Constellation_Diagram_Editable_Prop_Data::operator==(const Constellation_Diagram_Editable_Prop_Data&) const = default; +bool Constellation_Diagram_Prop_Data::operator==(const Constellation_Diagram_Prop_Data&) const = default; bool Constellation_Point::operator==(const Constellation_Point&) const = default; bool Constellation_Diagram::Prop::operator==(const Prop&) const = default; bool Constellation_Diagram::State::operator==(const State&) const = default; void Constellation_Diagram::append_point(Point_F point) { static_cast(*d).dispatch->append(this, point); } std::size_t Constellation_Diagram::point_count() const { return static_cast(*d).dispatch->count(this); } void Constellation_Diagram::fit_square_to_axes() { static_cast(*d).dispatch->fit(this); } } diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.hpp b/render_2D/render_2D/plottable/Constellation_Diagram.hpp index 14b6ace..2bd8740 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.hpp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.hpp @@ -9,17 +9,26 @@ namespace aethera::render_2d { enum class Constellation_Diagram_Type : std::uint8_t { psk4 = 4, psk8 = 8, psk16 = 16 }; struct Constellation_Point { Point_F point{}; Plot_Duration_Milliseconds submitted_at_ms{}; bool operator==(const Constellation_Point&) const; }; +struct Constellation_Diagram; +struct Constellation_Diagram_Editable_Prop_Data { + using Prop_Tag = Constellation_Diagram; + Plot_Duration_Milliseconds point_lifetime_ms{1000}; /* 接收点保留时间,单位为毫秒。 */ + Constellation_Diagram_Type type{Constellation_Diagram_Type::psk8}; /* 理想 PSK 锚点数量。 */ + Plot_Ratio phase_offset_radians{}; /* 理想锚点相位偏移,单位为弧度。 */ + bool operator==(const Constellation_Diagram_Editable_Prop_Data&) const; +}; +struct Constellation_Diagram_Prop_Data { + using Prop_Tag = Constellation_Diagram; + Axis_Range i_range{0.0, 100.0}; /* 同相分量显示范围。 */ + Axis_Range q_range{0.0, 100.0}; /* 正交分量显示范围。 */ + Color point_color{Color::red_color()}; /* 接收点颜色。 */ + Color anchor_color{Color::yellow()}; /* 理想星座锚点颜色。 */ + std::vector points{}; /* 已提交且尚未过期的点。 */ + bool operator==(const Constellation_Diagram_Prop_Data&) const; +}; struct Constellation_Diagram : Def> { using Scene_Object = Impl; using Axis_Object = Impl; - struct Prop : Prev_Prop { - Axis_Range i_range{0.0, 100.0}; /* 同相分量显示范围。 */ - Axis_Range q_range{0.0, 100.0}; /* 正交分量显示范围。 */ - Color point_color{Color::red_color()}; /* 接收点颜色。 */ - Color anchor_color{Color::yellow()}; /* 理想星座锚点颜色。 */ - Plot_Duration_Milliseconds point_lifetime_ms{1000}; /* 接收点保留时间,单位为毫秒。 */ - Constellation_Diagram_Type type{Constellation_Diagram_Type::psk8}; /* 理想 PSK 锚点数量。 */ - Plot_Ratio phase_offset_radians{}; /* 理想锚点相位偏移,单位为弧度。 */ - std::vector points{}; /* 已提交且尚未过期的点。 */ + struct Prop : Prev_Prop, Constellation_Diagram_Editable_Prop_Data, Constellation_Diagram_Prop_Data { bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Frequency_Trace.cpp b/render_2D/render_2D/plottable/Frequency_Trace.cpp index 40639e4..24afd22 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.cpp +++ b/render_2D/render_2D/plottable/Frequency_Trace.cpp @@ -1,5 +1,7 @@ #include "Frequency_Trace.hpp" namespace aethera::render_2d { +bool Frequency_Trace_Editable_Prop_Data::operator==(const Frequency_Trace_Editable_Prop_Data&) const = default; +bool Frequency_Trace_Prop_Data::operator==(const Frequency_Trace_Prop_Data&) const = default; bool Frequency_Trace_Sample::operator==(const Frequency_Trace_Sample&) const = default; bool Frequency_Trace::Prop::operator==(const Prop&) const = default; bool Frequency_Trace::State::operator==(const State&) const = default; diff --git a/render_2D/render_2D/plottable/Frequency_Trace.hpp b/render_2D/render_2D/plottable/Frequency_Trace.hpp index f18fc03..0a8b42d 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.hpp +++ b/render_2D/render_2D/plottable/Frequency_Trace.hpp @@ -12,15 +12,24 @@ struct Frequency_Trace_Sample { Plot_Value value{}; /* 该时间点对应的频率值。 */ bool operator==(const Frequency_Trace_Sample&) const; }; +struct Frequency_Trace; +struct Frequency_Trace_Editable_Prop_Data { + using Prop_Tag = Frequency_Trace; + Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ + bool operator==(const Frequency_Trace_Editable_Prop_Data&) const; +}; +struct Frequency_Trace_Prop_Data { + using Prop_Tag = Frequency_Trace; + Pen pen{Color::yellow()}; /* 频率轨迹折线样式。 */ + Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ + std::vector samples{}; /* 已提交轨迹样本的唯一权威集合。 */ + bool operator==(const Frequency_Trace_Prop_Data&) const; +}; struct Frequency_Trace : Def> { using Scene_Object = Impl; using Time_Object = Impl; using Value_Object = Impl; - struct Prop : Prev_Prop { - Pen pen{Color::yellow()}; /* 频率轨迹折线样式。 */ - Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ - Plot_Partition_Count partition_count{1}; /* fixed 模式的分块数量。 */ - std::vector samples{}; /* 已提交轨迹样本的唯一权威集合。 */ + struct Prop : Prev_Prop, Frequency_Trace_Editable_Prop_Data, Frequency_Trace_Prop_Data { bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp index 50cac33..3875914 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp @@ -1,5 +1,6 @@ #include "Selection_Rectangle_Overlay.hpp" namespace aethera::render_2d { +bool Selection_Rectangle_Overlay_Prop_Data::operator==(const Selection_Rectangle_Overlay_Prop_Data&) const = default; bool Selection_Rectangle_Overlay::Prop::operator==(const Prop&) const = default; bool Selection_Rectangle_Overlay::State::operator==(const State&) const = default; std::vector Selection_Rectangle_Overlay::selected_regions() const { return static_cast(*d).dispatch->selected_regions(this); } diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp index 193d775..86815d7 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp @@ -7,15 +7,20 @@ #include #include namespace aethera::render_2d { +struct Selection_Rectangle_Overlay; +struct Selection_Rectangle_Overlay_Prop_Data { + using Prop_Tag = Selection_Rectangle_Overlay; + Font label_font{}; /* 选择范围标签使用的字体。 */ + Pen label_pen{Color::white()}; /* 选择范围标签的文字样式。 */ + Brush selection_brush{Color{0, 0, 255, 50}, Brush_Style::solid}; /* 选择矩形内部填充。 */ + Pen selection_border_pen{Color::white(), 1.0, Line_Style::dash}; /* 选择矩形边框样式。 */ + std::vector selected_regions{}; /* 已完成选择的轴坐标矩形。 */ + bool operator==(const Selection_Rectangle_Overlay_Prop_Data&) const; +}; struct Selection_Rectangle_Overlay : Def> { using Scene_Object = Impl; using Axis_Object = Impl; - struct Prop : Prev_Prop { - Font label_font{}; /* 选择范围标签使用的字体。 */ - Pen label_pen{Color::white()}; /* 选择范围标签的文字样式。 */ - Brush selection_brush{Color{0, 0, 255, 50}, Brush_Style::solid}; /* 选择矩形内部填充。 */ - Pen selection_border_pen{Color::white(), 1.0, Line_Style::dash}; /* 选择矩形边框样式。 */ - std::vector selected_regions{}; /* 已完成选择的轴坐标矩形。 */ + struct Prop : Prev_Prop, Selection_Rectangle_Overlay_Prop_Data { bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Spectrum.cpp b/render_2D/render_2D/plottable/Spectrum.cpp index d8ab8ee..7a67ec0 100644 --- a/render_2D/render_2D/plottable/Spectrum.cpp +++ b/render_2D/render_2D/plottable/Spectrum.cpp @@ -1,5 +1,7 @@ #include "Spectrum.hpp" namespace aethera::render_2d { +bool Spectrum_Editable_Prop_Data::operator==(const Spectrum_Editable_Prop_Data&) const = default; +bool Spectrum_Prop_Data::operator==(const Spectrum_Prop_Data&) const = default; bool Spectrum_Frame::operator==(const Spectrum_Frame&) const = default; bool Spectrum::Prop::operator==(const Prop&) const = default; bool Spectrum::State::operator==(const State&) const = default; diff --git a/render_2D/render_2D/plottable/Spectrum.hpp b/render_2D/render_2D/plottable/Spectrum.hpp index fb6dad7..87acb69 100644 --- a/render_2D/render_2D/plottable/Spectrum.hpp +++ b/render_2D/render_2D/plottable/Spectrum.hpp @@ -23,35 +23,44 @@ struct Spectrum_Frame { bool operator==(const Spectrum_Frame&) const; }; /* 使用频率轴和功率轴分块准备、绘制当前值、保持曲线及频率标记。 */ +struct Spectrum; +struct Spectrum_Editable_Prop_Data { + using Prop_Tag = Spectrum; + Spectrum_Frequency center_frequency{50.0}; /* 中心频率标记位置,单位为 Hz。 */ + std::size_t partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ + bool max_hold_visible{}; /* 是否绘制逐点历史最大值曲线。 */ + bool min_hold_visible{}; /* 是否绘制逐点历史最小值曲线。 */ + bool max_marker_visible{}; /* 是否标记当前样本的最大值位置。 */ + bool min_marker_visible{}; /* 是否标记当前样本的最小值位置。 */ + bool sweep_region_visible{}; /* 是否绘制扫频范围背景。 */ + bool visible_range_only{true}; /* 是否裁掉频率轴当前范围外的线段。 */ + bool operator==(const Spectrum_Editable_Prop_Data&) const; +}; +struct Spectrum_Prop_Data { + using Prop_Tag = Spectrum; + Axis_Range frequency_range{}; /* 输入样本首尾对应的有向频率范围,单位为 Hz。 */ + Axis_Range sweep_frequency_range{40.0, 60.0}; /* 扫频背景覆盖的频率范围,单位为 Hz。 */ + Spectrum_Partition_Mode partition_mode{Spectrum_Partition_Mode::automatic}; /* Prepare 子图的分块策略。 */ + Line_Interpolation_Mode interpolation_mode{Line_Interpolation_Mode::linear_value}; /* 相邻样本间的插值规则。 */ + Brush max_brush{}; /* 最大保持曲线下方的填充样式。 */ + Brush current_brush{}; /* 当前频谱曲线下方的填充样式。 */ + Brush min_brush{}; /* 最小保持曲线下方的填充样式。 */ + Pen max_pen{Color::red_color()}; /* 最大保持曲线及最大值标记样式。 */ + Pen current_pen{Color::green_color()}; /* 当前频谱曲线样式。 */ + Pen min_pen{Color::white()}; /* 最小保持曲线及最小值标记样式。 */ + Pen selected_marker_pen{Color{0, 0, 139, 255}, 2.0}; /* 当前选中自定义标记的线条样式。 */ + Pen marker_pen{Color::red_color()}; /* 未选中自定义标记的线条样式。 */ + Pen middle_frequency_pen{Color::red_color()}; /* 中心频率垂线样式。 */ + Brush sweep_region_brush{Color{255, 255, 0, 100}, Brush_Style::solid}; /* 扫频区域背景样式。 */ + std::vector custom_markers{}; /* 自定义频率标记的唯一权威集合,单位为 Hz。 */ + Spectrum_Marker_Index selected_marker{-1}; /* 当前选中标记下标;-1 表示未选择。 */ + bool operator==(const Spectrum_Prop_Data&) const; +}; struct Spectrum : Def, Tagged_Buffer> { using Scene_Object = Impl; using Frequency_Object = Impl; using Power_Object = Impl; - struct Prop : Prev_Prop { - Axis_Range frequency_range{}; /* 输入样本首尾对应的有向频率范围,单位为 Hz。 */ - Spectrum_Frequency center_frequency{50.0}; /* 中心频率标记位置,单位为 Hz。 */ - Axis_Range sweep_frequency_range{40.0, 60.0}; /* 扫频背景覆盖的频率范围,单位为 Hz。 */ - Spectrum_Partition_Mode partition_mode{Spectrum_Partition_Mode::automatic}; /* Prepare 子图的分块策略。 */ - std::size_t partition_count{1}; /* fixed 模式使用的分块数;零值按 1 处理。 */ - bool max_hold_visible{}; /* 是否绘制逐点历史最大值曲线。 */ - bool min_hold_visible{}; /* 是否绘制逐点历史最小值曲线。 */ - bool max_marker_visible{}; /* 是否标记当前样本的最大值位置。 */ - bool min_marker_visible{}; /* 是否标记当前样本的最小值位置。 */ - bool sweep_region_visible{}; /* 是否绘制扫频范围背景。 */ - bool visible_range_only{true}; /* 是否裁掉频率轴当前范围以外的线段。 */ - Line_Interpolation_Mode interpolation_mode{Line_Interpolation_Mode::linear_value}; /* 相邻样本间的插值规则。 */ - Brush max_brush{}; /* 最大保持曲线下方的填充样式。 */ - Brush current_brush{}; /* 当前频谱曲线下方的填充样式。 */ - Brush min_brush{}; /* 最小保持曲线下方的填充样式。 */ - Pen max_pen{Color::red_color()}; /* 最大保持曲线及最大值标记样式。 */ - Pen current_pen{Color::green_color()}; /* 当前频谱曲线样式。 */ - Pen min_pen{Color::white()}; /* 最小保持曲线及最小值标记样式。 */ - Pen selected_marker_pen{Color{0, 0, 139, 255}, 2.0}; /* 当前选中自定义标记的线条样式。 */ - Pen marker_pen{Color::red_color()}; /* 未选中自定义标记的线条样式。 */ - Pen middle_frequency_pen{Color::red_color()}; /* 中心频率垂线样式。 */ - Brush sweep_region_brush{Color{255, 255, 0, 100}, Brush_Style::solid}; /* 扫频区域背景样式。 */ - std::vector custom_markers{}; /* 自定义频率标记的唯一权威集合,单位为 Hz。 */ - Spectrum_Marker_Index selected_marker{-1}; /* 当前选中标记下标;-1 表示未选择。 */ + struct Prop : Prev_Prop, Spectrum_Editable_Prop_Data, Spectrum_Prop_Data { bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.cpp b/render_2D/render_2D/plottable/Sweep_Spectrum.cpp index 36a8284..a1b0704 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.cpp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.cpp @@ -1,5 +1,7 @@ #include "Sweep_Spectrum.hpp" namespace aethera::render_2d { +bool Sweep_Spectrum_Editable_Prop_Data::operator==(const Sweep_Spectrum_Editable_Prop_Data&) const = default; +bool Sweep_Spectrum_Prop_Data::operator==(const Sweep_Spectrum_Prop_Data&) const = default; bool Sweep_Spectrum::Prop::operator==(const Prop&) const = default; bool Sweep_Spectrum::State::operator==(const State&) const = default; void Sweep_Spectrum::append_block(std::span values) { static_cast(*d).dispatch->append(this, values); } diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.hpp b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp index 96725ea..4e29594 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.hpp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp @@ -9,21 +9,30 @@ #include #include namespace aethera::render_2d { +struct Sweep_Spectrum; +struct Sweep_Spectrum_Editable_Prop_Data { + using Prop_Tag = Sweep_Spectrum; + std::size_t bins_per_block{}; /* 每个扫频块期望的功率点数;零值接受首块尺寸。 */ + std::size_t block_count{1}; /* 最多保留的扫频块数;零值按 1 处理。 */ + Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ + bool visible_range_only{true}; /* 是否裁掉频率轴可见范围外的线段。 */ + bool operator==(const Sweep_Spectrum_Editable_Prop_Data&) const; +}; +struct Sweep_Spectrum_Prop_Data { + using Prop_Tag = Sweep_Spectrum; + Axis_Range frequency_range{}; /* 全部扫描块覆盖的频率范围。 */ + Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ + Pen pen{Color::yellow()}; /* 扫频折线样式。 */ + Pen current_frequency_pen{Color::red_color(), 2.0}; /* 当前扫频位置垂线样式。 */ + Line_Interpolation_Mode interpolation_mode{Line_Interpolation_Mode::linear_value}; /* 相邻功率点插值方式。 */ + std::vector> blocks{}; /* 已提交扫描块的唯一权威集合。 */ + bool operator==(const Sweep_Spectrum_Prop_Data&) const; +}; struct Sweep_Spectrum : Def> { using Scene_Object = Impl; using Frequency_Object = Impl; using Power_Object = Impl; - struct Prop : Prev_Prop { - Axis_Range frequency_range{}; /* 全部扫描块覆盖的频率范围。 */ - std::size_t bins_per_block{}; /* 每个扫描块期望的功率点数;零值接受首块尺寸。 */ - std::size_t block_count{1}; /* 最多保留的扫描块数;零值按 1 处理。 */ - Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ - Plot_Partition_Count partition_count{1}; /* fixed 模式分块数。 */ - Pen pen{Color::yellow()}; /* 扫频折线样式。 */ - Pen current_frequency_pen{Color::red_color(), 2.0}; /* 当前扫频位置垂线样式。 */ - bool visible_range_only{true}; /* 是否裁掉频率轴可见范围外的线段。 */ - Line_Interpolation_Mode interpolation_mode{Line_Interpolation_Mode::linear_value}; /* 相邻功率点插值方式。 */ - std::vector> blocks{}; /* 已提交扫描块的唯一权威集合。 */ + struct Prop : Prev_Prop, Sweep_Spectrum_Editable_Prop_Data, Sweep_Spectrum_Prop_Data { bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Waterfall.cpp b/render_2D/render_2D/plottable/Waterfall.cpp index 8805282..6077f4c 100644 --- a/render_2D/render_2D/plottable/Waterfall.cpp +++ b/render_2D/render_2D/plottable/Waterfall.cpp @@ -1,3 +1,5 @@ #include "Waterfall.hpp" -namespace aethera::render_2d { bool Waterfall_Row::operator==(const Waterfall_Row&) const = default; bool Waterfall::Prop::operator==(const Prop&) const = default; +namespace aethera::render_2d { +bool Waterfall_Editable_Prop_Data::operator==(const Waterfall_Editable_Prop_Data&) const = default; +bool Waterfall_Prop_Data::operator==(const Waterfall_Prop_Data&) const = default; bool Waterfall_Row::operator==(const Waterfall_Row&) const = default; bool Waterfall::Prop::operator==(const Prop&) const = default; bool Waterfall::State::operator==(const State&) const = default; void Waterfall::append_row(Plot_Time_Tick tick, std::span values) { static_cast(*d).dispatch->append(this, tick, values); } void Waterfall::append_row(Plot_Time_Tick tick, std::pmr::vector&& values) { append_row(tick, std::span(values.data(), values.size())); } void Waterfall::append_row(Time_Of_Day time, std::span values) { static_cast(*d).dispatch->append_time(this, time, values); } void Waterfall::append_row(Time_Of_Day time, std::pmr::vector&& values) { append_row(time, std::span(values.data(), values.size())); } std::size_t Waterfall::row_count() const { return static_cast(*d).dispatch->row_count(this); } std::size_t Waterfall::stored_point_count() const { return static_cast(*d).dispatch->point_count(this); } std::size_t Waterfall::rendered_cell_count() const { return static_cast(*d).dispatch->rendered_count(this); } } diff --git a/render_2D/render_2D/plottable/Waterfall.hpp b/render_2D/render_2D/plottable/Waterfall.hpp index 1c2526a..474ab02 100644 --- a/render_2D/render_2D/plottable/Waterfall.hpp +++ b/render_2D/render_2D/plottable/Waterfall.hpp @@ -11,18 +11,27 @@ #include namespace aethera::render_2d { struct Waterfall_Row { Plot_Time_Tick tick{}; std::vector values{}; bool operator==(const Waterfall_Row&) const; }; +struct Waterfall; +struct Waterfall_Editable_Prop_Data { + using Prop_Tag = Waterfall; + std::size_t frequency_bin_count{}; /* 目标频率列数;零值使用最新行尺寸。 */ + Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ + bool visible_range_only{true}; /* 是否按频率轴范围裁剪栅格。 */ + bool operator==(const Waterfall_Editable_Prop_Data&) const; +}; +struct Waterfall_Prop_Data { + using Prop_Tag = Waterfall; + Axis_Range frequency_range{0.0, 10.0}; /* 每行频谱覆盖的频率范围。 */ + Axis_Range power_range{0.0, 10.0}; /* 颜色映射使用的功率范围。 */ + Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ + Image_Interpolation_Mode interpolation_mode{Image_Interpolation_Mode::nearest}; /* 栅格放大时的图像插值方式。 */ + Color_Map color_map{}; /* 功率到颜色的映射。 */ + std::vector rows{}; /* 从旧到新的瀑布行唯一权威集合。 */ + bool operator==(const Waterfall_Prop_Data&) const; +}; struct Waterfall : Def> { using Scene_Object = Impl; using Frequency_Object = Impl; using Time_Object = Impl; - struct Prop : Prev_Prop, Hover_Tooltip_Properties { - Axis_Range frequency_range{0.0, 10.0}; /* 每行频谱覆盖的频率范围。 */ - Axis_Range power_range{0.0, 10.0}; /* 颜色映射使用的功率范围。 */ - std::size_t frequency_bin_count{}; /* 目标频率列数;零值使用最新行尺寸。 */ - Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ - Plot_Partition_Count partition_count{1}; /* fixed 模式分块数。 */ - bool visible_range_only{true}; /* 是否按频率轴范围裁剪。 */ - Image_Interpolation_Mode interpolation_mode{Image_Interpolation_Mode::nearest}; /* 栅格放大时的图像插值方式。 */ - Color_Map color_map{}; /* 功率到颜色的映射。 */ - std::vector rows{}; /* 从旧到新的瀑布行唯一权威集合。 */ + struct Prop : Prev_Prop, Hover_Tooltip_Properties, Waterfall_Editable_Prop_Data, Waterfall_Prop_Data { bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/scene/Render_Scene_2D.cpp b/render_2D/render_2D/scene/Render_Scene_2D.cpp index c686127..316947d 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.cpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.cpp @@ -3,9 +3,8 @@ namespace aethera::render_2d { bool Render_Scene_2D::State::operator==(const State&) const = default; bool Render_Scene_2D::Prop::operator==(const Prop&) const = default; -Render_Frame_Status Render_Scene_2D::render_frame() { - return static_cast(*d).dispatch->render_frame(this); -} +void Render_Scene_2D::render() { static_cast(*d).dispatch->render(this); } +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(const Event& event) { static_cast(*d).dispatch->dispatch_event(this, event); } diff --git a/render_2D/render_2D/scene/Render_Scene_2D.hpp b/render_2D/render_2D/scene/Render_Scene_2D.hpp index eed7868..a38d94d 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.hpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.hpp @@ -2,20 +2,16 @@ #include "../base/Types.hpp" #include "../render/Blend2D_Cache.hpp" #include +#include namespace aethera::render_2d { struct Scene_Color_Cache_Tag {}; -enum class Render_Frame_Status : std::uint8_t { - rendered, - view_inactive, - empty_viewport -}; /* 执行二维 Renderable 图、合成颜色层并发布最终像素帧。 */ struct Render_Scene_2D : Def> { struct Prop : Prev_Prop { Size viewport{}; /* 最终帧的像素尺寸;空尺寸不执行渲染。 */ Color background{Color::black()}; /* 每帧合成前写入的背景颜色。 */ - bool view_active{}; /* 视图是否接受 render_frame 请求。 */ + bool view_active{}; /* 视图是否接受 render() 产生新的完成帧。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { @@ -23,13 +19,16 @@ struct Render_Scene_2D : Def; + /* 合成一帧;执行期间的重复调用合并为一次后继帧。 */ + void render(); + /* 安装完成帧回调;Image_View 仅在回调执行期间有效。 */ + void set_frame_callback(Frame_Callback callback); /* 按 Paint 图逆序派发事件,首个接受者终止传播。 */ void dispatch_event(const Event& event); - /* 激活后 render_frame 才会执行。 */ + /* 激活后 render 才会执行。 */ void activate_view(); - /* 停止后续 render_frame 请求,不清除最后一帧。 */ + /* 停止后续 render 调用,不清除最后一帧。 */ void deactivate_view(); /* 返回最后一次成功合成的只读像素视图;下次渲染后失效。 */ [[nodiscard]] Image_View frame_view() const; diff --git a/render_2D/render_2D/scene/Render_Scene_2D.ipp b/render_2D/render_2D/scene/Render_Scene_2D.ipp index ab8da57..5180d94 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.ipp +++ b/render_2D/render_2D/scene/Render_Scene_2D.ipp @@ -3,21 +3,26 @@ #include namespace aethera::render_2d { struct Render_Scene_2D::Private : Prev_Private { - using Render_Run = Render_Frame_Status (*)(Root*); + using Render_Run = void (*)(Root*); + using Callback_Run = void (*)(Root*, Frame_Callback); using Event_Run = void (*)(Root*, const Event&); using Active_Run = void (*)(Root*, bool); using Frame_View_Run = Image_View (*)(const Root*); struct Dispatch { - Render_Run render_frame; /* 执行最终 Scene 并合成颜色层。 */ + Render_Run render; /* 执行最终 Scene 并合成颜色层。 */ + Callback_Run set_frame_callback; /* 安装最终完成帧回调。 */ Event_Run dispatch_event; /* 向最终 Scene 当前 Paint 图派发事件。 */ Active_Run set_active; /* 修改最终 Scene 的视图活动状态。 */ Frame_View_Run frame_view; /* 访问最终 Scene 已合成的写侧帧。 */ }; const Dispatch* dispatch{}; /* Builder 绑定最终 Scene 类型后的静态分派表。 */ + Frame_Callback frame_callback{}; /* 合成完成后的唯一像素发布出口。 */ + bool rendering{}; /* 是否正在同步合成当前帧。 */ + bool render_pending{}; /* 合成期间是否又收到 render();多个调用合并。 */ /* Impl CRTP 实现:在对象锁内执行 Kernel Scene,再按 Paint 图拓扑顺序合成颜色层。 */ template - void process(Object* object, Callback&& callback) - requires std::invocable; + void process(Object* object, Callback&& callback) requires std::invocable; + template void render(Object* object); /* CRTP 业务实现:按 Paint 图拓扑逆序派发事件。 */ template void dispatch_event(Object* object, const Event& event); @@ -29,16 +34,12 @@ struct Render_Scene_2D::Private : Prev_Private { }; template void Render_Scene_2D::Private::process(Object* object, Callback&& callback) - requires std::invocable { + requires std::invocable { const auto& state = static_cast(*static_cast(*this).current); if (!state.view_active) { - const Render_Frame_Status status = Render_Frame_Status::view_inactive; - std::invoke(std::forward(callback), status); return; } if (state.viewport.empty()) { - const Render_Frame_Status status = Render_Frame_Status::empty_viewport; - std::invoke(std::forward(callback), status); return; } object->template current_dependency_graph().for_each( @@ -64,8 +65,17 @@ void Render_Scene_2D::Private::process(Object* object, Callback&& callback) }); if (!result) throw std::logic_error("render scene paint graph became invalid during composition"); }); - const Render_Frame_Status status = Render_Frame_Status::rendered; - std::invoke(std::forward(callback), status); + std::invoke(std::forward(callback)); +} +template +void Render_Scene_2D::Private::render(Object* object) { + if (rendering) { render_pending = true; return; } + rendering = true; + do { + render_pending = false; + object->process([&] { if (frame_callback) frame_callback(object->template pending_buffer().view()); }); + } while (render_pending); + rendering = false; } template void Render_Scene_2D::Private::dispatch_event(Object* object, const Event& event) { @@ -83,9 +93,11 @@ const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for static const Dispatch value{ [](Root* root) { auto* object = static_cast(root); - Render_Frame_Status result{}; - object->process([&](const Render_Frame_Status& status) { result = status; }); - return result; + static_cast(*object->d).render(object); + }, + [](Root* root, Frame_Callback callback) { + auto* object = static_cast(root); + static_cast(*object->d).frame_callback = std::move(callback); }, [](Root* root, const Event& event) { auto* object = static_cast(root); diff --git a/render_2D/tests/Plottable_Migration_Test.cpp b/render_2D/tests/Plottable_Migration_Test.cpp index 1c9fb54..08f504a 100644 --- a/render_2D/tests/Plottable_Migration_Test.cpp +++ b/render_2D/tests/Plottable_Migration_Test.cpp @@ -59,7 +59,7 @@ TEST(plottable_migration, curve_plots_share_partitioned_rendering) { sweep->append_block(block); scene->set<&Render_Scene_2D::Prop::viewport>(canvas); scene->activate_view(); - EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); + scene->render(); EXPECT_EQ(trace->sample_count(), 3u); EXPECT_GT(trace->rendered_point_count(), 0u); EXPECT_EQ(sweep->stored_block_count(), 1u); @@ -97,7 +97,7 @@ TEST(plottable_migration, raster_plots_share_partitioned_color_blocks) { waterfall->append_row(1, row); scene->set<&Render_Scene_2D::Prop::viewport>(canvas); scene->activate_view(); - EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); + scene->render(); EXPECT_EQ(glow->history_count(), 1u); EXPECT_GT(glow->rendered_cell_count(), 0u); EXPECT_EQ(waterfall->row_count(), 2u); @@ -125,7 +125,7 @@ TEST(plottable_migration, direct_overlay_and_constellation_build_and_render) { diagram->append_point({0.25, -0.25}); scene->set<&Render_Scene_2D::Prop::viewport>(canvas); scene->activate_view(); - EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); + scene->render(); EXPECT_EQ(diagram->point_count(), 1u); EXPECT_EQ(overlay->read_state().prepare_task_count, 0u); EXPECT_EQ(overlay->read_state().paint_task_count, 1u); diff --git a/render_2D/tests/Spectrum_Test.cpp b/render_2D/tests/Spectrum_Test.cpp index be1982e..1063390 100644 --- a/render_2D/tests/Spectrum_Test.cpp +++ b/render_2D/tests/Spectrum_Test.cpp @@ -107,7 +107,7 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) { EXPECT_TRUE(prepare_graph.depends_on(spectrum.get(), scene.get())); EXPECT_TRUE(prepare_graph.depends_on(spectrum.get(), frequency.get())); EXPECT_TRUE(prepare_graph.depends_on(spectrum.get(), power.get())); - EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); + scene->render(); EXPECT_GT(spectrum->rendered_point_count(), 0u); const auto& render_state = spectrum->read_state(); EXPECT_EQ(render_state.prepare_task_count, 4u); @@ -117,7 +117,7 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) { EXPECT_TRUE(contains_color(frame)); spectrum->set<&Spectrum::Prop::partition_count>(2u); spectrum->update_samples(samples); - EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); + scene->render(); const auto& rebuilt_state = spectrum->read_state(); EXPECT_TRUE(rebuilt_state.prepare_graph_rebuilt); EXPECT_EQ(rebuilt_state.prepare_task_count, 3u); @@ -126,7 +126,7 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) { scene->set<&Render_Scene_2D::Prop::viewport>(resized_canvas); frequency->set<&Abs_Axis::Prop::canvas_size>(resized_canvas); power->set<&Abs_Axis::Prop::canvas_size>(resized_canvas); - EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); + scene->render(); const Image_View resized_frame = scene->frame_view(); EXPECT_EQ(resized_frame.width, resized_canvas.width); EXPECT_EQ(resized_frame.height, resized_canvas.height); diff --git a/render_3D/render_3D/detail/Async_Render_Backend.cpp b/render_3D/render_3D/detail/Async_Render_Backend.cpp index d338933..b2e8e21 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.cpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.cpp @@ -11,28 +11,34 @@ float wheel_step(double pixel, double angle) { return static_cast(pixel ! } struct Async_Render_Backend::Implementation : std::enable_shared_from_this { struct Pending { std::optional frame{}; }; + struct Submission { Prepared_Visual visual{}; Scene_3D_Parameters parameters{}; }; std::shared_ptr render_domain; /* GPU 索引对应的单线程 Datoviz 域。 */ std::unique_ptr backend; /* 只允许 render_domain 线程访问。 */ - mutable std::mutex frame_mutex; /* 保护异步发布的最新像素帧。 */ - std::shared_ptr latest_frame{}; /* 最近完成读回的像素帧。 */ + mutable std::mutex render_mutex; /* 保护合并帧意图、在途标记和完成回调。 */ + std::optional pending_submission{}; /* 在途期间覆盖保存的最新绘制意图。 */ + Frame_Callback frame_callback{}; /* 完成帧的唯一发布出口。 */ + bool frame_in_flight{}; /* 是否已有帧占用唯一 Frame Target。 */ mutable std::mutex failure_mutex; /* 保护最后一次 Unknown Failure。 */ std::exception_ptr failure{}; /* 后端隔离边界捕获的最后一次 Unknown Failure。 */ - std::atomic_uint64_t next_sequence{1}; /* 下一次接受请求的帧序号。 */ - std::atomic_uint64_t submitted{}; /* 已提交帧计数。 */ - std::atomic_uint64_t completed{}; /* 已完成帧计数。 */ - std::atomic_uint64_t dropped{}; /* 已丢弃帧请求计数。 */ - std::atomic_bool frame_in_flight{}; /* 限制后端唯一 Frame_Target 同时只有一帧。 */ - std::atomic_bool available{}; /* 后端是否可接受事件和帧请求。 */ + std::atomic_uint64_t next_sequence{1}; /* 下一完成帧使用的单调序号。 */ + std::atomic_bool available{}; /* 后端是否可接受事件和绘制。 */ Implementation(std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family, Scene_3D_Parameters initial) : render_domain(Render_Domain::acquire(gpu_index)) { const auto initialized = render_domain->invoke([this, gpu_index, validation_enabled, visual_family, initial] { backend = std::make_unique(gpu_index, validation_enabled, visual_family, initial); }); if (!initialized) throw std::runtime_error("render domain stopped during 3D backend initialization"); available.store(true, std::memory_order_release); } ~Implementation() { available.store(false, std::memory_order_release); if (!backend) return; const auto destroyed = render_domain->invoke([this] { backend.reset(); }); if (!destroyed) backend.release(); } - void fail(std::exception_ptr value) noexcept { { std::lock_guard lock(failure_mutex); failure = std::move(value); } available.store(false, std::memory_order_release); frame_in_flight.store(false, std::memory_order_release); } - void finish(std::shared_ptr pending, Gpu_Completion_Service::Result completion) noexcept { auto self = shared_from_this(); try { const auto result = render_domain->post([self, pending = std::move(pending), completion] { if (!pending->frame) { self->frame_in_flight.store(false, std::memory_order_release); return; } if (completion.error == Gpu_Completion_Service::Completion_Error::none) { auto finished = self->backend->collect(std::move(*pending->frame)); { std::lock_guard lock(self->frame_mutex); self->latest_frame = std::move(finished.frame); } self->completed.fetch_add(1, std::memory_order_relaxed); } else { self->backend->discard(std::move(*pending->frame)); self->dropped.fetch_add(1, std::memory_order_relaxed); } self->frame_in_flight.store(false, std::memory_order_release); }, [self](std::exception_ptr value) { self->fail(std::move(value)); }); if (result != Render_Domain::Admission_Result::none) fail(std::make_exception_ptr(std::runtime_error("render domain stopped before GPU completion collection"))); } catch (...) { fail(std::current_exception()); } } - void enqueue(const Prepared_Visual& visual, Scene_3D_Parameters parameters) { if (!available.load(std::memory_order_acquire)) { dropped.fetch_add(1, std::memory_order_relaxed); return; } bool expected = false; if (!frame_in_flight.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) { dropped.fetch_add(1, std::memory_order_relaxed); return; } const auto sequence = next_sequence.fetch_add(1, std::memory_order_relaxed); auto self = shared_from_this(); auto prepared = std::make_shared(visual); auto pending = std::make_shared(); const auto queued = render_domain->try_post([self, parameters, sequence, prepared, pending] { auto reservation = Gpu_Completion_Service::instance().prepare([self, pending](Gpu_Completion_Service::Result result) { self->finish(pending, std::move(result)); }, [self](std::exception_ptr value) { self->fail(std::move(value)); }, false); if (!reservation) { self->frame_in_flight.store(false, std::memory_order_release); self->dropped.fetch_add(1, std::memory_order_relaxed); return; } pending->frame = self->backend->submit(parameters, *prepared, sequence, false); if (!pending->frame) { self->frame_in_flight.store(false, std::memory_order_release); return; } self->submitted.fetch_add(1, std::memory_order_relaxed); reservation.reservation.watch(pending->frame->device, pending->frame->fence); }, [self](std::exception_ptr value) { self->fail(std::move(value)); }); if (queued != Render_Domain::Try_Post_Result::queued) { frame_in_flight.store(false, std::memory_order_release); dropped.fetch_add(1, std::memory_order_relaxed); } } + void fail(std::exception_ptr value) noexcept; + void render(const Prepared_Visual& visual, Scene_3D_Parameters parameters); + void submit(Submission submission); + void finish(std::shared_ptr pending, Gpu_Completion_Service::Result completion) noexcept; + void complete(std::shared_ptr frame); }; Async_Render_Backend::Async_Render_Backend(std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family, Scene_3D_Parameters parameters) : implementation_(std::make_shared(gpu_index, validation_enabled, visual_family, parameters)) {} Async_Render_Backend::~Async_Render_Backend() = default; -void Async_Render_Backend::enqueue(const Prepared_Visual& visual, Scene_3D_Parameters parameters) { implementation_->enqueue(visual, parameters); } -std::shared_ptr Async_Render_Backend::latest_frame() const { std::lock_guard lock(implementation_->frame_mutex); return implementation_->latest_frame; } -Async_Render_Backend::Statistics Async_Render_Backend::statistics() const noexcept { return {implementation_->submitted.load(std::memory_order_relaxed), implementation_->completed.load(std::memory_order_relaxed), implementation_->dropped.load(std::memory_order_relaxed), implementation_->frame_in_flight.load(std::memory_order_acquire), implementation_->available.load(std::memory_order_acquire)}; } +void Async_Render_Backend::Implementation::fail(std::exception_ptr value) noexcept { { std::lock_guard lock(failure_mutex); failure = std::move(value); } available.store(false, std::memory_order_release); std::lock_guard lock(render_mutex); frame_in_flight = false; pending_submission.reset(); } +void Async_Render_Backend::Implementation::render(const Prepared_Visual& visual, Scene_3D_Parameters parameters) { if (!available.load(std::memory_order_acquire)) return; std::optional submission; { std::lock_guard lock(render_mutex); pending_submission = Submission{visual, parameters}; if (frame_in_flight) return; frame_in_flight = true; submission = std::move(pending_submission); pending_submission.reset(); } submit(std::move(*submission)); } +void Async_Render_Backend::Implementation::submit(Submission submission) { const auto sequence = next_sequence.fetch_add(1, std::memory_order_relaxed); auto self = shared_from_this(); auto prepared = std::make_shared(std::move(submission.visual)); auto pending = std::make_shared(); const auto parameters = submission.parameters; const auto queued = render_domain->try_post([self, parameters, sequence, prepared, pending] { auto reservation = Gpu_Completion_Service::instance().prepare([self, pending](Gpu_Completion_Service::Result result) { self->finish(pending, std::move(result)); }, [self](std::exception_ptr value) { self->fail(std::move(value)); }, false); if (!reservation) { self->complete({}); return; } pending->frame = self->backend->submit(parameters, *prepared, sequence, false); if (!pending->frame) { self->complete({}); return; } reservation.reservation.watch(pending->frame->device, pending->frame->fence); }, [self](std::exception_ptr value) { self->fail(std::move(value)); }); if (queued != Render_Domain::Try_Post_Result::queued) { std::lock_guard lock(render_mutex); pending_submission = Submission{*prepared, parameters}; frame_in_flight = false; } } +void Async_Render_Backend::Implementation::finish(std::shared_ptr pending, Gpu_Completion_Service::Result completion) noexcept { auto self = shared_from_this(); try { const auto result = render_domain->post([self, pending = std::move(pending), completion] { std::shared_ptr frame; if (pending->frame) { if (completion.error == Gpu_Completion_Service::Completion_Error::none) frame = self->backend->collect(std::move(*pending->frame)).frame; else self->backend->discard(std::move(*pending->frame)); } self->complete(std::move(frame)); }, [self](std::exception_ptr value) { self->fail(std::move(value)); }); if (result != Render_Domain::Admission_Result::none) fail(std::make_exception_ptr(std::runtime_error("render domain stopped before GPU completion collection"))); } catch (...) { fail(std::current_exception()); } } +void Async_Render_Backend::Implementation::complete(std::shared_ptr frame) { Frame_Callback callback; std::optional next; { std::lock_guard lock(render_mutex); callback = frame_callback; frame_in_flight = false; if (pending_submission && available.load(std::memory_order_acquire)) { frame_in_flight = true; next = std::move(pending_submission); pending_submission.reset(); } } if (frame && callback) callback(std::move(frame)); if (next) submit(std::move(*next)); } +void Async_Render_Backend::render(const Prepared_Visual& visual, Scene_3D_Parameters parameters) { implementation_->render(visual, parameters); } +void Async_Render_Backend::set_frame_callback(Frame_Callback callback) { std::lock_guard lock(implementation_->render_mutex); implementation_->frame_callback = std::move(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(const Event& event, Extent viewport) { auto& data = *implementation_; if (!data.available.load(std::memory_order_acquire)) return Dispatch_Event_Result::backend_unavailable; if (const auto* wheel = dynamic_cast(&event)) { const auto* pointer = dynamic_cast(&event); if (!pointer || !std::isfinite(pointer->position_x()) || !std::isfinite(pointer->position_y())) return Dispatch_Event_Result::invalid_event; const auto result = data.render_domain->invoke([&data, pointer, wheel, viewport] { data.backend->dispatch_wheel(static_cast(pointer->position_x()), static_cast(pointer->position_y()), wheel_step(wheel->pixel_delta_x_value(), wheel->angle_delta_x_value()), wheel_step(wheel->pixel_delta_y_value(), wheel->angle_delta_y_value()), pointer->keyboard_modifiers(), viewport); }); return result ? Dispatch_Event_Result::dispatched : Dispatch_Event_Result::backend_unavailable; } if (const auto* pointer = dynamic_cast(&event)) { if (!std::isfinite(pointer->position_x()) || !std::isfinite(pointer->position_y()) || (event.type != Event_Type::pointer_move && event.type != Event_Type::pointer_press && event.type != Event_Type::pointer_release)) return Dispatch_Event_Result::invalid_event; const auto result = data.render_domain->invoke([&data, pointer, &event, viewport] { data.backend->dispatch_pointer(event.type, static_cast(pointer->position_x()), static_cast(pointer->position_y()), pointer->pointer_button(), pointer->keyboard_modifiers(), viewport); }); return result ? Dispatch_Event_Result::dispatched : Dispatch_Event_Result::backend_unavailable; } if (const auto* key = dynamic_cast(&event)) { const auto result = data.render_domain->invoke([&data, key] { data.backend->dispatch_key(*key); }); return result ? Dispatch_Event_Result::dispatched : Dispatch_Event_Result::backend_unavailable; } return Dispatch_Event_Result::ignored; } } diff --git a/render_3D/render_3D/detail/Async_Render_Backend.hpp b/render_3D/render_3D/detail/Async_Render_Backend.hpp index bbcda7f..ed032ab 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.hpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.hpp @@ -1,24 +1,20 @@ #pragma once #include "Backend_Types.hpp" +#include #include namespace aethera::render_3d::detail { class Async_Render_Backend final { public: - struct Statistics { - std::uint64_t submitted{}; /* 已提交给 Vulkan 的帧数。 */ - std::uint64_t completed{}; /* 已完成读回并发布的帧数。 */ - std::uint64_t dropped{}; /* 因已有在途帧或队列已满而丢弃的请求数。 */ - bool frame_in_flight{}; /* 是否存在尚未完成的 GPU 帧。 */ - bool available{}; /* Datoviz 后端是否完成初始化且未隔离。 */ - }; + using Frame_Callback = std::function)>; Async_Render_Backend(std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family, Scene_3D_Parameters parameters); ~Async_Render_Backend(); Async_Render_Backend(const Async_Render_Backend&) = delete; Async_Render_Backend& operator=(const Async_Render_Backend&) = delete; - /* 无等待地把 Prepared_Visual 放入渲染域;队列忙时直接记录丢帧。 */ - void enqueue(const Prepared_Visual& visual, Scene_3D_Parameters parameters); - [[nodiscard]] std::shared_ptr latest_frame() const; - [[nodiscard]] Statistics statistics() const noexcept; + /* 记录最新绘制意图并无等待提交;在途期间的多次调用自动合并为一次后继帧。 */ + void render(const Prepared_Visual& visual, Scene_3D_Parameters parameters); + /* 设置完成帧出口;回调在 Datoviz Render Domain 线程执行。 */ + void set_frame_callback(Frame_Callback callback); + [[nodiscard]] bool available() const noexcept; enum class Dispatch_Event_Result : std::uint8_t { dispatched, ignored, invalid_event, backend_unavailable }; [[nodiscard]] Dispatch_Event_Result dispatch_event(const Event& event, Extent viewport); private: diff --git a/render_3D/render_3D/scene/Render_Scene_3D.cpp b/render_3D/render_3D/scene/Render_Scene_3D.cpp index 52d11c7..068d375 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.cpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.cpp @@ -2,9 +2,9 @@ namespace aethera::render_3d { bool Render_Scene_3D::Prop::operator==(const Prop&) const = default; bool Render_Scene_3D::State::operator==(const State&) const = default; -Render_Scene_3D::Request_Frame_Result Render_Scene_3D::request_frame() { return static_cast(*d).dispatch->request_frame(this); } +void Render_Scene_3D::render() { static_cast(*d).dispatch->render(this); } +void Render_Scene_3D::set_frame_callback(Frame_Callback callback) { static_cast(*d).dispatch->set_frame_callback(this, std::move(callback)); } Render_Scene_3D::Dispatch_Event_Result Render_Scene_3D::dispatch_event(const Event& event) { return static_cast(*d).dispatch->dispatch_event(this, event); } -std::shared_ptr Render_Scene_3D::latest_frame() const { return static_cast(*d).dispatch->latest_frame(this); } 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 de44fab..dab1ad2 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.hpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.hpp @@ -3,6 +3,7 @@ #include "../visual/Visuals.hpp" #include #include +#include #include namespace aethera::render_3d { /* 执行 3D Renderable 图,并把 Paint 阶段无等待地提交给 Datoviz 渲染域。 */ @@ -10,15 +11,10 @@ struct Render_Scene_3D : Def { struct Prop : Prev_Prop { Extent viewport{560, 320}; /* 离屏渲染目标尺寸,单位为像素。 */ Linear_Color clear_color{}; /* 每帧开始时写入的线性背景色。 */ - bool view_active{true}; /* 是否接受新帧请求;停用不清除最近完成帧。 */ + bool view_active{true}; /* 是否接受 render() 产生新的完成帧。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { - std::uint64_t submitted_frame_count{}; /* 已提交给 Vulkan 的累计帧数。 */ - std::uint64_t completed_frame_count{}; /* 已完成 GPU 读回并发布的累计帧数。 */ - std::uint64_t dropped_frame_count{}; /* 因背压或已有在途帧而丢弃的累计请求数。 */ - bool frame_in_flight{}; /* 是否存在尚未完成的 GPU 帧。 */ - bool backend_available{}; /* Datoviz 后端是否可接受新工作。 */ bool operator==(const State&) const; }; struct Private; @@ -37,14 +33,14 @@ struct Render_Scene_3D : Def { std::uint32_t gpu_index{}; /* Datoviz 使用的物理 GPU 下标。 */ bool validation_enabled{}; /* 是否启用 Vulkan 验证。 */ }; - enum class Request_Frame_Result : std::uint8_t { queued, view_inactive, empty_viewport, backend_unavailable }; - /* 执行 CPU Taskflow;Paint 节点只入队,不等待 Vulkan 或 GPU fence。 */ - [[nodiscard]] Request_Frame_Result request_frame(); + using Frame_Callback = std::function)>; + /* 触发绘制;在途期间的多次调用合并为一个使用最新数据的后继帧。 */ + void render(); + /* 安装完成帧回调;回调由 Datoviz Render Domain 线程调用。 */ + void set_frame_callback(Frame_Callback callback); enum class Dispatch_Event_Result : std::uint8_t { dispatched, ignored, invalid_event, backend_unavailable }; /* 在 Datoviz 单线程渲染域中同步派发输入事件。 */ [[nodiscard]] Dispatch_Event_Result dispatch_event(const Event& event); - /* 返回最近一次异步完成的 RGBA8 帧;尚无完成帧时为空。 */ - [[nodiscard]] std::shared_ptr latest_frame() const; 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 ad55f8b..6170f93 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.ipp +++ b/render_3D/render_3D/scene/Render_Scene_3D.ipp @@ -10,14 +10,14 @@ struct Scene_Paint_Context { }; } struct Render_Scene_3D::Private : Prev_Private { - using Request_Run = Request_Frame_Result (*)(Root*); + using Render_Run = void (*)(Root*); + using Callback_Run = void (*)(Root*, Frame_Callback); using Event_Run = Dispatch_Event_Result (*)(Root*, const Event&); - using Frame_Run = std::shared_ptr (*)(const Root*); using Active_Run = void (*)(Root*, bool); struct Dispatch { - Request_Run request_frame; /* 执行 CPU 图并异步提交 Paint。 */ + Render_Run render; /* 执行 CPU 图并异步提交 Paint。 */ + Callback_Run set_frame_callback; /* 安装最终完成帧回调。 */ Event_Run dispatch_event; /* 向 Datoviz 输入路由器派发事件。 */ - Frame_Run latest_frame; /* 获取最近异步完成帧。 */ Active_Run set_active; /* 修改最终 Scene 的活动属性。 */ }; std::shared_ptr backend{}; /* Scene 拥有的异步后端;已入队命令自行延长实现寿命。 */ @@ -28,21 +28,18 @@ struct Render_Scene_3D::Private : Prev_Private { /* CRTP 覆盖:绑定 Scene 机制和 Render_Scene_3D 公开薄壳。 */ template void bind_private_crtp(Object* object); /* CRTP 覆盖:执行 Kernel Scene Taskflow;其 Paint 子图只负责异步入队。 */ - template void process(Object* object, Callback&& callback) requires std::invocable; - /* CRTP 推进 hook:发布异步后端的统计快照。 */ - template void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); + template void process(Object* object, Callback&& callback) requires std::invocable; template [[nodiscard]] static const Dispatch& dispatch_for(); }; template template Render_Scene_3D::Builder::Builder(Visual_Object* visual_value, std::uint32_t gpu_index_value, bool validation_enabled_value) : Base(), visual(visual_value), visual_family(Visual_Object::Attached_Object::Specification::family), gpu_index(gpu_index_value), validation_enabled(validation_enabled_value) { - bind_visual = [](Root* root, std::shared_ptr context) { auto* object = static_cast(root); Base::private_access(object).template get().bind_paint_target(std::move(context), [](void* raw_context, const detail::Prepared_Visual& prepared) { auto& submission = *static_cast*>(raw_context); const auto& prop = submission.scene->template read_prop(); submission.backend->enqueue(prepared, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}); }); }; + bind_visual = [](Root* root, std::shared_ptr context) { auto* object = static_cast(root); Base::private_access(object).template get().bind_paint_target(std::move(context), [](void* raw_context, const detail::Prepared_Visual& prepared) { auto& submission = *static_cast*>(raw_context); const auto& prop = submission.scene->template read_prop(); submission.backend->render(prepared, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}); }); }; attach_visual = [](Object* scene, Root* root) { auto* object = static_cast(root); return scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { prepare.add(object); paint.add(object); prepare.template add_prop_dependency(object, scene); }); }; } template std::expected, Dependency_Graph_Error> Render_Scene_3D::Builder::build() { if (!visual || !bind_visual || !attach_visual) throw std::invalid_argument("Render_Scene_3D requires one Visual"); auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto scene = std::move(result).value(); auto& private_data = Base::private_access(scene.get()).template get(); private_data.initialize_backend(scene.get(), gpu_index, validation_enabled, visual_family); bind_visual(visual, private_data.paint_context); auto graph_result = attach_visual(scene.get(), visual); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(scene); } template void Render_Scene_3D::Private::initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family) { const auto& prop = object->template read_prop(); backend = std::make_shared(gpu_index, validation_enabled, visual_family, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}); paint_context = std::make_shared>(detail::Scene_Paint_Context{backend, object}); } -template void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requires std::invocable { const auto& prop = object->template read_prop(); if (!prop.view_active) { std::invoke(std::forward(callback), Request_Frame_Result::view_inactive); return; } if (prop.viewport.empty()) { std::invoke(std::forward(callback), Request_Frame_Result::empty_viewport); return; } if (!backend || !backend->statistics().available) { std::invoke(std::forward(callback), Request_Frame_Result::backend_unavailable); return; } Prev_Private::process(object, [](const Scene::Private::Result&) {}); std::invoke(std::forward(callback), Request_Frame_Result::queued); } -template void Render_Scene_3D::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type*, State_Access) { if (!backend) return; const auto statistics = backend->statistics(); auto& state = pending_states.template get(); state.submitted_frame_count = statistics.submitted; state.completed_frame_count = statistics.completed; state.dropped_frame_count = statistics.dropped; state.frame_in_flight = statistics.frame_in_flight; state.backend_available = statistics.available; } -template const Render_Scene_3D::Private::Dispatch& Render_Scene_3D::Private::dispatch_for() { static const Dispatch value{[](Root* root) { auto* object = static_cast(root); Request_Frame_Result result{}; object->process([&](Request_Frame_Result value) { result = value; }); return result; }, [](Root* root, const Event& event) { auto* object = static_cast(root); auto& data = static_cast(*object->d); if (!data.backend) return Dispatch_Event_Result::backend_unavailable; const auto viewport = object->template read_prop().viewport; switch (data.backend->dispatch_event(event, viewport)) { case detail::Async_Render_Backend::Dispatch_Event_Result::dispatched: return Dispatch_Event_Result::dispatched; case detail::Async_Render_Backend::Dispatch_Event_Result::ignored: return Dispatch_Event_Result::ignored; case detail::Async_Render_Backend::Dispatch_Event_Result::invalid_event: return Dispatch_Event_Result::invalid_event; case detail::Async_Render_Backend::Dispatch_Event_Result::backend_unavailable: return Dispatch_Event_Result::backend_unavailable; } return Dispatch_Event_Result::backend_unavailable; }, [](const Root* root) { const auto* object = static_cast(root); const auto& data = static_cast(*object->d); return data.backend ? data.backend->latest_frame() : std::shared_ptr{}; }, [](Root* root, bool active) { static_cast(root)->template set<&Prop::view_active>(active); }}; return value; } +template void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requires std::invocable { const auto& prop = object->template read_prop(); if (!prop.view_active || prop.viewport.empty() || !backend || !backend->available()) return; Prev_Private::process(object, [](const Scene::Private::Result&) {}); std::invoke(std::forward(callback)); } +template const Render_Scene_3D::Private::Dispatch& Render_Scene_3D::Private::dispatch_for() { static const Dispatch value{[](Root* root) { auto* object = static_cast(root); object->process([] {}); }, [](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, const Event& event) { auto* object = static_cast(root); auto& data = static_cast(*object->d); if (!data.backend) return Dispatch_Event_Result::backend_unavailable; const auto viewport = object->template read_prop().viewport; switch (data.backend->dispatch_event(event, viewport)) { case detail::Async_Render_Backend::Dispatch_Event_Result::dispatched: return Dispatch_Event_Result::dispatched; case detail::Async_Render_Backend::Dispatch_Event_Result::ignored: return Dispatch_Event_Result::ignored; case detail::Async_Render_Backend::Dispatch_Event_Result::invalid_event: return Dispatch_Event_Result::invalid_event; case detail::Async_Render_Backend::Dispatch_Event_Result::backend_unavailable: return Dispatch_Event_Result::backend_unavailable; } return Dispatch_Event_Result::backend_unavailable; }, [](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/render_3D/tests/Visual_Tests.cpp b/render_3D/tests/Visual_Tests.cpp index deca09d..d9a30e9 100644 --- a/render_3D/tests/Visual_Tests.cpp +++ b/render_3D/tests/Visual_Tests.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include namespace aethera::render_3d { @@ -55,12 +56,15 @@ TEST(Render_3D_Scene, Paint_Submits_Without_Waiting_For_Gpu) { auto scene_result = typename Scene_Object::Builder(visual.get()).build(); ASSERT_TRUE(scene_result.has_value()); auto scene = std::move(scene_result).value(); + std::atomic_bool frame_ready{}; + std::shared_ptr frame; + scene->set_frame_callback([&](std::shared_ptr value) { frame = std::move(value); frame_ready.store(true, std::memory_order_release); }); const auto started = std::chrono::steady_clock::now(); - EXPECT_EQ(scene->request_frame(), Render_Scene_3D::Request_Frame_Result::queued); + scene->render(); const auto submit_duration = std::chrono::steady_clock::now() - started; EXPECT_LT(submit_duration, std::chrono::seconds(1)); - for (std::size_t attempt = 0; attempt != 500 && !scene->latest_frame(); ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(10)); - EXPECT_NE(scene->latest_frame(), nullptr); + for (std::size_t attempt = 0; attempt != 500 && !frame_ready.load(std::memory_order_acquire); ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + EXPECT_NE(frame, nullptr); } catch (const std::exception& error) { GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what(); diff --git a/render_3D/third_party/datoviz/src/scene/annotation/annotation_api.c b/render_3D/third_party/datoviz/src/scene/annotation/annotation_api.c index aeb733b..d6da757 100644 --- a/render_3D/third_party/datoviz/src/scene/annotation/annotation_api.c +++ b/render_3D/third_party/datoviz/src/scene/annotation/annotation_api.c @@ -127,7 +127,7 @@ DvzAnnotation* dvz_annotation(DvzPanel* panel, const DvzAnnotationDesc* desc) annotation->version = 1; if (desc->text != NULL) dvz_strlcpy(annotation->text, desc->text, sizeof(annotation->text)); - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return annotation; } @@ -174,7 +174,7 @@ DvzResult dvz_annotation_set_style(DvzAnnotation* annotation, const DvzTextStyle annotation->style = resolved; annotation->dirty_flags |= DVZ_TEXT_DIRTY_STYLE | DVZ_TEXT_DIRTY_LAYOUT | DVZ_TEXT_DIRTY_RENDER; annotation->version++; - _scene_notify_request_frame(annotation->panel != NULL ? annotation->panel->figure : NULL); + _scene_notify_invalidated(annotation->panel != NULL ? annotation->panel->figure : NULL); return 0; } @@ -192,7 +192,7 @@ DvzResult dvz_annotation_set_placement( annotation->dirty_flags |= DVZ_TEXT_DIRTY_PLACEMENT | DVZ_TEXT_DIRTY_LAYOUT | DVZ_TEXT_DIRTY_RENDER; annotation->version++; - _scene_notify_request_frame(annotation->panel != NULL ? annotation->panel->figure : NULL); + _scene_notify_invalidated(annotation->panel != NULL ? annotation->panel->figure : NULL); return 0; } @@ -211,7 +211,7 @@ void dvz_annotation_destroy(DvzAnnotation* annotation) dvz_visual_set_visible(annotation->visual, false); if (annotation->scalebar_visual != NULL) dvz_visual_set_visible(annotation->scalebar_visual, false); - _scene_notify_request_frame(annotation->panel != NULL ? annotation->panel->figure : NULL); + _scene_notify_invalidated(annotation->panel != NULL ? annotation->panel->figure : NULL); annotation->scene = NULL; annotation->panel = NULL; annotation->has_format = false; @@ -235,6 +235,6 @@ DvzResult dvz_annotation_set_format(DvzAnnotation* annotation, const DvzFormatDe annotation->dirty_flags |= DVZ_TEXT_DIRTY_STRING | DVZ_TEXT_DIRTY_LAYOUT | DVZ_TEXT_DIRTY_RENDER; annotation->version++; - _scene_notify_request_frame(annotation->panel != NULL ? annotation->panel->figure : NULL); + _scene_notify_invalidated(annotation->panel != NULL ? annotation->panel->figure : NULL); return DVZ_OK; } diff --git a/render_3D/third_party/datoviz/src/scene/annotation/axis.c b/render_3D/third_party/datoviz/src/scene/annotation/axis.c index 4c0a1c0..29722bb 100644 --- a/render_3D/third_party/datoviz/src/scene/annotation/axis.c +++ b/render_3D/third_party/datoviz/src/scene/annotation/axis.c @@ -183,7 +183,7 @@ void _axis_mark_dirty(DvzAxis* axis) axis->dirty = true; axis->version++; _scene_panel_refresh_axis_reserve(axis->panel); - _scene_notify_request_frame(axis->panel != NULL ? axis->panel->figure : NULL); + _scene_notify_invalidated(axis->panel != NULL ? axis->panel->figure : NULL); } diff --git a/render_3D/third_party/datoviz/src/scene/annotation/colorbar.c b/render_3D/third_party/datoviz/src/scene/annotation/colorbar.c index f53c014..6d21a1f 100644 --- a/render_3D/third_party/datoviz/src/scene/annotation/colorbar.c +++ b/render_3D/third_party/datoviz/src/scene/annotation/colorbar.c @@ -87,7 +87,7 @@ void _scene_mark_colorbar_dirty(DvzColorbar* colorbar) colorbar->dirty = true; colorbar->version = colorbar->version == UINT64_MAX ? 1 : colorbar->version + 1; _colorbar_apply_auto_reserve(colorbar); - _scene_notify_request_frame(colorbar->panel != NULL ? colorbar->panel->figure : NULL); + _scene_notify_invalidated(colorbar->panel != NULL ? colorbar->panel->figure : NULL); } diff --git a/render_3D/third_party/datoviz/src/scene/annotation/guide.c b/render_3D/third_party/datoviz/src/scene/annotation/guide.c index 7f31dff..030ab84 100644 --- a/render_3D/third_party/datoviz/src/scene/annotation/guide.c +++ b/render_3D/third_party/datoviz/src/scene/annotation/guide.c @@ -444,7 +444,7 @@ DvzGuideLine* dvz_guide_line(DvzPanel* panel, const DvzGuideLineDesc* desc) guide->label = _guide_label_create(panel, resolved.label); _guide_attach_visual(panel, visual, DVZ_GENERATED_VISUAL_GUIDE_LINE, resolved.z_layer); - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return guide; } @@ -471,7 +471,7 @@ DvzResult dvz_guide_line_set_value(DvzGuideLine* guide, double value) guide->desc.value = value; guide->dirty = true; guide->version++; - _scene_notify_request_frame(guide->panel != NULL ? guide->panel->figure : NULL); + _scene_notify_invalidated(guide->panel != NULL ? guide->panel->figure : NULL); return 0; } @@ -523,7 +523,7 @@ DvzGuideSpan* dvz_guide_span(DvzPanel* panel, const DvzGuideSpanDesc* desc) _guide_attach_visual(panel, fill, DVZ_GENERATED_VISUAL_GUIDE_FILL, resolved.z_layer); if (outline != NULL) _guide_attach_visual(panel, outline, DVZ_GENERATED_VISUAL_GUIDE_OUTLINE, resolved.z_layer); - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return span; } @@ -564,7 +564,7 @@ DvzResult dvz_guide_span_set_range(DvzGuideSpan* span, double min_value, double span->desc.max_value = max_value; span->dirty = true; span->version++; - _scene_notify_request_frame(span->panel != NULL ? span->panel->figure : NULL); + _scene_notify_invalidated(span->panel != NULL ? span->panel->figure : NULL); return 0; } diff --git a/render_3D/third_party/datoviz/src/scene/annotation/legend.c b/render_3D/third_party/datoviz/src/scene/annotation/legend.c index c5d665d..30a407c 100644 --- a/render_3D/third_party/datoviz/src/scene/annotation/legend.c +++ b/render_3D/third_party/datoviz/src/scene/annotation/legend.c @@ -144,7 +144,7 @@ void _scene_mark_legend_dirty(DvzLegend* legend) return; legend->dirty = true; legend->version = legend->version == UINT64_MAX ? 1 : legend->version + 1; - _scene_notify_request_frame(legend->panel != NULL ? legend->panel->figure : NULL); + _scene_notify_invalidated(legend->panel != NULL ? legend->panel->figure : NULL); } diff --git a/render_3D/third_party/datoviz/src/scene/annotation/scalebar.c b/render_3D/third_party/datoviz/src/scene/annotation/scalebar.c index eaf6ef3..e388eca 100644 --- a/render_3D/third_party/datoviz/src/scene/annotation/scalebar.c +++ b/render_3D/third_party/datoviz/src/scene/annotation/scalebar.c @@ -1035,7 +1035,7 @@ DvzScaleBar* dvz_scale_bar(DvzPanel* panel, const DvzScaleBarDesc* desc) annotation->scalebar_units_format = _scalebar_descriptor_units(annotation); annotation->dirty_flags = DVZ_TEXT_DIRTY_ALL; annotation->version = 1; - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return (DvzScaleBar*)annotation; } @@ -1049,7 +1049,7 @@ DvzResult dvz_scale_bar_set_dimension(DvzScaleBar* scalebar, DvzDim dim) annotation->scalebar.dimension = dim; annotation->dirty_flags = DVZ_TEXT_DIRTY_ALL; annotation->version++; - _scene_notify_request_frame(annotation->panel != NULL ? annotation->panel->figure : NULL); + _scene_notify_invalidated(annotation->panel != NULL ? annotation->panel->figure : NULL); return 0; } @@ -1064,7 +1064,7 @@ DvzResult dvz_scale_bar_set_anchor(DvzScaleBar* scalebar, DvzSceneAnchor anchor) anchor; annotation->dirty_flags = DVZ_TEXT_DIRTY_ALL; annotation->version++; - _scene_notify_request_frame(annotation->panel != NULL ? annotation->panel->figure : NULL); + _scene_notify_invalidated(annotation->panel != NULL ? annotation->panel->figure : NULL); return 0; } @@ -1078,7 +1078,7 @@ DvzResult dvz_scale_bar_set_units(DvzScaleBar* scalebar, DvzUnits* units) annotation->scalebar_units_format = units; annotation->dirty_flags = DVZ_TEXT_DIRTY_ALL; annotation->version++; - _scene_notify_request_frame(annotation->panel != NULL ? annotation->panel->figure : NULL); + _scene_notify_invalidated(annotation->panel != NULL ? annotation->panel->figure : NULL); return 0; } @@ -1108,7 +1108,7 @@ DvzResult dvz_scale_bar_set_label_style(DvzScaleBar* scalebar, const DvzTextStyl annotation->style = resolved; annotation->dirty_flags |= DVZ_TEXT_DIRTY_STYLE | DVZ_TEXT_DIRTY_LAYOUT | DVZ_TEXT_DIRTY_RENDER; annotation->version++; - _scene_notify_request_frame(annotation->panel != NULL ? annotation->panel->figure : NULL); + _scene_notify_invalidated(annotation->panel != NULL ? annotation->panel->figure : NULL); return 0; } @@ -1127,7 +1127,7 @@ DvzResult dvz_scale_bar_set_placement( annotation->dirty_flags |= DVZ_TEXT_DIRTY_PLACEMENT | DVZ_TEXT_DIRTY_LAYOUT | DVZ_TEXT_DIRTY_RENDER; annotation->version++; - _scene_notify_request_frame(annotation->panel != NULL ? annotation->panel->figure : NULL); + _scene_notify_invalidated(annotation->panel != NULL ? annotation->panel->figure : NULL); return 0; } @@ -1144,6 +1144,6 @@ DvzResult dvz_scale_bar_set_format(DvzScaleBar* scalebar, const DvzFormatDesc* f annotation->dirty_flags |= DVZ_TEXT_DIRTY_STRING | DVZ_TEXT_DIRTY_LAYOUT | DVZ_TEXT_DIRTY_RENDER; annotation->version++; - _scene_notify_request_frame(annotation->panel != NULL ? annotation->panel->figure : NULL); + _scene_notify_invalidated(annotation->panel != NULL ? annotation->panel->figure : NULL); return 0; } diff --git a/render_3D/third_party/datoviz/src/scene/annotation/text.c b/render_3D/third_party/datoviz/src/scene/annotation/text.c index 122db26..5b53115 100644 --- a/render_3D/third_party/datoviz/src/scene/annotation/text.c +++ b/render_3D/third_party/datoviz/src/scene/annotation/text.c @@ -394,7 +394,7 @@ static void _text_mark_dirty(DvzText* text, uint32_t flags) ANN(text); text->dirty_flags |= flags; text->version++; - _scene_notify_request_frame(text->panel != NULL ? text->panel->figure : NULL); + _scene_notify_invalidated(text->panel != NULL ? text->panel->figure : NULL); } @@ -428,7 +428,7 @@ DvzText* dvz_text(DvzPanel* panel, uint32_t flags) text->flags = flags; text->dirty_flags = DVZ_TEXT_DIRTY_ALL; text->version = 1; - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return text; } @@ -460,7 +460,7 @@ void dvz_text_destroy(DvzText* text) dvz_visual_set_visible(text->visual, false); } _text_free_collection(text); - _scene_notify_request_frame(text->panel != NULL ? text->panel->figure : NULL); + _scene_notify_invalidated(text->panel != NULL ? text->panel->figure : NULL); text->scene = NULL; text->panel = NULL; text->legacy_string = NULL; diff --git a/render_3D/third_party/datoviz/src/scene/annotation/text_glyph_realize.c b/render_3D/third_party/datoviz/src/scene/annotation/text_glyph_realize.c index 6540085..f1278a1 100644 --- a/render_3D/third_party/datoviz/src/scene/annotation/text_glyph_realize.c +++ b/render_3D/third_party/datoviz/src/scene/annotation/text_glyph_realize.c @@ -198,7 +198,7 @@ static bool _text_sync_glyph_visual_attach( attach->has_generated_role = source_has_role; attach->generated_role = source_role; if (changed) - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return true; } if (dvz_panel_add_visual(panel, glyph_visual, desc) != 0) diff --git a/render_3D/third_party/datoviz/src/scene/core/_scene.h b/render_3D/third_party/datoviz/src/scene/core/_scene.h index c7dd8e8..e2a7e35 100644 --- a/render_3D/third_party/datoviz/src/scene/core/_scene.h +++ b/render_3D/third_party/datoviz/src/scene/core/_scene.h @@ -95,7 +95,7 @@ #define DVZ_SCENE_MAX_AXIS_TICKS 64 #define DVZ_SCENE_MAX_AXIS_TEXTS (DVZ_SCENE_MAX_AXIS_TICKS + 2) #define DVZ_SCENE_MAX_AXIS_MINOR_TICKS 8 -#define DVZ_SCENE_MAX_REQUEST_FRAME_SUBSCRIPTIONS 16 +#define DVZ_SCENE_MAX_INVALIDATION_SUBSCRIPTIONS 16 #define DVZ_SCENE_MAX_CONTROLLERS 128 #define DVZ_SCENE_MAX_CONTROLLER_LINKS 128 #define DVZ_SCENE_MAX_AXIS_LINES \ @@ -375,9 +375,9 @@ typedef enum } DvzGeneratedVisualRole; typedef struct DvzTextBlock DvzTextBlock; -typedef void (*DvzSceneRequestFrameCallback)(DvzFigure* figure, void* user_data); +typedef void (*DvzSceneInvalidationCallback)(DvzFigure* figure, void* user_data); -typedef struct DvzSceneRequestFrameSubscription DvzSceneRequestFrameSubscription; +typedef struct DvzSceneInvalidationSubscription DvzSceneInvalidationSubscription; typedef struct DvzPanelView2DResolved DvzPanelView2DResolved; DvzId _scene_next_id(DvzScene* scene); @@ -428,9 +428,9 @@ void _scene_panel_view3d_dirty(DvzPanel* panel); /* Request frame subscriptions */ /*************************************************************************************************/ -struct DvzSceneRequestFrameSubscription +struct DvzSceneInvalidationSubscription { - DvzSceneRequestFrameCallback callback; + DvzSceneInvalidationCallback callback; void* user_data; bool active; }; @@ -2220,8 +2220,8 @@ struct DvzScene uint32_t query_scope_count; DvzRequestFreshnessScope query_scopes[DVZ_SCENE_MAX_REQUEST_SCOPES]; DvzSampledField* text_bitmap_atlas; - DvzSceneRequestFrameSubscription - request_frame_subscriptions[DVZ_SCENE_MAX_REQUEST_FRAME_SUBSCRIPTIONS]; + DvzSceneInvalidationSubscription + invalidation_subscriptions[DVZ_SCENE_MAX_INVALIDATION_SUBSCRIPTIONS]; struct { diff --git a/render_3D/third_party/datoviz/src/scene/core/controllers.c b/render_3D/third_party/datoviz/src/scene/core/controllers.c index 184e4a2..10dc3ab 100644 --- a/render_3D/third_party/datoviz/src/scene/core/controllers.c +++ b/render_3D/third_party/datoviz/src/scene/core/controllers.c @@ -269,7 +269,7 @@ static void _scene_notify_controller_figures(const DvzController* controller) for (uint32_t pi = 0; pi < figure->panel_count; pi++) bound = bound || _scene_panel_has_controller(&figure->panels[pi], controller); if (bound) - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); } } @@ -327,7 +327,7 @@ static void _scene_controller_detach_from_panels(DvzController* controller) } for (uint32_t i = 0; i < affected_count; i++) - _scene_notify_request_frame(affected[i]); + _scene_notify_invalidated(affected[i]); } @@ -1664,7 +1664,7 @@ DvzResult dvz_panel_bind_controller(DvzPanel* panel, DvzController* controller, default: return -1; } - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return 0; } diff --git a/render_3D/third_party/datoviz/src/scene/core/generated_visual_policy.h b/render_3D/third_party/datoviz/src/scene/core/generated_visual_policy.h index e097d71..88a3da1 100644 --- a/render_3D/third_party/datoviz/src/scene/core/generated_visual_policy.h +++ b/render_3D/third_party/datoviz/src/scene/core/generated_visual_policy.h @@ -324,7 +324,7 @@ static inline int _scene_panel_add_generated_visual( slot->has_generated_role = true; slot->generated_role = role; if (changed) - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return 0; } if (dvz_panel_add_visual(panel, visual, &attach) != 0) diff --git a/render_3D/third_party/datoviz/src/scene/core/grid.c b/render_3D/third_party/datoviz/src/scene/core/grid.c index 608b71a..c8e8fbd 100644 --- a/render_3D/third_party/datoviz/src/scene/core/grid.c +++ b/render_3D/third_party/datoviz/src/scene/core/grid.c @@ -117,7 +117,7 @@ static void _scene_grid_mark_dirty(DvzGrid* grid) return; grid->dirty = true; if (grid->figure != NULL) - _scene_notify_request_frame(grid->figure); + _scene_notify_invalidated(grid->figure); } @@ -254,7 +254,7 @@ void dvz_grid_destroy(DvzGrid* grid) } dvz_memset(grid, sizeof(DvzGrid), 0, sizeof(DvzGrid)); - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); } diff --git a/render_3D/third_party/datoviz/src/scene/core/orientation_gizmo.c b/render_3D/third_party/datoviz/src/scene/core/orientation_gizmo.c index b2d1911..2a2da3f 100644 --- a/render_3D/third_party/datoviz/src/scene/core/orientation_gizmo.c +++ b/render_3D/third_party/datoviz/src/scene/core/orientation_gizmo.c @@ -1001,7 +1001,7 @@ DvzOrientationGizmo* dvz_orientation_gizmo( if (!_orientation_gizmo_sync_transform(gizmo)) goto fail; - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return gizmo; fail: @@ -1028,7 +1028,7 @@ void dvz_orientation_gizmo_destroy(DvzOrientationGizmo* gizmo) dvz_panel_destroy(gizmo->panel); _orientation_gizmo_free_geometry(gizmo); dvz_memset(gizmo, sizeof(DvzOrientationGizmo), 0, sizeof(DvzOrientationGizmo)); - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); } @@ -1050,7 +1050,7 @@ DvzResult dvz_orientation_gizmo_set_visible(DvzOrientationGizmo* gizmo, bool vis if (gizmo->rings_visual != NULL) (void)dvz_visual_set_visible(gizmo->rings_visual, visible && gizmo->desc.show_axes); gizmo->version = gizmo->version == UINT64_MAX ? 1 : gizmo->version + 1; - _scene_notify_request_frame(gizmo->source_panel != NULL ? gizmo->source_panel->figure : NULL); + _scene_notify_invalidated(gizmo->source_panel != NULL ? gizmo->source_panel->figure : NULL); return DVZ_OK; } diff --git a/render_3D/third_party/datoviz/src/scene/core/panel_layout.c b/render_3D/third_party/datoviz/src/scene/core/panel_layout.c index fe36a9d..24d00d4 100644 --- a/render_3D/third_party/datoviz/src/scene/core/panel_layout.c +++ b/render_3D/third_party/datoviz/src/scene/core/panel_layout.c @@ -369,7 +369,7 @@ void _panel_mark_layout_changed(DvzPanel* panel) legend->dirty = true; legend->version = legend->version == UINT64_MAX ? 1 : legend->version + 1; } - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); } diff --git a/render_3D/third_party/datoviz/src/scene/core/panel_view.c b/render_3D/third_party/datoviz/src/scene/core/panel_view.c index efda736..bb39ba4 100644 --- a/render_3D/third_party/datoviz/src/scene/core/panel_view.c +++ b/render_3D/third_party/datoviz/src/scene/core/panel_view.c @@ -98,7 +98,7 @@ void _scene_panel_view_dirty(DvzPanel* panel) _axis_mark_dirty(x_axis); if (y_axis != NULL && y_axis->panel != NULL) _axis_mark_dirty(y_axis); - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); } @@ -107,7 +107,7 @@ void _scene_panel_view3d_dirty(DvzPanel* panel) if (panel == NULL) return; panel->view3d_revision = _panel_view_next_revision(panel->view3d_revision); - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); } diff --git a/render_3D/third_party/datoviz/src/scene/core/reference_grid.c b/render_3D/third_party/datoviz/src/scene/core/reference_grid.c index 2125644..415ac26 100644 --- a/render_3D/third_party/datoviz/src/scene/core/reference_grid.c +++ b/render_3D/third_party/datoviz/src/scene/core/reference_grid.c @@ -476,7 +476,7 @@ DvzReferenceGrid* dvz_reference_grid(DvzPanel* panel, const DvzReferenceGridDesc if (dvz_panel_add_visual(panel, grid->visual, &attach) != 0) goto fail; - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return grid; fail: @@ -498,7 +498,7 @@ void dvz_reference_grid_destroy(DvzReferenceGrid* grid) if (grid->visual != NULL) dvz_visual_set_visible(grid->visual, false); dvz_memset(grid, sizeof(DvzReferenceGrid), 0, sizeof(DvzReferenceGrid)); - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); } @@ -518,6 +518,6 @@ DvzResult dvz_reference_grid_set_visible(DvzReferenceGrid* grid, bool visible) if (grid->visual != NULL) (void)dvz_visual_set_visible(grid->visual, visible); grid->version = grid->version == UINT64_MAX ? 1 : grid->version + 1; - _scene_notify_request_frame(grid->panel != NULL ? grid->panel->figure : NULL); + _scene_notify_invalidated(grid->panel != NULL ? grid->panel->figure : NULL); return DVZ_OK; } diff --git a/render_3D/third_party/datoviz/src/scene/core/scene.c b/render_3D/third_party/datoviz/src/scene/core/scene.c index 565f8db..ed30864 100644 --- a/render_3D/third_party/datoviz/src/scene/core/scene.c +++ b/render_3D/third_party/datoviz/src/scene/core/scene.c @@ -556,7 +556,7 @@ DvzResult dvz_figure_resize(DvzFigure* figure, uint32_t width, uint32_t height) dvz_camera_resize(panel->camera, panel_width, panel_height); } } - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); return DVZ_OK; } @@ -596,7 +596,7 @@ DvzResult dvz_figure_set_reserve(DvzFigure* figure, const DvzPanelReserve* reser figure->reserve = next; (void)_scene_figure_resolve_layouts(figure); (void)_scene_figure_resolve_panel_descs(figure); - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); return DVZ_OK; } @@ -666,7 +666,7 @@ DvzResult dvz_figure_set_color_pipeline(DvzFigure* figure, DvzColorPipeline pipe if (figure->color_pipeline == pipeline) return DVZ_OK; figure->color_pipeline = pipeline; - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); return DVZ_OK; } @@ -883,7 +883,7 @@ DvzResult dvz_panel_set_edl(DvzPanel* panel, const DvzEdlDesc* desc) ANN(panel); bool ok = _scene_technique_state_set_edl(&panel->techniques, desc); if (ok) - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return ok ? DVZ_OK : DVZ_ERROR; } @@ -900,7 +900,7 @@ DvzResult dvz_panel_set_msaa(DvzPanel* panel, const DvzMsaaDesc* desc) ANN(panel); bool ok = _scene_technique_state_set_msaa(&panel->techniques, desc); if (ok) - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return ok ? DVZ_OK : DVZ_ERROR; } @@ -917,7 +917,7 @@ DvzResult dvz_panel_set_ssao(DvzPanel* panel, const DvzSsaoDesc* desc) ANN(panel); bool ok = _scene_technique_state_set_ssao(&panel->techniques, desc); if (ok) - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return ok ? DVZ_OK : DVZ_ERROR; } @@ -940,7 +940,7 @@ DvzResult dvz_panel_set_scene_occlusion(DvzPanel* panel, const DvzSceneOcclusion dvz_memset( &panel->scene_occlusion, sizeof(DvzSceneOcclusionDesc), 0, sizeof(DvzSceneOcclusionDesc)); - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return 0; } @@ -953,7 +953,7 @@ DvzResult dvz_panel_set_scene_occlusion(DvzPanel* panel, const DvzSceneOcclusion if (panel->scene_occlusion.hidden_alpha > 1.0f) panel->scene_occlusion.hidden_alpha = 1.0f; panel->scene_occlusion_enabled = true; - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return 0; } @@ -979,7 +979,7 @@ DvzResult dvz_panel_set_volume_occluder( dvz_memset( &panel->volume_occlusion, sizeof(DvzVolumeOcclusionDesc), 0, sizeof(DvzVolumeOcclusionDesc)); - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return 0; } if (volume->type != DVZ_VISUAL_TYPE_VOLUME) @@ -1007,7 +1007,7 @@ DvzResult dvz_panel_set_volume_occluder( if (panel->volume_occlusion.occluded_alpha <= 0.0f) panel->volume_occlusion.occluded_alpha = 0.20f; panel->volume_occlusion_enabled = true; - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return 0; } diff --git a/render_3D/third_party/datoviz/src/scene/core/scene_notify.c b/render_3D/third_party/datoviz/src/scene/core/scene_notify.c index 747ea7f..e6b46bd 100644 --- a/render_3D/third_party/datoviz/src/scene/core/scene_notify.c +++ b/render_3D/third_party/datoviz/src/scene/core/scene_notify.c @@ -35,16 +35,16 @@ * @param user_data opaque pointer forwarded to the callback * @return true on success, false when the subscription table is full or input is invalid */ -bool _scene_add_request_frame_callback( - DvzScene* scene, DvzSceneRequestFrameCallback callback, void* user_data) +bool _scene_add_invalidation_callback( + DvzScene* scene, DvzSceneInvalidationCallback callback, void* user_data) { if (scene == NULL || callback == NULL) return false; - DvzSceneRequestFrameSubscription* free_slot = NULL; - for (uint32_t i = 0; i < DVZ_SCENE_MAX_REQUEST_FRAME_SUBSCRIPTIONS; i++) + DvzSceneInvalidationSubscription* free_slot = NULL; + for (uint32_t i = 0; i < DVZ_SCENE_MAX_INVALIDATION_SUBSCRIPTIONS; i++) { - DvzSceneRequestFrameSubscription* sub = &scene->request_frame_subscriptions[i]; + DvzSceneInvalidationSubscription* sub = &scene->invalidation_subscriptions[i]; if (sub->active && sub->callback == callback && sub->user_data == user_data) return true; if (!sub->active && free_slot == NULL) @@ -71,19 +71,19 @@ bool _scene_add_request_frame_callback( * @param callback callback pointer * @param user_data opaque pointer previously registered with the callback */ -void _scene_remove_request_frame_callback( - DvzScene* scene, DvzSceneRequestFrameCallback callback, void* user_data) +void _scene_remove_invalidation_callback( + DvzScene* scene, DvzSceneInvalidationCallback callback, void* user_data) { if (scene == NULL || callback == NULL) return; - for (uint32_t i = 0; i < DVZ_SCENE_MAX_REQUEST_FRAME_SUBSCRIPTIONS; i++) + for (uint32_t i = 0; i < DVZ_SCENE_MAX_INVALIDATION_SUBSCRIPTIONS; i++) { - DvzSceneRequestFrameSubscription* sub = &scene->request_frame_subscriptions[i]; + DvzSceneInvalidationSubscription* sub = &scene->invalidation_subscriptions[i]; if (sub->active && sub->callback == callback && sub->user_data == user_data) { - dvz_memset(sub, sizeof(DvzSceneRequestFrameSubscription), 0, - sizeof(DvzSceneRequestFrameSubscription)); + dvz_memset(sub, sizeof(DvzSceneInvalidationSubscription), 0, + sizeof(DvzSceneInvalidationSubscription)); return; } } @@ -95,16 +95,16 @@ void _scene_remove_request_frame_callback( * * @param figure figure requesting a frame */ -void _scene_notify_request_frame(DvzFigure* figure) +void _scene_notify_invalidated(DvzFigure* figure) { if (figure == NULL || figure->scene == NULL) return; figure->frame_revision = figure->frame_revision == UINT64_MAX ? 1 : figure->frame_revision + 1; DvzScene* scene = figure->scene; - for (uint32_t i = 0; i < DVZ_SCENE_MAX_REQUEST_FRAME_SUBSCRIPTIONS; i++) + for (uint32_t i = 0; i < DVZ_SCENE_MAX_INVALIDATION_SUBSCRIPTIONS; i++) { - const DvzSceneRequestFrameSubscription* sub = &scene->request_frame_subscriptions[i]; - DvzSceneRequestFrameCallback callback = sub->callback; + const DvzSceneInvalidationSubscription* sub = &scene->invalidation_subscriptions[i]; + DvzSceneInvalidationCallback callback = sub->callback; void* user_data = sub->user_data; if (sub->active && callback != NULL) callback(figure, user_data); @@ -139,7 +139,7 @@ void _scene_notify_visual_changed(DvzVisual* visual) } } if (contains_visual) - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); } } @@ -207,6 +207,6 @@ void _scene_notify_buffer_changed(DvzSceneBuffer* buffer) } } if (uses_buffer) - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); } } diff --git a/render_3D/third_party/datoviz/src/scene/core/scene_notify_internal.h b/render_3D/third_party/datoviz/src/scene/core/scene_notify_internal.h index 8508e97..1d2ab93 100644 --- a/render_3D/third_party/datoviz/src/scene/core/scene_notify_internal.h +++ b/render_3D/third_party/datoviz/src/scene/core/scene_notify_internal.h @@ -12,13 +12,13 @@ #include "_scene.h" -bool _scene_add_request_frame_callback( - DvzScene* scene, DvzSceneRequestFrameCallback callback, void* user_data); +bool _scene_add_invalidation_callback( + DvzScene* scene, DvzSceneInvalidationCallback callback, void* user_data); -void _scene_remove_request_frame_callback( - DvzScene* scene, DvzSceneRequestFrameCallback callback, void* user_data); +void _scene_remove_invalidation_callback( + DvzScene* scene, DvzSceneInvalidationCallback callback, void* user_data); -void _scene_notify_request_frame(DvzFigure* figure); +void _scene_notify_invalidated(DvzFigure* figure); void _scene_notify_visual_changed(DvzVisual* visual); diff --git a/render_3D/third_party/datoviz/src/scene/domain/compute.c b/render_3D/third_party/datoviz/src/scene/domain/compute.c index 614107f..f720c4f 100644 --- a/render_3D/third_party/datoviz/src/scene/domain/compute.c +++ b/render_3D/third_party/datoviz/src/scene/domain/compute.c @@ -45,7 +45,7 @@ static void _scene_compute_notify(DvzSceneCompute* compute) { if (figure->computes[j] == compute) { - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); break; } } @@ -307,7 +307,7 @@ DvzResult dvz_figure_add_compute(DvzFigure* figure, DvzSceneCompute* compute) return DVZ_ERROR; } figure->computes[figure->compute_count++] = compute; - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); return DVZ_OK; } @@ -323,7 +323,7 @@ DvzResult dvz_figure_remove_compute(DvzFigure* figure, DvzSceneCompute* compute) for (uint32_t j = i + 1; j < figure->compute_count; j++) figure->computes[j - 1] = figure->computes[j]; figure->compute_count--; - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); return DVZ_OK; } return DVZ_OK; diff --git a/render_3D/third_party/datoviz/src/scene/domain/polygon_composite.c b/render_3D/third_party/datoviz/src/scene/domain/polygon_composite.c index 5509e80..17ebf85 100644 --- a/render_3D/third_party/datoviz/src/scene/domain/polygon_composite.c +++ b/render_3D/third_party/datoviz/src/scene/domain/polygon_composite.c @@ -911,7 +911,7 @@ DvzResult dvz_panel_add_composite( slot->insertion_index = panel->visual_count; panel->visual_count++; } - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return 0; } diff --git a/render_3D/third_party/datoviz/src/scene/interaction/core.c b/render_3D/third_party/datoviz/src/scene/interaction/core.c index a8cfbe6..2f3501a 100644 --- a/render_3D/third_party/datoviz/src/scene/interaction/core.c +++ b/render_3D/third_party/datoviz/src/scene/interaction/core.c @@ -891,7 +891,7 @@ static void _selection_card_hide(DvzSelection* selection) selection->card_query = (DvzQueryResult){0}; selection->card.text[0] = '\0'; selection->card.dirty = true; - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); } @@ -923,7 +923,7 @@ static void _selection_card_update_from_query(DvzSelection* selection, const Dvz selection->card.anchor_px[1] = (float)query->panel_position[1]; _selection_card_refresh_text(selection, query); selection->card.dirty = true; - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); } @@ -2711,7 +2711,7 @@ DvzOverlayCard* dvz_overlay_card(DvzOverlay* overlay, const DvzOverlayCardDesc* card->card.offset_px[1] = resolved.offset_px[1]; card->card.visible = (card->flags & DVZ_OVERLAY_CARD_HIDDEN) == 0; card->card.dirty = true; - _scene_notify_request_frame(overlay->panel->figure); + _scene_notify_invalidated(overlay->panel->figure); return card; } @@ -2739,7 +2739,7 @@ void dvz_overlay_card_destroy(DvzOverlayCard* card) card->card.dirty = false; card->rich_enabled = false; card->rich_dirty = false; - _scene_notify_request_frame(figure); + _scene_notify_invalidated(figure); } @@ -2760,7 +2760,7 @@ DvzResult dvz_overlay_card_set_style(DvzOverlayCard* card, const DvzOverlayCardS DvzOverlayCardStyle resolved = style != NULL ? *style : dvz_overlay_card_style(); if (_scene_card_apply_style(&card->card, &resolved) != 0) return -1; - _scene_notify_request_frame(card->panel != NULL ? card->panel->figure : NULL); + _scene_notify_invalidated(card->panel != NULL ? card->panel->figure : NULL); return 0; } @@ -2788,7 +2788,7 @@ DvzResult dvz_overlay_card_set_text(DvzOverlayCard* card, const char* text) else card->card.text[0] = '\0'; card->card.dirty = true; - _scene_notify_request_frame(card->panel != NULL ? card->panel->figure : NULL); + _scene_notify_invalidated(card->panel != NULL ? card->panel->figure : NULL); return DVZ_OK; } @@ -2837,7 +2837,7 @@ DvzResult dvz_overlay_card_set_rich_text(DvzOverlayCard* card, const DvzOverlayR card->rich_dirty = true; card->card.content = DVZ_SCENE_CARD_CONTENT_IMAGE; card->card.dirty = true; - _scene_notify_request_frame(card->panel != NULL ? card->panel->figure : NULL); + _scene_notify_invalidated(card->panel != NULL ? card->panel->figure : NULL); return 0; } @@ -2859,7 +2859,7 @@ DvzResult dvz_overlay_card_clear_rich_text(DvzOverlayCard* card) card->card.content_size_px[0] = 0.0f; card->card.content_size_px[1] = 0.0f; card->card.dirty = true; - _scene_notify_request_frame(card->panel != NULL ? card->panel->figure : NULL); + _scene_notify_invalidated(card->panel != NULL ? card->panel->figure : NULL); return DVZ_OK; } @@ -2889,7 +2889,7 @@ DvzResult dvz_overlay_card_set_layout( card->card.offset_px[1] = offset_px[1]; } card->card.dirty = true; - _scene_notify_request_frame(card->panel != NULL ? card->panel->figure : NULL); + _scene_notify_invalidated(card->panel != NULL ? card->panel->figure : NULL); return DVZ_OK; } @@ -2914,7 +2914,7 @@ DvzResult dvz_overlay_card_set_placement( card->card.offset_px[1] = offset_px[1]; } card->card.dirty = true; - _scene_notify_request_frame(card->panel != NULL ? card->panel->figure : NULL); + _scene_notify_invalidated(card->panel != NULL ? card->panel->figure : NULL); return DVZ_OK; } @@ -2937,7 +2937,7 @@ DvzResult dvz_overlay_card_set_visible(DvzOverlayCard* card, bool visible) _scene_card_hide(&card->card); _overlay_card_hide_rich(card); } - _scene_notify_request_frame(card->panel != NULL ? card->panel->figure : NULL); + _scene_notify_invalidated(card->panel != NULL ? card->panel->figure : NULL); return DVZ_OK; } @@ -2982,7 +2982,7 @@ DvzPinnedReadout* dvz_pinned_readout_query(DvzPanel* panel, const DvzQueryResult readout->card.anchor_px[0] = (float)query->panel_position[0]; readout->card.anchor_px[1] = (float)query->panel_position[1]; panel->pinned_readouts[panel->pinned_readout_count++] = readout; - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return readout; } @@ -3044,6 +3044,6 @@ DvzResult dvz_pinned_readout_set_format(DvzPinnedReadout* readout, const DvzForm _readout_refresh_text(readout); dvz_strlcpy(readout->card.text, readout->text, sizeof(readout->card.text)); readout->card.dirty = true; - _scene_notify_request_frame(readout->panel != NULL ? readout->panel->figure : NULL); + _scene_notify_invalidated(readout->panel != NULL ? readout->panel->figure : NULL); return DVZ_OK; } diff --git a/render_3D/third_party/datoviz/src/scene/plot/band.c b/render_3D/third_party/datoviz/src/scene/plot/band.c index 1a059d1..e31ee78 100644 --- a/render_3D/third_party/datoviz/src/scene/plot/band.c +++ b/render_3D/third_party/datoviz/src/scene/plot/band.c @@ -535,7 +535,7 @@ DvzBand* dvz_band(DvzPanel* panel, const DvzBandDesc* desc) _band_attach_visual(panel, line, resolved.z_layer + 1); if (bounds != NULL) _band_attach_visual(panel, bounds, resolved.z_layer + 2); - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return band; } @@ -574,7 +574,7 @@ DvzResult dvz_band_set_bounds( band->count = count; band->dirty = true; band->version++; - _scene_notify_request_frame(band->panel != NULL ? band->panel->figure : NULL); + _scene_notify_invalidated(band->panel != NULL ? band->panel->figure : NULL); return 0; } @@ -608,7 +608,7 @@ DvzResult dvz_band_set_center(DvzBand* band, const double* x, const double* y, u band->has_center = count > 0; band->dirty = true; band->version++; - _scene_notify_request_frame(band->panel != NULL ? band->panel->figure : NULL); + _scene_notify_invalidated(band->panel != NULL ? band->panel->figure : NULL); return 0; } @@ -661,7 +661,7 @@ DvzResult dvz_band_set_style(DvzBand* band, const DvzBandDesc* desc) band->desc = resolved; band->dirty = true; band->version++; - _scene_notify_request_frame(band->panel != NULL ? band->panel->figure : NULL); + _scene_notify_invalidated(band->panel != NULL ? band->panel->figure : NULL); return 0; } diff --git a/render_3D/third_party/datoviz/src/scene/plot/bars.c b/render_3D/third_party/datoviz/src/scene/plot/bars.c index 00bb210..68bca83 100644 --- a/render_3D/third_party/datoviz/src/scene/plot/bars.c +++ b/render_3D/third_party/datoviz/src/scene/plot/bars.c @@ -416,7 +416,7 @@ DvzBars* dvz_bars(DvzPanel* panel, const DvzBarsDesc* desc) _bars_attach_visual(panel, fill, resolved.z_layer); if (outline != NULL) _bars_attach_visual(panel, outline, resolved.z_layer + 1); - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return bars; } @@ -460,7 +460,7 @@ DvzResult dvz_bars_set_intervals( bars->count = count; bars->dirty = true; bars->version++; - _scene_notify_request_frame(bars->panel != NULL ? bars->panel->figure : NULL); + _scene_notify_invalidated(bars->panel != NULL ? bars->panel->figure : NULL); return 0; } @@ -495,7 +495,7 @@ DvzResult dvz_bars_set_style(DvzBars* bars, const DvzBarsDesc* desc) bars->desc = resolved; bars->dirty = true; bars->version++; - _scene_notify_request_frame(bars->panel != NULL ? bars->panel->figure : NULL); + _scene_notify_invalidated(bars->panel != NULL ? bars->panel->figure : NULL); return 0; } diff --git a/render_3D/third_party/datoviz/src/scene/query/queue.c b/render_3D/third_party/datoviz/src/scene/query/queue.c index 55cfec6..33b7124 100644 --- a/render_3D/third_party/datoviz/src/scene/query/queue.c +++ b/render_3D/third_party/datoviz/src/scene/query/queue.c @@ -290,7 +290,7 @@ DvzResult dvz_panel_query_px(DvzPanel* panel, double x, double y, const DvzQuery pending->freshness_serial = _scene_next_request_serial(scene); pending->request = local; _query_track_request_serial(scene, panel, local.request_id, pending->freshness_serial); - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return 0; } diff --git a/render_3D/third_party/datoviz/src/scene/tests/panzoom_arcball.c b/render_3D/third_party/datoviz/src/scene/tests/panzoom_arcball.c index af5d7f7..e728c86 100644 --- a/render_3D/third_party/datoviz/src/scene/tests/panzoom_arcball.c +++ b/render_3D/third_party/datoviz/src/scene/tests/panzoom_arcball.c @@ -1718,7 +1718,7 @@ int test_controller_destroy_detaches_panels_links_and_reuses_slot( AT(link->active); ControllerDestroyRequestProbe probe = {0}; - AT(_scene_add_request_frame_callback( + AT(_scene_add_invalidation_callback( scene, _controller_destroy_request_frame_callback, &probe)); dvz_controller_destroy(source); @@ -1745,7 +1745,7 @@ int test_controller_destroy_detaches_panels_links_and_reuses_slot( scene, reused, target, DVZ_CONTROLLER_LINK_ROTATION, DVZ_CONTROLLER_LINK_ONE_WAY); AT(reused_link == link); - _scene_remove_request_frame_callback(scene, _controller_destroy_request_frame_callback, &probe); + _scene_remove_invalidation_callback(scene, _controller_destroy_request_frame_callback, &probe); dvz_scene_destroy(scene); return 0; } diff --git a/render_3D/third_party/datoviz/src/scene/tests/scene_interaction_graph.c b/render_3D/third_party/datoviz/src/scene/tests/scene_interaction_graph.c index f6a2feb..b6d8868 100644 --- a/render_3D/third_party/datoviz/src/scene/tests/scene_interaction_graph.c +++ b/render_3D/third_party/datoviz/src/scene/tests/scene_interaction_graph.c @@ -579,7 +579,7 @@ int test_scene_grid_destroy_detaches_panels_and_reuses_slot(TstContext* suite, c AT(grid->panel_count == 2); GridDestroyRequestProbe probe = {0}; - AT(_scene_add_request_frame_callback(scene, _grid_destroy_request_frame_callback, &probe)); + AT(_scene_add_invalidation_callback(scene, _grid_destroy_request_frame_callback, &probe)); dvz_grid_destroy(grid); AT(probe.calls == 1); @@ -598,7 +598,7 @@ int test_scene_grid_destroy_detaches_panels_and_reuses_slot(TstContext* suite, c AT(reused->cols == 1); AT(reused->panel_count == 0); - _scene_remove_request_frame_callback(scene, _grid_destroy_request_frame_callback, &probe); + _scene_remove_invalidation_callback(scene, _grid_destroy_request_frame_callback, &probe); dvz_scene_destroy(scene); return 0; } @@ -901,7 +901,7 @@ int test_scene_figure_destroy_cascades_and_reuses_slot(TstContext* suite, const AT(figure->compute_count == 1); GridDestroyRequestProbe probe = {0}; - AT(_scene_add_request_frame_callback(scene, _grid_destroy_request_frame_callback, &probe)); + AT(_scene_add_invalidation_callback(scene, _grid_destroy_request_frame_callback, &probe)); dvz_figure_destroy(figure); AT(probe.calls == 0); @@ -931,7 +931,7 @@ int test_scene_figure_destroy_cascades_and_reuses_slot(TstContext* suite, const AT(dvz_panel_bind_controller(reused_panel, controller, DVZ_DIM_MASK_XY) == 0); AT(dvz_figure_add_compute(reused, compute) == DVZ_OK); - _scene_remove_request_frame_callback(scene, _grid_destroy_request_frame_callback, &probe); + _scene_remove_invalidation_callback(scene, _grid_destroy_request_frame_callback, &probe); dvz_scene_destroy(scene); return 0; } diff --git a/render_3D/third_party/datoviz/src/scene/visuals/bounds.c b/render_3D/third_party/datoviz/src/scene/visuals/bounds.c index 15e953d..2538c11 100644 --- a/render_3D/third_party/datoviz/src/scene/visuals/bounds.c +++ b/render_3D/third_party/datoviz/src/scene/visuals/bounds.c @@ -991,7 +991,7 @@ DvzResult dvz_panel_set_bounds_visible(DvzPanel* panel, bool visible) dvz_visual_set_visible(panel->bounds_visual, visible); if (panel->bounds_occluded_visual != NULL) dvz_visual_set_visible(panel->bounds_occluded_visual, visible); - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return 0; } diff --git a/render_3D/third_party/datoviz/src/scene/visuals/families.c b/render_3D/third_party/datoviz/src/scene/visuals/families.c index b0dc9c4..2e311c3 100644 --- a/render_3D/third_party/datoviz/src/scene/visuals/families.c +++ b/render_3D/third_party/datoviz/src/scene/visuals/families.c @@ -717,7 +717,7 @@ static void _panel_background_detach(DvzPanel* panel) panel->background_type = DVZ_PANEL_BACKGROUND_NONE; panel->background = dvz_panel_background_desc(); if (panel->figure != NULL) - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); } @@ -751,7 +751,7 @@ static void _panel_border_detach(DvzPanel* panel) panel->border_visual = NULL; panel->border.visible = false; if (panel->figure != NULL) - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); } @@ -960,7 +960,7 @@ DvzResult dvz_panel_add_visual(DvzPanel* panel, DvzVisual* visual, const DvzVisu slot->viewport_rect = resolved.viewport_rect; slot->insertion_index = panel->visual_count; panel->visual_count++; - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return 0; } @@ -1279,7 +1279,7 @@ DvzResult dvz_panel_set_border(DvzPanel* panel, const DvzPanelBorderDesc* border panel->border = *border; dvz_visual_set_visible(panel->border_visual, true); - _scene_notify_request_frame(panel->figure); + _scene_notify_invalidated(panel->figure); return DVZ_OK; } diff --git a/web_server/src/Graph_Metadata.cpp b/web_server/src/Graph_Metadata.cpp new file mode 100644 index 0000000..b408eef --- /dev/null +++ b/web_server/src/Graph_Metadata.cpp @@ -0,0 +1,43 @@ +#include "Graph_Metadata.hpp" +#include +namespace { +struct State_Field_Metadata { + template auto operator()(Field field) const { + if constexpr (Index == 0) return field.label("Frame sequence").description("Latest completed frame sent by this graph."); + if constexpr (Index == 1) return field.label("Input elements").description("Primary published element count read from the graph State."); + return field.label("Rendered elements").description("Prepared render element count read from the graph State."); + } +}; +} +namespace adminive { +auto Type_Descriptor::get() { return reflected_object_with("graph_state", "Published graph state", State_Field_Metadata{}); } +} +namespace aethera::web { +const std::vector& graph_catalog() { + static const std::vector value{ + {"spectrum", "Spectrum", "Curves", "Current, maximum and minimum spectrum curves with markers.", false}, + {"frequency_trace", "Frequency trace", "Curves", "Time ordered frequency samples rendered as a partitioned curve.", false}, + {"sweep_spectrum", "Sweep spectrum", "Curves", "Incremental sweep blocks composed into one frequency curve.", false}, + {"afterglow", "Afterglow", "Raster", "Persistent spectrum energy rendered as reusable color blocks.", false}, + {"waterfall", "Waterfall", "Raster", "Time ordered spectrum rows rendered as a color raster.", false}, + {"constellation", "Constellation", "Signals", "I/Q samples and modulation anchors.", false}, + {"selection_overlay", "Selection overlay", "Interaction", "Direct-paint selection rectangle over numeric axes.", false}, + {"datoviz_point", "Datoviz point", "3D", "Asynchronous Vulkan point visual with GPU readback.", true} + }; + return value; +} +const Graph_Descriptor* find_graph(std::string_view graph_id) { const auto& catalog = graph_catalog(); const auto found = std::ranges::find(catalog, graph_id, &Graph_Descriptor::id); return found == catalog.end() ? nullptr : &*found; } +nlohmann::json graph_catalog_json() { nlohmann::json result = nlohmann::json::array(); for (const auto& graph : graph_catalog()) result.push_back({{"id", graph.id}, {"title", graph.title}, {"category", graph.category}, {"description", graph.description}, {"dimension", graph.three_dimensional ? "3D" : "2D"}, {"websocket", "/ws/graphs/" + graph.id}, {"state", "/api/graphs/" + graph.id + "/state"}, {"descriptor", "/api/graphs/" + graph.id + "/descriptor"}}); return result; } +nlohmann::json graph_state_descriptor_json() { return adminive::to_descriptor_json(); } +nlohmann::json graph_state_json(const Graph_State_Document& state) { return adminive::model_to_json(state, true); } +nlohmann::json graph_prop_descriptor_json(std::string_view graph_id) { + using namespace render_2d; + if (graph_id == "spectrum") return adminive::to_descriptor_json(); + if (graph_id == "frequency_trace") return adminive::to_descriptor_json(); + if (graph_id == "sweep_spectrum") return adminive::to_descriptor_json(); + if (graph_id == "afterglow") return adminive::to_descriptor_json(); + if (graph_id == "waterfall") return adminive::to_descriptor_json(); + if (graph_id == "constellation") return adminive::to_descriptor_json(); + return {{"fields", nlohmann::json::array()}}; +} +} diff --git a/web_server/src/Graph_Metadata.hpp b/web_server/src/Graph_Metadata.hpp new file mode 100644 index 0000000..4d1b08a --- /dev/null +++ b/web_server/src/Graph_Metadata.hpp @@ -0,0 +1,50 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace aethera::web { +namespace detail { +struct Editable_Prop_Field_Metadata { + template auto operator()(Field field) const { return field.editable(); } +}; +} +struct Graph_Descriptor { + std::string id{}; /* HTTP 与 WebSocket 使用的稳定图标识。 */ + std::string title{}; /* Gallery 卡片显示名称。 */ + std::string category{}; /* Gallery 分类筛选值。 */ + std::string description{}; /* 图用途和数据语义说明。 */ + bool three_dimensional{}; /* 是否使用 Datoviz 3D 后端。 */ +}; +struct Graph_State_Document { + std::uint64_t frame_sequence{}; /* 最近完成并发送的帧序号。 */ + std::uint64_t primary_count{}; /* 图自身发布的主要输入元素数量。 */ + std::uint64_t rendered_count{}; /* 最近 Prepare 发布的绘制元素数量。 */ +}; +[[nodiscard]] const std::vector& graph_catalog(); +[[nodiscard]] const Graph_Descriptor* find_graph(std::string_view graph_id); +[[nodiscard]] nlohmann::json graph_catalog_json(); +[[nodiscard]] nlohmann::json graph_state_descriptor_json(); +[[nodiscard]] nlohmann::json graph_state_json(const Graph_State_Document& state); +[[nodiscard]] nlohmann::json graph_prop_descriptor_json(std::string_view graph_id); +} +namespace adminive { +template <> struct Reflection_Adapter : Boost_Pfr_Reflection_Adapter {}; +template <> struct Type_Descriptor { static auto get(); }; +#define AETHERA_DECLARE_PROP_REFLECTION(Type, Id, Label) \ +template <> struct Reflection_Adapter : Boost_Pfr_Reflection_Adapter {}; \ +template <> struct Type_Descriptor { static auto get() { return reflected_object_with(Id, Label, aethera::web::detail::Editable_Prop_Field_Metadata{}); } }; +AETHERA_DECLARE_PROP_REFLECTION(Spectrum_Editable_Prop_Data, "spectrum_prop", "Spectrum properties") +AETHERA_DECLARE_PROP_REFLECTION(Frequency_Trace_Editable_Prop_Data, "frequency_trace_prop", "Frequency trace properties") +AETHERA_DECLARE_PROP_REFLECTION(Sweep_Spectrum_Editable_Prop_Data, "sweep_spectrum_prop", "Sweep spectrum properties") +AETHERA_DECLARE_PROP_REFLECTION(Afterglow_Editable_Prop_Data, "afterglow_prop", "Afterglow properties") +AETHERA_DECLARE_PROP_REFLECTION(Waterfall_Editable_Prop_Data, "waterfall_prop", "Waterfall properties") +AETHERA_DECLARE_PROP_REFLECTION(Constellation_Diagram_Editable_Prop_Data, "constellation_prop", "Constellation properties") +#undef AETHERA_DECLARE_PROP_REFLECTION +} diff --git a/web_server/src/Graph_Session.cpp b/web_server/src/Graph_Session.cpp new file mode 100644 index 0000000..ea24810 --- /dev/null +++ b/web_server/src/Graph_Session.cpp @@ -0,0 +1,122 @@ +#include "Graph_Session.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace aethera::web { +namespace { +using namespace render_2d; +using namespace render_3d; +using Scene_2D = Impl; +using Frequency_Axis_Object = Impl; +using Numeric_Axis_Object = Impl; +using Time_Axis_Object = Impl; +template +std::unique_ptr build(Arguments&&... arguments) { typename Object::Builder builder(std::forward(arguments)...); auto result = builder.build(); if (!result) throw std::logic_error("gallery graph dependency graph is invalid"); return std::move(result).value(); } +template +void configure_axis(Axis* axis, Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, Size canvas) { axis->template set<&Abs_Axis::Prop::orientation>(orientation); axis->template set<&Abs_Axis::Prop::position>(position); axis->template set<&Abs_Axis::Prop::pixel_length>(length); axis->template set<&Abs_Axis::Prop::canvas_size>(canvas); } +template +void append_binary(std::string& output, Integer value) { const auto start = output.size(); output.resize(start + sizeof(Integer)); std::memcpy(output.data() + start, &value, sizeof(Integer)); } +std::string encode_frame(Image_View image, std::uint64_t sequence) { std::string output; output.reserve(24 + static_cast(image.width) * image.height * 4); append_binary(output, std::uint32_t{0x41544852}); append_binary(output, std::uint16_t{1}); append_binary(output, std::uint16_t{}); append_binary(output, static_cast(image.width)); append_binary(output, static_cast(image.height)); append_binary(output, sequence); for (int y = 0; y < image.height; ++y) { const auto* row = reinterpret_cast(image.data + static_cast(y) * image.stride); for (int x = 0; x < image.width; ++x) { const auto* pixel = row + x * 4; output.push_back(static_cast(pixel[2])); output.push_back(static_cast(pixel[1])); output.push_back(static_cast(pixel[0])); output.push_back(static_cast(pixel[3])); } } return output; } +std::string encode_frame(const render_3d::Pixel_Frame& frame, std::uint64_t sequence) { std::string output; output.reserve(24 + frame.rgba8.size()); append_binary(output, std::uint32_t{0x41544852}); append_binary(output, std::uint16_t{1}); append_binary(output, std::uint16_t{}); append_binary(output, frame.extent.width); append_binary(output, frame.extent.height); append_binary(output, sequence); output.append(reinterpret_cast(frame.rgba8.data()), frame.rgba8.size()); return output; } +struct State_Query { Graph_Session::State_Handler handler{}; }; +struct Prop_Query { Graph_Session::Prop_Handler handler{}; }; +struct Prop_Patch { nlohmann::json patch{}; Graph_Session::Prop_Handler handler{}; }; +using Session_Event = std::variant; +struct Plot_2D { + std::unique_ptr scene{}; /* 最终二维 Scene。 */ + std::unique_ptr frequency{}; /* 频率轴;不用时仍为空。 */ + std::unique_ptr horizontal{}; /* 星座图水平数值轴。 */ + std::unique_ptr vertical{}; /* 功率或星座图垂直轴。 */ + std::unique_ptr time{}; /* 时间轴;不用时为空。 */ + std::unique_ptr plot{}; /* 具体 Plottable 的唯一所有权。 */ + std::function update{}; /* 根据浏览器时钟更新权威 Prop。 */ + std::function state{}; /* 从具体 State 即时计算 HTTP 文档。 */ + std::function prop{}; + std::function patch_prop{}; +}; +template +void bind_prop_api(Plot_2D& plot, Object* object) { + plot.prop = [object] { const auto& prop = object->template read_prop(); return adminive::model_to_json(static_cast(prop), true); }; + plot.patch_prop = [object](const nlohmann::json& patch) { Data updated = static_cast(object->template read_prop()); const auto result = adminive::apply_frontend_patch(updated, patch); if (!result.success) return result.to_json(); ([&] { if (object->template get() != updated.*Members) object->template set(updated.*Members); }(), ...); auto output = result.to_json(); output["prop"] = adminive::model_to_json(updated, true); return output; }; +} +Plot_2D make_plot_2d(std::string_view id) { + Plot_2D result; + const Size canvas{720, 420}; + result.scene = build(); + result.scene->set<&Render_Scene_2D::Prop::viewport>(canvas); + result.scene->set<&Render_Scene_2D::Prop::background>(Color{7, 13, 24, 255}); + result.scene->activate_view(); + auto make_frequency = [&] { result.frequency = build(); configure_axis(result.frequency.get(), Axis_Orientation::horizontal, Point_F{64.0, 370.0}, 620.0, canvas); result.frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0}); }; + auto make_vertical = [&](Axis_Range range) { result.vertical = build(); configure_axis(result.vertical.get(), Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas); result.vertical->set<&Numeric_Axis::Prop::coordinate_range>(range); }; + if (id == "spectrum") { + make_frequency(); make_vertical({-110.0, 0.0}); auto object = build>(result.scene.get(), result.frequency.get(), result.vertical.get()); object->set<&Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); object->set<&Spectrum::Prop::max_hold_visible>(true); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); result.update = [raw](double time) { std::array samples{}; for (std::size_t i = 0; i < samples.size(); ++i) { const double x = static_cast(i) / samples.size(); samples[i] = -92.0 + 54.0 * std::exp(-180.0 * std::pow(x - 0.28 - 0.03 * std::sin(time * 0.001), 2.0)) + 42.0 * std::exp(-260.0 * std::pow(x - 0.68, 2.0)) + 2.5 * std::sin(i * 0.31 + time * 0.004); } raw->update_samples(samples); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.sample_count, state.rendered_point_count}; }; result.plot = std::move(object); + } else if (id == "frequency_trace") { + result.time = build(); configure_axis(result.time.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); make_vertical({-1.2, 1.2}); auto object = build>(result.scene.get(), result.time.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); auto tick = std::make_shared(); result.update = [raw, tick](double time) { raw->append_sample((*tick)++, std::sin(time * 0.0025) * 0.8 + std::sin(time * 0.0007) * 0.2); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.sample_count, state.rendered_point_count}; }; result.plot = std::move(object); + } else if (id == "sweep_spectrum") { + make_frequency(); make_vertical({-110.0, 0.0}); auto object = build>(result.scene.get(), result.frequency.get(), result.vertical.get()); object->set<&Sweep_Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); result.update = [raw](double time) { std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -90.0 + 35.0 * std::sin(i * 0.08 + time * 0.002); raw->append_block(values); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.stored_block_count, state.rendered_point_count}; }; result.plot = std::move(object); + } else if (id == "afterglow") { + make_frequency(); make_vertical({-110.0, 0.0}); auto object = build>(result.scene.get(), result.frequency.get(), result.vertical.get()); object->set<&Afterglow::Prop::frequency_range>(Axis_Range{0.0, 100.0}); object->set<&Afterglow::Prop::power_range>(Axis_Range{-110.0, 0.0}); object->set<&Afterglow::Prop::power_point_size>(96); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); result.update = [raw](double time) { std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -95.0 + 62.0 * std::exp(-220.0 * std::pow(static_cast(i) / values.size() - 0.5 - 0.18 * std::sin(time * 0.0008), 2.0)); raw->append_spectrum(values); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.history_count, state.rendered_cell_count}; }; result.plot = std::move(object); + } else if (id == "waterfall") { + make_frequency(); result.time = build(); configure_axis(result.time.get(), Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas); auto object = build>(result.scene.get(), result.frequency.get(), result.time.get()); object->set<&Waterfall::Prop::frequency_range>(Axis_Range{0.0, 100.0}); object->set<&Waterfall::Prop::power_range>(Axis_Range{-110.0, 0.0}); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); auto tick = std::make_shared(); result.update = [raw, tick](double time) { std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow(static_cast(i) / values.size() - 0.5 - 0.22 * std::sin(time * 0.0006), 2.0)); raw->append_row((*tick)++, values); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.row_count, state.rendered_cell_count}; }; result.plot = std::move(object); + } else if (id == "constellation") { + result.horizontal = build(); configure_axis(result.horizontal.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); result.horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-1.2, 1.2}); make_vertical({-1.2, 1.2}); auto object = build>(result.scene.get(), result.horizontal.get(), result.vertical.get()); object->set<&Constellation_Diagram::Prop::i_range>(Axis_Range{-1.2, 1.2}); object->set<&Constellation_Diagram::Prop::q_range>(Axis_Range{-1.2, 1.2}); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); result.update = [raw](double time) { const double phase = time * 0.003; raw->append_point({std::cos(phase) * 0.82 + 0.04 * std::sin(phase * 7.0), std::sin(phase) * 0.82 + 0.04 * std::cos(phase * 5.0)}); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.point_count, state.point_count}; }; result.plot = std::move(object); + } else { + result.horizontal = build(); configure_axis(result.horizontal.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); result.horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0}); make_vertical({0.0, 100.0}); auto object = build>(result.scene.get(), result.horizontal.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); result.prop = [] { return nlohmann::json::object(); }; result.patch_prop = [](const nlohmann::json&) { return nlohmann::json{{"success", true}, {"prop", nlohmann::json::object()}}; }; result.update = [](double) {}; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.paint_task_count, state.paint_executed ? 1U : 0U}; }; result.plot = std::move(object); + } + return result; +} +struct Plot_3D { + std::unique_ptr> visual{}; /* 点图元权威对象。 */ + std::unique_ptr> scene{}; /* 异步 Datoviz Scene。 */ +}; +Plot_3D make_plot_3d() { Plot_3D result; result.visual = build>(); static_cast(result.visual->update_items({render_3d::Point{.position = {-0.55F, -0.2F, 0.0F}, .color = Color::red_color(), .diameter_px = 24.0F}, render_3d::Point{.position = {0.0F, 0.5F, 0.0F}, .color = Color::green_color(), .diameter_px = 30.0F}, render_3d::Point{.position = {0.55F, -0.1F, 0.0F}, .color = Color{42, 120, 255, 255}, .diameter_px = 26.0F}})); result.visual->advance(); result.scene = build>(result.visual.get()); result.scene->activate_view(); return result; } +std::variant make_plot(const Graph_Descriptor& graph) { if (graph.three_dimensional) return std::variant{std::in_place_type, make_plot_3d()}; return std::variant{std::in_place_type, make_plot_2d(graph.id)}; } +} +struct Graph_Session::Private { + asio::strand strand; /* 图对象串行访问域。 */ + asio::experimental::concurrent_channel events; /* 浏览器事件与 HTTP 状态读取队列。 */ + Graph_Descriptor descriptor; /* Catalog 中的不可变图描述。 */ + std::variant plot; /* 具体二维或三维 Scene 所有权。 */ + std::mutex handlers_mutex; /* 保护跨 Drogon 线程的连接集合。 */ + std::unordered_map handlers; /* 当前订阅该图像素帧的 WebSocket。 */ + std::atomic_uint64_t frame_sequence{}; /* 传输协议的完成帧序号。 */ + explicit Private(asio::any_io_executor executor, const Graph_Descriptor& graph) : strand(asio::make_strand(std::move(executor))), events(strand, 32), descriptor(graph), plot(make_plot(graph)) {} + void publish(std::string pixels) { std::vector outputs; { std::lock_guard lock(handlers_mutex); outputs.reserve(handlers.size()); for (const auto& [owner, handler] : handlers) outputs.push_back(handler); } for (auto& output : outputs) output(pixels); } + Graph_State_Document state_document() const { const auto sequence = frame_sequence.load(std::memory_order_acquire); if (const auto* value = std::get_if(&plot)) return value->state(sequence); const auto& value = std::get(plot); const auto& state = value.visual->read_state(); return {sequence, state.item_count, state.prepared_item_count}; } + nlohmann::json prop_document() const { if (const auto* value = std::get_if(&plot)) return value->prop(); return nlohmann::json::object(); } + nlohmann::json patch_prop(const nlohmann::json& patch) { if (auto* value = std::get_if(&plot)) return value->patch_prop(patch); return {{"success", true}, {"prop", nlohmann::json::object()}}; } +}; +Graph_Session::Graph_Session(std::unique_ptr private_data) : d(std::move(private_data)) {} +std::shared_ptr Graph_Session::create(asio::any_io_executor executor, const Graph_Descriptor& descriptor) { auto result = std::shared_ptr(new Graph_Session(std::make_unique(std::move(executor), descriptor))); result->start(); return result; } +Graph_Session::~Graph_Session() { d->events.close(); } +void Graph_Session::start() { auto self = shared_from_this(); if (auto* plot = std::get_if(&d->plot)) plot->scene->set_frame_callback([weak = weak_from_this()](Image_View image) { if (auto owner = weak.lock()) { const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1; owner->d->publish(encode_frame(image, sequence)); } }); else std::get(d->plot).scene->set_frame_callback([weak = weak_from_this()](std::shared_ptr frame) { if (auto owner = weak.lock()) { const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1; owner->d->publish(encode_frame(*frame, sequence)); } }); asio::co_spawn(d->strand, [self]() -> asio::awaitable { for (;;) { asio::error_code error; auto event = co_await self->d->events.async_receive(asio::redirect_error(asio::use_awaitable, error)); if (error) co_return; if (auto* state = std::get_if(&event)) { state->handler(graph_state_json(self->d->state_document())); continue; } if (auto* prop = std::get_if(&event)) { prop->handler(self->d->prop_document()); continue; } if (auto* patch = std::get_if(&event)) { patch->handler(self->d->patch_prop(patch->patch)); continue; } const auto value = std::get(event); if (auto* plot = std::get_if(&self->d->plot)) { const Size viewport{static_cast(std::clamp(value.width, 160U, 1920U)), static_cast(std::clamp(value.height, 120U, 1080U))}; plot->scene->set<&Render_Scene_2D::Prop::viewport>(viewport); if (plot->frequency) plot->frequency->set<&Abs_Axis::Prop::canvas_size>(viewport); if (plot->horizontal) plot->horizontal->set<&Abs_Axis::Prop::canvas_size>(viewport); if (plot->vertical) plot->vertical->set<&Abs_Axis::Prop::canvas_size>(viewport); if (plot->time) plot->time->set<&Abs_Axis::Prop::canvas_size>(viewport); plot->update(value.time_milliseconds); plot->scene->render(); } else { auto& plot_3d = std::get(self->d->plot); plot_3d.scene->set<&Render_Scene_3D::Prop::viewport>(render_3d::Extent{std::clamp(value.width, 160U, 1920U), std::clamp(value.height, 120U, 1080U)}); plot_3d.scene->render(); } } }, [](std::exception_ptr exception) { if (exception) std::rethrow_exception(exception); }); } +void Graph_Session::attach(const void* owner, Frame_Handler handler) { { std::lock_guard lock(d->handlers_mutex); d->handlers.insert_or_assign(owner, std::move(handler)); } submit({}); } +void Graph_Session::detach(const void* owner) { std::lock_guard lock(d->handlers_mutex); d->handlers.erase(owner); } +void Graph_Session::submit(Graph_Event event) { static_cast(d->events.try_send(asio::error_code{}, Session_Event{event})); } +void Graph_Session::async_state(State_Handler handler) { if (!d->events.try_send(asio::error_code{}, Session_Event{State_Query{std::move(handler)}})) throw std::runtime_error("graph state queue is unavailable"); } +void Graph_Session::async_prop(Prop_Handler handler) { if (!d->events.try_send(asio::error_code{}, Session_Event{Prop_Query{std::move(handler)}})) throw std::runtime_error("graph prop queue is unavailable"); } +void Graph_Session::async_patch_prop(nlohmann::json patch, Prop_Handler handler) { if (!d->events.try_send(asio::error_code{}, Session_Event{Prop_Patch{std::move(patch), std::move(handler)}})) throw std::runtime_error("graph prop queue is unavailable"); } +const Graph_Descriptor& Graph_Session::descriptor() const noexcept { return d->descriptor; } +struct Graph_Registry::Private { asio::any_io_executor executor; std::mutex mutex; std::unordered_map> sessions; explicit Private(asio::any_io_executor value) : executor(std::move(value)) {} }; +Graph_Registry::Graph_Registry(asio::any_io_executor executor) : d(std::make_unique(std::move(executor))) {} +Graph_Registry::~Graph_Registry() = default; +std::shared_ptr Graph_Registry::acquire(std::string_view graph_id) { const auto* descriptor = find_graph(graph_id); if (!descriptor) return {}; std::lock_guard lock(d->mutex); auto& session = d->sessions[std::string(graph_id)]; if (!session) session = Graph_Session::create(d->executor, *descriptor); return session; } +} diff --git a/web_server/src/Graph_Session.hpp b/web_server/src/Graph_Session.hpp new file mode 100644 index 0000000..7d369f3 --- /dev/null +++ b/web_server/src/Graph_Session.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "Graph_Metadata.hpp" +#include +#include +#include +#include +#include +namespace aethera::web { +struct Graph_Event { + double time_milliseconds{}; /* 浏览器 performance.now() 时钟值。 */ + std::uint32_t width{720}; /* 浏览器画布当前像素宽度。 */ + std::uint32_t height{420}; /* 浏览器画布当前像素高度。 */ +}; +class Graph_Session final : public std::enable_shared_from_this { +public: + using Frame_Handler = std::function; + using State_Handler = std::function; + using Prop_Handler = std::function; + [[nodiscard]] static std::shared_ptr create(asio::any_io_executor executor, const Graph_Descriptor& descriptor); + ~Graph_Session(); + Graph_Session(const Graph_Session&) = delete; + Graph_Session& operator=(const Graph_Session&) = delete; + void attach(const void* owner, Frame_Handler handler); + void detach(const void* owner); + void submit(Graph_Event event); + void async_state(State_Handler handler); + void async_prop(Prop_Handler handler); + void async_patch_prop(nlohmann::json patch, Prop_Handler handler); + [[nodiscard]] const Graph_Descriptor& descriptor() const noexcept; +private: + struct Private; + explicit Graph_Session(std::unique_ptr private_data); + void start(); + std::unique_ptr d; /* 单一协程事件循环及图对象所有权。 */ +}; +class Graph_Registry final { +public: + explicit Graph_Registry(asio::any_io_executor executor); + ~Graph_Registry(); + [[nodiscard]] std::shared_ptr acquire(std::string_view graph_id); +private: + struct Private; + std::unique_ptr d; /* 图标识到长期 Scene 会话的注册表。 */ +}; +} diff --git a/web_server/src/Graph_WebSocket.cpp b/web_server/src/Graph_WebSocket.cpp new file mode 100644 index 0000000..bc2e597 --- /dev/null +++ b/web_server/src/Graph_WebSocket.cpp @@ -0,0 +1,25 @@ +#include "Graph_WebSocket.hpp" +#include +#include +#include +#include +namespace aethera::web { +namespace { +std::string graph_id_from_path(std::string_view path) { const auto split = path.find_last_of('/'); return split == std::string_view::npos ? std::string{} : std::string(path.substr(split + 1)); } +} +struct Graph_WebSocket::Private { + std::weak_ptr connection; /* 不延长已关闭 Drogon 连接生命周期。 */ + std::shared_ptr session; /* 连接期间保持图会话存活。 */ + const void* owner{}; /* 会话订阅表使用的稳定连接身份。 */ + bool attached{}; /* 是否已安装像素订阅。 */ +}; +Graph_WebSocket::Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr session) : d(std::make_unique(Private{connection, std::move(session), connection.get(), false})) {} +Graph_WebSocket::~Graph_WebSocket() { close(); } +void Graph_WebSocket::start() { if (d->attached) return; const auto weak = weak_from_this(); d->session->attach(d->owner, [weak](std::string pixels) { const auto socket = weak.lock(); if (!socket) return; const auto connection = socket->d->connection.lock(); if (connection && connection->connected()) connection->send(pixels.data(), pixels.size(), drogon::WebSocketMessageType::Binary); }); d->attached = true; } +void Graph_WebSocket::receive(std::string_view message) { const auto json = nlohmann::json::parse(message, nullptr, false); if (json.is_discarded() || !json.is_object()) return; Graph_Event event; if (const auto value = json.find("time"); value != json.end() && value->is_number()) event.time_milliseconds = value->get(); if (const auto value = json.find("width"); value != json.end() && value->is_number_unsigned()) event.width = std::clamp(value->get(), 160U, 1920U); if (const auto value = json.find("height"); value != json.end() && value->is_number_unsigned()) event.height = std::clamp(value->get(), 120U, 1080U); d->session->submit(event); } +void Graph_WebSocket::close() { if (!d->attached) return; d->session->detach(d->owner); d->attached = false; } +Graph_WebSocket_Controller::Graph_WebSocket_Controller(std::shared_ptr value) : registry(std::move(value)) {} +void Graph_WebSocket_Controller::handleNewConnection(const drogon::HttpRequestPtr& request, const drogon::WebSocketConnectionPtr& connection) { auto session = registry->acquire(graph_id_from_path(request->path())); if (!session) { connection->shutdown(drogon::CloseCode::kViolation, "Unknown Aethera graph"); return; } auto socket = std::make_shared(connection, std::move(session)); connection->setContext(socket); connection->setPingMessage("aethera-gallery", std::chrono::seconds(20)); socket->start(); } +void Graph_WebSocket_Controller::handleNewMessage(const drogon::WebSocketConnectionPtr& connection, std::string&& message, const drogon::WebSocketMessageType& type) { if (type != drogon::WebSocketMessageType::Text || message.size() > 64 * 1024) return; if (const auto socket = connection->getContext()) socket->receive(message); } +void Graph_WebSocket_Controller::handleConnectionClosed(const drogon::WebSocketConnectionPtr& connection) { if (const auto socket = connection->getContext()) socket->close(); connection->clearContext(); } +} diff --git a/web_server/src/Graph_WebSocket.hpp b/web_server/src/Graph_WebSocket.hpp new file mode 100644 index 0000000..8230edd --- /dev/null +++ b/web_server/src/Graph_WebSocket.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "Graph_Session.hpp" +#include +#include +namespace aethera::web { +class Graph_WebSocket final : public std::enable_shared_from_this { +public: + Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr session); + ~Graph_WebSocket(); + Graph_WebSocket(const Graph_WebSocket&) = delete; + Graph_WebSocket& operator=(const Graph_WebSocket&) = delete; + void start(); + void receive(std::string_view message); + void close(); +private: + struct Private; + std::unique_ptr d; /* Drogon 连接与图会话绑定。 */ +}; +class Graph_WebSocket_Controller final : public drogon::WebSocketController { +public: + explicit Graph_WebSocket_Controller(std::shared_ptr registry); + void handleNewMessage(const drogon::WebSocketConnectionPtr& connection, std::string&& message, const drogon::WebSocketMessageType& type) override; + void handleNewConnection(const drogon::HttpRequestPtr& request, const drogon::WebSocketConnectionPtr& connection) override; + void handleConnectionClosed(const drogon::WebSocketConnectionPtr& connection) override; + WS_PATH_LIST_BEGIN + WS_ADD_PATH_VIA_REGEX("^/ws/graphs/[^/]+$"); + WS_PATH_LIST_END +private: + std::shared_ptr registry; /* 所有图 WebSocket 共用的会话注册表。 */ +}; +} diff --git a/web_server/src/Web_Server.cpp b/web_server/src/Web_Server.cpp new file mode 100644 index 0000000..bd98723 --- /dev/null +++ b/web_server/src/Web_Server.cpp @@ -0,0 +1,32 @@ +#include "Web_Server.hpp" +#include "Graph_Metadata.hpp" +#include "Graph_Session.hpp" +#include "Graph_WebSocket.hpp" +#include +#include +#include +#include +#include +#include +namespace aethera::web { +namespace { +drogon::HttpResponsePtr json_response(nlohmann::json value) { auto response = drogon::HttpResponse::newHttpResponse(); response->setContentTypeCode(drogon::CT_APPLICATION_JSON); response->setBody(value.dump()); return response; } +drogon::HttpResponsePtr error_response(drogon::HttpStatusCode status, std::string message) { auto response = json_response({{"error", std::move(message)}}); response->setStatusCode(status); return response; } +} +int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) { + const auto hardware_threads = std::max(2U, std::thread::hardware_concurrency()); + auto graph_pool = std::make_shared(std::min(8U, hardware_threads)); + auto registry = std::make_shared(graph_pool->get_executor()); + auto websocket = std::make_shared(registry); + auto& app = drogon::app(); + app.registerHandler("/api/graphs", [](const drogon::HttpRequestPtr&, std::function&& callback) { callback(json_response(graph_catalog_json())); }, {drogon::Get}); + app.registerHandler("/api/graphs/{1}/descriptor", [](const drogon::HttpRequestPtr&, std::function&& callback, std::string graph_id) { if (!find_graph(graph_id)) { callback(error_response(drogon::k404NotFound, "unknown graph")); return; } callback(json_response({{"prop", graph_prop_descriptor_json(graph_id)}, {"state", graph_state_descriptor_json()}, {"state_api", "/api/graphs/" + graph_id + "/state"}, {"prop_api", "/api/graphs/" + graph_id + "/prop"}})); }, {drogon::Get}); + app.registerHandler("/api/graphs/{1}/state", [registry](const drogon::HttpRequestPtr&, std::function&& callback, std::string graph_id) { auto session = registry->acquire(graph_id); if (!session) { callback(error_response(drogon::k404NotFound, "unknown graph")); return; } auto output = std::make_shared>(std::move(callback)); session->async_state([output](nlohmann::json state) { (*output)(json_response(std::move(state))); }); }, {drogon::Get}); + app.registerHandler("/api/graphs/{1}/prop", [registry](const drogon::HttpRequestPtr&, std::function&& callback, std::string graph_id) { auto session = registry->acquire(graph_id); if (!session) { callback(error_response(drogon::k404NotFound, "unknown graph")); return; } auto output = std::make_shared>(std::move(callback)); session->async_prop([output](nlohmann::json prop) { (*output)(json_response(std::move(prop))); }); }, {drogon::Get}); + app.registerHandler("/api/graphs/{1}/prop", [registry](const drogon::HttpRequestPtr& request, std::function&& callback, std::string graph_id) { auto session = registry->acquire(graph_id); if (!session) { callback(error_response(drogon::k404NotFound, "unknown graph")); return; } nlohmann::json patch; try { patch = nlohmann::json::parse(request->body()); } catch (const nlohmann::json::exception&) { callback(error_response(drogon::k400BadRequest, "invalid JSON patch")); return; } auto output = std::make_shared>(std::move(callback)); session->async_patch_prop(std::move(patch), [output](nlohmann::json result) { (*output)(json_response(std::move(result))); }); }, {drogon::Patch}); + app.registerController(websocket).setDocumentRoot(asset_root.string()).setHomePage("index.html").setStaticFileHeaders({{"Cache-Control", "no-store"}}).addListener("127.0.0.1", port).setThreadNum(std::min(8U, hardware_threads)).setIdleConnectionTimeout(90).run(); + graph_pool->stop(); + graph_pool->join(); + return 0; +} +} diff --git a/web_server/src/Web_Server.hpp b/web_server/src/Web_Server.hpp new file mode 100644 index 0000000..97e0bad --- /dev/null +++ b/web_server/src/Web_Server.hpp @@ -0,0 +1,6 @@ +#pragma once +#include +#include +namespace aethera::web { +int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root); +} diff --git a/web_server/src/main.cpp b/web_server/src/main.cpp index 33c14ce..3f6805c 100644 --- a/web_server/src/main.cpp +++ b/web_server/src/main.cpp @@ -1,3 +1,11 @@ -int main() { - return 0; +#include "Web_Server.hpp" +#include +#include +#include +#include +#include +#include +namespace { +std::uint16_t parse_port(int argc, char** argv) { constexpr std::uint16_t fallback = 8848; if (argc == 1) return fallback; if (argc != 3 || std::string_view(argv[1]) != "--port") return 0; unsigned value{}; const std::string_view text(argv[2]); const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); return error == std::errc{} && end == text.data() + text.size() && value > 0 && value <= 65535 ? static_cast(value) : 0; } } +int main(int argc, char** argv) { const auto port = parse_port(argc, argv); if (port == 0) return 2; aethera::initialize_runtime(); const auto executable = std::filesystem::absolute(argv[0]); std::cout << "Aethera Gallery http://127.0.0.1:" << port << std::endl; return aethera::web::run_web_server(port, executable.parent_path() / "webapp_gallery"); } diff --git a/webapp_gallery/index.html b/webapp_gallery/index.html index f016955..368a3d8 100644 --- a/webapp_gallery/index.html +++ b/webapp_gallery/index.html @@ -4,7 +4,7 @@ - Renderive Core2 性能画廊 + Aethera Chart Gallery diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx new file mode 100644 index 0000000..6b36e6b --- /dev/null +++ b/webapp_gallery/src/app.tsx @@ -0,0 +1,12 @@ +import {memo, useEffect, useMemo, useRef, useState} from "react"; +type Graph = {id: string; title: string; category: string; description: string; dimension: "2D" | "3D"; websocket: string; state: string; descriptor: string}; +type Field = {name: string; value_type: string; editable: boolean; presentation: {label: string; description?: string; control: string; options?: Array<{value: string; label: string}>}}; +type Descriptor = {state?: {fields: Field[]}; prop?: {fields: Field[]}; state_api: string; prop_api: string}; +const protocolHeaderSize = 24; +function socketUrl(path: string) { return `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}${path}`; } +function drawPixels(canvas: HTMLCanvasElement, bytes: ArrayBuffer) { if (bytes.byteLength < protocolHeaderSize) return; const view = new DataView(bytes); if (view.getUint32(0, true) !== 0x41544852 || view.getUint16(4, true) !== 1) return; const width = view.getUint32(8, true); const height = view.getUint32(12, true); const pixels = new Uint8ClampedArray(bytes, protocolHeaderSize); if (pixels.byteLength !== width * height * 4) return; if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } canvas.getContext("2d", {alpha: false})?.putImageData(new ImageData(pixels, width, height), 0, 0); } +function useGraphStream(graph: Graph, canvasRef: React.RefObject) { const statusRef = useRef(null); useEffect(() => { let stopped = false; let animation = 0; const socket = new WebSocket(socketUrl(graph.websocket)); socket.binaryType = "arraybuffer"; socket.onopen = () => { if (statusRef.current) statusRef.current.textContent = "LIVE"; }; socket.onclose = () => { if (statusRef.current) statusRef.current.textContent = "OFFLINE"; }; socket.onmessage = event => { if (event.data instanceof ArrayBuffer && canvasRef.current) drawPixels(canvasRef.current, event.data); }; const tick = (time: number) => { const canvas = canvasRef.current; if (!stopped && socket.readyState === WebSocket.OPEN && canvas) socket.send(JSON.stringify({time, width: Math.max(320, canvas.clientWidth * devicePixelRatio), height: Math.max(220, canvas.clientHeight * devicePixelRatio)})); if (!stopped) animation = requestAnimationFrame(tick); }; animation = requestAnimationFrame(tick); return () => { stopped = true; cancelAnimationFrame(animation); socket.close(); }; }, [graph.websocket, canvasRef]); return statusRef; } +function FieldControl({field, value, onChange}: {field: Field; value: unknown; onChange: (value: unknown) => void}) { const control = field.presentation.control === "automatic" ? field.value_type === "boolean" ? "boolean" : field.value_type === "number" || field.value_type === "integer" ? "number" : field.presentation.options ? "select" : "text" : field.presentation.control; if (control === "boolean") return ; if (control === "select" && field.presentation.options) return ; return ; } +function Inspector({graph, descriptor}: {graph: Graph; descriptor: Descriptor}) { const [state, setState] = useState>({}); const [prop, setProp] = useState>({}); useEffect(() => { const requests = [fetch(descriptor.state_api).then(response => response.json()).then(setState)]; if (descriptor.prop_api) requests.push(fetch(descriptor.prop_api).then(response => response.ok ? response.json() : {}).then(setProp)); void Promise.all(requests); }, [descriptor.prop_api, descriptor.state_api]); const update = async (name: string, value: unknown) => { const next = {...prop, [name]: value}; setProp(next); await fetch(descriptor.prop_api, {method: "PATCH", headers: {"Content-Type": "application/json"}, body: JSON.stringify({[name]: value})}); }; return

Editable Prop

{descriptor.prop?.fields.filter(field => field.editable).map(field => void update(field.name, value)}/>) ??

No editable Prop fields published.

}

Published State

{descriptor.state?.fields.map(field =>
{field.presentation.label}
{String(state[field.name] ?? "—")}
)}
; } +const GraphCard = memo(function GraphCard({graph}: {graph: Graph}) { const canvasRef = useRef(null); const statusRef = useGraphStream(graph, canvasRef); const [descriptor, setDescriptor] = useState(null); const [open, setOpen] = useState(false); const inspect = async () => { if (!descriptor) setDescriptor(await fetch(graph.descriptor).then(response => response.json())); setOpen(value => !value); }; return
{graph.category} · {graph.dimension}

{graph.title}

CONNECTING

{graph.description}

{open && descriptor ? : null}
; }); +export function App() { const [graphs, setGraphs] = useState([]); const [category, setCategory] = useState("All"); useEffect(() => { void fetch("/api/graphs").then(response => response.json()).then(setGraphs); }, []); const categories = useMemo(() => ["All", ...new Set(graphs.map(graph => graph.category))], [graphs]); const visible = category === "All" ? graphs : graphs.filter(graph => graph.category === category); return
AETHERA RENDER LAB

Live chart gallery

Browser time drives each isolated Scene. Completed pixels return directly from the Scene callback; properties and state are fetched only when inspected.

{graphs.length}live components
{visible.map(graph => )}
; } diff --git a/webapp_gallery/src/main.tsx b/webapp_gallery/src/main.tsx new file mode 100644 index 0000000..b099816 --- /dev/null +++ b/webapp_gallery/src/main.tsx @@ -0,0 +1,5 @@ +import {StrictMode} from "react"; +import {createRoot} from "react-dom/client"; +import {App} from "./app"; +import "./styles.css"; +createRoot(document.getElementById("root")!).render(); diff --git a/webapp_gallery/src/styles.css b/webapp_gallery/src/styles.css new file mode 100644 index 0000000..db3b81d --- /dev/null +++ b/webapp_gallery/src/styles.css @@ -0,0 +1 @@ +:root { font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #e8f0ff; background: #050912; font-synthesis: none; } * { box-sizing: border-box; } body { margin: 0; min-width: 320px; background: radial-gradient(circle at 16% 0%, #14233e 0, transparent 36rem), #050912; } button, input, select { font: inherit; } main { width: min(1500px, calc(100% - 40px)); margin: 0 auto; padding: 48px 0 72px; } .hero { display: flex; align-items: flex-end; justify-content: space-between; gap: 28px; padding: 24px 4px 38px; border-bottom: 1px solid #24324a; } h1 { max-width: 780px; margin: 8px 0 12px; font-size: clamp(44px, 7vw, 92px); line-height: .92; letter-spacing: -.065em; } .hero p { max-width: 720px; margin: 0; color: #9cafca; font-size: 17px; line-height: 1.65; } .eyebrow { color: #5ce4c2; font-size: 11px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; } .heroMetric { display: grid; min-width: 160px; padding: 20px; border: 1px solid #2b3e5e; border-radius: 18px; background: #0b1422cc; } .heroMetric strong { font-size: 48px; line-height: 1; } .heroMetric span { color: #8fa2bd; margin-top: 6px; } nav { display: flex; flex-wrap: wrap; gap: 9px; padding: 24px 0; } nav button, .inspect { color: #aabbd3; border: 1px solid #2a3b55; background: #0a1220; border-radius: 999px; padding: 9px 15px; cursor: pointer; } nav button:hover, nav button.active, .inspect:hover { color: #07110f; border-color: #5ce4c2; background: #5ce4c2; } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 460px), 1fr)); gap: 18px; align-items: start; } .card { content-visibility: auto; overflow: hidden; border: 1px solid #1f2f48; border-radius: 22px; background: linear-gradient(145deg, #0d1727, #080e19); box-shadow: 0 18px 55px #0007; } .card header { display: flex; justify-content: space-between; gap: 16px; padding: 22px 22px 8px; } h2 { margin: 5px 0 0; font-size: 25px; letter-spacing: -.03em; } .card > p { min-height: 44px; margin: 0; padding: 0 22px 18px; color: #8fa2bd; line-height: 1.5; } .status { align-self: flex-start; padding: 5px 8px; color: #5ce4c2; border: 1px solid #27594f; border-radius: 7px; font: 700 10px/1 ui-monospace, monospace; letter-spacing: .1em; } canvas { display: block; width: 100%; height: 310px; background: #070d18; image-rendering: auto; } .inspect { margin: 16px 20px 20px; } .inspector { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 18px 20px 22px; border-top: 1px solid #20314b; background: #080f1b; } .inspector h4 { margin: 0 0 12px; } .control { display: grid; gap: 5px; margin-bottom: 12px; color: #c8d5e8; } .control input, .control select { width: 100%; padding: 9px 10px; color: #eaf1ff; border: 1px solid #2d405f; border-radius: 8px; background: #0d1828; } .control input[type="checkbox"] { width: 20px; height: 20px; } .control small, .muted { color: #71839e; } dl { display: grid; gap: 8px; margin: 0; } dl div { display: flex; justify-content: space-between; gap: 12px; padding-bottom: 7px; border-bottom: 1px solid #1a2940; } dt { color: #8fa2bd; } dd { margin: 0; font-family: ui-monospace, monospace; } @media (max-width: 720px) { main { width: min(100% - 22px, 1500px); padding-top: 20px; } .hero { align-items: stretch; flex-direction: column; } .heroMetric { min-width: 0; } .inspector { grid-template-columns: 1fr; } canvas { height: 240px; } } diff --git a/webapp_gallery/tsconfig.app.tsbuildinfo b/webapp_gallery/tsconfig.app.tsbuildinfo index b7b2d9a..936babd 100644 --- a/webapp_gallery/tsconfig.app.tsbuildinfo +++ b/webapp_gallery/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/theme.ts","./src/capture/capture_frames.tsx","./src/capture/capture_panel.tsx","./src/capture/capture_sessions.tsx","./src/capture/capture_toolbar.tsx","./src/capture/frame_summary.tsx","./src/capture/node_detail.tsx","./src/capture/perfetto_export.ts","./src/capture/plan_comparison.tsx","./src/capture/worker_timeline.tsx","./src/common/copy_button.tsx","./src/common/dashboard_field_grid.tsx","./src/common/echarts_view.tsx","./src/common/empty_state.tsx","./src/common/json_viewer.tsx","./src/common/metric_grid.tsx","./src/common/metric_value.tsx","./src/common/status_chip.tsx","./src/dag/dag_layout.ts","./src/dag/dag_legend.tsx","./src/dag/dag_model.ts","./src/dag/dag_node.tsx","./src/dag/dag_types.ts","./src/dag/render_dag.tsx","./src/gallery/category_filter.tsx","./src/gallery/frame_mode_tabs.tsx","./src/gallery/gallery_page.tsx","./src/gallery/gallery_summary.tsx","./src/gallery/gallery_toolbar.tsx","./src/gallery/plot_card.tsx","./src/hooks/use_element_size.ts","./src/hooks/use_gallery_catalog.ts","./src/hooks/use_plot_session.ts","./src/inspector/actions_panel.tsx","./src/inspector/control_field.tsx","./src/inspector/controls_panel.tsx","./src/inspector/inspector_panel.tsx","./src/inspector/inspector_tabs.tsx","./src/inspector/observer_panel.tsx","./src/inspector/performance_panel.tsx","./src/plot/kernel_observer_summary.tsx","./src/plot/performance_strip.tsx","./src/plot/plot_canvas.tsx","./src/plot/plot_status.tsx","./src/protocol/format.ts","./src/protocol/gallery_descriptor.ts","./src/protocol/gallery_messages.ts","./src/protocol/gallery_parser.ts","./src/protocol/gallery_types.ts","./src/protocol/pixel_frame.ts","./src/runtime/client_performance.ts","./src/runtime/frame_request_controller.ts","./src/runtime/pixel_presenter.ts","./src/session/gallery_plot_session.ts","./src/session/gallery_plot_snapshot.ts","./src/transport/catalog_loader.ts","./src/transport/gallery_socket.ts","./tests/setup.ts","./tests/components/control_field.test.tsx","./tests/dag/dag_model.test.ts","./tests/e2e/gallery.spec.ts","./tests/protocol/gallery_parser.test.ts","./tests/protocol/pixel_frame.test.ts","./tests/runtime/client_performance.test.ts","./tests/runtime/frame_request_controller.test.ts","./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file