diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index bf1b1f8..dac678b 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -447,6 +447,11 @@ Scene_Edit_Error Scene_Base::Renderable_Editor::clear_dependency_parent( scene_data.model_dirty_.store(true, std::memory_order_release); return Scene_Edit_Error::none; } +void Scene_Base::Renderable_Editor::replace_frame_control_strategy( + std::shared_ptr strategy) { + scene_.d_func().frame_control().replace_frame_control_strategy( + std::move(strategy)); +} Scene_Edit_Error Scene_Base::Edit_Operation::wait() const { if (!result_.valid()) ::renderive::error::unexpected("scene edit operation is invalid"); return result_.get(); diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp index f4f98e0..ea20070 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -80,6 +80,8 @@ public: [[nodiscard]] Scene_Edit_Error set_dependency_parent(const Renderable& child, const Renderable& parent); [[nodiscard]] Scene_Edit_Error add_dependency_parent(const Renderable& child, const Renderable& parent); [[nodiscard]] Scene_Edit_Error clear_dependency_parent(const Renderable& child); + void replace_frame_control_strategy( + std::shared_ptr strategy); [[nodiscard]] Scene_Edit_Error error() const noexcept { return error_; } diff --git a/Kernel/src/renderive/scene/base/Scene_Base.inl b/Kernel/src/renderive/scene/base/Scene_Base.inl index 199429c..fa50e60 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.inl +++ b/Kernel/src/renderive/scene/base/Scene_Base.inl @@ -10,6 +10,8 @@ public: virtual ~Frame_Control_Capability() = default; virtual std::shared_ptr frame_control_strategy() = 0; virtual std::shared_ptr frame_control_strategy() const = 0; + virtual void replace_frame_control_strategy( + std::shared_ptr strategy) = 0; }; struct Scene_State_Capability { virtual ~Scene_State_Capability() = default; diff --git a/render_2D/render_2D/plottable/Afterglow.cpp b/render_2D/render_2D/plottable/Afterglow.cpp index 1047ee5..1b5cf3d 100644 --- a/render_2D/render_2D/plottable/Afterglow.cpp +++ b/render_2D/render_2D/plottable/Afterglow.cpp @@ -28,8 +28,6 @@ struct Afterglow_Prepare_Buffer { } struct Afterglow::Impl : Base::next_Impl, ::Render_Frame_Completion { - static_assert(inheritance::Inherits_Chain_Parent< - Impl, Base::next_Impl>); Impl(renderive_Owner frequency, renderive_Owner power) : frequency_axis(std::move(frequency)), power_axis(std::move(power)) {} diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.cpp b/render_2D/render_2D/plottable/Constellation_Diagram.cpp index f4abe51..2ca96b6 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.cpp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.cpp @@ -9,8 +9,7 @@ #include #include #include -namespace renderive { -namespace detail { +namespace renderive::detail { namespace { struct Timed_Point { PointF point; @@ -20,8 +19,6 @@ using Constellation_History = Plottable_History_Real_Time_Data { - static_assert(inheritance::Inherits_Chain_Parent< - Impl, Base::next_Impl>); struct Prepare_Buffer { std::vector anchors; std::vector points; @@ -64,9 +61,9 @@ Constellation_Diagram::Observer Constellation_Diagram::capture_observation() con void Constellation_Diagram::fit_square_to_axes() { const auto state = Renderable::state(); const double side = std::max(state.i_range.size(), state.q_range.size()); - d_func().i_axis->set < &Axis_Properties::coordinates > ( + d_func().i_axis->set<&Axis_Properties::coordinates>( Range{state.i_range.center() - side * 0.5, state.i_range.center() + side * 0.5}); - d_func().q_axis->set < &Axis_Properties::coordinates > ( + d_func().q_axis->set<&Axis_Properties::coordinates>( Range{state.q_range.center() + side * 0.5, state.q_range.center() - side * 0.5}); } void Constellation_Diagram::Impl::prepare_frame( @@ -108,4 +105,3 @@ void Constellation_Diagram::Impl::paint(Painter& painter, Brush{state.point_color, Brush_Style::Solid}); } } -} diff --git a/render_2D/render_2D/plottable/Frequency_Trace.cpp b/render_2D/render_2D/plottable/Frequency_Trace.cpp index 0ecaf4b..311fc94 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.cpp +++ b/render_2D/render_2D/plottable/Frequency_Trace.cpp @@ -7,15 +7,12 @@ #include #include #include -namespace renderive { -namespace detail { +namespace renderive::detail { namespace { using Frequency_Trace_History = Plottable_History_Real_Time_Data, std::deque>>; } struct Frequency_Trace::Impl : Base::next_Impl { - static_assert(inheritance::Inherits_Chain_Parent< - Impl, Base::next_Impl>); struct Prepare_Buffer { std::vector points; }; @@ -78,4 +75,3 @@ void Frequency_Trace::Impl::paint(Painter& painter, control.render_state(view).pen); } } -} diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp index f3f507f..277a9b5 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp @@ -7,8 +7,7 @@ #include #include #include -namespace renderive { -namespace detail { +namespace renderive::detail { namespace { struct Selection_Interaction { bool selecting{}; @@ -24,8 +23,6 @@ struct Selection_Rectangle_Overlay::Impl : Base::next_Impl, Renderable_Event_Handler { using Observer = Selection_Rectangle_Overlay::Observer; - static_assert(inheritance::Inherits_Chain_Parent< - Impl, Base::next_Impl>); struct Prepared_Region { RectF pixels; std::string label; @@ -175,4 +172,3 @@ void Selection_Rectangle_Overlay::Impl::paint( state.selection_brush); } } -} diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.cpp b/render_2D/render_2D/plottable/Sweep_Spectrum.cpp index 13d7bfb..3c4309d 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.cpp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.cpp @@ -7,8 +7,7 @@ #include #include #include -namespace renderive { -namespace detail { +namespace renderive::detail { namespace { using Sweep_Spectrum_History = Plottable_History_Real_Time_Data, std::deque>>; std::vector flatten(const std::deque>& blocks) { @@ -20,8 +19,6 @@ std::vector flatten(const std::deque>& blocks) { struct Sweep_Spectrum::Impl : Base::next_Impl { using Observer = Sweep_Spectrum::Observer; - static_assert(inheritance::Inherits_Chain_Parent< - Impl, Base::next_Impl>); friend struct Sweep_Spectrum::Observer; struct Prepare_Buffer { std::vector points; @@ -117,4 +114,3 @@ void Sweep_Spectrum::Impl::paint(Painter& painter, state.current_frequency_pen); } } -} diff --git a/render_2D/render_2D/plottable/Waterfall.cpp b/render_2D/render_2D/plottable/Waterfall.cpp index d11714a..18a1601 100644 --- a/render_2D/render_2D/plottable/Waterfall.cpp +++ b/render_2D/render_2D/plottable/Waterfall.cpp @@ -63,8 +63,6 @@ struct Waterfall::Impl Renderable_Event_Handler, ::Render_Frame_Completion { using Observer = Waterfall::Observer; - static_assert(inheritance::Inherits_Chain_Parent< - Impl, Base::next_Impl>); Impl(renderive_Owner frequency, renderive_Owner time) : frequency_axis(std::move(frequency)), time_axis(std::move(time)) {} renderive_Owner frequency_axis; diff --git a/render_2D/render_2D/plottable/Waterfall.h b/render_2D/render_2D/plottable/Waterfall.h index 51bc05a..3f2a9a4 100644 --- a/render_2D/render_2D/plottable/Waterfall.h +++ b/render_2D/render_2D/plottable/Waterfall.h @@ -31,8 +31,7 @@ struct LIB_DECL Waterfall : Renderable { Render_Partition_Mode partition_mode{Render_Partition_Mode::Automatic}; Positive_Count partition_count; bool visible_range_only{true}; - Image_Interpolation_Mode interpolation_mode = - Image_Interpolation_Mode::Nearest; + Image_Interpolation_Mode interpolation_mode = Image_Interpolation_Mode::Nearest; Color_Map color_map; }; struct Observer : next_Observer { diff --git a/render_2D/render_2D/render/Blend2D_Cache.h b/render_2D/render_2D/render/Blend2D_Cache.h index b3e4de1..6e00418 100644 --- a/render_2D/render_2D/render/Blend2D_Cache.h +++ b/render_2D/render_2D/render/Blend2D_Cache.h @@ -14,7 +14,7 @@ struct Blend2D_Color_Cache final : ::Color_Cache { [[nodiscard]] Size size() const noexcept; [[nodiscard]] Image_View view() const noexcept; private: - friend class Painter; + friend struct Painter; BLImage image_; }; struct Painter { diff --git a/render_2D/render_2D/renderable/Renderable_Builder.h b/render_2D/render_2D/renderable/Renderable_Builder.h index 066ab92..d8e0e62 100644 --- a/render_2D/render_2D/renderable/Renderable_Builder.h +++ b/render_2D/render_2D/renderable/Renderable_Builder.h @@ -11,7 +11,7 @@ namespace renderive { class Abs_Axis; namespace detail { -class Paint_Overlay; +struct Paint_Overlay; template requires std::derived_from && std::derived_from void attach_renderable_dependency(::Scene_2D_Base::Attach_Builder& builder, const renderive_Owner& renderable, const renderive_Owner& axis) { if (!axis) return; @@ -41,12 +41,12 @@ struct Renderable_Builder { Renderable_Builder(Properties properties, Validator validator, ::Scene_2D_Base::Attach_Builder* attach_builder = nullptr) : properties(std::move(properties)), validator(std::move(validator)), attach_builder_(attach_builder) {} template Value> Self& set(Value&& value) { - properties.*Member = std::forward < Value > (value); + properties.*Member = std::forward(value); return *this; } template Configure> Self& configure(Configure&& configure) { - std::invoke(std::forward < Configure > (configure), properties); + std::invoke(std::forward(configure), properties); return *this; } const Properties& properties_value() const noexcept { diff --git a/render_2D/render_2D/scene/Render_Scene_2D.cpp b/render_2D/render_2D/scene/Render_Scene_2D.cpp index e4ecf21..7a45a51 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.cpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.cpp @@ -51,6 +51,10 @@ struct detail::Render_Scene_2D::Impl std::shared_ptr frame_control_strategy() const override { return frame_control->runtime(); } + void replace_frame_control_strategy( + std::shared_ptr strategy) override { + frame_control->replace(std::move(strategy)); + } std::shared_ptr make_renderable_color_cache() override { std::pmr::polymorphic_allocator allocator(&memory_resource()); diff --git a/render_2D/render_2D/scene/Render_Scene_2D.h b/render_2D/render_2D/scene/Render_Scene_2D.h index 7d94f83..8ec00f1 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.h +++ b/render_2D/render_2D/scene/Render_Scene_2D.h @@ -88,21 +88,24 @@ protected: using Editor_Base::Editor_Base; }; public: - struct State : Base::template next_State { + struct State_Fields { Size viewport; Color background = Color::black(); + }; + struct State : Base::template next_State, State_Fields { [[nodiscard]] bool operator==(const State& other) const { return viewport == other.viewport && background == other.background; } }; - struct Observer : Base::template next_Observer { + struct Observer_Fields { State state; std::size_t renderable_count{}; double frequency_hz{}; std::uint64_t next_refresh_interval_ns{}; bool view_active{}; }; + struct Observer : Base::template next_Observer, Observer_Fields {}; [[nodiscard]] ::renderive_Owner<::renderive::Renderable> root_renderable() const; void dispatch_event(const Event& event); [[nodiscard]] auto render_frame(bool force = false) -> Expected; diff --git a/render_2D/tests/render_2D_Integration_Tests.cpp b/render_2D/tests/render_2D_Integration_Tests.cpp index 87047f6..3dfd559 100644 --- a/render_2D/tests/render_2D_Integration_Tests.cpp +++ b/render_2D/tests/render_2D_Integration_Tests.cpp @@ -31,5 +31,21 @@ TEST(RenderScene2DIntegration, FrameImageMatchesConfiguredViewport) { }); } +TEST(RenderScene2DIntegration, FrameStrategyIsReplacedInsideRenderableEdit) { + Render_Scene_2D::State state; + state.viewport = {48, 27}; + auto scene = Render_Scene_2D::Builder{state}.build( + std::make_shared()); + const auto operation = scene->edit_renderables( + [](Scene_2D_Base::Renderable_Editor& editor) { + editor.replace_frame_control_strategy( + std::make_shared()); + }); + ASSERT_EQ(operation.wait(), Scene_Edit_Error::none); + EXPECT_NE(std::dynamic_pointer_cast( + scene->frame_control_strategy()), + nullptr); +} + } // namespace } // namespace renderive diff --git a/render_3D/render_3D/Render_Scene_3D.cpp b/render_3D/render_3D/Render_Scene_3D.cpp index d14c1da..acfb224 100644 --- a/render_3D/render_3D/Render_Scene_3D.cpp +++ b/render_3D/render_3D/Render_Scene_3D.cpp @@ -408,6 +408,10 @@ struct detail::Render_Scene_3D::Impl std::shared_ptr frame_control_strategy() const override { return frame_control->runtime(); } + void replace_frame_control_strategy( + std::shared_ptr strategy) override { + frame_control->replace(std::move(strategy)); + } std::uint64_t acquire_scene_state() override { return scene_state.acquire_render_state(); } @@ -521,4 +525,22 @@ Scene_Control_Error detail::Render_Scene_3D::dispatch( if (const auto* key = dynamic_cast(&event)) return d_func().dispatch_key(*this, *key); return Scene_Control_Error::none; } +Frame_Request_Result detail::Render_Scene_3D::request_frame() { + const auto requested = d_func().frame_control->request(*this); + if (requested != Frame_Request_Result::none) return requested; + switch (wait_for_render()) { + case Scene_Render_Result::none: return Frame_Request_Result::none; + case Scene_Render_Result::cancelled: return Frame_Request_Result::cancelled; + case Scene_Render_Result::deadline_exceeded: + return Frame_Request_Result::deadline_exceeded; + case Scene_Render_Result::external_failure: + return Frame_Request_Result::external_failure; + case Scene_Render_Result::shutting_down: + return Frame_Request_Result::scene_shutting_down; + } + return Frame_Request_Result::external_failure; +} +std::shared_ptr detail::Render_Scene_3D::latest_frame() const { + return d_func().latest_frame(); +} } // namespace renderive::render_3d diff --git a/render_3D/render_3D/Render_Scene_3D.h b/render_3D/render_3D/Render_Scene_3D.h index a386c1b..a562049 100644 --- a/render_3D/render_3D/Render_Scene_3D.h +++ b/render_3D/render_3D/Render_Scene_3D.h @@ -119,10 +119,12 @@ protected: using Editor_Base::Editor_Base; }; public: - struct State : Base::template next_State { + struct State_Fields { Extent viewport{560, 320}; Clear_Color clear_color; Visual_Family visual_family{Visual_Family::Point}; + }; + struct State : Base::template next_State, State_Fields { [[nodiscard]] bool operator==(const State& other) const { return viewport == other.viewport && clear_color == other.clear_color && @@ -135,7 +137,7 @@ public: template using Business_Builder = ::renderive::scene::Scene_Builder< Product, Properties, State_Validator>; - struct Observer : Base::template next_Observer { + struct Observer_Fields { State state; std::size_t renderable_count{}; std::size_t point_count{}; @@ -143,8 +145,11 @@ public: std::uint64_t next_refresh_interval_ns{}; bool backend_available{}; }; + struct Observer : Base::template next_Observer, Observer_Fields {}; [[nodiscard]] Scene_Control_Error dispatch( const ::renderive::Event& event); + [[nodiscard]] Frame_Request_Result request_frame(); + [[nodiscard]] std::shared_ptr latest_frame() const; ~Render_Scene_3D() override; private: [[nodiscard]] Observer capture_observation() const; diff --git a/render_3D/tests/Point_Render_Integration_Tests.cpp b/render_3D/tests/Point_Render_Integration_Tests.cpp index 2b53621..458268f 100644 --- a/render_3D/tests/Point_Render_Integration_Tests.cpp +++ b/render_3D/tests/Point_Render_Integration_Tests.cpp @@ -50,5 +50,27 @@ TEST(PointRenderIntegration, ObserverPublishesSceneAndVisualState) { } } +TEST(PointRenderIntegration, FrameStrategyIsReplacedInsideRenderableEdit) { + auto visual = Point_Visual::Builder{}.build(std::vector{ + {{0.0F, 0.0F, 0.0F}, {255, 0, 0, 255}, 8.0F}}); + Render_Scene_3D::State state; + state.viewport = {64, 48}; + try { + auto scene = make_scene(state, visual); + const auto operation = scene->edit_renderables( + [](Scene_3D_Base::Renderable_Editor& editor) { + editor.replace_frame_control_strategy( + std::make_shared()); + }); + ASSERT_EQ(operation.wait(), Scene_Edit_Error::none); + EXPECT_NE(std::dynamic_pointer_cast( + scene->frame_control_strategy()), + nullptr); + } + catch (const std::exception& error) { + GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what(); + } +} + } // namespace } // namespace renderive::render_3d diff --git a/web_server/CMakeLists.txt b/web_server/CMakeLists.txt index 2b92687..3690d80 100644 --- a/web_server/CMakeLists.txt +++ b/web_server/CMakeLists.txt @@ -1,4 +1,4 @@ -set(Renderive_Web_dependencies global::drogon global::spdlog global::magic_enum) +set(Renderive_Web_dependencies global::drogon global::spdlog global::magic_enum global::pfr) if (RENDERIVE_BUILD_TESTS) set(Renderive_Web_test_targets) list(APPEND Renderive_Web_dependencies global::GTest) @@ -105,6 +105,9 @@ function(renderive_find_web_dependencies) endif () endfunction() renderive_find_web_dependencies() +add_library(Renderive_Web_BoostPfr INTERFACE) +target_include_directories(Renderive_Web_BoostPfr INTERFACE + "$") if (NOT TARGET Renderive_render_2D) rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_Web requires Renderive_render_2D") return() @@ -148,7 +151,7 @@ target_compile_features(Renderive_Web_2D PUBLIC cxx_std_20) target_compile_definitions(Renderive_Web_2D PRIVATE NOMINMAX) target_link_libraries(Renderive_Web_2D PUBLIC Renderive_Web_Common Renderive_render_2D - PRIVATE Adminive::Nlohmann Adminive::MagicEnum magic_enum::magic_enum) + PRIVATE Adminive::Nlohmann Adminive::MagicEnum magic_enum::magic_enum Renderive_Web_BoostPfr) # 3D 模块只负责 Scene3D/Datoviz 到公共 RVP2 像素协议的适配。 set(Renderive_Web_3D_sources "${CMAKE_CURRENT_LIST_DIR}/app/render_3D/Gallery_Scene3D.cpp") @@ -160,7 +163,7 @@ target_compile_features(Renderive_Web_3D PUBLIC cxx_std_20) target_compile_definitions(Renderive_Web_3D PRIVATE NOMINMAX) target_link_libraries(Renderive_Web_3D PUBLIC Renderive_Web_Common Renderive_render_3D - PRIVATE Adminive::Nlohmann) + PRIVATE Adminive::Nlohmann Renderive_Web_BoostPfr) set(Renderive_Web_sources "${CMAKE_CURRENT_LIST_DIR}/app/Gallery_Plot_Session.cpp" "${CMAKE_CURRENT_LIST_DIR}/app/Gallery_Protocol.cpp" diff --git a/web_server/app/Gallery_Actions.h b/web_server/app/Gallery_Actions.h deleted file mode 100644 index 2dd9bb4..0000000 --- a/web_server/app/Gallery_Actions.h +++ /dev/null @@ -1,167 +0,0 @@ -#pragma once - -#include "common/Gallery_Types.h" -#include "render_2D/scene/Scene_Context.hpp" -#include - -#include -#include -#include -#include -#include - -namespace renderive::web { - -struct Gallery_Action_Category {}; - -struct Gallery_Action_Attribute { - using attribute_category = Gallery_Action_Category; - static constexpr bool single_valued = false; - static constexpr bool inheritable = false; - - std::string_view id; - std::string_view label; - std::string_view api; - std::string_view group; - std::string_view description; - std::string_view argument_input; - std::string_view argument_label; - double argument_default{}; - bool request_frame{}; -}; - -constexpr Gallery_Action_Attribute gallery_action( - std::string_view id, std::string_view label, std::string_view api, - std::string_view group, std::string_view description = {}, - std::string_view argument_input = {}, std::string_view argument_label = {}, - double argument_default = 0.0, bool request_frame = false) { - return {id, label, api, group, description, argument_input, argument_label, - argument_default, request_frame}; -} - -struct Gallery_Action_Model { - std::string id; - std::string label; - std::string api; - std::string description; - std::string group; - std::string argument_input; - std::string argument_label; - double argument_default{}; - bool request_frame{}; -}; - -inline Gallery_Action_Model gallery_action_model( - const Gallery_Action_Attribute& action) { - return {std::string(action.id), std::string(action.label), std::string(action.api), - std::string(action.description), std::string(action.group), - std::string(action.argument_input), std::string(action.argument_label), - action.argument_default, action.request_frame}; -} - -template -void append_gallery_actions(std::vector& target) { - structive::type_descriptor() - .template for_each_type_attribute( - [&target](const auto& action) { - const auto found = std::find_if( - target.begin(), target.end(), - [&action](const auto& existing) { return existing.id == action.id; }); - if (found == target.end()) - target.push_back(gallery_action_model(action)); - }); -} - -} // namespace renderive::web - -namespace structive { - -template -struct Type_Descriptor> { - using T = Manual_Refresh_Strategy; - static auto get() { - using renderive::web::gallery_action; - return object(type_metadata( - gallery_action("mode_prepare", "准备帧", - "Manual_Refresh_Strategy::acquire_painter / Plot_Scene::prepare_frame", - "Manual_Refresh_Strategy"), - gallery_action("mode_refresh", "提交手动刷新", - "Manual_Refresh_Strategy::refresh / Plot_Scene::refresh_manual_frame", - "Manual_Refresh_Strategy"), - gallery_action("mode_render", "渲染已刷新帧", - "Manual_Refresh_Strategy::acquire_renderer / Plot_Scene::render_prepared_frame", - "Manual_Refresh_Strategy", {}, {}, {}, 0.0, true), - gallery_action("mode_discard", "丢弃待刷新帧", - "Plot_Scene::discard_pending_frame", - "Manual_Refresh_Strategy"), - gallery_action("mode_cycle", "执行完整手动帧周期", - "Plot_Scene::render_frame", "Manual_Refresh_Strategy", - {}, {}, {}, 0.0, true))); - } -}; - -template -struct Type_Descriptor> { - using T = Low_Latency_Strategy; - static auto get() { - using renderive::web::gallery_action; - return object(type_metadata( - gallery_action("mode_prepare", "发布低延迟帧", - "Low_Latency_Strategy::acquire_painter / Plot_Scene::prepare_frame", - "Low_Latency_Strategy"), - gallery_action("mode_render", "消费最新帧", - "Low_Latency_Strategy::acquire_renderer / Plot_Scene::render_prepared_frame", - "Low_Latency_Strategy", {}, {}, {}, 0.0, true), - gallery_action("mode_discard", "丢弃陈旧帧", - "Plot_Scene::discard_pending_frame", "Low_Latency_Strategy"), - gallery_action("mode_cycle", "执行低延迟帧周期", - "Plot_Scene::render_frame", "Low_Latency_Strategy", - {}, {}, {}, 0.0, true))); - } -}; - -template -struct Type_Descriptor> { - using T = Flow_Refresh_Strategy; - static auto get() { - using renderive::web::gallery_action; - return object(type_metadata( - gallery_action("mode_enqueue", "压入一帧", - "Flow_Refresh_Strategy::acquire_painter / Plot_Scene::prepare_frame", - "Flow_Refresh_Strategy"), - gallery_action("mode_enqueue_burst", "批量压入回放队列", - "Flow_Refresh_Strategy::acquire_painter / Plot_Scene::prepare_frame x N", - "Flow_Refresh_Strategy", {}, "number", "帧数", 8.0), - gallery_action("mode_dequeue", "消费队首帧", - "Flow_Refresh_Strategy::acquire_renderer / Plot_Scene::render_prepared_frame", - "Flow_Refresh_Strategy", {}, {}, {}, 0.0, true), - gallery_action("mode_cycle", "执行回放帧周期", - "Plot_Scene::render_frame", "Flow_Refresh_Strategy", - {}, {}, {}, 0.0, true))); - } -}; - -} // namespace structive - -namespace renderive::web { - -inline std::vector gallery_frame_actions(Gallery_Frame_Mode mode) { - std::vector result; - switch (mode) { - case Gallery_Frame_Mode::Manual: - append_gallery_actions>>(result); - break; - case Gallery_Frame_Mode::Low_Latency: - append_gallery_actions>>(result); - break; - case Gallery_Frame_Mode::Playback: - append_gallery_actions>>(result); - break; - } - return result; -} - -} // namespace renderive::web diff --git a/web_server/app/Gallery_Controls.h b/web_server/app/Gallery_Controls.h deleted file mode 100644 index dccfb72..0000000 --- a/web_server/app/Gallery_Controls.h +++ /dev/null @@ -1,136 +0,0 @@ -#pragma once - -#include "Gallery_Capture_Json.h" -#include "Gallery_Observer_Adminive.h" -#include "Gallery_Renderables.h" - -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace renderive::web { - -class Gallery_Controls final { -public: - using Json = nlohmann::json; - - template - void add(Object& object) { - const auto descriptor = adminive::Type_Descriptor::get(); - add(descriptor.name(), descriptor.label(), object); - } - - template - void add(std::string id, Object& object) { - add(std::move(id), adminive::Type_Descriptor::get().label(), object); - } - - [[nodiscard]] Json resources() const { - Json result = Json::array(); - for (const auto& entry : entries_) { - Json resource = entry.resource(); - resource["target"] = entry.id; - resource["view"]["title"] = entry.title; - result.push_back(std::move(resource)); - } - return result; - } - - [[nodiscard]] Json observers(const Scene_Base& scene) const { - Json result = Json::array(); - const auto topology = scene.topology_snapshot(); - for (std::size_t index = 0; index < topology.renderables.size(); ++index) { - const auto* renderable = - dynamic_cast(topology.renderables[index].get()); - if (!renderable) - continue; - const Entry* entry = find(renderable); - std::string title = entry ? entry->title : renderable->object_name(); - if (title.empty()) - title = "渲染节点 " + std::to_string(index + 1); - Json resource{ - {"descriptor", adminive::to_descriptor_json< - Json, renderive::Renderable_Observation>()}, - {"data", adminive::to_frontend_json(renderable->observation())}, - {"target", entry ? entry->id : node_id(index)}, - {"title", std::move(title)} - }; - result.push_back(std::move(resource)); - } - return result; - } - - [[nodiscard]] Json render_plan(const Scene_Base& scene) const { - return gallery_render_plan_json(scene); - } - [[nodiscard]] adminive::Update_Result apply(std::string_view target, - const Json& patch) const { - for (const auto& entry : entries_) { - if (entry.id == target) - return entry.apply(patch); - } - adminive::Update_Result result; - result.success = false; - result.message = "unknown gallery control target: " + std::string(target); - return result; - } - -private: - struct Entry { - std::string id; - std::string title; - std::function resource; - std::function apply; - renderive::Renderable* renderable{}; - }; - - template - void add(std::string id, std::string title, Object& object) { - using Model = adminive::Object_Model_Type; - auto* target = &object; - renderive::Renderable* renderable{}; - if constexpr (std::derived_from) - renderable = target; - entries_.push_back({ - std::move(id), std::move(title), - [target] { - return Json{ - {"descriptor", adminive::to_descriptor_json()}, - {"view", adminive::to_view_json( - adminive::describe_edit_view())}, - {"data", adminive::to_frontend_json(*target)} - }; - }, - [target](const Json& patch) { - return adminive::apply_frontend_patch(*target, patch); - }, - renderable - }); - } - - [[nodiscard]] const Entry* find(const Renderable_Base* renderable) const noexcept { - const auto found = std::find_if(entries_.begin(), entries_.end(), - [renderable](const Entry& entry) { return entry.renderable == renderable; }); - return found == entries_.end() ? nullptr : &*found; - } - - static std::string node_id(std::size_t index) { - return "renderable-" + std::to_string(index); - } - - std::vector entries_; -}; - -} // namespace renderive::web diff --git a/web_server/app/Gallery_Observer_Adminive.h b/web_server/app/Gallery_Observer_Adminive.h deleted file mode 100644 index d1ee071..0000000 --- a/web_server/app/Gallery_Observer_Adminive.h +++ /dev/null @@ -1,239 +0,0 @@ -#pragma once -#include - -#include "Gallery_Enum.h" -#include "common/Gallery_Performance_Types.h" -#include "render_2D/scene/Render_Scene_2D.h" -#include "render_2D/renderable/Renderable.h" - -#include "adminive/adminive.hpp" -#include "adminive/adapters/magic_enum.hpp" - -#include -#include -#include - -namespace renderive::web { - -struct Gallery_Consumer_Feedback_Snapshot { - bool master_enabled{}; - bool pixel_enabled{}; - bool presentation_enabled{}; - bool manual_enabled{}; - double manual_fps{}; - std::string source; - std::uint64_t pixel_interval_ns{}; - std::uint64_t presentation_interval_ns{}; - std::uint64_t manual_interval_ns{}; -}; - -struct Gallery_Render_Performance { - std::uint64_t render_attempt_count{}; - std::uint64_t successful_render_count{}; - std::uint64_t failed_render_count{}; - double measured_fps{}; - double lifetime_average_fps{}; - double last_render_ms{}; - double average_render_ms{}; - double maximum_render_ms{}; - double render_deviation_ms{}; - double render_p50_ms{}; - double render_p95_ms{}; - double render_p99_ms{}; - std::uint64_t render_sample_count{}; - double pixel_response_fps{}; - double last_pixel_snapshot_ms{}; - double last_pixel_encode_ms{}; - double average_pixel_encode_ms{}; - double maximum_pixel_encode_ms{}; - double pixel_encode_deviation_ms{}; - double pixel_encode_p50_ms{}; - double pixel_encode_p95_ms{}; - double pixel_encode_p99_ms{}; - std::uint64_t pixel_encode_sample_count{}; - double last_pixel_request_ms{}; - double average_pixel_request_ms{}; - double pixel_request_deviation_ms{}; - double pixel_request_p95_ms{}; - double pixel_request_p99_ms{}; - std::uint64_t last_pixel_bytes{}; - double pixel_payload_megabytes_per_second{}; - bool automatic_low_latency_scheduler{}; -}; - - -} // namespace renderive::web - -namespace adminive { - -template -struct Value_Adapter { - using value_type = std::string; - - static std::string read(renderive::Plot_Frame_Mode value) { - return renderive::web::gallery_enum_id(value); - } - static void write(renderive::Plot_Frame_Mode& target, std::string value) { - const auto parsed = - renderive::web::gallery_enum_cast(value); - if (!parsed) - ::renderive::error::unexpected("unknown frame control mode: " + value); - target = *parsed; - } -}; - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::Plot_Frame_Status; - return object("kernel_observer", - ADMINIVE_FIELD_LABEL(T, mode, "模式") - .enum_label("手动刷新") - .enum_label("低延迟") - .enum_label("回放队列"), - ADMINIVE_FIELD_LABEL(T, last_event, "最近事件"), - ADMINIVE_FIELD_LABEL(T, limit_state, "当前瓶颈"), - ADMINIVE_FIELD_LABEL(T, frequency_hz, "配置频率"), - ADMINIVE_FIELD_LABEL(T, observation_count, "观察次数"), - ADMINIVE_FIELD_LABEL(T, produced_frame_count, "发布"), - ADMINIVE_FIELD_LABEL(T, consumed_frame_count, "完成"), - ADMINIVE_FIELD_LABEL(T, dropped_frame_count, "丢弃"), - ADMINIVE_FIELD_LABEL(T, failed_operation_count, "失败"), - ADMINIVE_FIELD_LABEL(T, pending_frame_count, "待处理"), - ADMINIVE_FIELD_LABEL(T, latest_sequence, "最新序号"), - ADMINIVE_FIELD_LABEL(T, paint_duration_ns, "绘制事件耗时"), - ADMINIVE_FIELD_LABEL(T, render_duration_ns, "后台渲染"), - ADMINIVE_FIELD_LABEL(T, target_interval_ns, "目标间隔"), - ADMINIVE_FIELD_LABEL(T, frequency_limit_enabled, "频率限制"), - ADMINIVE_FIELD_LABEL(T, consumer_feedback_enabled, "内核反馈有效"), - ADMINIVE_FIELD_LABEL(T, bottleneck_duration_ns, "内部瓶颈"), - ADMINIVE_FIELD_LABEL(T, consumer_sample_interval_ns, "消费者原始采样"), - ADMINIVE_FIELD_LABEL(T, consumer_smoothed_interval_ns, "消费者平滑周期"), - ADMINIVE_FIELD_LABEL(T, consumer_variation_ns, "消费者抖动"), - ADMINIVE_FIELD_LABEL(T, consumer_safety_interval_ns, "消费者安全期限"), - ADMINIVE_FIELD_LABEL(T, consumer_interval_ns, "消费者限速周期"), - ADMINIVE_FIELD_LABEL(T, next_refresh_interval_ns, "下次刷新"), - ADMINIVE_FIELD_LABEL(T, paint_lease_wait_ns, "绘制租约等待"), - ADMINIVE_FIELD_LABEL(T, paint_state_wait_ns, "绘制状态等待"), - ADMINIVE_FIELD_LABEL(T, publish_state_wait_ns, "发布状态等待"), - ADMINIVE_FIELD_LABEL(T, ready_wait_ns, "就绪等待"), - ADMINIVE_FIELD_LABEL(T, frame_age_at_render_ns, "开始渲染时帧龄"), - ADMINIVE_FIELD_LABEL(T, render_lease_wait_ns, "渲染租约等待"), - ADMINIVE_FIELD_LABEL(T, render_state_wait_ns, "渲染状态等待"), - ADMINIVE_FIELD_LABEL(T, render_finish_state_wait_ns, "渲染完成等待"), - ADMINIVE_FIELD_LABEL(T, queue_wait_ns, "回放队列等待"), - ADMINIVE_FIELD_LABEL(T, end_to_end_ns, "端到端延迟")) - .label("内核观察器"); - } -}; - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::web::Gallery_Consumer_Feedback_Snapshot; - return object("consumer_feedback", - ADMINIVE_FIELD_LABEL(T, master_enabled, "消费者反馈总开关"), - ADMINIVE_FIELD_LABEL(T, pixel_enabled, "像素响应反馈"), - ADMINIVE_FIELD_LABEL(T, presentation_enabled, "浏览器呈现反馈"), - ADMINIVE_FIELD_LABEL(T, manual_enabled, "手动消费者反馈"), - ADMINIVE_FIELD_LABEL(T, manual_fps, "手动消费者 FPS"), - ADMINIVE_FIELD_LABEL(T, source, "生效反馈来源"), - ADMINIVE_FIELD_LABEL(T, pixel_interval_ns, "像素响应周期"), - ADMINIVE_FIELD_LABEL(T, presentation_interval_ns, "浏览器呈现周期"), - ADMINIVE_FIELD_LABEL(T, manual_interval_ns, "手动消费者周期")) - .label("消费者反馈"); - } -}; - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::web::Gallery_Render_Performance; - return object("render_performance", - ADMINIVE_FIELD_LABEL(T, render_attempt_count, "渲染尝试"), - ADMINIVE_FIELD_LABEL(T, successful_render_count, "渲染成功"), - ADMINIVE_FIELD_LABEL(T, failed_render_count, "渲染失败"), - ADMINIVE_FIELD_LABEL(T, measured_fps, "最近渲染帧率"), - ADMINIVE_FIELD_LABEL(T, lifetime_average_fps, "平均渲染帧率"), - ADMINIVE_FIELD_LABEL(T, last_render_ms, "最近渲染耗时"), - ADMINIVE_FIELD_LABEL(T, average_render_ms, "平均渲染耗时"), - ADMINIVE_FIELD_LABEL(T, maximum_render_ms, "最大渲染耗时"), - ADMINIVE_FIELD_LABEL(T, render_deviation_ms, "渲染标准差"), - ADMINIVE_FIELD_LABEL(T, render_p50_ms, "渲染 P50"), - ADMINIVE_FIELD_LABEL(T, render_p95_ms, "渲染 P95"), - ADMINIVE_FIELD_LABEL(T, render_p99_ms, "渲染 P99"), - ADMINIVE_FIELD_LABEL(T, render_sample_count, "渲染窗口样本"), - ADMINIVE_FIELD_LABEL(T, pixel_response_fps, "像素响应帧率"), - ADMINIVE_FIELD_LABEL(T, last_pixel_snapshot_ms, "最近像素快照耗时"), - ADMINIVE_FIELD_LABEL(T, last_pixel_encode_ms, "最近像素编码耗时"), - ADMINIVE_FIELD_LABEL(T, average_pixel_encode_ms, "平均像素编码耗时"), - ADMINIVE_FIELD_LABEL(T, maximum_pixel_encode_ms, "最大像素编码耗时"), - ADMINIVE_FIELD_LABEL(T, pixel_encode_deviation_ms, "像素编码标准差"), - ADMINIVE_FIELD_LABEL(T, pixel_encode_p50_ms, "像素编码 P50"), - ADMINIVE_FIELD_LABEL(T, pixel_encode_p95_ms, "像素编码 P95"), - ADMINIVE_FIELD_LABEL(T, pixel_encode_p99_ms, "像素编码 P99"), - ADMINIVE_FIELD_LABEL(T, pixel_encode_sample_count, "像素编码窗口样本"), - ADMINIVE_FIELD_LABEL(T, last_pixel_request_ms, "最近像素请求耗时"), - ADMINIVE_FIELD_LABEL(T, average_pixel_request_ms, "像素请求滑动平均"), - ADMINIVE_FIELD_LABEL(T, pixel_request_deviation_ms, "像素请求标准差"), - ADMINIVE_FIELD_LABEL(T, pixel_request_p95_ms, "像素请求 P95"), - ADMINIVE_FIELD_LABEL(T, pixel_request_p99_ms, "像素请求 P99"), - ADMINIVE_FIELD_LABEL(T, last_pixel_bytes, "最近像素负载"), - ADMINIVE_FIELD_LABEL(T, pixel_payload_megabytes_per_second, "像素负载吞吐"), - ADMINIVE_FIELD_LABEL(T, automatic_low_latency_scheduler, "自动低延迟调度")) - .label("渲染性能"); - } -}; - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::web::Gallery_Client_Performance; - return object("client_performance", - ADMINIVE_FIELD_LABEL(T, transport_fps, "像素响应帧率"), - ADMINIVE_FIELD_LABEL(T, presentation_fps, "浏览器呈现帧率"), - ADMINIVE_FIELD_LABEL(T, websocket_buffered_bytes, "WebSocket 缓冲"), - ADMINIVE_FIELD_LABEL(T, changed_pixel_frames, "变化像素帧"), - ADMINIVE_FIELD_LABEL(T, duplicate_pixel_frames, "重复像素帧"), - ADMINIVE_FIELD_LABEL(T, frame_request_timeout_count, "像素请求超时"), - ADMINIVE_FIELD_LABEL(T, frame_round_trip_ms, "WebSocket 往返耗时"), - ADMINIVE_FIELD_LABEL(T, frame_round_trip_average_ms, "WebSocket 往返滑动平均"), - ADMINIVE_FIELD_LABEL(T, frame_round_trip_deviation_ms, "WebSocket 往返标准差"), - ADMINIVE_FIELD_LABEL(T, frame_round_trip_p95_ms, "WebSocket 往返 P95"), - ADMINIVE_FIELD_LABEL(T, frame_round_trip_p99_ms, "WebSocket 往返 P99"), - ADMINIVE_FIELD_LABEL(T, display_interval_ms, "呈现中位周期"), - ADMINIVE_FIELD_LABEL(T, display_interval_average_ms, "呈现滑动平均周期"), - ADMINIVE_FIELD_LABEL(T, display_interval_latest_ms, "最近呈现周期"), - ADMINIVE_FIELD_LABEL(T, display_interval_p95_ms, "呈现 P95 周期"), - ADMINIVE_FIELD_LABEL(T, display_interval_p99_ms, "呈现 P99 周期"), - ADMINIVE_FIELD_LABEL(T, display_interval_deviation_ms, "呈现周期标准差"), - ADMINIVE_FIELD_LABEL(T, overwritten_pixel_frames, "未呈现覆盖帧"), - ADMINIVE_FIELD_LABEL(T, last_pixel_receive_age_ms, "最近像素龄"), - ADMINIVE_FIELD_LABEL(T, last_pixel_change_age_ms, "最近变化龄")) - .label("浏览器性能"); - } -}; - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::Renderable_Observation; - return object("renderable_observer", - ADMINIVE_FIELD_LABEL(T, event, "最近状态事件") - .enum_label("尚无事件") - .enum_label("缓存状态已更新") - .enum_label("渲染状态已发布"), - ADMINIVE_FIELD_LABEL(T, event_time_ns, "事件时间"), - ADMINIVE_FIELD_LABEL(T, cache_update_count, "缓存更新次数"), - ADMINIVE_FIELD_LABEL(T, publish_count, "状态发布次数")) - .label("渲染对象观察器"); - } -}; - -static_assert(Described_Type); -static_assert(Described_Type); -static_assert(Described_Type); -static_assert(Described_Type); -static_assert(Described_Type); - -} // namespace adminive diff --git a/web_server/app/Gallery_Protocol.cpp b/web_server/app/Gallery_Protocol.cpp index 9dfdd59..3401aef 100644 --- a/web_server/app/Gallery_Protocol.cpp +++ b/web_server/app/Gallery_Protocol.cpp @@ -1,780 +1,244 @@ -#include #include "Gallery_Protocol.h" -#include "Gallery_Actions.h" -#include "Gallery_Enum.h" -#include "Gallery_Observer_Adminive.h" -#include "Gallery_Renderables.h" -#include "Gallery_Session_Control_Adminive.h" -#include "common/Gallery_Scheduler_Adminive.h" -#include "adminive/adminive.hpp" -#include "adminive/adapters/nlohmann_json.hpp" +#include #include #include -#include -#include -#include -#include -#include -#include +#include #include -#include - -namespace renderive::web::gallery_detail { - -using Json = nlohmann::json; - -struct Case_Model { - std::string id; - std::string title; - std::string component; - std::string category; - std::string description; - int order{}; - int preferred_width = 560; - int preferred_height = 320; -}; - -using Action_Model = Gallery_Action_Model; - -const std::vector& cases() { - static const std::vector value{ - {"axis_lab", "坐标轴实验室", "Axis / Frequency_Axis / Time_Axis", "基础控件", - "普通轴、频率轴和时间轴的布局、格式化、滚轮与拖拽 API。", 10}, - {"spectrum", "实时频谱", "Spectrum", "三种频率图", - "频率控制模式 1:曲线、保持线、扫频区、峰值和自定义 Marker。", 20}, - {"afterglow", "余辉频谱", "Afterglow", "三种频率图", - "频率控制模式 2:二维功率密度、衰减和功率 bin 插值。", 30}, - {"sweep_spectrum", "扫频频谱", "Sweep_Spectrum", "三种频率图", - "频率控制模式 3:分块扫频、当前频率游标和六种线插值。", 40}, - {"waterfall", "瀑布图", "Waterfall", "热力图控件", - "逐行时频热力图;Nearest、Bilinear、Bicubic 三种图像模式均可切换。", 50}, - {"frequency_trace", "时间轨迹", "Frequency_Trace", "曲线控件", - "Time_Axis 驱动的连续轨迹,覆盖时间格式和属性化笔刷 API。", 60}, - {"selection_overlay", "框选叠层", "Selection_Rectangle_Overlay", "交互控件", - "鼠标框选、多区域保留、标注样式和清空。", 70}, - {"constellation", "星座图", "Constellation_Diagram", "点图控件", - "PSK4/PSK8/PSK16 模式、相位、寿命、坐标范围和方形拟合。", 80}, - {"datoviz_3d", "Datoviz Visuals", "Point_Visual", "Datoviz Visuals", - "Renderive 3D Renderable、双缓冲实时数据与 Datoviz Vulkan 后端。", 90}, - {"datoviz_splat", "Splat", "Splat_Visual", "Datoviz Visuals", "各向异性高斯 splat。", 91}, - {"datoviz_pixel", "Pixel", "Pixel_Visual", "Datoviz Visuals", "屏幕空间像素。", 92}, - {"datoviz_marker", "Marker", "Marker_Visual", "Datoviz Visuals", "可旋转的参数化标记。", 93}, - {"datoviz_sphere", "Sphere", "Sphere_Visual", "Datoviz Visuals", "带深度的球体。", 94}, - {"datoviz_segment", "Segment", "Segment_Visual", "Datoviz Visuals", "独立线段。", 95}, - {"datoviz_vector", "Vector", "Vector_Visual", "Datoviz Visuals", "方向向量。", 96}, - {"datoviz_primitive", "Primitive", "Primitive_Visual", "Datoviz Visuals", "可选拓扑的基础图元。", 97}, - {"datoviz_mesh", "Mesh", "Mesh_Visual", "Datoviz Visuals", "法线与纹理坐标网格。", 98}, - {"datoviz_path", "Path", "Path_Visual", "Datoviz Visuals", "连续描边路径。", 99}, - {"datoviz_image", "Image", "Image_Visual", "Datoviz Visuals", "采样图像实例。", 100}, - {"datoviz_labels", "Labels", "Labels_Visual", "Datoviz Visuals", "图集标签实例。", 101}, - {"datoviz_glyph", "Glyph", "Glyph_Visual", "Datoviz Visuals", "MSDF 字形。", 102}, - {"datoviz_text", "Text", "Text_Visual", "Datoviz Visuals", "基于 glyph 的高层文字注记。", 103}, - {"datoviz_volume", "Volume", "Volume_Visual", "Datoviz Visuals", "三维标量场。", 104} - }; - return value; -} - -const Case_Model* find_case(std::string_view id) { - const auto& all = cases(); - const auto found = std::find_if(all.begin(), all.end(), - [id](const Case_Model& item) { return item.id == id; }); - return found == all.end() ? nullptr : &*found; -} - -void append_session_actions(Gallery_Frame_Mode frame_mode, - std::vector& result) { - switch (frame_mode) { - case Gallery_Frame_Mode::Manual: - append_gallery_actions>(result); - return; - case Gallery_Frame_Mode::Low_Latency: - append_gallery_actions>(result); - return; - case Gallery_Frame_Mode::Playback: - append_gallery_actions>(result); - return; - } - ::renderive::error::unexpected("unknown gallery frame mode"); -} -std::size_t session_control_count(std::string_view case_id, Gallery_Frame_Mode frame_mode) { - if (case_id.starts_with("datoviz_")) - return 0; - switch (frame_mode) { - case Gallery_Frame_Mode::Manual: - return gallery_control_count>(); - case Gallery_Frame_Mode::Low_Latency: - return gallery_control_count>(); - case Gallery_Frame_Mode::Playback: - return gallery_control_count>(); - } - ::renderive::error::unexpected("unknown gallery frame mode"); -} -void append_3d_actions(Gallery_Frame_Mode frame_mode, std::vector& result) { - const auto append = [&result](const Gallery_Action_Attribute& action) { - result.push_back(gallery_action_model(action)); - }; - switch (frame_mode) { - case Gallery_Frame_Mode::Manual: - append(gallery_action("mode_prepare", "准备 3D 帧", "Manual_Scene3D::prepare_frame", "3D 帧策略")); - append(gallery_action("mode_refresh", "提交手动刷新", "Manual_Scene3D::refresh", "3D 帧策略")); - append(gallery_action("mode_render", "渲染已刷新帧", "Manual_Scene3D::render_frame", "3D 帧策略", {}, {}, {}, 0.0, true)); - break; - case Gallery_Frame_Mode::Low_Latency: - append(gallery_action("mode_prepare", "发布 3D 帧", "Scene3D::prepare_frame", "3D 帧策略")); - append(gallery_action("mode_render", "消费 3D 帧", "Scene3D::render_frame", "3D 帧策略", {}, {}, {}, 0.0, true)); - break; - case Gallery_Frame_Mode::Playback: - append(gallery_action("mode_enqueue", "压入 3D 帧", "Playback_Scene3D::prepare_frame", "3D 帧策略")); - append(gallery_action("mode_dequeue", "消费队首 3D 帧", "Playback_Scene3D::render_frame", "3D 帧策略", {}, {}, {}, 0.0, true)); - break; - } - append(gallery_action("move_mesh", "移动蓝色对象", "Mesh::place", "3D 场景")); - append(gallery_action("add_mesh", "添加黄色对象", "Scene_Edit::attach", "3D 场景")); - append(gallery_action("remove_mesh", "移除黄色对象", "Scene_Edit::detach", "3D 场景")); - append(gallery_action("mesh_churn", "Attach/Detach 验证", "Scene_Edit::attach/detach", "3D 场景")); - append(gallery_action("reset_camera", "重置相机", "Camera::look_at/use_perspective", "3D 相机")); - append(gallery_action("toggle_projection", "切换投影", "Camera::use_perspective/use_orthographic", "3D 相机")); - append(gallery_action("toggle_texture", "切换纹理", "Material::use_texture", "3D 材质")); - append(gallery_action("change_text", "修改 MSDF 文本", "Text::set_string", "3D 文本")); - append(gallery_action("reset", "恢复 3D 场景", "Gallery_Scene3D factory", "3D 场景")); -} -std::vector registered_actions(std::string_view case_id, Gallery_Frame_Mode frame_mode) { - std::vector result; - if (case_id.starts_with("datoviz_")) { - append_3d_actions(frame_mode, result); - return result; - } - append_session_actions(frame_mode, result); - for (auto& action : gallery_frame_actions(frame_mode)) - result.push_back(std::move(action)); - append_gallery_renderable_actions(case_id, result); - return result; -} - -Json parse_request(std::string_view message) { - return Json::parse(message.begin(), message.end()); -} -adminive::Table_View action_view(); - -} // namespace renderive::web::gallery_detail - -namespace adminive { - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::web::gallery_detail::Case_Model; - return object("renderive_gallery_case", - ADMINIVE_FIELD_LABEL(T, id, "标识"), - ADMINIVE_FIELD_LABEL(T, title, "标题"), - ADMINIVE_FIELD_LABEL(T, component, "Renderive 组件"), - ADMINIVE_FIELD_LABEL(T, category, "分类"), - ADMINIVE_FIELD_LABEL(T, description, "说明"), - ADMINIVE_FIELD_LABEL(T, order, "顺序"), - ADMINIVE_FIELD_LABEL(T, preferred_width, "建议宽度"), - ADMINIVE_FIELD_LABEL(T, preferred_height, "建议高度")) - .label("Renderive 控件用例"); - } -}; - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::web::gallery_detail::Action_Model; - return object("renderive_gallery_action", - ADMINIVE_FIELD_LABEL(T, id, "动作"), - ADMINIVE_FIELD_LABEL(T, label, "名称"), - ADMINIVE_FIELD_LABEL(T, api, "Renderive API"), - ADMINIVE_FIELD_LABEL(T, description, "说明"), - ADMINIVE_FIELD_LABEL(T, group, "分组"), - ADMINIVE_FIELD_LABEL(T, argument_input, "参数类型"), - ADMINIVE_FIELD_LABEL(T, argument_label, "参数名称"), - ADMINIVE_FIELD_LABEL(T, argument_default, "参数默认值"), - ADMINIVE_FIELD_LABEL(T, request_frame, "执行后请求像素帧")) - .label("后端动作菜单"); - } -}; - -} // namespace adminive - -namespace renderive::web::gallery_detail { - -adminive::Table_View action_view() { - using T = Action_Model; - return adminive::table_view( - adminive::column<&T::label>("动作"), - adminive::column<&T::group>("分组"), - adminive::column<&T::api>("Renderive API")) - .titled("保留 API 动作"); -} - -} // namespace renderive::web::gallery_detail namespace renderive::web { namespace { +using Json = nlohmann::json; +struct Case_Info { std::string_view id, title, component, category; }; +constexpr std::array cases{ + Case_Info{"axis_lab", "Axis Lab", "Axis", "2D"}, + Case_Info{"spectrum", "Spectrum", "Spectrum", "2D"}, + Case_Info{"afterglow", "Afterglow", "Afterglow", "2D"}, + Case_Info{"sweep_spectrum", "Sweep Spectrum", "Sweep_Spectrum", "2D"}, + Case_Info{"waterfall", "Waterfall", "Waterfall", "2D"}, + Case_Info{"frequency_trace", "Frequency Trace", "Frequency_Trace", "2D"}, + Case_Info{"selection_overlay", "Selection Overlay", "Selection", "2D"}, + Case_Info{"constellation", "Constellation", "Constellation_Diagram", "2D"}, + Case_Info{"datoviz_3d", "Datoviz 3D", "Point", "3D"}, + Case_Info{"datoviz_splat", "Datoviz Splat", "Splat", "3D"}, + Case_Info{"datoviz_pixel", "Datoviz Pixel", "Pixel", "3D"}, + Case_Info{"datoviz_marker", "Datoviz Marker", "Marker", "3D"}, + Case_Info{"datoviz_sphere", "Datoviz Sphere", "Sphere", "3D"}, + Case_Info{"datoviz_segment", "Datoviz Segment", "Segment", "3D"}, + Case_Info{"datoviz_vector", "Datoviz Vector", "Vector", "3D"}, + Case_Info{"datoviz_primitive", "Datoviz Primitive", "Primitive", "3D"}, + Case_Info{"datoviz_mesh", "Datoviz Mesh", "Mesh", "3D"}, + Case_Info{"datoviz_path", "Datoviz Path", "Path", "3D"}, + Case_Info{"datoviz_image", "Datoviz Image", "Image", "3D"}, + Case_Info{"datoviz_labels", "Datoviz Labels", "Labels", "3D"}, + Case_Info{"datoviz_glyph", "Datoviz Glyph", "Glyph", "3D"}, + Case_Info{"datoviz_text", "Datoviz Text", "Text", "3D"}, + Case_Info{"datoviz_volume", "Datoviz Volume", "Volume", "3D"}, +}; -using gallery_detail::Json; - -Json protocol_base() { - return {{"category", "gallery"}, - {"protocol", "renderive.control-gallery"}, - {"protocol_version", 5}}; -} - -Json case_contract(std::string_view case_id) { - const auto* item = gallery_detail::find_case(case_id); - return item ? adminive::to_frontend_json(*item) : Json::object(); -} - -Json dashboard_field(std::string_view label, std::string_view source, - std::string_view format = "integer", int digits = -1) { - Json result{{"label", label}, {"source", source}, {"format", format}}; - if (digits >= 0) - result["digits"] = digits; - return result; -} -Json dashboard_trend_field(std::string_view label, std::string_view source, - std::string_view trend_group, - std::string_view format = "integer", int digits = -1) { - Json result = dashboard_field(label, source, format, digits); - result["trend_group"] = trend_group; - return result; -} -Json dashboard_summary_field(std::string_view label, std::string_view source, - std::string_view trend_group, - std::string_view format = "integer", int digits = -1) { - Json result = dashboard_trend_field(label, source, trend_group, format, digits); - result["summary"] = true; - return result; -} - -template -const Json& described_field(std::string_view name) { - static const Json descriptor = adminive::to_descriptor_json(); - const auto& fields = descriptor.at("fields"); - const auto found = std::find_if(fields.begin(), fields.end(), [name](const Json& field) { - return field.at("name").template get() == name; - }); - if (found == fields.end()) - ::renderive::error::unexpected("missing Adminive dashboard field: " + std::string(name)); - return *found; -} - -template -std::string described_source(std::string_view prefix, std::string_view name) { - static_cast(described_field(name)); - return std::string(prefix) + "." + std::string(name); -} - -template -Json described_dashboard_field(std::string_view prefix, std::string_view name) { - const Json& field = described_field(name); - const std::string value_type = field.at("value_type"); - std::string format = "integer"; - if (name.ends_with("_ns")) - format = "nanoseconds"; - else if (name.ends_with("_fps")) - format = "fps"; - else if (name.ends_with("frequency_hz")) - format = "frequency"; - else if (name == "source") - format = "flags"; - else if (value_type == "boolean" || name == "limit_state") - format = "enum"; - else if (value_type == "string" || value_type == "enum") - format = "text"; - Json result = dashboard_field( - field.at("presentation").at("label").template get(), - described_source(prefix, name), format, - name.ends_with("_fps") ? 2 : -1); - if (value_type == "boolean") - result["value_map"] = "enabled"; - else if (name == "limit_state") - result["value_map"] = "limit_state"; - else if (name == "source") - result["value_map"] = "consumer_feedback_source"; - return result; -} - -bool observer_header_field(std::string_view name) { - return name == "mode" || name == "last_event" || name == "limit_state"; -} - -bool observer_counter_field(std::string_view name) { - return name == "frequency_hz" || name == "latest_sequence" || - name.ends_with("_count"); -} - -bool observer_detail_field(std::string_view name) { - return name.ends_with("_wait_ns") || name == "frame_age_at_render_ns"; -} - -template -Json observer_fields(Predicate&& predicate) { - const Json descriptor = - adminive::to_descriptor_json(); - Json result = Json::array(); - for (const Json& field : descriptor.at("fields")) { - const std::string name = field.at("name"); - if (predicate(name)) { - Json field = described_dashboard_field( - "kernel_observer", name); - if (name == "frequency_hz") { - field["enabled_source"] = - described_source( - "kernel_observer", "frequency_limit_enabled"); - field["disabled_label"] = "已关闭"; - } else if (name == "end_to_end_ns") { - field["cell_class"] = "critical"; - } - result.push_back(std::move(field)); - } - } - return result; -} - -Json consumer_feedback_fields() { - const Json descriptor = - adminive::to_descriptor_json(); - Json result = Json::array(); - for (const Json& field : descriptor.at("fields")) { - const std::string name = field.at("name"); - result.push_back(described_dashboard_field( - "consumer_feedback", name)); - } - return result; -} - -Json observer_summary_fields() { - Json result = observer_fields([](std::string_view name) { - return !observer_header_field(name) && !observer_counter_field(name) && - !observer_detail_field(name); - }); - for (Json& field : consumer_feedback_fields()) - result.push_back(std::move(field)); - return result; -} - -Json dashboard_contract() { - Json point_pair{{"label", "输入→绘制"}, {"format", "pair"}, - {"sources", Json::array({"data_shape.input_points", "data_shape.rendered_elements"})}, - {"separator", "→"}}; - Json bottleneck = described_dashboard_field( - "kernel_observer", "limit_state"); - bottleneck["format"] = "duration_enum"; - bottleneck["duration_sources"] = { - {"frequency_limited", described_source( - "kernel_observer", "target_interval_ns")}, - {"paint_limited", described_source( - "kernel_observer", "paint_duration_ns")}, - {"render_limited", described_source( - "kernel_observer", "render_duration_ns")}, - {"consumer_limited", described_source( - "kernel_observer", "consumer_interval_ns")} - }; - Json dashboard{ - {"value_maps", { - {"enabled", {{"true", "启用"}, {"false", "关闭"}}}, - {"consumer_feedback_source", { - {"disabled", "总开关关闭"}, {"none", "无"}, {"pixel", "像素响应"}, - {"presentation", "浏览器呈现"}, {"manual", "手动"} - }}, - {"limit_state", { - {"frequency_limited", "内核频率受限"}, {"paint_limited", "绘制事件受限"}, - {"render_limited", "后台渲染受限"}, {"consumer_limited", "消费者反馈受限"}, - {"unlimited", "无限制"}, {"not_applicable", "N/A"} - }} - }}, - {"performance", { - {"fields", Json::array({ - dashboard_summary_field("后端渲染 FPS", "performance.measured_fps", "FPS", "fixed", 1), - dashboard_summary_field("像素响应 FPS", "performance.pixel_response_fps", "FPS", "fixed", 1), - dashboard_trend_field("浏览器呈现 FPS", "client_performance.presentation_fps", "FPS", "fixed", 1), - dashboard_trend_field("WS 往返 ms", "client_performance.frame_round_trip_ms", "帧延迟", "fixed", 2), - dashboard_field("未呈现覆盖", "client_performance.overwritten_pixel_frames"), - dashboard_summary_field("帧完成延迟 ms", "performance.last_render_ms", "帧延迟", "fixed", 2), - dashboard_field("Core 滑动平均 ms", "performance.average_render_ms", "fixed", 2), - dashboard_field("Core P95 ms", "performance.render_p95_ms", "fixed", 2), - dashboard_field("Core P99 ms", "performance.render_p99_ms", "fixed", 2), - dashboard_summary_field("像素编码 ms", "performance.last_pixel_encode_ms", "帧延迟", "fixed", 2), - dashboard_field("编码 P95 ms", "performance.pixel_encode_p95_ms", "fixed", 2), - dashboard_field("编码 P99 ms", "performance.pixel_encode_p99_ms", "fixed", 2), - dashboard_field("WS 平均 ms", "client_performance.frame_round_trip_average_ms", "fixed", 2), - dashboard_field("WS P95 ms", "client_performance.frame_round_trip_p95_ms", "fixed", 2), - dashboard_field("WS P99 ms", "client_performance.frame_round_trip_p99_ms", "fixed", 2), - dashboard_field("呈现平均 ms", "client_performance.display_interval_average_ms", "fixed", 2), - dashboard_field("呈现 P95 ms", "client_performance.display_interval_p95_ms", "fixed", 2), - dashboard_field("呈现 P99 ms", "client_performance.display_interval_p99_ms", "fixed", 2), - dashboard_field("响应负载 MB/s", "performance.pixel_payload_megabytes_per_second", "fixed", 1), - dashboard_field("TBB 并发度", "scheduler.concurrency"), - dashboard_trend_field("活动 Worker", "scheduler.active_workers", "调度与队列"), - dashboard_field("峰值 Worker", "scheduler.peak_workers"), - dashboard_field("活动外部线程", "scheduler.active_external_threads"), - dashboard_trend_field("Scene 排队", "queue_pressure.scene_execution.queued", "调度与队列"), - dashboard_trend_field("Render Domain 排队", "queue_pressure.render_domain.queued", "调度与队列"), - dashboard_trend_field("GPU Completion 活动", "queue_pressure.gpu_completion.active", "调度与队列"), - dashboard_field("Scene Backpressure", "queue_pressure.scene_execution.backpressure_count"), - dashboard_field("Render Backpressure", "queue_pressure.render_domain.backpressure_count"), - dashboard_field("GPU Backpressure", "queue_pressure.gpu_completion.backpressure_count"), - described_dashboard_field( - "kernel_observer", "pending_frame_count"), - described_dashboard_field( - "kernel_observer", "dropped_frame_count"), - std::move(point_pair), std::move(bottleneck), - described_dashboard_field( - "kernel_observer", "last_event"), - dashboard_field("像素超时", "client_performance.frame_request_timeout_count"), - dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 0) - })} - }}, - {"menu_views", { - {"observer", { - {"kernel", { - {"title", "内核帧观察器"}, - {"source", "kernel_observer"}, - {"descriptor", adminive::to_descriptor_json< - Json, renderive::Plot_Frame_Status>()} - }}, - {"renderables_source", "renderable_observers"}, - {"renderable_fields", Json::array({ - "event", "event_time_ns", "cache_update_count", "publish_count" - })} - }}, - {"performance", { - {"renderables_source", "renderable_observers"}, - {"renderable_fields", Json::array({ - "event", "event_time_ns", "cache_update_count", "publish_count" - })}, - {"resources", Json::array({ - { - {"title", "渲染性能"}, - {"source", "performance"}, - {"descriptor", adminive::to_descriptor_json< - Json, Gallery_Render_Performance>()} - }, - { - {"title", "浏览器性能"}, - {"source", "client_performance"}, - {"descriptor", adminive::to_descriptor_json< - Json, Gallery_Client_Performance>()} - }, - { - {"title", "oneTBB 调度器"}, - {"source", "scheduler"}, - {"descriptor", adminive::to_descriptor_json< - Json, renderive::scheduling::Scheduler_Statistics>()} - } - })} - }} - }}, - {"limits", { - {"aria_label", "低延迟限速来源"}, {"title", "限速来源"}, - {"current_source", described_source( - "kernel_observer", "limit_state")}, - {"active_label", "当前瓶颈"}, {"inactive_label", "未受限"}, - {"disabled_label", "已关闭"}, - {"fields", Json::array({ - {{"label", "内核用户频率"}, {"active_value", "frequency_limited"}, - {"duration_source", described_source( - "kernel_observer", "target_interval_ns")}, - {"enabled_source", described_source( - "kernel_observer", "frequency_limit_enabled")}}, - {{"label", "内核绘制事件"}, {"active_value", "paint_limited"}, - {"duration_source", described_source( - "kernel_observer", "paint_duration_ns")}}, - {{"label", "内核后台渲染"}, {"active_value", "render_limited"}, - {"duration_source", described_source( - "kernel_observer", "render_duration_ns")}}, - {{"label", "消费者反馈"}, {"active_value", "consumer_limited"}, - {"duration_source", described_source( - "kernel_observer", "consumer_interval_ns")}, - {"enabled_source", described_source( - "kernel_observer", "consumer_feedback_enabled")}} - })} - }}, - {"observer", { - {"aria_label", "内核低延迟全量统计"}, - {"header", { - {"prefix", "内核"}, {"suffix", "观察器"}, {"event_label", "事件"}, - {"mode", described_dashboard_field( - "kernel_observer", "mode")}, - {"limit", described_dashboard_field( - "kernel_observer", "limit_state")}, - {"event", described_dashboard_field( - "kernel_observer", "last_event")} - }}, - {"sections", Json::array({ - {{"class_name", "observer-counters"}, - {"fields", observer_fields(observer_counter_field)}}, - {{"class_name", "latency-summary"}, - {"fields", observer_summary_fields()}}, - {{"class_name", "client-summary"}, {"aria_label", "浏览器消费者反馈全量统计"}, - {"fields", Json::array({ - dashboard_field("RAF 最新周期", "client_performance.display_interval_latest_ms", "milliseconds", 3), - dashboard_field("RAF 中位周期", "client_performance.display_interval_ms", "milliseconds", 3), - dashboard_field("RAF 平均周期", "client_performance.display_interval_average_ms", "milliseconds", 3), - dashboard_field("RAF P95 周期", "client_performance.display_interval_p95_ms", "milliseconds", 3), - dashboard_field("RAF P99 周期", "client_performance.display_interval_p99_ms", "milliseconds", 3), - dashboard_field("RAF 周期标准差", "client_performance.display_interval_deviation_ms", "milliseconds", 3), - dashboard_field("WS 往返", "client_performance.frame_round_trip_ms", "milliseconds", 3), - dashboard_field("WS 往返平均", "client_performance.frame_round_trip_average_ms", "milliseconds", 3), - dashboard_field("WS 往返 P95", "client_performance.frame_round_trip_p95_ms", "milliseconds", 3), - dashboard_field("WS 往返 P99", "client_performance.frame_round_trip_p99_ms", "milliseconds", 3), - dashboard_field("WS 往返标准差", "client_performance.frame_round_trip_deviation_ms", "milliseconds", 3), - dashboard_field("像素响应 FPS", "client_performance.transport_fps", "fps", 2), - dashboard_field("浏览器呈现 FPS", "client_performance.presentation_fps", "fps", 2), - dashboard_field("WS 缓冲", "client_performance.websocket_buffered_bytes", "bytes"), - dashboard_field("未呈现覆盖", "client_performance.overwritten_pixel_frames"), - dashboard_field("变化像素帧", "client_performance.changed_pixel_frames"), - dashboard_field("重复像素帧", "client_performance.duplicate_pixel_frames"), - dashboard_field("像素请求超时", "client_performance.frame_request_timeout_count"), - dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 3), - dashboard_field("最近变化龄", "client_performance.last_pixel_change_age_ms", "milliseconds", 3) - })}}, - {{"class_name", "latency-details"}, {"aria_label", "内核各阶段等待耗时"}, - {"fields", observer_fields(observer_detail_field)}} - })} - }} - }; - dashboard["observer"]["descriptor"] = - adminive::to_descriptor_json(); - dashboard["observer"]["consumer_feedback_descriptor"] = - adminive::to_descriptor_json(); - return dashboard; -} - -Json frame_mode_contract(Gallery_Frame_Mode mode) { +std::string_view mode_id(Gallery_Frame_Mode mode) { switch (mode) { - case Gallery_Frame_Mode::Manual: - return {{"id", gallery_enum_id(mode)}, {"title", "手动刷新"}, - {"strategy", "Manual_Refresh_Strategy"}, - {"description", "显式准备、刷新和渲染;页面不会自动拉取像素。"}, - {"automatic", false}, {"request_after_response", false}, - {"request_on_animation_frame", false}, {"observer_visible", false}, - {"frame_button_label", "手动刷新一帧"}, {"accent", "#ffd166aa"}, {"order", 10}}; - case Gallery_Frame_Mode::Low_Latency: - return {{"id", gallery_enum_id(mode)}, {"title", "低延迟"}, - {"strategy", "Low_Latency_Strategy"}, - {"description", "以最大 FPS 自动发布并消费最新帧,可观测丢帧与端到端延迟。"}, - {"automatic", true}, {"request_after_response", true}, - {"request_on_animation_frame", false}, {"observer_visible", true}, - {"frame_button_label", "立即刷新"}, {"accent", "#45ddbeaa"}, {"order", 20}}; - case Gallery_Frame_Mode::Playback: - return {{"id", gallery_enum_id(mode)}, {"title", "回放队列"}, - {"strategy", "Flow_Refresh_Strategy"}, - {"description", "所有帧按队列顺序入队和消费,可观测队深与排队时间。"}, - {"automatic", true}, {"request_after_response", true}, - {"request_on_animation_frame", true}, {"observer_visible", false}, - {"frame_button_label", "消费下一帧"}, {"accent", "#6aa9ffaa"}, {"order", 30}}; + case Gallery_Frame_Mode::Manual: return "manual"; + case Gallery_Frame_Mode::Low_Latency: return "low_latency"; + case Gallery_Frame_Mode::Playback: return "playback"; } - return Json::object(); + return "low_latency"; +} +std::optional parse_mode(std::string_view value) { + if (value == "manual") return Gallery_Frame_Mode::Manual; + if (value == "low_latency") return Gallery_Frame_Mode::Low_Latency; + if (value == "playback") return Gallery_Frame_Mode::Playback; + return std::nullopt; +} +Json parse_object(std::string_view text) { + Json value = Json::parse(text, nullptr, false); + return value.is_object() ? std::move(value) : Json::object(); +} +Json actions() { + return Json::array({ + {{"id", "frame_strategy_low_latency"}, {"label", "Low latency"}, + {"api", "Renderable_Editor::replace_frame_control_strategy"}, + {"group", "Frame strategy"}, {"request_frame", true}}, + {{"id", "frame_strategy_manual"}, {"label", "Manual"}, + {"api", "Renderable_Editor::replace_frame_control_strategy"}, + {"group", "Frame strategy"}, {"request_frame", true}}, + {{"id", "frame_strategy_playback"}, {"label", "Playback"}, + {"api", "Renderable_Editor::replace_frame_control_strategy"}, + {"group", "Frame strategy"}, {"request_frame", true}}, + {{"id", "mode_render"}, {"label", "Render frame"}, + {"api", "Scene::render"}, {"group", "Frame"}, {"request_frame", true}}, + {{"id", "capture_next_frame"}, {"label", "Capture next frame"}, + {"api", "Scene_Base::capture_next_frame"}, {"group", "Performance Capture"}, + {"request_frame", true}}, + {{"id", "capture_frames"}, {"label", "Capture N frames"}, + {"api", "Scene_Base::capture_frames"}, {"group", "Performance Capture"}, + {"argument_input", "number"}, {"argument_default", 20}, {"request_frame", true}}, + {{"id", "reset"}, {"label", "Reset scene"}, + {"api", "Scene::Builder"}, {"group", "Scene"}, {"request_frame", true}} + }); +} +template +Gallery_Request_Result fail(Gallery_Request_Error error) { + return {.value = std::nullopt, .error = error}; } - } // namespace std::string Gallery_Protocol::catalog_json() { - Json result = protocol_base(); - result["type"] = "catalog"; - result["transport"] = {{"events", "websocket-text"}, {"pixels", "websocket-binary-rvp2"}, - {"http_api", false}, {"socket_per_canvas", true}}; - result["navigation"] = { - {"default_mode", gallery_enum_id(Gallery_Frame_Mode::Low_Latency)}, - {"all_categories_label", "全部"}, - {"catalog_loaded_text", "Kernel 策略目录已加载"}, - {"hero_eyebrow", "REAL KERNEL STRATEGIES"}, - {"hero_title", "对称策略页面,同一批控件,直接比较"} - }; - result["dashboard"] = dashboard_contract(); - result["case_descriptor"] = - adminive::to_descriptor_json(); - result["frame_modes"] = Json::array(); - constexpr auto frame_modes = magic_enum::enum_values(); - for (const auto mode : frame_modes) - result["frame_modes"].push_back(frame_mode_contract(mode)); - result["cases"] = Json::array(); - std::size_t controls_total{}; - std::size_t actions_total{}; - for (const auto& item : gallery_detail::cases()) { - Json entry = adminive::to_frontend_json(item); - entry["control_count_by_mode"] = Json::object(); - entry["action_count_by_mode"] = Json::object(); - const std::size_t renderable_controls = item.id.starts_with("datoviz_") ? 0 : gallery_renderable_control_count(item.id); - for (const auto mode : frame_modes) { - const std::string key = gallery_enum_id(mode); - const std::size_t controls = - gallery_detail::session_control_count(item.id, mode) + renderable_controls; - const std::size_t actions = - gallery_detail::registered_actions(item.id, mode).size(); - entry["control_count_by_mode"][key] = controls; - entry["action_count_by_mode"][key] = actions; - controls_total += controls; - actions_total += actions; - } - entry["control_count"] = - gallery_detail::session_control_count(item.id, Gallery_Frame_Mode::Low_Latency) + - renderable_controls; - entry["action_count"] = - gallery_detail::registered_actions( - item.id, Gallery_Frame_Mode::Low_Latency).size(); - result["cases"].push_back(std::move(entry)); + Json published_cases = Json::array(); + for (std::size_t index = 0; index < cases.size(); ++index) { + const auto& item = cases[index]; + published_cases.push_back({ + {"id", item.id}, {"title", item.title}, {"component", item.component}, + {"category", item.category}, {"description", "Live Renderive scene"}, + {"order", index}, + {"control_count_by_mode", {{"manual", 0}, {"low_latency", 0}, {"playback", 0}}}, + {"action_count_by_mode", {{"manual", 7}, {"low_latency", 7}, {"playback", 7}}} + }); } - result["coverage"] = {{"case_count", gallery_detail::cases().size()}, - {"page_count", std::size(frame_modes)}, - {"canvas_count", gallery_detail::cases().size() * std::size(frame_modes)}, - {"manual_control_count", controls_total}, - {"manual_action_count", actions_total}, - {"frequency_modes", Json::array({"spectrum", "afterglow", "sweep_spectrum"})}, - {"image_interpolation_modes", magic_enum::enum_names()}}; - return result.dump(); + Json field_mode{{"label", "Mode"}, {"source", "kernel_observer.mode"}}; + Json field_limit{{"label", "Limit"}, {"source", "kernel_observer.limit"}}; + Json field_event{{"label", "Event"}, {"source", "kernel_observer.event"}}; + Json root{ + {"category", "gallery"}, {"type", "catalog"}, + {"protocol", "renderive.control-gallery"}, {"protocol_version", 5}, + {"transport", {{"pixels", "websocket-binary-rvp2"}, {"socket_per_canvas", true}}}, + {"navigation", {{"default_mode", "low_latency"}, {"all_categories_label", "All"}, + {"hero_eyebrow", "Renderive"}, {"hero_title", "Scene Gallery"}, + {"catalog_loaded_text", "Gallery ready"}}}, + {"frame_modes", Json::array({ + {{"id", "manual"}, {"title", "Manual"}, {"strategy", "Manual"}, + {"description", "Explicit frame requests"}, {"order", 0}, {"accent", "#ffbd69"}, + {"automatic", false}, {"request_after_response", false}, + {"request_on_animation_frame", false}, {"observer_visible", true}, + {"frame_button_label", "Render frame"}}, + {{"id", "low_latency"}, {"title", "Low latency"}, {"strategy", "Low_Latency"}, + {"description", "Latest-state rendering"}, {"order", 1}, {"accent", "#59d6c5"}, + {"automatic", true}, {"request_after_response", true}, + {"request_on_animation_frame", false}, {"observer_visible", true}, + {"frame_button_label", "Render frame"}}, + {{"id", "playback"}, {"title", "Playback"}, {"strategy", "Flow"}, + {"description", "Ordered frame playback"}, {"order", 2}, {"accent", "#84b3ce"}, + {"automatic", false}, {"request_after_response", false}, + {"request_on_animation_frame", true}, {"observer_visible", true}, + {"frame_button_label", "Render frame"}} + })}, + {"cases", std::move(published_cases)}, + {"coverage", {{"case_count", cases.size()}, {"page_count", 1}, + {"canvas_count", cases.size()}, {"manual_control_count", 0}, + {"manual_action_count", cases.size() * 7}}}, + {"dashboard", { + {"performance", {{"fields", Json::array({ + {{"label", "Rendered"}, {"source", "performance.successful_render_count"}, + {"format", "integer"}, {"summary", true}}, + {{"label", "Transport FPS"}, {"source", "client_performance.transport_fps"}, + {"digits", 1}, {"summary", true}} + })}}}, + {"limits", {{"title", "Frame limits"}, {"aria_label", "Frame limits"}, + {"current_source", "kernel_observer.limit"}, {"active_label", "active"}, + {"inactive_label", "inactive"}, {"disabled_label", "disabled"}, + {"fields", Json::array()}}}, + {"observer", {{"aria_label", "Kernel observer"}, + {"header", {{"prefix", ""}, {"suffix", ""}, {"event_label", ""}, + {"mode", field_mode}, {"limit", field_limit}, + {"event", field_event}}}, + {"sections", Json::array()}}}, + {"menu_views", Json::object()} + }} + }; + return root.dump(); } bool Gallery_Protocol::is_case(std::string_view case_id) { - return gallery_detail::find_case(case_id) != nullptr; -} - -std::string Gallery_Protocol::case_json_from_controls( - std::string_view case_id, - std::string_view controls_json, - std::string_view telemetry_json, - std::string_view notice, - Gallery_Frame_Mode frame_mode, - bool manual_refresh) { - Json result = protocol_base(); - result["type"] = manual_refresh ? "refresh_state" : "case_state"; - result["case"] = case_contract(case_id); - result["frame_mode"] = frame_mode_contract(frame_mode); - result["controls"] = Json::parse(controls_json.begin(), controls_json.end()); - result["actions"] = { - {"descriptor", adminive::to_descriptor_json()}, - {"view", adminive::to_view_json( - gallery_detail::action_view())}, - {"data", Json::array()} - }; - for (const auto& action : gallery_detail::registered_actions(case_id, frame_mode)) - result["actions"]["data"].push_back( - adminive::to_frontend_json(action)); - result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end()); - if (!notice.empty()) - result["notice"] = notice; - return result.dump(); + return std::ranges::any_of(cases, [case_id](const auto& item) { + return item.id == case_id; + }); } std::string Gallery_Protocol::error_json(std::string_view message, std::string_view field) { - Json result = protocol_base(); - result["type"] = "error"; - result["message"] = message; - result["field_errors"] = Json::object(); - if (!field.empty()) - result["field_errors"][std::string(field)] = message; - return result.dump(); + Json root{{"category", "gallery"}, {"type", "error"}, {"message", message}}; + if (!field.empty()) root["field_errors"] = {{field, message}}; + return root.dump(); } std::string Gallery_Protocol::observer_json(std::string_view case_id, Gallery_Frame_Mode frame_mode, std::string_view telemetry_json) { - Json result = protocol_base(); - result["type"] = "observer_state"; - result["case_id"] = case_id; - result["frame_mode"] = frame_mode_contract(frame_mode); - result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end()); - return result.dump(); + return Json{{"category", "gallery"}, {"type", "observer_state"}, + {"case", case_id}, {"frame_mode", mode_id(frame_mode)}, + {"telemetry", parse_object(telemetry_json)}}.dump(); } -Gallery_Request_Result Gallery_Protocol::open_request(std::string_view message) { - Json request; - try { - request = gallery_detail::parse_request(message); - } catch (const Json::exception&) { - return {{}, Gallery_Request_Error::invalid_json}; - } - if (!request.is_object() || !request.contains("category") || - !request.at("category").is_string() || - request.at("category").get_ref() != "event" || - !request.contains("type") || !request.at("type").is_string() || - request.at("type").get_ref() != "gallery_open") - return {{}, Gallery_Request_Error::wrong_message_type}; - if (!request.contains("case") || !request.contains("frame_mode")) - return {{}, Gallery_Request_Error::missing_field}; - if (!request.at("case").is_string() || !request.at("frame_mode").is_string()) - return {{}, Gallery_Request_Error::invalid_field}; - const std::string value = request.at("case").get(); - const auto mode = gallery_enum_cast( - request.at("frame_mode").get_ref()); - if (!is_case(value) || !mode) - return {{}, Gallery_Request_Error::invalid_field}; - return {Gallery_Open_Request{value, *mode}, Gallery_Request_Error::none}; +std::string Gallery_Protocol::case_json_from_controls( + std::string_view case_id, std::string_view controls_json, + std::string_view telemetry_json, std::string_view notice, + Gallery_Frame_Mode frame_mode, bool manual_refresh) { + return Json{{"category", "gallery"}, + {"type", manual_refresh ? "refresh_state" : "case_state"}, + {"case", case_id}, {"frame_mode", mode_id(frame_mode)}, + {"controls", parse_object(controls_json)}, + {"actions", {{"data", actions()}}}, + {"telemetry", parse_object(telemetry_json)}, {"notice", notice}}.dump(); +} + +Gallery_Request_Result Gallery_Protocol::open_request( + std::string_view message) { + const Json root = Json::parse(message, nullptr, false); + if (!root.is_object()) return fail(Gallery_Request_Error::invalid_json); + if (root.value("category", "") != "event" || root.value("type", "") != "gallery_open") + return fail(Gallery_Request_Error::wrong_message_type); + if (!root.contains("case") || !root["case"].is_string() || + !root.contains("frame_mode") || !root["frame_mode"].is_string()) + return fail(Gallery_Request_Error::missing_field); + const auto case_id = root["case"].get(); + const auto mode = parse_mode(root["frame_mode"].get()); + if (!is_case(case_id) || !mode) + return fail(Gallery_Request_Error::invalid_field); + return {.value = Gallery_Open_Request{case_id, *mode}, + .error = Gallery_Request_Error::none}; } Gallery_Request_Result Gallery_Protocol::action_request( std::string_view message) { - Json request; - try { - request = gallery_detail::parse_request(message); - } catch (const Json::exception&) { - return {{}, Gallery_Request_Error::invalid_json}; + const Json root = Json::parse(message, nullptr, false); + if (!root.is_object()) return fail(Gallery_Request_Error::invalid_json); + if (root.value("category", "") != "event" || root.value("type", "") != "gallery_action") + return fail(Gallery_Request_Error::wrong_message_type); + if (!root.contains("action") || !root["action"].is_string()) + return fail(Gallery_Request_Error::missing_field); + Gallery_Action_Request request{root["action"].get(), std::nullopt}; + if (const auto argument = root.find("argument"); argument != root.end()) { + if (argument->is_boolean()) request.argument = argument->get(); + else if (argument->is_number()) request.argument = argument->get(); + else if (argument->is_string()) request.argument = argument->get(); + else return fail(Gallery_Request_Error::invalid_field); } - if (!request.is_object() || !request.contains("category") || - !request.at("category").is_string() || - request.at("category").get_ref() != "event" || - !request.contains("type") || !request.at("type").is_string() || - request.at("type").get_ref() != "gallery_action") - return {{}, Gallery_Request_Error::wrong_message_type}; - if (!request.contains("action")) - return {{}, Gallery_Request_Error::missing_field}; - if (!request.at("action").is_string()) - return {{}, Gallery_Request_Error::invalid_field}; - Gallery_Action_Request result{request.at("action").get()}; - if (request.contains("argument")) { - const Json& argument = request.at("argument"); - if (argument.is_boolean()) - result.argument = argument.get(); - else if (argument.is_number()) { - const double value = argument.get(); - if (!std::isfinite(value)) - return {{}, Gallery_Request_Error::invalid_field}; - result.argument = value; - } else if (argument.is_string()) - result.argument = argument.get(); - else - return {{}, Gallery_Request_Error::invalid_field}; - } - return {std::move(result), Gallery_Request_Error::none}; + return {.value = std::move(request), .error = Gallery_Request_Error::none}; } -Gallery_Request_Result Gallery_Protocol::control_patch_request( - std::string_view message) { - Json request; - try { - request = gallery_detail::parse_request(message); - } catch (const Json::exception&) { - return {{}, Gallery_Request_Error::invalid_json}; - } - if (!request.is_object() || !request.contains("category") || - !request.at("category").is_string() || - request.at("category").get_ref() != "event" || - !request.contains("type") || !request.at("type").is_string() || - request.at("type").get_ref() != "gallery_patch") - return {{}, Gallery_Request_Error::wrong_message_type}; - if (!request.contains("target") || !request.contains("patch")) - return {{}, Gallery_Request_Error::missing_field}; - if (!request.at("target").is_string() || !request.at("patch").is_object()) - return {{}, Gallery_Request_Error::invalid_field}; - return {Gallery_Control_Patch_Request{ - request.at("target").get(), request.at("patch").dump()}, - Gallery_Request_Error::none}; +Gallery_Request_Result +Gallery_Protocol::control_patch_request(std::string_view message) { + const Json root = Json::parse(message, nullptr, false); + if (!root.is_object()) return fail(Gallery_Request_Error::invalid_json); + if (root.value("category", "") != "event" || root.value("type", "") != "gallery_patch") + return fail(Gallery_Request_Error::wrong_message_type); + if (!root.contains("target") || !root["target"].is_string() || + !root.contains("patch") || !root["patch"].is_object()) + return fail(Gallery_Request_Error::missing_field); + return {.value = Gallery_Control_Patch_Request{ + root["target"].get(), root["patch"].dump()}, + .error = Gallery_Request_Error::none}; } bool Gallery_Protocol::action_available(std::string_view case_id, - Gallery_Frame_Mode frame_mode, + Gallery_Frame_Mode, std::string_view action_id) { - const auto available = gallery_detail::registered_actions(case_id, frame_mode); - return std::any_of(available.begin(), available.end(), - [action_id](const gallery_detail::Action_Model& item) { - return item.id == action_id; - }); + return is_case(case_id) && + (action_id == "reset" || action_id == "mode_render" || + action_id == "capture_next_frame" || action_id == "capture_frames" || + action_id == "frame_strategy_low_latency" || + action_id == "frame_strategy_manual" || + action_id == "frame_strategy_playback"); } - } // namespace renderive::web diff --git a/web_server/app/Gallery_Renderable_Types.h b/web_server/app/Gallery_Renderable_Types.h deleted file mode 100644 index 8c4c4a0..0000000 --- a/web_server/app/Gallery_Renderable_Types.h +++ /dev/null @@ -1,142 +0,0 @@ -#pragma once - -#include "render_2D/axis/Axis.h" -#include "render_2D/axis/Frequency_Axis.h" -#include "render_2D/axis/Time_Axis.h" -#include "render_2D/plottable/Afterglow.h" -#include "render_2D/plottable/Constellation_Diagram.h" -#include "render_2D/plottable/Frequency_Trace.h" -#include "render_2D/plottable/Selection_Rectangle_Overlay.h" -#include "render_2D/plottable/Spectrum.h" -#include "render_2D/plottable/Sweep_Spectrum.h" -#include "render_2D/plottable/Waterfall.h" -#include - -#include -#include - -namespace renderive::web { - -template -struct Gallery_Property_Accessor { - using object_type = Object; - using Properties = typename Object::Properties; - using value_type = std::remove_cvref_t().*Member)>; - using storage_identity = void; - using dependency_spec = structive::No_Property_Dependencies; - static constexpr bool readable = true; - static constexpr bool writable = true; - static constexpr bool synchronized_view_read = false; - static constexpr bool trusted_object_access = true; - - value_type read(const Object& object) const { - return object.template adminive_read(); - } - void write(Object& object, value_type value) const { - object.template adminive_write(std::move(value)); - } -}; - -template -struct Gallery_Plottable : Base { - using Properties = typename Base::Properties; - using Base::Base; - -private: - template - auto adminive_read() const { - return this->template get(); - } - template - void adminive_write(Value&& value) { - this->template set(std::forward(value)); - } - template - friend struct Gallery_Property_Accessor; -}; - -class Gallery_Spectrum final : public Gallery_Plottable { -public: - using Gallery_Plottable::Gallery_Plottable; - using Builder = Renderable_Builder; -}; - -class Gallery_Waterfall final : public Gallery_Plottable { -public: - using Gallery_Plottable::Gallery_Plottable; - using Builder = Renderable_Builder; -}; - -class Gallery_Afterglow final : public Gallery_Plottable { -public: - using Gallery_Plottable::Gallery_Plottable; - using Builder = Renderable_Builder; -}; - -class Gallery_Sweep_Spectrum final : public Gallery_Plottable { -public: - using Gallery_Plottable::Gallery_Plottable; - using Builder = Renderable_Builder; -}; - -class Gallery_Frequency_Trace final : public Gallery_Plottable { -public: - using Gallery_Plottable::Gallery_Plottable; - using Builder = Renderable_Builder; -}; - -class Gallery_Selection_Rectangle_Overlay final - : public Gallery_Plottable { -public: - using Gallery_Plottable::Gallery_Plottable; - using Builder = Renderable_Builder; -}; - -class Gallery_Constellation_Diagram final - : public Gallery_Plottable { -public: - using Gallery_Plottable::Gallery_Plottable; - using Builder = Renderable_Builder; -}; - -template -struct Gallery_Axis_Base : Base { - using Properties = Properties_Type; - using Base::Base; - -private: - template - auto adminive_read() const { - return this->read([](const auto& state) { - return state.*Member; - }); - } - template - void adminive_write(Value&& value) { - this->template set(std::forward(value)); - } - template - friend struct Gallery_Property_Accessor; -}; - -class Gallery_Axis final : public Gallery_Axis_Base { -public: - using Gallery_Axis_Base::Gallery_Axis_Base; - using Builder = Renderable_Builder; -}; - -class Gallery_Frequency_Axis final - : public Gallery_Axis_Base { -public: - using Gallery_Axis_Base::Gallery_Axis_Base; - using Builder = Renderable_Builder; -}; - -class Gallery_Time_Axis final - : public Gallery_Axis_Base { -public: - using Gallery_Axis_Base::Gallery_Axis_Base; - using Builder = Renderable_Builder; -}; - -} // namespace renderive::web diff --git a/web_server/app/Gallery_Renderables.h b/web_server/app/Gallery_Renderables.h deleted file mode 100644 index 8768d56..0000000 --- a/web_server/app/Gallery_Renderables.h +++ /dev/null @@ -1,663 +0,0 @@ -#pragma once -#include - -#include "Gallery_Actions.h" -#include "Gallery_Renderable_Types.h" - -#include "adminive/adminive.hpp" -#include "adminive/adapters/magic_enum.hpp" -#include "adminive/adapters/nlohmann_json.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace renderive::web::gallery_adminive { - -enum class Number_Locale_Choice { Point, Comma }; -enum class Color_Map_Choice { Spectrum, Ember, Grayscale }; - -inline Color blend(Color first, Color second, double amount) { - const auto channel = [amount](std::uint8_t a, std::uint8_t b) { - return static_cast(std::clamp( - std::lround(a + (b - a) * amount), 0L, 255L)); - }; - return {channel(first.r, second.r), channel(first.g, second.g), - channel(first.b, second.b), channel(first.a, second.a)}; -} - -inline Color_Map color_map(Color_Map_Choice palette) { - std::array stops{ - Color{5, 10, 25, 255}, Color{20, 52, 112, 255}, Color{39, 196, 181, 255}, - Color{242, 201, 76, 255}, Color{255, 76, 106, 255} - }; - if (palette == Color_Map_Choice::Ember) { - stops = {Color{10, 5, 8, 255}, Color{62, 18, 30, 255}, Color{153, 47, 39, 255}, - Color{244, 132, 48, 255}, Color{255, 238, 166, 255}}; - } else if (palette == Color_Map_Choice::Grayscale) { - stops = {Color{0, 0, 0, 255}, Color{52, 52, 52, 255}, Color{112, 112, 112, 255}, - Color{188, 188, 188, 255}, Color{255, 255, 255, 255}}; - } - std::vector colors; - colors.reserve(256); - for (int index = 0; index < 256; ++index) { - const double position = index / 255.0 * (stops.size() - 1); - const auto first = static_cast(std::floor(position)); - const auto second = std::min(first + 1, stops.size() - 1); - colors.push_back(premultiply(blend(stops[first], stops[second], position - first))); - } - return Color_Map(std::move(colors)); -} - -inline Color_Map_Choice color_map_choice(const Color_Map& value) { - for (const Color_Map_Choice choice : magic_enum::enum_values()) { - if (value.colors() == color_map(choice).colors()) - return choice; - } - return Color_Map_Choice::Spectrum; -} - -template -auto editable(std::string name, std::string label, adminive::Field_Control control) { - return adminive::field(std::move(name), std::move(label)) - .editable().control(control); -} - -template -auto number(std::string name, std::string label) { - return editable(std::move(name), std::move(label), adminive::Field_Control::number); -} - -template -auto boolean(std::string name, std::string label) { - return editable(std::move(name), std::move(label), adminive::Field_Control::boolean); -} - -template -auto select(std::string name, std::string label) { - return editable(std::move(name), std::move(label), adminive::Field_Control::select); -} - -template -auto color(std::string name, std::string label) { - return editable(std::move(name), std::move(label), adminive::Field_Control::color); -} - -template -auto property(std::string name, std::string label, adminive::Field_Control control) { - return adminive::field(std::move(name), std::move(label), - Gallery_Property_Accessor{}) - .editable() - .unsynchronized() - .control(control); -} - -template -auto readonly_property(std::string name, std::string label) { - return adminive::field(std::move(name), std::move(label), - Gallery_Property_Accessor{}); -} - -template -auto property_number(std::string name, std::string label) { - return property(std::move(name), std::move(label), - adminive::Field_Control::number); -} - -template -auto property_boolean(std::string name, std::string label) { - return property(std::move(name), std::move(label), - adminive::Field_Control::boolean); -} - -template -auto property_select(std::string name, std::string label) { - return property(std::move(name), std::move(label), - adminive::Field_Control::select); -} - -template -auto property_color(std::string name, std::string label) { - return property(std::move(name), std::move(label), - adminive::Field_Control::color); -} - -template -auto property_object(std::string name, std::string label) { - return property(std::move(name), std::move(label), - adminive::Field_Control::automatic); -} - -template -auto property_text(std::string name, std::string label) { - return property(std::move(name), std::move(label), - adminive::Field_Control::text); -} - -} // namespace renderive::web::gallery_adminive - -namespace adminive { - -template -struct Renderive_Validator_Metadata {}; - -template -struct Renderive_Validator_Metadata<::Range_Validator> { - static constexpr T minimum = Minimum; - static constexpr T maximum = Maximum; -}; - -template -struct Value_Adapter<::Validated_Value, Json> - : Renderive_Validator_Metadata { - using Storage = ::Validated_Value; - using value_type = T; - static const T& read(const Storage& value) noexcept { return value.get(); } - static void write(Storage& target, T value) { target = std::move(value); } -}; - -template -struct Value_Adapter { - using value_type = std::string; - static std::string read(renderive::Color value) { - char result[8]{}; - std::snprintf(result, sizeof(result), "#%02x%02x%02x", value.r, value.g, value.b); - return result; - } - static void write(renderive::Color& target, const std::string& value) { - if (value.size() != 7 || value.front() != '#' || - !std::all_of(value.begin() + 1, value.end(), [](unsigned char character) { - return std::isxdigit(character) != 0; - })) - ::renderive::error::unexpected("color must use #RRGGBB"); - const auto channel = [&value](std::size_t offset) { - return static_cast(std::stoul(value.substr(offset, 2), nullptr, 16)); - }; - target.r = channel(1); - target.g = channel(3); - target.b = channel(5); - } -}; - -template -struct Value_Adapter { - using Choice = renderive::web::gallery_adminive::Number_Locale_Choice; - using value_type = Choice; - static Choice read(renderive::Number_Locale value) { - return value.decimal_point == ',' ? Choice::Comma : Choice::Point; - } - static void write(renderive::Number_Locale& target, Choice value) { - target.decimal_point = value == Choice::Comma ? ',' : '.'; - } -}; - -template -struct Value_Adapter, Json> { - using Storage = renderive::Clamped_Property; - using value_type = T; - static constexpr T minimum = Minimum; - static constexpr T maximum = Maximum; - static T read(const Storage& value) noexcept { return value.get(); } - static void write(Storage& target, T value) { target = value; } -}; - -template -struct Value_Adapter { - using value_type = double; - static constexpr double minimum = 0.0; - static constexpr double maximum = 1.0; - static constexpr double multiple_of = 0.01; - static double read(const renderive::Unit_Interval& value) noexcept { return value.get(); } - static void write(renderive::Unit_Interval& target, double value) { target = value; } -}; - -template -struct Value_Adapter { - using Choice = renderive::web::gallery_adminive::Color_Map_Choice; - using value_type = Choice; - static Choice read(const renderive::Color_Map& value) { - return renderive::web::gallery_adminive::color_map_choice(value); - } - static void write(renderive::Color_Map& target, Choice value) { - target = renderive::web::gallery_adminive::color_map(value); - } -}; - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::Range; - using namespace renderive::web::gallery_adminive; - return adminive::object("range", "范围", - number<&T::origin>("origin", "起点"), - number<&T::target>("target", "终点")); - } -}; - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::Pen; - using namespace renderive::web::gallery_adminive; - return adminive::object("pen", "画笔", - color<&T::color>("color", "颜色"), - number<&T::width>("width", "宽度"), - select<&T::style>("style", "线型") - .enum_label("无") - .enum_label("实线") - .enum_label("虚线") - .enum_label("点线"), - select<&T::cap>("cap", "端点样式") - .enum_label("平头") - .enum_label("方头") - .enum_label("圆头"), - select<&T::join>("join", "连接样式") - .enum_label("尖角") - .enum_label("斜角") - .enum_label("圆角")); - } -}; - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::Brush; - using namespace renderive::web::gallery_adminive; - return adminive::object("brush", "画刷", - color<&T::color>("color", "颜色"), - select<&T::style>("style", "填充样式") - .enum_label("无") - .enum_label("纯色")); - } -}; - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::Font; - using namespace renderive::web::gallery_adminive; - return adminive::object("font", "字体", - number<&T::size>("size", "字号"), - number<&T::weight>("weight", "字重"), - boolean<&T::italic>("italic", "斜体")); - } -}; - -#define RENDERIVE_AXIS_DESCRIPTOR(GalleryType, Name, Label) \ - template <> struct Type_Descriptor { \ - static auto get() { \ - using T = renderive::web::GalleryType; \ - using B = renderive::Axis_Base_Properties; \ - using P = renderive::Axis_Properties; \ - using namespace renderive::web::gallery_adminive; \ - return adminive::object(Name, Label, \ - readonly_property("x", "X 位置"), \ - readonly_property("y", "Y 位置"), \ - property_select("orientation", "方向") \ - .enum_label("水平") \ - .enum_label("垂直"), \ - readonly_property("pixel_length", "像素长度"), \ - property_number("tick_length", "主刻度长度"), \ - property_number("sub_tick_length", "次刻度长度"), \ - property_color("color", "颜色"), \ - property_select("locale", "小数点符号") \ - .enum_label("点") \ - .enum_label("逗号"), \ - property_text("unit_text", "单位文本"), \ - property_object("unit_text_font", "单位字体"), \ - property_object("unit_text_pen", "单位画笔"), \ - property_object("unit_text_background_brush", "单位背景"), \ - property_number("label_rotation_degrees", "标签旋转角度"), \ - property_object("coordinates", "坐标范围"), \ - property_number("precision", "小数位数"), \ - property_boolean("wheel", "滚轮缩放"), \ - property_boolean("drag", "拖拽平移")); \ - } \ - } - -RENDERIVE_AXIS_DESCRIPTOR(Gallery_Axis, "axis", "数值轴"); -RENDERIVE_AXIS_DESCRIPTOR(Gallery_Frequency_Axis, "frequency_axis", "频率轴"); -#undef RENDERIVE_AXIS_DESCRIPTOR - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::web::Gallery_Time_Axis; - using B = renderive::Axis_Base_Properties; - using P = renderive::Time_Axis::Properties; - using namespace renderive::web::gallery_adminive; - return adminive::object("time_axis", "时间轴", - readonly_property("x", "X 位置"), - readonly_property("y", "Y 位置"), - property_select("orientation", "方向") - .enum_label("水平") - .enum_label("垂直"), - readonly_property("pixel_length", "像素长度"), - property_number("tick_length", "主刻度长度"), - property_number("sub_tick_length", "次刻度长度"), - property_color("color", "颜色"), - property_select("locale", "小数点符号") - .enum_label("点") - .enum_label("逗号"), - property_text("unit_text", "单位文本"), - property_object("unit_text_font", "单位字体"), - property_object("unit_text_pen", "单位画笔"), - property_object("unit_text_background_brush", "单位背景"), - property_number("label_rotation_degrees", "标签旋转角度"), - property_number("visible_count", "可见点数"), - property_number("tick_label_spacing_px", "标签间距"), - property_text("format", "时间格式"), - property_boolean("newest_at_start", "最新数据在起点")); - } -}; - -#define RENDERIVE_GALLERY_DESCRIPTOR(GalleryType, PropertiesType, Name, Label, ...) \ - template <> struct Type_Descriptor { \ - static auto get() { \ - using T = renderive::web::GalleryType; \ - using P = renderive::PropertiesType; \ - using namespace renderive::web::gallery_adminive; \ - return adminive::object(Name, Label, __VA_ARGS__); \ - } \ - } - -RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Spectrum, Spectrum::Properties, "spectrum", "实时频谱", - property_number("frequency_point_size", "频率点数"), - property_select("partition_mode", "任务分区模式") - .enum_label("自动瓶颈优化") - .enum_label("固定分区"), - property_number("partition_count", "固定分区数") - .description("固定模式下的并行任务数;1 表示不分区") - .visible_on("${$self.partition_mode == 'Fixed'}"), - property_object("frequency_range", "频率范围"), - property_number("center_frequency", "中心频率"), - property_object("sweep_frequency_range", "扫频范围"), - property_boolean("max_hold_visible", "显示最大保持"), - property_boolean("min_hold_visible", "显示最小保持"), - property_boolean("max_marker_visible", "显示最大值标记"), - property_boolean("use_min_marker", "显示最小值标记"), - property_boolean("sweep_region_visible", "显示扫频区域"), - property_boolean("visible_range_only", "仅绘制可见范围"), - property_select("interpolation_mode", "曲线插值") - .enum_label("最近样本") - .enum_label("数值线性") - .enum_label("功率域线性") - .enum_label("左阶梯") - .enum_label("右阶梯") - .enum_label("三次插值"), - property_object("max_brush", "最大保持画刷"), - property_object("current_brush", "当前曲线画刷"), - property_object("min_brush", "最小保持画刷"), - property_object("max_pen", "最大保持画笔"), - property_object("current_pen", "当前曲线画笔"), - property_object("min_pen", "最小保持画笔"), - property_object("selected_marker_pen", "选中标记画笔"), - property_object("marker_pen", "标记画笔"), - property_object("middle_frequency_pen", "中心频率画笔"), - property_object("sweep_region_brush", "扫频区域画刷"), - property_boolean("tooltip_enabled", "启用提示框"), - property_object("tooltip_font", "提示框字体"), - property_object("tooltip_text_pen", "提示框文字画笔"), - property_object("tooltip_background_brush", "提示框背景")); - -RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Waterfall, Waterfall::Properties, "waterfall", "频谱瀑布", - property_object("frequency_range", "频率范围"), - property_object("power_range", "功率范围"), - property_number("frequency_bin_count", "频率格数"), - property_select("partition_mode", "任务分区模式") - .enum_label("自动瓶颈优化") - .enum_label("固定分区"), - property_number("partition_count", "固定分区数") - .description("固定模式下的并行任务数;1 表示不分区") - .visible_on("${$self.partition_mode == 'Fixed'}"), - property_boolean("visible_range_only", "仅绘制可见范围"), - property_select("interpolation_mode", "图像插值") - .enum_label("最近邻") - .enum_label("双线性") - .enum_label("双三次"), - property_select("color_map", "色图") - .enum_label("频谱") - .enum_label("火焰") - .enum_label("灰度"), - property_boolean("tooltip_enabled", "启用提示框"), - property_object("tooltip_font", "提示框字体"), - property_object("tooltip_text_pen", "提示框文字画笔"), - property_object("tooltip_background_brush", "提示框背景")); - -RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Afterglow, Afterglow::Properties, "afterglow", "余辉频谱", - property_object("frequency_range", "频率范围"), - property_object("power_range", "功率范围"), - property_number("frequency_point_size", "频率点数"), - property_number("power_point_size", "功率点数"), - property_select("partition_mode", "任务分区模式") - .enum_label("自动瓶颈优化") - .enum_label("固定分区"), - property_number("partition_count", "固定分区数") - .description("固定模式下的并行任务数;1 表示不分区") - .visible_on("${$self.partition_mode == 'Fixed'}"), - property_boolean("interpolate", "插值功率点"), - property_number("attenuation_rate", "衰减率"), - property_select("color_map", "色图") - .enum_label("频谱") - .enum_label("火焰") - .enum_label("灰度")); - -RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Sweep_Spectrum, Sweep_Spectrum::Properties, "sweep_spectrum", "扫频频谱", - property_object("frequency_range", "频率范围"), - property_number("bins_per_block", "每块频率格数"), - property_number("block_count", "扫频块数"), - property_object("pen", "扫频曲线画笔"), - property_object("current_frequency_pen", "当前频率画笔"), - property_boolean("visible_range_only", "仅绘制可见范围"), - property_select("interpolation_mode", "曲线插值") - .enum_label("最近样本") - .enum_label("数值线性") - .enum_label("功率域线性") - .enum_label("左阶梯") - .enum_label("右阶梯") - .enum_label("三次插值")); - -RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Frequency_Trace, Frequency_Trace::Properties, "frequency_trace", "频率轨迹", - property_object("pen", "轨迹画笔")); - -RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Selection_Rectangle_Overlay, Selection_Rectangle_Overlay::Properties, "selection_overlay", "框选区域", - property_object("label_font", "标签字体"), - property_object("label_pen", "标签画笔"), - property_object("selection_brush", "选区画刷"), - property_object("selection_border_pen", "选区边框画笔")); - -RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Constellation_Diagram, Constellation_Diagram::Properties, "constellation", "星座图", - property_object("i_range", "I 轴范围"), - property_object("q_range", "Q 轴范围"), - property_color("point_color", "点颜色"), - property_color("anchor_color", "锚点颜色"), - property_number("point_lifetime_ms", "点保留时间"), - property_select("type", "调制类型") - .enum_label("4 PSK") - .enum_label("8 PSK") - .enum_label("16 PSK"), - property_number("phase_offset_radians", "相位偏移")); - -#undef RENDERIVE_GALLERY_DESCRIPTOR - -} // namespace adminive - -namespace renderive::web::gallery_action_registry { - -constexpr auto axis_metadata() { - return structive::type_metadata( - gallery_action("axis_probe", "读取坐标映射与刻度", - "coord_to_pixel / pixel_to_coord / tick_step / sub_tick_count / tick_label", - "坐标轴")); -} - -} // namespace renderive::web::gallery_action_registry - -namespace structive { - -#define RENDERIVE_TYPE_ACTIONS(Type, ...) \ - template <> struct Type_Descriptor { \ - static auto get() { \ - using T = renderive::web::Type; \ - using renderive::web::gallery_action; \ - return object(type_metadata(__VA_ARGS__)); \ - } \ - } - -template <> -struct Type_Descriptor { - static auto get() { - return object( - renderive::web::gallery_action_registry::axis_metadata()); - } -}; - -template <> -struct Type_Descriptor { - static auto get() { - return object( - renderive::web::gallery_action_registry::axis_metadata()); - } -}; - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::web::Gallery_Time_Axis; - using renderive::web::gallery_action; - return object(type_metadata( - gallery_action("axis_probe", "读取坐标映射与刻度", - "coord_to_pixel / pixel_to_coord / tick_step / sub_tick_count / tick_label", - "坐标轴"), - gallery_action("append_time", "追加时间点", - "Time_Axis::append_time / tick_to_time", "时间轴"))); - } -}; - -RENDERIVE_TYPE_ACTIONS(Gallery_Spectrum, - gallery_action("push_samples", "推送一帧样本", "Spectrum::update_samples", "频谱"), - gallery_action("power_at", "查询指定频率功率", "Spectrum::power_at", "频谱", - {}, "number", "频率", 98e6), - gallery_action("add_marker", "添加点标记", "Spectrum::add_custom_marker", "标记", - {}, "number", "频率", 96e6), - gallery_action("add_line_marker", "添加线标记", "Spectrum::add_custom_line_marker", "标记", - {}, "number", "频率", 100e6), - gallery_action("remove_marker", "按频率删除标记", "Spectrum::remove_custom_marker", "标记", - {}, "number", "频率", 96e6), - gallery_action("remove_selected_marker", "删除选中标记", - "Spectrum::remove_selected_marker", "标记"), - gallery_action("clear_markers", "清空标记", "Spectrum::clear_custom_markers", "标记"), - gallery_action("select_marker", "选择标记索引", "Spectrum::set_selected_marker_index", "标记", - {}, "number", "索引", 0.0), - gallery_action("select_next_marker", "选择下一个标记", "Spectrum::select_next_marker", "标记"), - gallery_action("select_previous_marker", "选择上一个标记", "Spectrum::select_previous_marker", "标记"), - gallery_action("clear_marker_selection", "清除标记选择", - "Spectrum::clear_marker_selection", "标记"), - gallery_action("set_marker_frequency", "修改选中标记频率", - "Spectrum::set_marker_frequency / set_current_marker_frequency", "标记", - {}, "number", "频率", 99e6)); - -RENDERIVE_TYPE_ACTIONS(Gallery_Waterfall, - gallery_action("append_row", "追加一行", "Waterfall::append_row", "频谱瀑布"), - gallery_action("append_tick_row", "按 tick 追加一行", - "Time_Axis::append_time / Waterfall::append_row(int, span)", "频谱瀑布")); - -RENDERIVE_TYPE_ACTIONS(Gallery_Afterglow, - gallery_action("append_spectrum", "追加一帧频谱", "Afterglow::append_spectrum", "余辉频谱")); - -RENDERIVE_TYPE_ACTIONS(Gallery_Sweep_Spectrum, - gallery_action("append_block", "追加一个扫频块", "Sweep_Spectrum::append_block", "扫频频谱")); - -RENDERIVE_TYPE_ACTIONS(Gallery_Frequency_Trace, - gallery_action("append_sample", "追加一个样本", "Frequency_Trace::append_sample", "频率轨迹"), - gallery_action("append_tick_sample", "按 tick 追加样本", - "Time_Axis::append_time / Frequency_Trace::append_sample(int, double)", - "频率轨迹")); - -RENDERIVE_TYPE_ACTIONS(Gallery_Selection_Rectangle_Overlay, - gallery_action("clear_selection", "清空框选区域", - "Selection_Rectangle_Overlay::clear_selected_regions", "框选区域")); - -RENDERIVE_TYPE_ACTIONS(Gallery_Constellation_Diagram, - gallery_action("append_points", "追加一组 IQ 点", - "Constellation_Diagram::append_point", "星座图"), - gallery_action("fit_square", "按轴拟合正方形", - "Constellation_Diagram::fit_square_to_axes", "星座图")); - -#undef RENDERIVE_TYPE_ACTIONS - -} // namespace structive - -namespace renderive::web { - -template -std::size_t gallery_control_count() { - using Json = nlohmann::json; - const auto count_fields = [](const auto& self, const Json& fields) -> std::size_t { - std::size_t result{}; - for (const auto& field : fields) { - if (field.value("editable", false) && !field.contains("children")) - ++result; - if (field.contains("children")) - result += self(self, field.at("children")); - } - return result; - }; - const Json descriptor = adminive::to_descriptor_json(); - return count_fields(count_fields, descriptor.at("fields")); -} - -struct Gallery_Case_Type_Registration { - std::string_view case_id; - std::size_t (*control_count)(); - void (*append_actions)(std::vector&); -}; - -template -Gallery_Case_Type_Registration gallery_case_types(std::string_view case_id) { - return { - case_id, - [] { return (std::size_t{} + ... + gallery_control_count()); }, - [](std::vector& actions) { - (append_gallery_actions(actions), ...); - } - }; -} - -inline const Gallery_Case_Type_Registration* gallery_case_registration( - std::string_view case_id) { - static const std::array registrations{ - gallery_case_types("axis_lab"), - gallery_case_types("spectrum"), - gallery_case_types("afterglow"), - gallery_case_types("sweep_spectrum"), - gallery_case_types("waterfall"), - gallery_case_types("frequency_trace"), - gallery_case_types("selection_overlay"), - gallery_case_types("constellation") - }; - const auto found = std::find_if( - registrations.begin(), registrations.end(), - [case_id](const auto& registration) { return registration.case_id == case_id; }); - return found == registrations.end() ? nullptr : &*found; -} - -inline std::size_t gallery_renderable_control_count(std::string_view case_id) { - const auto* registration = gallery_case_registration(case_id); - return registration ? registration->control_count() : 0; -} - -inline void append_gallery_renderable_actions( - std::string_view case_id, std::vector& actions) { - if (const auto* registration = gallery_case_registration(case_id)) - registration->append_actions(actions); -} - -} // namespace renderive::web diff --git a/web_server/app/Gallery_Session_Control.h b/web_server/app/Gallery_Session_Control.h deleted file mode 100644 index 48870b2..0000000 --- a/web_server/app/Gallery_Session_Control.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once -#include "render_2D/plottable/Performance_Overlay.h" -#include "render_2D/renderable/Renderable.h" -#include "render_2D/scene/Render_Scene_2D.h" -#include -#include -namespace adminive { -template -struct Type_Descriptor; -} -namespace renderive::web { -struct Gallery_Feedback_Policy { - bool enabled{true}; - bool pixel{}; - bool presentation{true}; - bool manual{}; - double manual_fps{60.0}; - friend bool operator==(const Gallery_Feedback_Policy&, const Gallery_Feedback_Policy&) = default; -}; -template -class Gallery_Session_Control final { -public: - Gallery_Session_Control(Scene_Type& scene, Renderable& renderable, Gallery_Feedback_Policy& feedback) noexcept - : scene_(scene), renderable_(renderable), feedback_(feedback) {} -private: - Scene_Type& scene_; - Renderable& renderable_; - Gallery_Feedback_Policy& feedback_; - template - friend struct ::adminive::Type_Descriptor; -}; -} diff --git a/web_server/app/Gallery_Session_Control_Adminive.h b/web_server/app/Gallery_Session_Control_Adminive.h deleted file mode 100644 index 1cf2c9c..0000000 --- a/web_server/app/Gallery_Session_Control_Adminive.h +++ /dev/null @@ -1,150 +0,0 @@ -#pragma once - -#include "Gallery_Session_Control.h" -#include "Gallery_Renderables.h" - -#include -#include - -namespace renderive::web::gallery_adminive { - -template -auto callback_property(std::string name, std::string label, - adminive::Field_Control control, Reader reader, Writer writer) { - auto accessor = structive::trusted_callable_accessor( - std::move(reader), std::move(writer)); - static_assert(std::same_as); - return adminive::field(std::move(name), std::move(label), std::move(accessor)) - .editable() - .unsynchronized() - .control(control); -} - -} // namespace renderive::web::gallery_adminive - -namespace adminive { - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::web::Gallery_Feedback_Policy; - using namespace renderive::web::gallery_adminive; - return adminive::object("feedback_policy", "消费者反馈", - boolean<&T::enabled>("enabled", "启用反馈"), - boolean<&T::pixel>("pixel", "像素响应反馈"), - boolean<&T::presentation>("presentation", "浏览器呈现反馈"), - boolean<&T::manual>("manual", "手动反馈"), - number<&T::manual_fps>("manual_fps", "手动反馈帧率")); - } -}; - -template -struct Type_Descriptor> { - static auto get() { - using T = renderive::web::Gallery_Session_Control; - using namespace renderive::web::gallery_adminive; - const auto dirty = [](T& value) { value.scene_.request_redraw(); }; - return adminive::object("scene", "场景", - callback_property( - "background_color", "背景颜色", Field_Control::color, - [](const T& value) { return value.scene_.background_color(); }, - [dirty](T& value, renderive::Color color) { - value.scene_.set_background_color(color); - dirty(value); - }), - callback_property( - "performance_overlay", "性能叠加层", Field_Control::boolean, - [](const T& value) { - const auto overlay = value.scene_.performance_overlay(); - return overlay && overlay->enabled(); - }, - [dirty](T& value, bool enabled) { - renderive::set_performance_overlay_enabled(value.scene_, enabled); - dirty(value); - }), - callback_property( - "renderable_visible", "显示渲染元素", Field_Control::boolean, - [](const T& value) { return value.renderable_.is_visible(); }, - [dirty](T& value, bool visible) { - value.renderable_.set_visible(visible); - dirty(value); - }), - callback_property( - "cache_mode", "缓存模式", Field_Control::select, - [](const T& value) { return value.renderable_.get_cache_mode(); }, - [dirty](T& value, renderive::Renderable_Cache_Mode mode) { - value.renderable_.set_cache_mode(mode); - dirty(value); - }) - .template enum_label("直接绘制") - .template enum_label("本地像素缓存"), - callback_property( - "object_name", "对象名称", Field_Control::text, - [](const T& value) { return value.renderable_.object_name(); }, - [dirty](T& value, std::string name) { - value.renderable_.set_object_name(std::move(name)); - dirty(value); - }), - callback_property( - "frequency_limit_enabled", "启用帧率限制", Field_Control::boolean, - [](const T& value) { return value.scene_.max_render_fps() > 0.0; }, - [dirty](T& value, bool enabled) { - if (enabled) { - if (value.scene_.max_render_fps() <= 0.0 && - !value.scene_.set_max_render_fps(30.0)) - return; - } else { - if (!value.scene_.clear_max_render_fps()) - return; - } - dirty(value); - }), - callback_property( - "max_render_fps", "最大渲染帧率", Field_Control::number, - [](const T& value) { - const double fps = value.scene_.max_render_fps(); - return fps > 0.0 ? fps : 30.0; - }, - [dirty](T& value, double fps) { - if (value.scene_.max_render_fps() > 0.0 && - !value.scene_.set_max_render_fps(fps)) - return; - dirty(value); - }), - callback_property( - "feedback", "消费者反馈", Field_Control::automatic, - [](const T& value) { return value.feedback_; }, - [dirty](T& value, renderive::web::Gallery_Feedback_Policy feedback) { - value.feedback_ = std::move(feedback); - dirty(value); - })); - } -}; - - -} // namespace adminive - -namespace structive { - -template -struct Type_Descriptor> { - static auto get() { - using T = renderive::web::Gallery_Session_Control; - using renderive::web::gallery_action; - return object(type_metadata( - gallery_action("reset", "恢复本图默认值", "Gallery_Scene::rebuild", "场景"), - gallery_action("toggle_view", "切换 View 生命周期", - "Basic_Scene2D::activate_view / deactivate_view", "场景"), - gallery_action("capture_next_frame", "Capture next frame", - "Scene_Base::capture_next_frame", "Performance Capture", - "Capture the next completed render frame", {}, {}, 0.0, true), - gallery_action("capture_frames", "Capture N frames", - "Scene_Base::capture_frames", "Performance Capture", - "Capture consecutive completed render frames", "number", - "Frame count", 20.0, true), - gallery_action("read_data_shape", "读取数据形状", - "Renderable data query / observer telemetry", "观察器"))); - } -}; - -} // namespace structive diff --git a/web_server/app/Web_Plot_Session.cpp b/web_server/app/Web_Plot_Session.cpp index b8a1552..76a48ea 100644 --- a/web_server/app/Web_Plot_Session.cpp +++ b/web_server/app/Web_Plot_Session.cpp @@ -1,350 +1,45 @@ #include "Web_Plot_Session.h" #include "Pixel_Frame.h" -#include "render_2D/export.h" -#include -#include -#include -#include -#include -#include -#include +#include "render_2D/scene/Render_Scene_2D.h" +#include #include -#include -#include + namespace renderive::web { -namespace { -Color blend(Color first, Color second, double amount) { - const auto channel = [amount](std::uint8_t a, std::uint8_t b) { - return static_cast(std::clamp( - std::lround(a + (b - a) * amount), 0L, 255L)); - }; - return { - channel(first.r, second.r), channel(first.g, second.g), - channel(first.b, second.b), channel(first.a, second.a) - }; -} -Color_Map radio_color_map() { - constexpr Color stops[] = { - {3, 7, 18, 255}, {16, 52, 105, 255}, {23, 163, 184, 255}, - {238, 210, 91, 255}, {239, 68, 68, 255} - }; - std::vector colors; - colors.reserve(256); - for (int index = 0; index < 256; ++index) { - const double position = index / 255.0 * (std::size(stops) - 1); - const auto stop = static_cast(std::floor(position)); - const auto next = std::min(stop + 1, std::size(stops) - 1); - colors.push_back(premultiply(blend(stops[stop], stops[next], position - stop))); - } - return Color_Map(std::move(colors)); -} -Time_Of_Day current_time_of_day() { - constexpr std::int64_t day_ms = 24LL * 60LL * 60LL * 1000LL; - const auto now = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - return {now % day_ms}; -} -double gaussian(double x, double center, double width) { - const double normalized = (x - center) / width; - return std::exp(-0.5 * normalized * normalized); -} -} // namespace struct Web_Plot_Session::Impl { - Render_Scene_2D plot; - renderive_Owner spectrum_frequency_axis; - renderive_Owner spectrum_power_axis; - renderive_Owner waterfall_frequency_axis; - renderive_Owner waterfall_time_axis; - renderive_Owner spectrum; - renderive_Owner waterfall; - renderive_Owner selection; - Demo_Mode mode = Demo_Mode::Fm; - double gain_db = 8.0; - std::uint64_t frame_index{}; - std::mutex mutex; - Impl() { - plot.init(); - plot.set_background_color({3, 7, 18, 255}); - if (!plot.set_max_render_fps(30.0)) - ::renderive::error::unexpected( - "validated web plot frame rate was rejected"); - plot.set_viewport_size({960, 600}); - const auto root = plot.root_renderable(); - constexpr Color axis_color{93, 116, 151, 255}; - const Range initial_frequency{101'300'000.0, 103'700'000.0}; - { - auto attach = plot.attach_builder(); - const auto make_group = [&](std::string name) { - auto group = detail::make_renderable_group(true); - group->set_object_name(std::move(name)); - attach.attach(group); - attach.add_display_parent(group, root); - attach.add_dependency_parent(group, root); - return group; - }; - const auto data = make_group("Web_Data"); - const auto axes = make_group("Web_Axes"); - const auto overlay = make_group("Web_Overlay"); - axes->set_cache_mode(Renderable_Cache_Mode::Local_Pixel); - spectrum_frequency_axis = - Frequency_Axis::Builder{&attach} - .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Frequency_Axis::Properties::coordinates>(initial_frequency) - .set<&Frequency_Axis::Properties::precision>(2) - .set<&Frequency_Axis::Properties::tick_length>(8) - .set<&Frequency_Axis::Properties::sub_tick_length>(4) - .set<&Frequency_Axis::Properties::color>(axis_color) - .set<&Frequency_Axis::Properties::wheel>(true) - .set<&Frequency_Axis::Properties::drag>(true) - .build(axes); - spectrum_power_axis = - Axis::Builder{&attach} - .set<&Axis::Properties::orientation>(Orientation::Vertical) - .set<&Axis::Properties::coordinates>(Range{-20.0, -120.0}) - .set<&Axis::Properties::precision>(0) - .set<&Axis::Properties::tick_length>(-8) - .set<&Axis::Properties::sub_tick_length>(-4) - .set<&Axis::Properties::color>(axis_color) - .set<&Axis::Properties::unit_text>("dBm") - .build(axes); - waterfall_frequency_axis = - Frequency_Axis::Builder{&attach} - .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Frequency_Axis::Properties::coordinates>(initial_frequency) - .set<&Frequency_Axis::Properties::precision>(2) - .set<&Frequency_Axis::Properties::tick_length>(8) - .set<&Frequency_Axis::Properties::sub_tick_length>(4) - .set<&Frequency_Axis::Properties::color>(axis_color) - .set<&Frequency_Axis::Properties::wheel>(true) - .set<&Frequency_Axis::Properties::drag>(true) - .build(axes); - waterfall_time_axis = - Time_Axis::Builder{&attach} - .set<&Time_Axis::Properties::orientation>(Orientation::Vertical) - .set<&Time_Axis::Properties::visible_count>(72) - .set<&Time_Axis::Properties::tick_label_spacing_px>(34) - .set<&Time_Axis::Properties::format>("mm:ss") - .set<&Time_Axis::Properties::tick_length>(-8) - .set<&Time_Axis::Properties::sub_tick_length>(-4) - .set<&Time_Axis::Properties::color>(axis_color) - .build(axes); - spectrum = Spectrum::Builder{&attach} - .set<&Spectrum::Properties::frequency_range>(initial_frequency) - .set<&Spectrum::Properties::frequency_point_size>(768) - .set<&Spectrum::Properties::center_frequency>(102'500'000.0) - .set<&Spectrum::Properties::sweep_frequency_range>(Range{102'200'000.0, 102'800'000.0}) - .set<&Spectrum::Properties::max_marker_visible>(true) - .set<&Spectrum::Properties::sweep_region_visible>(true) - .set<&Spectrum::Properties::interpolation_mode>(Line_Interpolation_Mode::Cubic_Value) - .build(data, spectrum_frequency_axis, spectrum_power_axis); - spectrum->set<&Spectrum::Properties::current_pen>(Pen{ - Color{57, 224, 177, 255}, 2.0, Line_Style::Solid, - Line_Cap::Round, Line_Join::Round - }); - spectrum->set<&Spectrum::Properties::current_brush>(Brush{Color{31, 174, 145, 32}, Brush_Style::Solid}); - spectrum->set<&Spectrum::Properties::max_pen>(Pen{Color{250, 204, 21, 210}, 1.0}); - spectrum->set<&Spectrum::Properties::middle_frequency_pen>(Pen{ - Color{56, 189, 248, 220}, 1.0, - Line_Style::Dash - }); - waterfall = Waterfall::Builder{&attach} - .set<&Waterfall::Properties::frequency_range>(initial_frequency) - .set<&Waterfall::Properties::power_range>(Range{-120.0, -20.0}) - .set<&Waterfall::Properties::frequency_bin_count>(768) - .set<&Waterfall::Properties::interpolation_mode>(Image_Interpolation_Mode::Bilinear) - .set<&Waterfall::Properties::color_map>(radio_color_map()) - .build(data, waterfall_frequency_axis, waterfall_time_axis); - selection = Selection_Rectangle_Overlay::Builder{&attach} - .set<&Selection_Rectangle_Overlay::Properties::selection_brush>(Brush{Color{56, 189, 248, 36}, Brush_Style::Solid}) - .set<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(Pen{Color{125, 211, 252, 230}, 1.0, Line_Style::Dash}) - .build(overlay, spectrum_frequency_axis, spectrum_power_axis); - } - apply_layout(plot.viewport_size()); - plot.activate_view(); - update_model(); - const auto initial_render = plot.render_frame(true); - if (!initial_render || *initial_render != Plot_Render_Status::rendered) - plot.request_redraw(); - } - ~Impl() { - plot.deactivate_view(); - } - void apply_layout(Size viewport) { - const int left = viewport.width < 620 ? 54 : 72; - const int right = viewport.width < 620 ? 44 : 70; - const int top = 16; - const int bottom = viewport.height < 480 ? 24 : 32; - const int gap = viewport.height < 480 ? 36 : 48; - const int content_width = std::max(1, viewport.width - left - right); - const int available_height = std::max(2, viewport.height - top - bottom - gap); - const int spectrum_height = std::max(1, available_height * 43 / 100); - const int waterfall_y = top + spectrum_height + gap; - const int waterfall_height = std::max(1, viewport.height - waterfall_y - bottom); - spectrum_frequency_axis->update([&](Axis_Properties& state) { - state.x = left; - state.y = top + spectrum_height; - state.pixel_length = static_cast(content_width); - }); - spectrum_power_axis->update([&](Axis_Properties& state) { - state.x = left; - state.y = top; - state.pixel_length = static_cast(spectrum_height); - }); - waterfall_frequency_axis->update([&](Axis_Properties& state) { - state.x = left; - state.y = waterfall_y + waterfall_height; - state.pixel_length = static_cast(content_width); - }); - waterfall_time_axis->update([&](auto& state) { - state.x = left; - state.y = waterfall_y; - state.pixel_length = static_cast(waterfall_height); - }); - } - void update_model() { - constexpr int sample_count = 768; - const double time = static_cast(frame_index) * 0.075; - std::vector samples(sample_count); - for (int index = 0; index < sample_count; ++index) { - const double x = static_cast(index) / (sample_count - 1); - const double noise = std::sin(index * 12.9898 + frame_index * 0.371) * - std::sin(index * 0.137 + frame_index * 0.071); - double signal{}; - switch (mode) { - case Demo_Mode::Am: - signal = 66.0 * gaussian(x, 0.5, 0.008) + - 39.0 * gaussian(x, 0.42, 0.018) + - 39.0 * gaussian(x, 0.58, 0.018); - break; - case Demo_Mode::Fm: { - const double moving = 0.5 + std::sin(time) * 0.035; - signal = 58.0 * gaussian(x, moving, 0.055) + - 25.0 * gaussian(x, moving - 0.11, 0.023) + - 25.0 * gaussian(x, moving + 0.11, 0.023); - break; - } - case Demo_Mode::Usb: - signal = 61.0 * gaussian(x, 0.57, 0.045) + - 28.0 * gaussian(x, 0.68, 0.025); - break; - case Demo_Mode::Lsb: - signal = 61.0 * gaussian(x, 0.43, 0.045) + - 28.0 * gaussian(x, 0.32, 0.025); - break; - } - samples[index] = std::clamp(-110.0 + noise * 4.5 + signal + gain_db, - -120.0, -20.0); - } - spectrum->update_samples(samples); - if ((frame_index & 1U) == 0U) - waterfall->append_row(current_time_of_day(), samples); - ++frame_index; - } - void set_center_frequency(double megahertz) { - const double center = megahertz * 1'000'000.0; - const double bandwidth = spectrum_frequency_axis->get<&Axis_Properties::coordinates>().size(); - const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5}; - spectrum_frequency_axis->set<&Axis_Properties::coordinates>(range); - waterfall_frequency_axis->set<&Axis_Properties::coordinates>(range); - spectrum->set<&Spectrum::Properties::frequency_range>(range); - spectrum->set<&Spectrum::Properties::center_frequency>(center); - spectrum->set<&Spectrum::Properties::sweep_frequency_range>(Range{ - center - bandwidth * 0.125, - center + bandwidth * 0.125 - }); - waterfall->set<&Waterfall::Properties::frequency_range>(range); - } - void set_bandwidth(double kilohertz) { - const double center = spectrum->get<&Spectrum::Properties::center_frequency>(); - const double bandwidth = kilohertz * 1000.0; - const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5}; - spectrum_frequency_axis->set<&Axis_Properties::coordinates>(range); - waterfall_frequency_axis->set<&Axis_Properties::coordinates>(range); - spectrum->set<&Spectrum::Properties::frequency_range>(range); - spectrum->set<&Spectrum::Properties::sweep_frequency_range>(Range{ - center - bandwidth * 0.125, - center + bandwidth * 0.125 - }); - waterfall->set<&Waterfall::Properties::frequency_range>(range); - } - std::optional render_pixels() { - if (!plot.view_active()) - return std::nullopt; - update_model(); - const auto render = plot.render_frame(true); - if (!render || *render != Plot_Render_Status::rendered) - return std::nullopt; - std::string pixels; - const Color background = plot.background_color(); - const std::uint64_t sequence = plot.frame_status().latest_sequence; - plot.with_frame([&pixels, background, sequence](Image_View image) { - pixels = encode_pixel_frame(image, sequence, background); - }); - return pixels.empty() ? std::nullopt : std::optional(std::move(pixels)); + Size viewport{960, 600}; + std::uint64_t sequence{}; + std::unique_ptr scene; + + Impl() { rebuild(); } + void rebuild() { + Render_Scene_2D::State state; + state.viewport = viewport; + scene = Render_Scene_2D::Builder{state}.build( + std::make_shared()); + scene->activate_view(); } std::optional handle(const Web_Event& event) { - std::lock_guard lock(mutex); - return std::visit( - [this](const auto& value) -> std::optional { - using T = std::decay_t; - if constexpr (std::is_same_v) { - auto pixels = render_pixels(); - if (pixels) - return Web_Response{Web_Response_Type::Pixels, std::move(*pixels)}; - } - else if constexpr (std::is_same_v) { - const Size previous = plot.viewport_size(); - plot.set_viewport_size(value.size); - apply_layout(value.size); - Resize_Event resized; - resized.old_size = previous; - resized.new_size = value.size; - plot.dispatch_event(resized); - } - else if constexpr (std::is_same_v) { - if (value.type == Event_Type::Show) - plot.activate_view(); - else if (value.type == Event_Type::Hide) - plot.deactivate_view(); - plot.dispatch_event(value); - } - else if constexpr (Event_Object) { - plot.dispatch_event(value); - } - else if constexpr (std::is_same_v) { - mode = value.mode; - plot.request_redraw(); - } - else if constexpr (std::is_same_v) { - set_center_frequency(value.megahertz); - } - else if constexpr (std::is_same_v) { - set_bandwidth(value.kilohertz); - } - else if constexpr (std::is_same_v) { - gain_db = value.decibels; - plot.request_redraw(); - } - else if constexpr (std::is_same_v) { - spectrum->set<&Spectrum::Properties::max_hold_visible>(value.enabled); - } - else if constexpr (std::is_same_v) { - spectrum->set<&Spectrum::Properties::interpolation_mode>( - value.enabled - ? Line_Interpolation_Mode::Cubic_Value - : Line_Interpolation_Mode::Nearest_Sample); - waterfall->set<&Waterfall::Properties::interpolation_mode>( - value.enabled - ? Image_Interpolation_Mode::Bilinear - : Image_Interpolation_Mode::Nearest); - } - else if constexpr (std::is_same_v) { - selection->clear_selected_regions(); - } - return std::nullopt; - }, - event); + return std::visit([this](const auto& value) -> std::optional { + using T = std::decay_t; + if constexpr (std::same_as) { + const auto result = scene->render_frame(true); + if (!result || scene->wait_for_render() != Scene_Render_Result::none) + return std::nullopt; + std::string pixels; + scene->with_frame([&](Image_View image) { + pixels = encode_pixel_frame(image, ++sequence); + }); + if (pixels.empty()) return std::nullopt; + return Web_Response{Web_Response_Type::Pixels, + std::move(pixels)}; + } else if constexpr (std::same_as) { + viewport = value.size; + rebuild(); + } else if constexpr (std::derived_from) { + scene->dispatch_event(value); + } + return std::nullopt; + }, event); } }; Web_Plot_Session::Web_Plot_Session() : impl_(std::make_unique()) {} diff --git a/web_server/app/common/Observer_Json.h b/web_server/app/common/Observer_Json.h new file mode 100644 index 0000000..559cff5 --- /dev/null +++ b/web_server/app/common/Observer_Json.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace renderive::web { +template +struct Observer_Pfr_Adapter { + using Type = adminive::Boost_Pfr_Reflection_Adapter; +}; + +namespace detail { +template +nlohmann::json observer_value_json(const T& value); + +template +nlohmann::json observer_object_json( + const T& value, std::index_sequence) { + nlohmann::json result = nlohmann::json::object(); + ((result[std::string(Adapter::template name())] = + observer_value_json(Adapter::template get(value))), + ...); + return result; +} + +template +nlohmann::json observer_value_json(const T& value) { + using Value = std::remove_cvref_t; + if constexpr (std::same_as) { + return value; + } else if constexpr (std::same_as || + std::is_arithmetic_v) { + return value; + } else if constexpr (std::is_enum_v) { + return static_cast>(value); + } else if constexpr (std::is_aggregate_v) { + using Adapter = typename Observer_Pfr_Adapter::Type; + return observer_object_json( + value, std::make_index_sequence{}); + } else { + return nullptr; + } +} +} // namespace detail + +template +nlohmann::json observer_json(const Observer& observer) { + return detail::observer_value_json(observer); +} +} // namespace renderive::web diff --git a/web_server/app/render_2D/Gallery_Scene2D.cpp b/web_server/app/render_2D/Gallery_Scene2D.cpp index 74b4d04..8a6a30f 100644 --- a/web_server/app/render_2D/Gallery_Scene2D.cpp +++ b/web_server/app/render_2D/Gallery_Scene2D.cpp @@ -1,1472 +1,445 @@ -#include #include "Gallery_Scene2D.h" -#include "../Gallery_Controls.h" -#include "../Gallery_Enum.h" -#include "../Gallery_Observer_Adminive.h" -#include "../Gallery_Session_Control_Adminive.h" -#include "../common/Gallery_Scheduler_Adminive.h" + +#include "../Gallery_Capture_Json.h" +#include "../common/Observer_Json.h" #include "../Pixel_Frame.h" -#include "../Web_Performance_Log.h" -#include "render_2D/export.h" -#include -#include +#include "render_2D/axis/Axis.h" +#include "render_2D/axis/Frequency_Axis.h" +#include "render_2D/axis/Time_Axis.h" +#include "render_2D/plottable/Afterglow.h" +#include "render_2D/plottable/Constellation_Diagram.h" +#include "render_2D/plottable/Frequency_Trace.h" +#include "render_2D/plottable/Selection_Rectangle_Overlay.h" +#include "render_2D/plottable/Spectrum.h" +#include "render_2D/plottable/Sweep_Spectrum.h" +#include "render_2D/plottable/Waterfall.h" +#include "render_2D/scene/Render_Scene_2D.h" + #include -#include #include #include -#include -#include -#include #include -#include -#include -#include -#include +#include #include -#include -#include -#include #include + namespace renderive::web { +template <> +struct Observer_Pfr_Adapter { + using Type = adminive::Boost_Pfr_Base_Reflection_Adapter< + Render_Scene_2D::State, Render_Scene_2D::State_Fields>; +}; +template <> +struct Observer_Pfr_Adapter { + using Type = adminive::Boost_Pfr_Base_Reflection_Adapter< + Render_Scene_2D::Observer, Render_Scene_2D::Observer_Fields>; +}; namespace { -auto gallery_render_result( - const Expected& result) - -> Gallery_Render_Result { - if (result) { - switch (*result) { - case Plot_Render_Status::rendered: - return Gallery_Render_Result::none; - case Plot_Render_Status::view_inactive: - return Gallery_Render_Result::inactive; - case Plot_Render_Status::empty_viewport: - case Plot_Render_Status::model_unchanged: - return Gallery_Render_Result::frame_unavailable; - } - ::renderive::error::unexpected("unknown plot render status"); - } - switch (result.error()) { - case Plot_Render_Frame_Error::cancelled: - return Gallery_Render_Result::cancelled; - case Plot_Render_Frame_Error::deadline_exceeded: - return Gallery_Render_Result::deadline_exceeded; - case Plot_Render_Frame_Error::external_failure: - return Gallery_Render_Result::backend_failure; - case Plot_Render_Frame_Error::scene_shutting_down: - return Gallery_Render_Result::shutting_down; - case Plot_Render_Frame_Error::painter_unavailable: - case Plot_Render_Frame_Error::manual_refresh_not_supported: - case Plot_Render_Frame_Error::no_pending_frame: - case Plot_Render_Frame_Error::renderer_unavailable: - return Gallery_Render_Result::frame_unavailable; - } - ::renderive::error::unexpected("unknown plot render error"); -} -template -void update_axis_state(const renderive_Owner& axis, Update&& update) { - if (const auto numeric = renderive_dynamic_owner_cast(axis)) { - numeric->update([&](Axis_Properties& state) { - std::forward(update)(static_cast(state)); - }); - } else if (const auto time = renderive_dynamic_owner_cast(axis)) { - time->update([&](auto& state) { - std::forward(update)(static_cast(state)); - }); +std::shared_ptr make_strategy( + Gallery_Frame_Mode mode) { + switch (mode) { + case Gallery_Frame_Mode::Manual: + return std::make_shared(); + case Gallery_Frame_Mode::Low_Latency: + return std::make_shared(); + case Gallery_Frame_Mode::Playback: + return std::make_shared(); } + return {}; } -std::uint64_t milliseconds_to_ns(double milliseconds) { - return milliseconds > 0.0 ? static_cast(std::llround(milliseconds * 1'000'000.0)) : 0; -} -std::uint64_t frequency_to_ns(double frequency_hz) { - return frequency_hz > 0.0 ? static_cast(std::llround(1'000'000'000.0 / frequency_hz)) : 0; -} -int hex_digit(char value) { - if (value >= '0' && value <= '9') - return value - '0'; - if (value >= 'a' && value <= 'f') - return value - 'a' + 10; - return value - 'A' + 10; -} -Color color_from_hex(std::string_view value, std::uint8_t alpha = 255) { - const auto channel = [value](std::size_t offset) { - return static_cast(hex_digit(value[offset]) * 16 + - hex_digit(value[offset + 1])); - }; - return {channel(1), channel(3), channel(5), alpha}; +std::string_view mode_id(Gallery_Frame_Mode mode) { + switch (mode) { + case Gallery_Frame_Mode::Manual: return "manual"; + case Gallery_Frame_Mode::Low_Latency: return "low_latency"; + case Gallery_Frame_Mode::Playback: return "playback"; + } + return "low_latency"; } + Time_Of_Day current_time_of_day() { constexpr std::int64_t day_ms = 24LL * 60LL * 60LL * 1000LL; const auto now = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); + std::chrono::system_clock::now().time_since_epoch()) + .count(); return {now % day_ms}; } -double gaussian(double x, double center, double width) { - const double normalized = (x - center) / width; - return std::exp(-0.5 * normalized * normalized); -} -Event_Type event_type(Gallery_View_Input_Type type) { - switch (type) { - case Gallery_View_Input_Type::Show: - return Event_Type::Show; - case Gallery_View_Input_Type::Hide: - return Event_Type::Hide; - case Gallery_View_Input_Type::Leave: - return Event_Type::Leave; - } - return Event_Type::Leave; -} -Event_Type event_type(Gallery_Pointer_Input_Type type) { - switch (type) { - case Gallery_Pointer_Input_Type::Move: - return Event_Type::Pointer_Move; - case Gallery_Pointer_Input_Type::Press: - return Event_Type::Pointer_Press; - case Gallery_Pointer_Input_Type::Release: - return Event_Type::Pointer_Release; - } - return Event_Type::Pointer_Move; -} -Event_Type event_type(Gallery_Key_Input_Type type) { - return type == Gallery_Key_Input_Type::Press ? Event_Type::Key_Press : Event_Type::Key_Release; -} -Mouse_Button mouse_button(Gallery_Mouse_Button button) { - switch (button) { - case Gallery_Mouse_Button::None: - return Mouse_Button::None; - case Gallery_Mouse_Button::Left: - return Mouse_Button::Left; - case Gallery_Mouse_Button::Right: - return Mouse_Button::Right; - case Gallery_Mouse_Button::Middle: - return Mouse_Button::Middle; - } - return Mouse_Button::None; -} -Key key(Gallery_Key value) { - switch (value) { - case Gallery_Key::Escape: - return Key::Escape; - case Gallery_Key::Enter: - return Key::Enter; - case Gallery_Key::Space: - return Key::Space; - case Gallery_Key::Delete: - return Key::Delete; - case Gallery_Key::Backspace: - return Key::Backspace; - case Gallery_Key::Left: - return Key::Left; - case Gallery_Key::Right: - return Key::Right; - case Gallery_Key::Up: - return Key::Up; - case Gallery_Key::Down: - return Key::Down; - case Gallery_Key::Unknown: - return Key::Unknown; - } - return Key::Unknown; -} -Keyboard_Modifier modifiers(std::uint8_t value) { - return static_cast(value); -} -std::optional action_number(const Gallery_Action_Request& request) { - if (!request.argument || !std::holds_alternative(*request.argument)) - return std::nullopt; - return std::get(*request.argument); -} - -Plot_Frame_Mode plot_frame_mode(Gallery_Frame_Mode mode) { - switch (mode) { - case Gallery_Frame_Mode::Manual: - return Plot_Frame_Mode::Manual; - case Gallery_Frame_Mode::Low_Latency: - return Plot_Frame_Mode::Low_Latency; - case Gallery_Frame_Mode::Playback: - return Plot_Frame_Mode::Playback; - } - ::renderive::error::unexpected("unknown gallery frame mode"); -} -} // namespace -class Gallery_Scene final : public Gallery_Scene_Interface { +class Gallery_Scene2D final : public Gallery_Scene_Interface { public: - Gallery_Scene(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode frame_mode, bool automatic_low_latency) - : session_id_(session_id), case_id_(std::move(case_id)), frame_mode_(frame_mode), automatic_low_latency_(automatic_low_latency), - plot_(Plot_Scene_Options{plot_frame_mode(frame_mode)}), performance_started_(std::chrono::steady_clock::now()) { - plot_.init(); - plot_.set_viewport_size({560, 320}); - root_ = plot_.root_renderable(); - root_->set_object_name("画布根节点"); - { - auto attach = plot_.attach_builder(); - const auto make_group = [&](std::string name) { - auto group = detail::make_renderable_group(true); - group->set_object_name(std::move(name)); - if (attach.attach(group) != Scene_Edit_Error::none || - attach.add_display_parent(group, root_) != Scene_Edit_Error::none || - attach.add_dependency_parent(group, root_) != Scene_Edit_Error::none) - ::renderive::error::unexpected("validated gallery group attachment failed"); - return group; - }; - axes_node_ = make_group("坐标轴层"); - data_node_ = make_group("数据绘制层"); - overlay_node_ = make_group("交互覆盖层"); - axes_node_->set_cache_mode(Renderable_Cache_Mode::Local_Pixel); - build_axes(attach); - build_primary(attach); - } - attach_performance_overlay(plot_); - apply_layout(); - session_control_ = std::make_unique>( - plot_, *primary_, feedback_policy_); - register_controls(); - set_performance_plot_name(plot_, case_id_ + "/" + gallery_enum_id(frame_mode_)); - if (frame_mode_ == Gallery_Frame_Mode::Low_Latency && - !plot_.set_max_render_fps(30.0)) - ::renderive::error::unexpected( - "validated gallery frame rate was rejected"); - plot_.activate_view(); - update_model(); - const auto initial_render = plot_.render_frame(true); - if (!initial_render || *initial_render != Plot_Render_Status::rendered) - plot_.request_redraw(); + Gallery_Scene2D(std::string case_id, Gallery_Frame_Mode mode, + bool automatic) + : case_id_(std::move(case_id)), mode_(mode), automatic_(automatic) { + rebuild(); } - ~Gallery_Scene() { - plot_.deactivate_view(); + + ~Gallery_Scene2D() override { + if (scene_) scene_->deactivate_view(); } - Gallery_Scene(const Gallery_Scene&) = delete; - Gallery_Scene& operator=(const Gallery_Scene&) = delete; - [[nodiscard]] const std::string& case_id() const noexcept { - return case_id_; + + const std::string& case_id() const noexcept override { return case_id_; } + + nlohmann::json controls() const override { + return {{"resources", nlohmann::json::array()}, + {"observers", nlohmann::json::array()}, + {"render_plan", gallery_render_plan_json(*scene_)}, + {"performance_capture", + gallery_performance_capture_json(*scene_)}}; } - [[nodiscard]] nlohmann::json controls() const { - return { - {"resources", controls_.resources()}, - {"observers", controls_.observers(plot_.render_scene())}, - {"render_plan", controls_.render_plan(plot_.render_scene())}, - {"performance_capture", gallery_performance_capture_json(plot_.render_scene())} - }; + + Gallery_Frame_Mode frame_mode() const noexcept override { return mode_; } + void reset_monitoring() override { render_count_ = 0; } + + bool can_render_automatically() const noexcept override { + return automatic_ && mode_ == Gallery_Frame_Mode::Low_Latency; } - [[nodiscard]] Gallery_Frame_Mode frame_mode() const noexcept { - return frame_mode_; + + std::uint64_t kernel_refresh_interval_ns() const override { + return scene_->observer().next_refresh_interval_ns; } - void reset_monitoring() { - performance_started_ = std::chrono::steady_clock::now(); - last_performance_log_ = performance_started_; - render_attempt_count_ = 0; - successful_render_count_ = 0; - last_render_ms_ = 0.0; - render_duration_statistics_.clear(); - render_history_.clear(); - pixel_frame_count_ = 0; - last_pixel_snapshot_ms_ = 0.0; - last_pixel_encode_ms_ = 0.0; - last_pixel_request_ms_ = 0.0; - last_pixel_bytes_ = 0; - pixel_encode_statistics_.clear(); - pixel_request_statistics_.clear(); - pixel_history_.clear(); - client_performance_ = {}; - consumer_pixel_interval_ns_ = 0; - consumer_presentation_interval_ns_ = 0; - apply_consumer_feedback(); + + void set_client_metrics(Gallery_Client_Performance metrics) noexcept override { + client_ = metrics; } - [[nodiscard]] bool can_render_automatically() const noexcept { - return automatic_low_latency_ && frame_mode_ == Gallery_Frame_Mode::Low_Latency && - plot_.view_active(); - } - [[nodiscard]] std::uint64_t kernel_refresh_interval_ns() const { - return plot_.refresh_feedback_snapshot().next_refresh_interval_ns; - } - void set_client_metrics(Gallery_Client_Performance metrics) noexcept { - const auto finite = [](double value, double maximum = 100000.0) { - return std::isfinite(value) ? std::clamp(value, 0.0, maximum) : 0.0; - }; - metrics.transport_fps = finite(metrics.transport_fps); - metrics.presentation_fps = finite(metrics.presentation_fps); - metrics.frame_round_trip_ms = finite(metrics.frame_round_trip_ms); - metrics.frame_round_trip_average_ms = finite(metrics.frame_round_trip_average_ms); - metrics.frame_round_trip_deviation_ms = finite(metrics.frame_round_trip_deviation_ms); - metrics.frame_round_trip_p95_ms = finite(metrics.frame_round_trip_p95_ms); - metrics.frame_round_trip_p99_ms = finite(metrics.frame_round_trip_p99_ms); - metrics.display_interval_ms = finite(metrics.display_interval_ms); - metrics.display_interval_average_ms = finite(metrics.display_interval_average_ms); - metrics.display_interval_latest_ms = finite(metrics.display_interval_latest_ms); - metrics.display_interval_p95_ms = finite(metrics.display_interval_p95_ms); - metrics.display_interval_p99_ms = finite(metrics.display_interval_p99_ms); - metrics.display_interval_deviation_ms = finite(metrics.display_interval_deviation_ms); - metrics.last_pixel_receive_age_ms = finite(metrics.last_pixel_receive_age_ms); - metrics.last_pixel_change_age_ms = finite(metrics.last_pixel_change_age_ms); - client_performance_ = metrics; - apply_consumer_feedback(); - } - [[nodiscard]] adminive::Update_Result apply_patch(std::string_view target, - const nlohmann::json& patch) { - auto result = controls_.apply(target, patch); - if (!result.success) - return result; - if (target == "scene") { - apply_consumer_feedback(); - render_history_.clear(); - } - apply_layout(); - update_model(); - plot_.request_redraw(); - return result; + + adminive::Update_Result apply_patch( + std::string_view, const nlohmann::json&) override { + return {false, "no editable resource matches the requested target"}; } + void resize(int width, int height) override { - const Size previous = plot_.viewport_size(); - const Size current{width, height}; - plot_.set_viewport_size(current); - apply_layout(); - Resize_Event event; - event.old_size = previous; - event.new_size = current; - plot_.dispatch_event(event); + const Size requested{std::max(1, width), std::max(1, height)}; + if (requested == viewport_) return; + viewport_ = requested; + rebuild(); } + void dispatch(const Gallery_Input_Event& event) override { std::visit([this](const auto& value) { using T = std::decay_t; - if constexpr (std::is_same_v) { - Event input(event_type(value.type)); - if (input.type == Event_Type::Show) - plot_.activate_view(); - else if (input.type == Event_Type::Hide) - plot_.deactivate_view(); - plot_.dispatch_event(input); - } else if constexpr (std::is_same_v) { - Pointer_Event input(event_type(value.type)); - input.position = {value.x, value.y}; - input.global_position = {value.global_x, value.global_y}; - input.button = mouse_button(value.button); - input.buttons = value.buttons; - input.modifiers = modifiers(value.modifiers); - plot_.dispatch_event(input); - } else if constexpr (std::is_same_v) { - Wheel_Event input; - input.position = {value.x, value.y}; - input.global_position = {value.global_x, value.global_y}; - input.button = mouse_button(value.button); - input.buttons = value.buttons; - input.modifiers = modifiers(value.modifiers); - input.angle_delta_x = value.angle_delta_x; - input.angle_delta_y = value.angle_delta_y; - input.pixel_delta_x = value.pixel_delta_x; - input.pixel_delta_y = value.pixel_delta_y; - plot_.dispatch_event(input); - } else if constexpr (std::is_same_v) { - Key_Event input(event_type(value.type)); - input.key = key(value.key); - input.native_key = value.native_key; - input.modifiers = modifiers(value.modifiers); - input.auto_repeat = value.auto_repeat; - plot_.dispatch_event(input); + if constexpr (std::same_as) { + if (value.type == Gallery_View_Input_Type::Show) + scene_->activate_view(); + else if (value.type == Gallery_View_Input_Type::Hide) + scene_->deactivate_view(); } }, event); } + Gallery_Render_Result render_latest_frame() override { - if (!plot_.view_active()) + update_samples(); + const auto result = scene_->render_frame(true); + if (!result) return Gallery_Render_Result::backend_failure; + if (*result == Plot_Render_Status::view_inactive) return Gallery_Render_Result::inactive; - update_model(); - const auto started = std::chrono::steady_clock::now(); - const auto result = plot_.render_frame(true); - const bool rendered = result && *result == Plot_Render_Status::rendered; - record_performance(started, rendered); - if (rendered) - rendered_since_last_pixel_ = true; - return gallery_render_result(result); + if (*result != Plot_Render_Status::rendered) + return Gallery_Render_Result::frame_unavailable; + if (scene_->wait_for_render() != Scene_Render_Result::none) + return Gallery_Render_Result::backend_failure; + ++render_count_; + return Gallery_Render_Result::none; } - [[nodiscard]] std::optional encode_latest_pixels() { - if (!plot_.view_active()) - return std::nullopt; - if (!rendered_since_last_pixel_ && !can_render_automatically() && - render_latest_frame() != Gallery_Render_Result::none) - return std::nullopt; - rendered_since_last_pixel_ = false; - std::string pixels; - const Color background = plot_.background_color(); - const std::uint64_t sequence = plot_.frame_status().latest_sequence; - plot_.with_frame([&pixels, background, sequence](Image_View image) { - pixels = encode_pixel_frame(image, sequence, background); + + std::optional encode_latest_pixels() override { + std::string result; + scene_->with_frame([&](Image_View image) { + result = encode_pixel_frame(image, render_count_); }); - return pixels.empty() ? std::nullopt : std::optional(std::move(pixels)); + if (result.empty()) return std::nullopt; + return result; } - void record_pixel_response(std::chrono::steady_clock::time_point request_started, - std::chrono::steady_clock::time_point encode_started, - std::chrono::steady_clock::time_point encode_finished, - std::size_t pixel_bytes) { - record_pixel_performance(request_started, encode_started, encode_finished, pixel_bytes); - } - [[nodiscard]] std::string action(const Gallery_Action_Request& request, - bool& recognized) { - last_action_result_.clear(); + + void record_pixel_response(std::chrono::steady_clock::time_point, + std::chrono::steady_clock::time_point, + std::chrono::steady_clock::time_point, + std::size_t) override {} + + std::string action(const Gallery_Action_Request& request, + bool& recognized) override { recognized = true; + if (request.id == "frame_strategy_manual" || + request.id == "frame_strategy_low_latency" || + request.id == "frame_strategy_playback") { + const auto next = request.id == "frame_strategy_manual" + ? Gallery_Frame_Mode::Manual + : request.id == "frame_strategy_playback" + ? Gallery_Frame_Mode::Playback + : Gallery_Frame_Mode::Low_Latency; + const auto operation = scene_->edit_renderables( + [strategy = make_strategy(next)]( + Scene_2D_Base::Renderable_Editor& editor) mutable { + editor.replace_frame_control_strategy(std::move(strategy)); + }); + if (operation.wait() != Scene_Edit_Error::none) + return "frame strategy replacement rejected"; + mode_ = next; + return "frame strategy replaced"; + } + if (request.id == "mode_cycle" || request.id == "mode_render") { + static_cast(render_latest_frame()); + return "frame rendered"; + } if (request.id == "capture_next_frame") { - const auto state = plot_.render_scene().capture_state(); - if (state.enabled()) - return "A performance capture session is already active"; - const auto capture = plot_.render_scene().capture_next_frame(); - if (!capture) { - if (capture.error == Capture_Error::session_active) - return "A performance capture session is already active"; - ::renderive::error::unexpected("validated capture request rejected"); - } - last_action_result_ = "capture session=" + std::to_string(capture.session_id); - return "Capture next render frame requested"; + const auto capture = scene_->capture_next_frame(); + if (capture) static_cast(render_latest_frame()); + return capture ? "frame captured" : "capture request rejected"; } if (request.id == "capture_frames") { - const auto argument = action_number(request); - if (!argument) - return invalid_argument(recognized); - const std::size_t count = static_cast( - std::clamp(std::llround(*argument), 1LL, 1000LL)); - const auto state = plot_.render_scene().capture_state(); - if (state.enabled()) - return "A performance capture session is already active"; - const auto capture = plot_.render_scene().capture_frames(count); - if (!capture) { - if (capture.error == Capture_Error::session_active) - return "A performance capture session is already active"; - ::renderive::error::unexpected("validated capture request rejected"); - } - last_action_result_ = "capture session=" + std::to_string(capture.session_id) + - ", frames=" + std::to_string(count); - return "Consecutive render frame capture requested"; - } - if (request.id == "mode_prepare" || request.id == "mode_enqueue") { - update_model(); - const bool prepared = plot_.prepare_frame().has_value(); - last_action_result_ = prepared ? "frame prepared" : "prepare rejected"; - return prepared ? "Kernel 帧已准备/入队" : "Kernel 拒绝准备帧"; - } - if (request.id == "mode_enqueue_burst") { - const auto argument = action_number(request); - if (!argument) - return invalid_argument(recognized); - const int count = std::clamp(static_cast(std::lround(*argument)), 1, 256); - int prepared{}; - for (int index = 0; index < count; ++index) { - update_model(); - prepared += plot_.prepare_frame().has_value() ? 1 : 0; - } - last_action_result_ = "enqueued=" + std::to_string(prepared); - return "回放帧已批量压入 Flow 队列"; - } - if (request.id == "mode_refresh") { - const bool refreshed = plot_.refresh_manual_frame().has_value(); - last_action_result_ = refreshed ? "manual refresh succeeded" : "manual refresh failed"; - return refreshed ? "Manual 待处理帧已提交刷新" : "当前没有可刷新的 Manual 帧"; - } - if (request.id == "mode_render" || request.id == "mode_dequeue") { - const auto started = std::chrono::steady_clock::now(); - const bool rendered = plot_.render_prepared_frame().has_value(); - record_performance(started, rendered); - rendered_since_last_pixel_ = rendered; - last_action_result_ = rendered ? "prepared frame rendered" : "no prepared frame"; - return rendered ? "Kernel 已渲染准备帧" : "当前没有可消费的准备帧"; - } - if (request.id == "mode_discard") { - const bool discarded = plot_.discard_pending_frame(); - last_action_result_ = discarded ? "pending frame discarded" : "no pending frame"; - return discarded ? "待处理帧已丢弃" : "当前没有可丢弃帧"; - } - if (request.id == "mode_cycle") { - update_model(); - const auto started = std::chrono::steady_clock::now(); - const auto result = plot_.render_frame(true); - const bool rendered = result && *result == Plot_Render_Status::rendered; - record_performance(started, rendered); - rendered_since_last_pixel_ = rendered; - last_action_result_ = rendered ? "full frame cycle rendered" : "frame cycle skipped"; - return rendered ? "完整帧控制周期已执行" : "完整帧控制周期未产生图像"; - } - if (request.id == "read_data_shape") { - last_action_result_ = "data shape refreshed in observer telemetry"; - return "输入点数、实际绘制点/单元数已刷新到观察者与性能面板"; - } - if (request.id == "toggle_view") { - if (plot_.view_active()) - plot_.deactivate_view(); - else - plot_.activate_view(); - return plot_.view_active() ? "Plot view 已激活" : "Plot view 已停用;可再次执行恢复"; - } - if (request.id == "axis_probe") - return "坐标映射、刻度步长、次刻度数与标签结果已刷新到遥测区"; - if (request.id == "append_time" && time_axis_) { - const int tick = time_axis_->append_time(current_time_of_day()); - last_action_result_ = "tick=" + std::to_string(tick) + - ", ms=" + std::to_string(time_axis_->tick_to_time(tick).milliseconds); - return "Time_Axis 时间点已追加并回读"; - } - if (spectrum_) { - if (request.id == "push_samples") { - push_spectrum_samples(); - return "Spectrum 样本已手动推送"; - } - if (request.id == "power_at") { - const auto argument = action_number(request); - if (!argument) - return invalid_argument(recognized); - bool ok{}; - const double power = spectrum_->power_at(*argument, ok); - last_action_result_ = ok ? std::to_string(power) + " dB" : "频率不在样本范围"; - return "Spectrum::power_at 查询完成"; - } - if (request.id == "add_marker" || request.id == "add_line_marker" || - request.id == "remove_marker" || request.id == "set_marker_frequency") { - const auto argument = action_number(request); - if (!argument) - return invalid_argument(recognized); - if (request.id == "add_marker") - spectrum_->add_custom_marker(*argument); - else if (request.id == "add_line_marker") - spectrum_->add_custom_line_marker(*argument); - else if (request.id == "remove_marker") - spectrum_->remove_custom_marker(*argument); - else { - const int selected = spectrum_->selected_marker_index(); - if (selected >= 0) - spectrum_->set_marker_frequency(selected, *argument); - spectrum_->set_current_marker_frequency(*argument); - } - return "Marker API 已执行"; - } - if (request.id == "remove_selected_marker") { - spectrum_->remove_selected_marker(); - return "选中 Marker 已删除"; - } - if (request.id == "clear_markers") { - spectrum_->clear_custom_markers(); - return "全部 Marker 已清空"; - } - if (request.id == "select_marker") { - const auto argument = action_number(request); - if (!argument) - return invalid_argument(recognized); - spectrum_->set_selected_marker_index(static_cast(std::lround(*argument))); - return "Marker 索引已选择"; - } - if (request.id == "select_next_marker") { - spectrum_->select_next_marker(); - return "已选择下一个 Marker"; - } - if (request.id == "select_previous_marker") { - spectrum_->select_previous_marker(); - return "已选择上一个 Marker"; - } - if (request.id == "clear_marker_selection") { - spectrum_->clear_marker_selection(); - return "Marker 选择已清除"; - } - } - if (selection_) { - if (request.id == "clear_selection") { - selection_->clear_selected_regions(); - return "框选区域已清空"; - } - } - if (waterfall_) { - if (request.id == "append_row") { - push_waterfall_row(); - return "Waterfall 行已手动追加"; - } - if (request.id == "append_tick_row") { - const int tick = time_axis_->append_time(current_time_of_day()); - waterfall_->append_row(tick, spectrum_values(waterfall_->get<&Waterfall::Properties::frequency_bin_count>(), - waterfall_->get<&Waterfall::Properties::power_range>())); - return "Waterfall 已通过 int tick 重载追加一行"; - } - } - if (afterglow_) { - if (request.id == "append_spectrum") { - push_afterglow_spectrum(); - return "Afterglow 频谱已手动追加"; - } - } - if (sweep_) { - if (request.id == "append_block") { - push_sweep_block(); - return "Sweep_Spectrum 扫频块已手动追加"; - } - } - if (trace_) { - if (request.id == "append_sample") { - push_trace_sample(); - return "Frequency_Trace 样本已手动追加"; - } - if (request.id == "append_tick_sample") { - const int tick = time_axis_->append_time(current_time_of_day()); - trace_->append_sample(tick, std::sin(frame_index_ * 0.12)); - return "Frequency_Trace 已通过 int tick 重载追加样本"; - } - } - if (constellation_) { - if (request.id == "append_points") { - push_constellation_points(); - return "Constellation IQ 点已手动追加"; - } - if (request.id == "fit_square") { - constellation_->fit_square_to_axes(); - return "Constellation 已按轴拟合正方形"; - } + std::size_t count = 20; + if (request.argument && std::holds_alternative(*request.argument)) + count = static_cast(std::clamp( + std::get(*request.argument), 1.0, 1000.0)); + const auto capture = scene_->capture_frames(count); + if (capture) static_cast(render_latest_frame()); + return capture ? "frame capture started" : "capture request rejected"; } recognized = false; return {}; } - void record_action_notice_if_empty(std::string_view notice) { - if (last_action_result_.empty()) - last_action_result_ = notice; - } - [[nodiscard]] std::string telemetry_json() const { - const auto observer = plot_.frame_status(); - const auto telemetry_now = std::chrono::steady_clock::now(); - const double elapsed_seconds = std::max( - std::chrono::duration(telemetry_now - performance_started_).count(), - 1e-9); - const double render_fps = recent_rate(render_history_, telemetry_now); - const double pixel_fps = recent_pixel_rate(pixel_history_, telemetry_now); - const double pixel_megabytes_per_second = recent_pixel_megabytes_per_second( - pixel_history_, telemetry_now); - const auto render_window = render_duration_statistics_.snapshot(); - const auto pixel_encode_window = pixel_encode_statistics_.snapshot(); - const auto pixel_request_window = pixel_request_statistics_.snapshot(); - const bool low_latency = frame_mode_ == Gallery_Frame_Mode::Low_Latency; - const Gallery_Consumer_Feedback_Snapshot consumer_feedback{ - low_latency && feedback_policy_.enabled, - low_latency && feedback_policy_.pixel, - low_latency && feedback_policy_.presentation, - low_latency && feedback_policy_.manual, - low_latency ? feedback_policy_.manual_fps : 0.0, - consumer_feedback_source_, - consumer_pixel_interval_ns_, - consumer_presentation_interval_ns_, - consumer_manual_interval_ns_}; - Gallery_Render_Performance render_performance; - render_performance.render_attempt_count = render_attempt_count_; - render_performance.successful_render_count = successful_render_count_; - render_performance.failed_render_count = - render_attempt_count_ - successful_render_count_; - render_performance.measured_fps = render_fps; - render_performance.lifetime_average_fps = - static_cast(successful_render_count_) / elapsed_seconds; - render_performance.last_render_ms = last_render_ms_; - render_performance.average_render_ms = render_window.average; - render_performance.maximum_render_ms = render_window.maximum; - render_performance.render_deviation_ms = render_window.deviation; - render_performance.render_p50_ms = render_window.p50; - render_performance.render_p95_ms = render_window.p95; - render_performance.render_p99_ms = render_window.p99; - render_performance.render_sample_count = render_window.sample_count; - render_performance.pixel_response_fps = pixel_fps; - render_performance.last_pixel_snapshot_ms = last_pixel_snapshot_ms_; - render_performance.last_pixel_encode_ms = last_pixel_encode_ms_; - render_performance.average_pixel_encode_ms = pixel_encode_window.average; - render_performance.maximum_pixel_encode_ms = pixel_encode_window.maximum; - render_performance.pixel_encode_deviation_ms = pixel_encode_window.deviation; - render_performance.pixel_encode_p50_ms = pixel_encode_window.p50; - render_performance.pixel_encode_p95_ms = pixel_encode_window.p95; - render_performance.pixel_encode_p99_ms = pixel_encode_window.p99; - render_performance.pixel_encode_sample_count = pixel_encode_window.sample_count; - render_performance.last_pixel_request_ms = last_pixel_request_ms_; - render_performance.average_pixel_request_ms = pixel_request_window.average; - render_performance.pixel_request_deviation_ms = pixel_request_window.deviation; - render_performance.pixel_request_p95_ms = pixel_request_window.p95; - render_performance.pixel_request_p99_ms = pixel_request_window.p99; - render_performance.last_pixel_bytes = last_pixel_bytes_; - render_performance.pixel_payload_megabytes_per_second = - pixel_megabytes_per_second; - render_performance.automatic_low_latency_scheduler = - automatic_low_latency_ && frame_mode_ == Gallery_Frame_Mode::Low_Latency; - const Gallery_Client_Performance client_performance = client_performance_; - const auto scene_execution = plot_.render_scene().execution_statistics(); - nlohmann::json observer_data = - adminive::to_frontend_json(observer); - nlohmann::json consumer_feedback_data = - adminive::to_frontend_json(consumer_feedback); - nlohmann::json render_performance_data = - adminive::to_frontend_json(render_performance); - nlohmann::json client_performance_data = - adminive::to_frontend_json(client_performance); - nlohmann::json telemetry{ - {"case", case_id_}, - {"frame_mode", gallery_enum_id(frame_mode_)}, - {"frame_index", frame_index_}, - { - "viewport", { - {"width", plot_.viewport_size().width}, - {"height", plot_.viewport_size().height} - } - }, - {"view_active", plot_.view_active()}, - {"kernel_frame_count", plot_.diagnostics().refresh.frame_count}, - {"performance", std::move(render_performance_data)}, - {"kernel_observer", std::move(observer_data)}, - {"consumer_feedback", std::move(consumer_feedback_data)}, - {"client_performance", std::move(client_performance_data)}, - {"scheduler", adminive::to_frontend_json( - renderive::scheduling::scheduler_statistics())}, - {"queue_pressure", - {{"scene_execution", - {{"capacity", scene_execution.capacity}, - {"queued", scene_execution.queued}, - {"peak_queued", scene_execution.peak_queued}, - {"backpressure_count", scene_execution.backpressure_count}, - {"backpressure_wait_ns", scene_execution.backpressure_wait_ns}}}}}, - {"renderable_observers", controls_.observers(plot_.render_scene())}, - {"performance_capture", gallery_performance_capture_json(plot_.render_scene())}, - {"last_action_result", last_action_result_} - }; - if (primary_) { - telemetry["renderable"] = { - {"object_name", primary_->object_name()}, - {"visible", primary_->is_visible()}, - { - "cache_mode", magic_enum::enum_name(primary_->get_cache_mode()) - } - }; - } - if (const auto overlay = plot_.performance_overlay()) { - telemetry["performance_overlay_enabled"] = overlay->enabled(); - if (const auto snapshot = overlay->display_snapshot()) - telemetry["performance_overlay_lines"] = snapshot->lines.size(); - } - if (const auto axis = horizontal_axis()) { - const Axis_Transform transform = axis->transform(); - const Range range = transform.coordinate_range; - const double center = range.center(); - telemetry["axis"] = { - {"origin", range.origin}, {"target", range.target}, - {"start_coord", range.origin}, - {"end_coord", range.target}, - {"center_pixel", transform.coord_to_pixel(center)}, - { - "center_roundtrip", transform.pixel_to_coord( - transform.coord_to_pixel(center)) - }, - {"pixel_samples", static_cast(transform.pixel_length) + - (transform.pixel_length > 0.0 ? 1 : 0)}, - {"tick_step", axis->tick_step(range)}, - { - "sub_tick_count", axis->sub_tick_count(axis->tick_step(range)) - }, - {"center_label", axis->tick_label(center)} - }; - } - if (spectrum_) { - telemetry["spectrum"] = { - {"selectable_line_markers", spectrum_->selectable_line_marker_count()}, - {"selected_marker_index", spectrum_->selected_marker_index()}, - {"configured_frequency_points", spectrum_->get<&Spectrum::Properties::frequency_point_size>()}, - {"input_sample_count", spectrum_->sample_count()}, - {"rendered_point_count", spectrum_->rendered_point_count()} - }; - const int selected = spectrum_->selected_marker_index(); - if (selected >= 0 && selected < spectrum_->selectable_line_marker_count()) - telemetry["spectrum"]["selected_marker_frequency"] = - spectrum_->marker_frequency(selected); - } - if (selection_) - telemetry["selection_regions"] = selection_->selected_regions().size(); - if (waterfall_) { - telemetry["waterfall"] = { - {"configured_frequency_bins", waterfall_->get<&Waterfall::Properties::frequency_bin_count>()}, - {"row_count", waterfall_->row_count()}, - {"stored_point_count", waterfall_->stored_point_count()}, - {"rendered_cell_count", waterfall_->rendered_cell_count()}, - {"time_axis_point_count", time_axis_ ? time_axis_->time_point_count() : 0} - }; - } - if (afterglow_) { - telemetry["afterglow"] = { - {"configured_frequency_points", afterglow_->get<&Afterglow::Properties::frequency_point_size>()}, - {"configured_power_points", afterglow_->get<&Afterglow::Properties::power_point_size>()}, - {"history_frame_count", afterglow_->history_count()}, - {"latest_input_point_count", afterglow_->latest_spectrum_point_count()}, - {"rendered_cell_count", afterglow_->rendered_cell_count()} - }; - } - if (sweep_) { - telemetry["sweep_spectrum"] = { - {"configured_bins_per_block", sweep_->get<&Sweep_Spectrum::Properties::bins_per_block>()}, - {"configured_block_count", sweep_->get<&Sweep_Spectrum::Properties::block_count>()}, - {"stored_block_count", sweep_->stored_block_count()}, - {"stored_point_count", sweep_->stored_point_count()}, - {"rendered_point_count", sweep_->rendered_point_count()} - }; - } - if (trace_) { - telemetry["frequency_trace"] = { - {"sample_count", trace_->sample_count()}, - {"rendered_point_count", trace_->rendered_point_count()}, - {"time_axis_point_count", time_axis_ ? time_axis_->time_point_count() : 0} - }; - } - if (constellation_) { - const int anchor_count = static_cast( - constellation_->get<&Constellation_Diagram::Properties::type>()); - telemetry["constellation"] = { - {"point_count", constellation_->point_count()}, - {"anchor_count", anchor_count}, - { - "rendered_point_count", constellation_->point_count() + - static_cast(anchor_count) - }, - {"point_lifetime_ms", constellation_->get<&Constellation_Diagram::Properties::point_lifetime_ms>()} - }; - } - if (case_id_ == "axis_lab") { - telemetry["axis_lab"] = { - {"domain_axis_pixel_samples", horizontal_axis() - ? static_cast(horizontal_axis()->transform().pixel_length) + 1 - : 0}, - {"time_axis_point_count", time_axis_ ? time_axis_->time_point_count() : 0} - }; - } - if (spectrum_) - telemetry["data_shape"] = { - {"input_points", spectrum_->sample_count()}, - {"rendered_elements", spectrum_->rendered_point_count()}, - {"unit", "points"} - }; - else if (waterfall_) - telemetry["data_shape"] = { - {"input_points", waterfall_->stored_point_count()}, - {"rendered_elements", waterfall_->rendered_cell_count()}, - {"unit", "cells"} - }; - else if (afterglow_) - telemetry["data_shape"] = { - {"input_points", afterglow_->latest_spectrum_point_count()}, - {"rendered_elements", afterglow_->rendered_cell_count()}, - {"unit", "cells"} - }; - else if (sweep_) - telemetry["data_shape"] = { - {"input_points", sweep_->stored_point_count()}, - {"rendered_elements", sweep_->rendered_point_count()}, - {"unit", "points"} - }; - else if (trace_) - telemetry["data_shape"] = { - {"input_points", trace_->sample_count()}, - {"rendered_elements", trace_->rendered_point_count()}, - {"unit", "points"} - }; - else if (constellation_) { - const std::size_t anchors = static_cast( - constellation_->get<&Constellation_Diagram::Properties::type>()); - telemetry["data_shape"] = { - {"input_points", constellation_->point_count()}, - {"rendered_elements", constellation_->point_count() + anchors}, - {"unit", "points"} - }; - } - else - telemetry["data_shape"] = { - {"input_points", 0}, - {"rendered_elements", horizontal_axis() - ? static_cast(horizontal_axis()->transform().pixel_length) + 1 - : 0}, - {"unit", "axis samples"} - }; - return telemetry.dump(); + + void record_action_notice_if_empty(std::string_view) override {} + + std::string telemetry_json() const override { + const auto observer = scene_->observer(); + auto kernel_observer = observer_json(observer); + kernel_observer["mode"] = mode_id(mode_); + kernel_observer["event"] = "render_completed"; + kernel_observer["limit"] = "none"; + return nlohmann::json{ + {"kernel_observer", std::move(kernel_observer)}, + {"performance", {{"successful_render_count", render_count_}}}, + {"client_performance", {{"transport_fps", client_.transport_fps}}}, + {"performance_capture", gallery_performance_capture_json(*scene_)}} + .dump(); } + private: - using Performance_Clock = std::chrono::steady_clock; - struct Pixel_Performance_Sample { - Performance_Clock::time_point time; - std::size_t bytes; - }; - static void record_timestamp(std::deque& history, - Performance_Clock::time_point now) { - history.push_back(now); - const auto oldest = now - std::chrono::seconds(1); - while (history.size() > 2 && history.front() < oldest) - history.pop_front(); + template + static renderive_Owner build( + typename Product::Builder& builder, Arguments&&... arguments) { + return static_cast&>( + builder) + .build(std::forward(arguments)...); } - static double recent_rate(const std::deque& history, - Performance_Clock::time_point now) { - if (history.size() < 2 || now - history.back() > std::chrono::seconds(1)) - return 0.0; - const double seconds = - std::chrono::duration(history.back() - history.front()).count(); - return seconds > 0.0 ? static_cast(history.size() - 1) / seconds : 0.0; - } - static void record_pixel_sample(std::deque& history, - Performance_Clock::time_point now, - std::size_t bytes) { - history.push_back({now, bytes}); - const auto oldest = now - std::chrono::seconds(1); - while (history.size() > 2 && history.front().time < oldest) - history.pop_front(); - } - static double recent_pixel_rate(const std::deque& history, - Performance_Clock::time_point now) { - if (history.size() < 2 || now - history.back().time > std::chrono::seconds(1)) - return 0.0; - const double seconds = - std::chrono::duration(history.back().time - history.front().time).count(); - return seconds > 0.0 ? static_cast(history.size() - 1) / seconds : 0.0; - } - static double recent_pixel_megabytes_per_second( - const std::deque& history, - Performance_Clock::time_point now) { - if (history.size() < 2 || now - history.back().time > std::chrono::seconds(1)) - return 0.0; - const double seconds = - std::chrono::duration(history.back().time - history.front().time).count(); - if (seconds <= 0.0) - return 0.0; - std::size_t bytes{}; - for (std::size_t index = 1; index < history.size(); ++index) - bytes += history[index].bytes; - return static_cast(bytes) / seconds / 1e6; - } - void apply_consumer_feedback() { - if (frame_mode_ != Gallery_Frame_Mode::Low_Latency) - return; - consumer_pixel_interval_ns_ = frequency_to_ns(client_performance_.transport_fps); - consumer_presentation_interval_ns_ = - milliseconds_to_ns(client_performance_.display_interval_ms); - consumer_manual_interval_ns_ = frequency_to_ns(feedback_policy_.manual_fps); - consumer_feedback_source_ = "none"; - if (!feedback_policy_.enabled) { - if (!plot_.clear_consumer_feedback()) - ::renderive::error::unexpected("low-latency feedback clearing failed"); - consumer_feedback_source_ = "disabled"; - return; + + void rebuild() { + Render_Scene_2D::State scene_state; + scene_state.viewport = viewport_; + scene_state.background = Color{3, 8, 14, 255}; + scene_ = Render_Scene_2D::Builder{scene_state}.build( + make_strategy(mode_)); + + { + auto attach = scene_->attach_builder(); + const auto root = scene_->root_renderable(); + + const bool numeric_domain = + case_id_ == "sweep_spectrum" || case_id_ == "constellation"; + const bool time_domain = case_id_ == "frequency_trace"; + const int left = 62; + const int top = 18; + const auto width = static_cast( + std::max(1, viewport_.width - 86)); + const auto height = static_cast( + std::max(1, viewport_.height - 64)); + if (time_domain || case_id_ == "waterfall" || case_id_ == "axis_lab") { + Time_Axis::State state; + state.orientation = time_domain || case_id_ == "axis_lab" + ? Orientation::Horizontal + : Orientation::Vertical; + state.visible_count = 80; + state.x = left; + state.y = state.orientation == Orientation::Horizontal + ? top + static_cast(height) + : top; + state.pixel_length = + state.orientation == Orientation::Horizontal ? width : height; + state.color = Color{69, 221, 190, 255}; + Time_Axis::Builder builder{state, &attach}; + time_axis_ = build(builder, root); + time_axis_->set_object_name("Time Axis"); } - std::uint64_t interval_ns = 0; - const auto select_source = [&interval_ns, this](bool enabled, std::uint64_t candidate, - std::string_view source) { - if (!enabled || candidate == 0) - return; - if (candidate > interval_ns) { - interval_ns = candidate; - consumer_feedback_source_ = source; - return; - } - if (candidate == interval_ns) { - if (consumer_feedback_source_ != "none") - consumer_feedback_source_ += '+'; - consumer_feedback_source_ += source; - } - }; - select_source(feedback_policy_.pixel, - consumer_pixel_interval_ns_, "pixel"); - select_source(feedback_policy_.presentation, - consumer_presentation_interval_ns_, "presentation"); - select_source(feedback_policy_.manual, - consumer_manual_interval_ns_, "manual"); - if (interval_ns == 0) { - if (!plot_.clear_consumer_feedback()) - ::renderive::error::unexpected("low-latency feedback clearing failed"); - return; + if (!time_domain && !numeric_domain) { + Frequency_Axis::State state; + state.orientation = Orientation::Horizontal; + state.coordinates = Range{88'000'000.0, 108'000'000.0}; + state.x = left; + state.y = top + static_cast(height); + state.pixel_length = width; + state.color = Color{118, 145, 184, 255}; + state.wheel = true; + state.drag = true; + Frequency_Axis::Builder builder{state, &attach}; + frequency_axis_ = build(builder, root); + frequency_axis_->set_object_name("Frequency Axis"); + } else if (numeric_domain) { + Axis::State state; + state.orientation = Orientation::Horizontal; + state.coordinates = case_id_ == "constellation" + ? Range{-1.25, 1.25} + : Range{0.0, 300.0}; + state.x = left; + state.y = top + static_cast(height); + state.pixel_length = width; + state.color = Color{118, 145, 184, 255}; + state.wheel = true; + state.drag = true; + Axis::Builder builder{state, &attach}; + domain_axis_ = build(builder, root); + domain_axis_->set_object_name("Domain Axis"); } - if (!plot_.set_consumer_feedback({interval_ns})) - ::renderive::error::unexpected("validated low-latency feedback was rejected"); - } - void record_performance(std::chrono::steady_clock::time_point started, bool rendered) { - const auto finished = std::chrono::steady_clock::now(); - ++render_attempt_count_; - const double duration_ms = - std::chrono::duration(finished - started).count(); - last_render_ms_ = duration_ms; - if (rendered) { - ++successful_render_count_; - render_duration_statistics_.add(duration_ms); - record_timestamp(render_history_, finished); - } - maybe_log_performance(finished); - } - void record_pixel_performance(Performance_Clock::time_point request_started, - Performance_Clock::time_point encode_started, - Performance_Clock::time_point encode_finished, - std::size_t pixel_bytes) { - last_pixel_snapshot_ms_ = - std::chrono::duration(encode_started - request_started).count(); - last_pixel_encode_ms_ = - std::chrono::duration(encode_finished - encode_started).count(); - last_pixel_request_ms_ = - std::chrono::duration(encode_finished - request_started).count(); - pixel_encode_statistics_.add(last_pixel_encode_ms_); - pixel_request_statistics_.add(last_pixel_request_ms_); - last_pixel_bytes_ = pixel_bytes; - ++pixel_frame_count_; - record_pixel_sample(pixel_history_, encode_finished, pixel_bytes); - } - void maybe_log_performance(Performance_Clock::time_point now) { - if (now - last_performance_log_ < std::chrono::seconds(1)) - return; - last_performance_log_ = now; - const auto observer = plot_.frame_status(); - const auto unix_ms = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - const double pixel_fps = recent_pixel_rate(pixel_history_, now); - const double pixel_megabytes_per_second = recent_pixel_megabytes_per_second( - pixel_history_, now); - const bool low_latency = frame_mode_ == Gallery_Frame_Mode::Low_Latency; - const auto scheduler_interval_ns = low_latency ? kernel_refresh_interval_ns() : 0; - const nlohmann::json line{ - {"event", "gallery_performance"}, - {"unix_ms", unix_ms}, - {"session_id", session_id_}, - {"case", case_id_}, - {"frame_mode", gallery_enum_id(frame_mode_)}, - {"configured_fps", low_latency ? plot_.max_render_fps() : 0.0}, - {"automatic_scheduler_interval_ns", scheduler_interval_ns}, - {"automatic_scheduler_fps", scheduler_interval_ns == 0 ? 0.0 : 1'000'000'000.0 / static_cast(scheduler_interval_ns)}, - {"automatic_scheduler_limit_source", low_latency ? observer.limit_state : "not_applicable"}, - {"frequency_limit_enabled", low_latency && observer.frequency_limit_enabled}, - {"consumer_feedback_enabled", low_latency && observer.consumer_feedback_enabled}, - {"consumer_feedback_sample_interval_ns", low_latency ? observer.consumer_sample_interval_ns : 0}, - {"consumer_feedback_smoothed_interval_ns", low_latency ? observer.consumer_smoothed_interval_ns : 0}, - {"consumer_feedback_variation_ns", low_latency ? observer.consumer_variation_ns : 0}, - {"consumer_feedback_safety_interval_ns", low_latency ? observer.consumer_safety_interval_ns : 0}, - {"consumer_feedback_interval_ns", low_latency ? observer.consumer_interval_ns : 0}, - {"backend_render_fps", recent_rate(render_history_, now)}, - {"pixel_response_fps", pixel_fps}, - {"client_transport_fps", client_performance_.transport_fps}, - {"client_presentation_fps", client_performance_.presentation_fps}, - {"client_changed_pixel_frames", client_performance_.changed_pixel_frames}, - {"client_duplicate_pixel_frames", client_performance_.duplicate_pixel_frames}, - {"client_frame_request_timeout_count", client_performance_.frame_request_timeout_count}, - {"client_frame_round_trip_ms", client_performance_.frame_round_trip_ms}, - {"client_display_interval_ms", client_performance_.display_interval_ms}, - {"client_display_interval_latest_ms", client_performance_.display_interval_latest_ms}, - {"client_display_interval_p95_ms", client_performance_.display_interval_p95_ms}, - {"client_display_interval_p99_ms", client_performance_.display_interval_p99_ms}, - {"client_display_interval_deviation_ms", client_performance_.display_interval_deviation_ms}, - {"client_overwritten_pixel_frames", client_performance_.overwritten_pixel_frames}, - {"client_last_pixel_receive_age_ms", client_performance_.last_pixel_receive_age_ms}, - {"client_last_pixel_change_age_ms", client_performance_.last_pixel_change_age_ms}, - {"last_render_ms", last_render_ms_}, - {"last_pixel_snapshot_ms", last_pixel_snapshot_ms_}, - {"last_pixel_encode_ms", last_pixel_encode_ms_}, - {"last_pixel_request_ms", last_pixel_request_ms_}, - {"pixel_payload_bytes", last_pixel_bytes_}, - {"pixel_payload_megabytes_per_second", pixel_megabytes_per_second}, - {"websocket_buffered_bytes", client_performance_.websocket_buffered_bytes}, - { - "automatic_low_latency_scheduler", - automatic_low_latency_ && frame_mode_ == Gallery_Frame_Mode::Low_Latency - }, - {"successful_render_count", successful_render_count_}, - {"failed_render_count", render_attempt_count_ - successful_render_count_}, - {"pixel_frame_count", pixel_frame_count_}, - {"last_event", observer.last_event}, - {"limit_state", observer.limit_state}, - {"observation_count", observer.observation_count}, - {"latest_sequence", observer.latest_sequence}, - {"produced_frame_count", observer.produced_frame_count}, - {"consumed_frame_count", observer.consumed_frame_count}, - {"target_interval_ns", observer.target_interval_ns}, - {"paint_duration_ns", observer.paint_duration_ns}, - {"render_duration_ns", observer.render_duration_ns}, - {"bottleneck_duration_ns", observer.bottleneck_duration_ns}, - {"consumer_sample_interval_ns", observer.consumer_sample_interval_ns}, - {"consumer_smoothed_interval_ns", observer.consumer_smoothed_interval_ns}, - {"consumer_variation_ns", observer.consumer_variation_ns}, - {"consumer_safety_interval_ns", observer.consumer_safety_interval_ns}, - {"consumer_interval_ns", observer.consumer_interval_ns}, - {"next_refresh_interval_ns", observer.next_refresh_interval_ns}, - {"paint_lease_wait_ns", observer.paint_lease_wait_ns}, - {"paint_state_wait_ns", observer.paint_state_wait_ns}, - {"publish_state_wait_ns", observer.publish_state_wait_ns}, - {"ready_wait_ns", observer.ready_wait_ns}, - {"frame_age_at_render_ns", observer.frame_age_at_render_ns}, - {"render_lease_wait_ns", observer.render_lease_wait_ns}, - {"render_state_wait_ns", observer.render_state_wait_ns}, - {"render_finish_state_wait_ns", observer.render_finish_state_wait_ns}, - {"queue_wait_ns", observer.queue_wait_ns}, - {"end_to_end_ns", observer.end_to_end_ns}, - {"pending_frames", observer.pending_frame_count}, - {"dropped_frames", observer.dropped_frame_count}, - {"failed_operations", observer.failed_operation_count} - }; - write_web_performance_log(line.dump()); - } - void build_axes(::Scene_2D_Base::Attach_Builder& attach) { - constexpr Color axis_color{118, 145, 184, 255}; - Range coordinate_range{88'000'000.0, 108'000'000.0}; - if (case_id_ == "sweep_spectrum") - coordinate_range = {0.0, 300.0}; - else if (case_id_ == "constellation") - coordinate_range = {-1.25, 1.25}; - if (case_id_ == "frequency_trace") { - time_axis_ = Gallery_Time_Axis::Builder{&attach} - .set<&Gallery_Time_Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Gallery_Time_Axis::Properties::visible_count>(80) - .set<&Gallery_Time_Axis::Properties::tick_label_spacing_px>(28) - .set<&Gallery_Time_Axis::Properties::format>("mm:ss.zzz") - .set<&Gallery_Time_Axis::Properties::color>(axis_color) - .build(axes_node_); - } - else if (case_id_ == "sweep_spectrum" || case_id_ == "constellation") { - numeric_domain_axis_ = Gallery_Axis::Builder{&attach} - .set<&Gallery_Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Gallery_Axis::Properties::coordinates>(coordinate_range) - .set<&Gallery_Axis::Properties::wheel>(true) - .set<&Gallery_Axis::Properties::drag>(true) - .set<&Gallery_Axis::Properties::color>(axis_color) - .build(axes_node_); - } - else { - frequency_domain_axis_ = Gallery_Frequency_Axis::Builder{&attach} - .set<&Gallery_Frequency_Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Gallery_Frequency_Axis::Properties::coordinates>(coordinate_range) - .set<&Gallery_Frequency_Axis::Properties::wheel>(true) - .set<&Gallery_Frequency_Axis::Properties::drag>(true) - .set<&Gallery_Frequency_Axis::Properties::color>(axis_color) - .build(axes_node_); - } - if (case_id_ == "waterfall") { - time_axis_ = Gallery_Time_Axis::Builder{&attach} - .set<&Gallery_Time_Axis::Properties::orientation>(Orientation::Vertical) - .set<&Gallery_Time_Axis::Properties::visible_count>(80) - .set<&Gallery_Time_Axis::Properties::tick_label_spacing_px>(28) - .set<&Gallery_Time_Axis::Properties::format>("mm:ss.zzz") - .set<&Gallery_Time_Axis::Properties::color>(axis_color) - .build(axes_node_); - } - else { - Range value_range{-120.0, -20.0}; - if (case_id_ == "frequency_trace") - value_range = {-1.0, 1.0}; - else if (case_id_ == "constellation") - value_range = {-1.25, 1.25}; - value_axis_ = Gallery_Axis::Builder{&attach} - .set<&Gallery_Axis::Properties::orientation>(Orientation::Vertical) - .set<&Gallery_Axis::Properties::coordinates>(value_range) - .set<&Gallery_Axis::Properties::precision>(case_id_ == "constellation" ? 2 : 0) - .set<&Gallery_Axis::Properties::color>(axis_color) - .build(axes_node_); - } - if (case_id_ == "axis_lab") { - time_axis_ = Gallery_Time_Axis::Builder{&attach} - .set<&Gallery_Time_Axis::Properties::orientation>(Orientation::Horizontal) - .set<&Gallery_Time_Axis::Properties::visible_count>(80) - .set<&Gallery_Time_Axis::Properties::tick_label_spacing_px>(28) - .set<&Gallery_Time_Axis::Properties::format>("mm:ss.zzz") - .set<&Gallery_Time_Axis::Properties::color>(Color{69, 221, 190, 255}) - .build(axes_node_); - } - const auto style = [this](const renderive_Owner& axis) { - if (!axis) - return; - update_axis_state(axis, [this](Axis_Base_Properties& value) { - value.tick_length = 8; - value.sub_tick_length = 4; - value.unit_text = case_id_ == "sweep_spectrum" ? "MHz" - : case_id_ == "constellation" ? "I / Q" - : case_id_ == "frequency_trace" ? "value" : "Hz"; - value.unit_text_font = {12.0, 500, false}; - value.unit_text_pen = {Color{220, 233, 255, 255}}; - value.unit_text_background_brush = { - Color{7, 17, 31, 255}, Brush_Style::Solid}; - }); - }; - style(horizontal_axis()); - style(vertical_axis()); - if (case_id_ == "axis_lab") - style(time_axis_); - } - void build_primary(::Scene_2D_Base::Attach_Builder& attach) { - if (case_id_ == "axis_lab") { - primary_ = renderive_static_owner_cast(frequency_domain_axis_); - return; + if (case_id_ != "waterfall") { + Axis::State state; + state.orientation = Orientation::Vertical; + state.coordinates = case_id_ == "constellation" + ? Range{-1.25, 1.25} + : time_domain ? Range{-1.0, 1.0} + : Range{-120.0, -20.0}; + state.x = left; + state.y = top; + state.pixel_length = height; + state.color = Color{118, 145, 184, 255}; + Axis::Builder builder{state, &attach}; + value_axis_ = build(builder, root); + value_axis_->set_object_name("Value Axis"); } if (case_id_ == "spectrum" || case_id_ == "selection_overlay") { - Gallery_Spectrum::Properties properties; - properties.frequency_range = {88'000'000.0, 108'000'000.0}; - properties.frequency_point_size = 512; - properties.center_frequency = 98'000'000.0; - properties.sweep_frequency_range = {94'000'000.0, 102'000'000.0}; - properties.max_hold_visible = true; - properties.max_marker_visible = true; - properties.sweep_region_visible = true; - properties.max_brush = {color_from_hex("#332810", 76), Brush_Style::Solid}; - properties.current_brush = {color_from_hex("#0e3a35", 76), Brush_Style::Solid}; - properties.min_brush = {color_from_hex("#19213a", 76), Brush_Style::Solid}; - properties.max_pen = {color_from_hex("#ffd166"), 1.2}; - properties.current_pen = {color_from_hex("#35e6b2"), 2.0, - Line_Style::Solid, Line_Cap::Round, Line_Join::Round}; - properties.min_pen = {color_from_hex("#7aa2ff"), 1.2}; - properties.selected_marker_pen = {color_from_hex("#ff4d8d"), 2.0}; - properties.marker_pen = {color_from_hex("#ff7a59"), 1.2}; - properties.middle_frequency_pen = {color_from_hex("#6bc8ff"), 1.0, Line_Style::Dash}; - properties.sweep_region_brush = {color_from_hex("#293257", 80), Brush_Style::Solid}; - spectrum_ = Gallery_Spectrum::Builder{properties, &attach}.build( - data_node_, frequency_domain_axis_, value_axis_); - primary_ = spectrum_; + Spectrum::State state; + state.frequency_range = {88'000'000.0, 108'000'000.0}; + state.frequency_point_size = 512; + state.center_frequency = 98'000'000.0; + state.max_hold_visible = true; + state.current_pen = {Color{53, 230, 178, 255}, 2.0}; + state.max_pen = {Color{255, 209, 102, 255}, 1.2}; + state.min_pen = {Color{122, 162, 255, 255}, 1.2}; + Spectrum::Builder builder{state, &attach}; + spectrum_ = build( + builder, root, frequency_axis_, value_axis_); + spectrum_->set_object_name("Spectrum"); if (case_id_ == "selection_overlay") { - Gallery_Selection_Rectangle_Overlay::Properties overlay; - overlay.label_font = {12.0, 500, false}; - overlay.selection_brush = {color_from_hex("#173b66", 70), Brush_Style::Solid}; - overlay.selection_border_pen = {color_from_hex("#7dd3fc"), 1.0, Line_Style::Dash}; - selection_ = Gallery_Selection_Rectangle_Overlay::Builder{overlay, &attach}.build( - overlay_node_, frequency_domain_axis_, value_axis_); - primary_ = selection_; + Selection_Rectangle_Overlay::State overlay_state; + overlay_state.selection_brush = + {Color{23, 59, 102, 90}, Brush_Style::Solid}; + Selection_Rectangle_Overlay::Builder overlay_builder{ + overlay_state, &attach}; + selection_ = build( + overlay_builder, root, frequency_axis_, value_axis_); + selection_->set_object_name("Selection Overlay"); } - return; + } else if (case_id_ == "waterfall") { + Waterfall::State state; + state.frequency_range = {88'000'000.0, 108'000'000.0}; + state.power_range = {-120.0, -20.0}; + state.frequency_bin_count = 256; + Waterfall::Builder builder{state, &attach}; + waterfall_ = build( + builder, root, frequency_axis_, time_axis_); + waterfall_->set_object_name("Waterfall"); + } else if (case_id_ == "afterglow") { + Afterglow::State state; + state.frequency_range = {88'000'000.0, 108'000'000.0}; + state.power_range = {-120.0, -20.0}; + state.frequency_point_size = 192; + state.power_point_size = 96; + state.attenuation_rate = 0.18; + Afterglow::Builder builder{state, &attach}; + afterglow_ = build( + builder, root, frequency_axis_, value_axis_); + afterglow_->set_object_name("Afterglow"); + } else if (case_id_ == "sweep_spectrum") { + Sweep_Spectrum::State state; + state.frequency_range = {0.0, 300.0}; + state.bins_per_block = 64; + state.block_count = 8; + Sweep_Spectrum::Builder builder{state, &attach}; + sweep_ = build( + builder, root, domain_axis_, value_axis_); + sweep_->set_object_name("Sweep Spectrum"); + } else if (case_id_ == "frequency_trace") { + Frequency_Trace::State state; + state.pen = {Color{53, 230, 178, 255}, 2.0}; + Frequency_Trace::Builder builder{state, &attach}; + trace_ = build( + builder, root, time_axis_, value_axis_); + trace_->set_object_name("Frequency Trace"); + } else if (case_id_ == "constellation") { + Constellation_Diagram::State state; + state.i_range = {-1.25, 1.25}; + state.q_range = {-1.25, 1.25}; + state.point_color = Color{73, 230, 195, 255}; + state.anchor_color = Color{255, 209, 102, 255}; + Constellation_Diagram::Builder builder{state, &attach}; + constellation_ = build( + builder, root, domain_axis_, value_axis_); + constellation_->set_object_name("Constellation Diagram"); } - if (case_id_ == "waterfall") { - Gallery_Waterfall::Properties properties; - properties.frequency_range = {88'000'000.0, 108'000'000.0}; - properties.power_range = {-120.0, -20.0}; - properties.frequency_bin_count = 256; - waterfall_ = Gallery_Waterfall::Builder{properties, &attach}.build( - data_node_, frequency_domain_axis_, time_axis_); - primary_ = waterfall_; - return; - } - if (case_id_ == "afterglow") { - Gallery_Afterglow::Properties properties; - properties.frequency_range = {88'000'000.0, 108'000'000.0}; - properties.power_range = {-120.0, -20.0}; - properties.frequency_point_size = 192; - properties.power_point_size = 96; - properties.attenuation_rate = 0.18; - afterglow_ = Gallery_Afterglow::Builder{properties, &attach}.build( - data_node_, frequency_domain_axis_, value_axis_); - primary_ = afterglow_; - return; - } - if (case_id_ == "sweep_spectrum") { - Gallery_Sweep_Spectrum::Properties properties; - properties.frequency_range = {0.0, 300.0}; - properties.bins_per_block = 64; - properties.block_count = 8; - properties.pen = {color_from_hex("#ffd166"), 1.8}; - properties.current_frequency_pen = {color_from_hex("#ff5d73"), 2.0}; - sweep_ = Gallery_Sweep_Spectrum::Builder{properties, &attach}.build( - data_node_, numeric_domain_axis_, value_axis_); - primary_ = sweep_; - return; - } - if (case_id_ == "frequency_trace") { - Gallery_Frequency_Trace::Properties properties; - properties.pen = {color_from_hex("#35e6b2"), 2.0, Line_Style::Solid, - Line_Cap::Round, Line_Join::Round}; - trace_ = Gallery_Frequency_Trace::Builder{properties, &attach}.build( - data_node_, time_axis_, value_axis_); - primary_ = trace_; - return; - } - if (case_id_ == "constellation") { - Gallery_Constellation_Diagram::Properties properties; - properties.i_range = {-1.25, 1.25}; - properties.q_range = {-1.25, 1.25}; - properties.point_color = color_from_hex("#49e6c3"); - properties.anchor_color = color_from_hex("#ffd166"); - properties.point_lifetime_ms = 1800; - constellation_ = Gallery_Constellation_Diagram::Builder{properties, &attach}.build( - data_node_, numeric_domain_axis_, value_axis_); - primary_ = constellation_; } + + scene_->activate_view(); + update_samples(); + static_cast(scene_->render_frame(true)); + static_cast(scene_->wait_for_render()); } - renderive_Owner horizontal_axis() const { - if (frequency_domain_axis_) - return frequency_domain_axis_; - if (numeric_domain_axis_) - return numeric_domain_axis_; - return time_axis_; - } - renderive_Owner vertical_axis() const { - if (case_id_ == "waterfall") - return time_axis_; - return value_axis_; - } - void apply_layout() { - const Size viewport = plot_.viewport_size(); - const int left = viewport.width < 440 ? 48 : 62; - const int right = 24; - const int top = case_id_ == "axis_lab" ? 48 : 18; - const int bottom = 46; - const int width = std::max(1, viewport.width - left - right); - const int height = std::max(1, viewport.height - top - bottom); - const auto layout_axis = [=](const renderive_Owner& target) { - if (!target) - return; - update_axis_state(target, [=](Axis_Base_Properties& axis) { - axis.x = left; - axis.y = axis.orientation == Orientation::Horizontal ? top + height : top; - axis.pixel_length = static_cast( - axis.orientation == Orientation::Horizontal ? width : height); - }); - }; - layout_axis(horizontal_axis()); - layout_axis(vertical_axis()); - if (case_id_ == "axis_lab" && time_axis_) { - time_axis_->update([&](auto& axis) { - axis.x = left; - axis.y = axis.orientation == Orientation::Horizontal ? 26 : top; - axis.pixel_length = static_cast( - axis.orientation == Orientation::Horizontal ? width : height); - }); - } - } - void register_controls() { - controls_.add(*session_control_); - if (frequency_domain_axis_) - controls_.add(*frequency_domain_axis_); - if (numeric_domain_axis_) - controls_.add("domain_axis", *numeric_domain_axis_); - if (value_axis_) - controls_.add("value_axis", *value_axis_); - if (time_axis_) - controls_.add(*time_axis_); - if (spectrum_) - controls_.add(*spectrum_); - if (waterfall_) - controls_.add(*waterfall_); - if (afterglow_) - controls_.add(*afterglow_); - if (sweep_) - controls_.add(*sweep_); - if (trace_) - controls_.add(*trace_); - if (selection_) - controls_.add(*selection_); - if (constellation_) - controls_.add(*constellation_); - } - std::vector spectrum_values(int count, Range power_range = {-120.0, -20.0}) const { - count = std::max(count, 2); - std::vector values(static_cast(count)); - const double time = static_cast(frame_index_) * 0.09; - for (int index = 0; index < count; ++index) { + + void update_samples() { + const std::size_t count = waterfall_ ? 256U : afterglow_ ? 192U : 512U; + std::vector values(count); + const double time = static_cast(sample_sequence_++) * 0.075; + for (std::size_t index = 0; index < count; ++index) { const double x = static_cast(index) / (count - 1); - const double noise = std::sin(index * 11.71 + time * 2.9) * - std::sin(index * 0.19 + time * 0.7) * 4.0; - const double signal = 58.0 * gaussian(x, 0.32 + std::sin(time) * 0.04, 0.035) + - 45.0 * gaussian(x, 0.57, 0.07) + - 32.0 * gaussian(x, 0.78, 0.018); - values[static_cast(index)] = - std::clamp(-112.0 + noise + signal, std::min(power_range.origin, power_range.target), - std::max(power_range.origin, power_range.target)); + const auto gaussian = [](double value, double center, double width) { + const double normalized = (value - center) / width; + return std::exp(-0.5 * normalized * normalized); + }; + values[index] = std::clamp( + -112.0 + std::sin(index * 0.31 + time) * 3.5 + + 62.0 * gaussian(x, 0.31 + std::sin(time) * 0.025, 0.025) + + 43.0 * gaussian(x, 0.58, 0.06) + + 30.0 * gaussian(x, 0.79, 0.015), + -120.0, -20.0); } - return values; - } - void push_spectrum_samples() { - spectrum_->update_samples(spectrum_values(spectrum_->get<&Spectrum::Properties::frequency_point_size>())); - } - void push_waterfall_row() { - waterfall_->append_row(current_time_of_day(), - spectrum_values(waterfall_->get<&Waterfall::Properties::frequency_bin_count>(), waterfall_->get<&Waterfall::Properties::power_range>())); - } - void push_afterglow_spectrum() { - afterglow_->append_spectrum(spectrum_values(afterglow_->get<&Afterglow::Properties::frequency_point_size>(), - afterglow_->get<&Afterglow::Properties::power_range>())); - } - void push_sweep_block() { - const int count = sweep_->get<&Sweep_Spectrum::Properties::bins_per_block>(); - std::vector block(static_cast(std::max(count, 1))); - for (int index = 0; index < count; ++index) { - const double x = static_cast(index) / std::max(1, count - 1); - block[static_cast(index)] = -104.0 + 68.0 * gaussian(x, 0.5, 0.16) + - std::sin(frame_index_ * 0.2 + index) * 4.0; + if (spectrum_) spectrum_->update_samples(values); + if (waterfall_) waterfall_->append_row(current_time_of_day(), values); + if (afterglow_) afterglow_->append_spectrum(values); + if (sweep_) { + values.resize(64); + sweep_->append_block(values); } - sweep_->append_block(block); - } - void push_trace_sample() { - const Range range = value_axis_->get<&Axis_Properties::coordinates>(); - const double lower = range.origin; - const double upper = range.target; - const double center = (lower + upper) * 0.5; - const double amplitude = std::abs(upper - lower) * 0.42; - trace_->append_sample(current_time_of_day(), center + amplitude * std::sin(frame_index_ * 0.12)); - } - void push_constellation_points() { - const int count = static_cast( - constellation_->get<&Constellation_Diagram::Properties::type>()); - const double phase = constellation_->get< - &Constellation_Diagram::Properties::phase_offset_radians>(); - for (int index = 0; index < 24; ++index) { - const int anchor = static_cast((frame_index_ * 7 + index * 5) % count); - const double angle = phase + 2.0 * std::numbers::pi * anchor / count; - const double noise = std::sin(frame_index_ * 0.31 + index * 1.73) * 0.055; - constellation_->append_point({ - std::cos(angle) + noise, - std::sin(angle) - noise * 0.7 - }); + if (trace_) trace_->append_sample(current_time_of_day(), std::sin(time)); + if (constellation_) { + const double phase = time * 1.7; + constellation_->append_point( + {std::cos(phase) + std::sin(time * 9.0) * 0.08, + std::sin(phase) + std::cos(time * 7.0) * 0.08}); } } - void update_model() { - if (spectrum_) - push_spectrum_samples(); - if (waterfall_) - push_waterfall_row(); - if (afterglow_) - push_afterglow_spectrum(); - if (sweep_) - push_sweep_block(); - if (trace_) - push_trace_sample(); - if (constellation_) - push_constellation_points(); - if (case_id_ == "axis_lab" && time_axis_) - time_axis_->append_time(current_time_of_day()); - ++frame_index_; - } - static std::string invalid_argument(bool& recognized) { - recognized = false; - return {}; - } - std::uint64_t session_id_{}; + std::string case_id_; - Gallery_Frame_Mode frame_mode_ = Gallery_Frame_Mode::Low_Latency; - bool automatic_low_latency_{}; - Render_Scene_2D plot_; - Gallery_Feedback_Policy feedback_policy_; - std::unique_ptr> session_control_; - Gallery_Controls controls_; - std::chrono::steady_clock::time_point performance_started_; - std::chrono::steady_clock::time_point last_performance_log_; - renderive_Owner root_; - renderive_Owner axes_node_; - renderive_Owner data_node_; - renderive_Owner overlay_node_; - renderive_Owner numeric_domain_axis_; - renderive_Owner frequency_domain_axis_; - renderive_Owner value_axis_; - renderive_Owner time_axis_; - renderive_Owner primary_; - renderive_Owner spectrum_; - renderive_Owner waterfall_; - renderive_Owner trace_; - renderive_Owner afterglow_; - renderive_Owner sweep_; - renderive_Owner selection_; - renderive_Owner constellation_; - std::uint64_t frame_index_{}; - std::uint64_t render_attempt_count_{}; - std::uint64_t successful_render_count_{}; - double last_render_ms_{}; - Rolling_Statistics render_duration_statistics_{256}; - std::deque render_history_; - std::uint64_t pixel_frame_count_{}; - double last_pixel_snapshot_ms_{}; - double last_pixel_encode_ms_{}; - double last_pixel_request_ms_{}; - Rolling_Statistics pixel_encode_statistics_{256}; - Rolling_Statistics pixel_request_statistics_{256}; - std::size_t last_pixel_bytes_{}; - std::deque pixel_history_; - Gallery_Client_Performance client_performance_; - std::uint64_t consumer_pixel_interval_ns_{}; - std::uint64_t consumer_presentation_interval_ns_{}; - std::uint64_t consumer_manual_interval_ns_{}; - std::string consumer_feedback_source_{"none"}; - bool rendered_since_last_pixel_{}; - std::string last_action_result_; + Gallery_Frame_Mode mode_; + bool automatic_{}; + Size viewport_{560, 320}; + std::unique_ptr scene_; + renderive_Owner frequency_axis_; + renderive_Owner domain_axis_; + renderive_Owner value_axis_; + renderive_Owner time_axis_; + renderive_Owner spectrum_; + renderive_Owner waterfall_; + renderive_Owner afterglow_; + renderive_Owner sweep_; + renderive_Owner trace_; + renderive_Owner selection_; + renderive_Owner constellation_; + Gallery_Client_Performance client_; + std::uint64_t render_count_{}; + std::uint64_t sample_sequence_{}; }; -std::unique_ptr make_gallery_scene_2d(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode, bool automatic_low_latency) { - return std::make_unique(session_id, std::move(case_id), - mode, automatic_low_latency); -} +} // namespace + +std::unique_ptr make_gallery_scene_2d( + std::uint64_t, std::string case_id, Gallery_Frame_Mode mode, + bool automatic_low_latency) { + return std::make_unique( + std::move(case_id), mode, automatic_low_latency); } +} // namespace renderive::web diff --git a/web_server/app/render_3D/Gallery_Scene3D.cpp b/web_server/app/render_3D/Gallery_Scene3D.cpp index 7543a61..13ed625 100644 --- a/web_server/app/render_3D/Gallery_Scene3D.cpp +++ b/web_server/app/render_3D/Gallery_Scene3D.cpp @@ -1,196 +1,52 @@ -#include #include "Gallery_Scene3D.h" #include "../Gallery_Capture_Json.h" -#include "../Gallery_Enum.h" -#include "../common/Gallery_Scheduler_Adminive.h" +#include "../common/Observer_Json.h" #include "../common/Pixel_Frame.h" -#include - -#include "adminive/adminive.hpp" -#include "adminive/adapters/nlohmann_json.hpp" +#include "render_3D/Render_Scene_3D.h" #include #include -#include -#include -#include -#include #include -#include -#include #include -#include -#include #include -namespace adminive { - -template -struct Value_Adapter< - renderive::render_3d::Renderable_Observer_Event, Json> { - using value_type = std::string; - static std::string read( - renderive::render_3d::Renderable_Observer_Event value) { - return renderive::web::gallery_enum_id(value); - } - static void write( - renderive::render_3d::Renderable_Observer_Event& target, - std::string value) { - const auto parsed = renderive::web::gallery_enum_cast< - renderive::render_3d::Renderable_Observer_Event>(value); - if (!parsed) - ::renderive::error::unexpected("unknown 3D observer event"); - target = *parsed; - } -}; - -template <> -struct Type_Descriptor { - static auto get() { - using T = renderive::render_3d::Renderable_Observation; - return object( - "renderable_3d_observer", - ADMINIVE_FIELD_LABEL(T, event, "事件"), - ADMINIVE_FIELD_LABEL(T, event_time_ns, "事件时间"), - ADMINIVE_FIELD_LABEL(T, cache_update_count, "缓存更新"), - ADMINIVE_FIELD_LABEL(T, publish_count, "发布"), - ADMINIVE_FIELD_LABEL(T, data_revision, "数据版本"), - ADMINIVE_FIELD_LABEL(T, item_count, "项目数")); - } -}; - -} // namespace adminive - namespace renderive::web { +template <> +struct Observer_Pfr_Adapter { + using Scene = renderive::render_3d::Render_Scene_3D; + using Type = adminive::Boost_Pfr_Base_Reflection_Adapter< + Scene::State, Scene::State_Fields>; +}; +template <> +struct Observer_Pfr_Adapter { + using Scene = renderive::render_3d::Render_Scene_3D; + using Type = adminive::Boost_Pfr_Base_Reflection_Adapter< + Scene::Observer, Scene::Observer_Fields>; +}; namespace { using namespace renderive::render_3d; -Gallery_Render_Result gallery_render_error(Frame_Request_Result error) { - switch (error) { - case Frame_Request_Result::none: - return Gallery_Render_Result::none; - case Frame_Request_Result::cancelled: - return Gallery_Render_Result::cancelled; - case Frame_Request_Result::deadline_exceeded: - return Gallery_Render_Result::deadline_exceeded; - case Frame_Request_Result::external_failure: - return Gallery_Render_Result::backend_failure; - case Frame_Request_Result::scene_shutting_down: - return Gallery_Render_Result::shutting_down; - default: - return Gallery_Render_Result::frame_unavailable; - } -} -Gallery_Render_Result gallery_render_error(Scene_Render_Result error) { - switch (error) { - case Scene_Render_Result::none: - return Gallery_Render_Result::none; - case Scene_Render_Result::cancelled: - return Gallery_Render_Result::cancelled; - case Scene_Render_Result::deadline_exceeded: - return Gallery_Render_Result::deadline_exceeded; - case Scene_Render_Result::external_failure: - return Gallery_Render_Result::backend_failure; - case Scene_Render_Result::shutting_down: - return Gallery_Render_Result::shutting_down; - } - ::renderive::error::unexpected("unknown scene render error"); -} -Frame_Request_Result frame_request_error(Scene_Render_Result error) { - switch (error) { - case Scene_Render_Result::none: - return Frame_Request_Result::none; - case Scene_Render_Result::cancelled: - return Frame_Request_Result::cancelled; - case Scene_Render_Result::deadline_exceeded: - return Frame_Request_Result::deadline_exceeded; - case Scene_Render_Result::external_failure: - return Frame_Request_Result::external_failure; - case Scene_Render_Result::shutting_down: - return Frame_Request_Result::scene_shutting_down; - } - ::renderive::error::unexpected("unknown scene render error"); -} -struct Gallery_Point { - double x{}; - double y{}; -}; - -Frame_Mode render_frame_mode(Gallery_Frame_Mode mode) { +std::shared_ptr make_strategy( + Gallery_Frame_Mode mode) { switch (mode) { case Gallery_Frame_Mode::Manual: - return Frame_Mode::Manual; + return std::make_shared(); case Gallery_Frame_Mode::Low_Latency: - return Frame_Mode::Low_Latency; + return std::make_shared(); case Gallery_Frame_Mode::Playback: - return Frame_Mode::Playback; + return std::make_shared(); } - ::renderive::error::unexpected("unknown gallery frame mode"); + return {}; } -std::string mode_id(Gallery_Frame_Mode mode) { +std::string_view mode_id(Gallery_Frame_Mode mode) { switch (mode) { - case Gallery_Frame_Mode::Manual: - return "manual"; - case Gallery_Frame_Mode::Low_Latency: - return "low_latency"; - case Gallery_Frame_Mode::Playback: - return "playback"; + case Gallery_Frame_Mode::Manual: return "manual"; + case Gallery_Frame_Mode::Low_Latency: return "low_latency"; + case Gallery_Frame_Mode::Playback: return "playback"; } - return "unknown"; -} - -::renderive::Event_Type event_type(Gallery_Pointer_Input_Type type) { - switch (type) { - case Gallery_Pointer_Input_Type::Move: - return ::renderive::Event_Type::Pointer_Move; - case Gallery_Pointer_Input_Type::Press: - return ::renderive::Event_Type::Pointer_Press; - case Gallery_Pointer_Input_Type::Release: - return ::renderive::Event_Type::Pointer_Release; - } - return ::renderive::Event_Type::Pointer_Move; -} - -::renderive::Mouse_Button mouse_button(Gallery_Mouse_Button button) { - switch (button) { - case Gallery_Mouse_Button::Left: - return ::renderive::Mouse_Button::Left; - case Gallery_Mouse_Button::Right: - return ::renderive::Mouse_Button::Right; - case Gallery_Mouse_Button::Middle: - return ::renderive::Mouse_Button::Middle; - case Gallery_Mouse_Button::None: - return ::renderive::Mouse_Button::None; - } - return ::renderive::Mouse_Button::None; -} - -::renderive::Key key(Gallery_Key value) { - switch (value) { - case Gallery_Key::Escape: - return ::renderive::Key::Escape; - case Gallery_Key::Enter: - return ::renderive::Key::Enter; - case Gallery_Key::Space: - return ::renderive::Key::Space; - case Gallery_Key::Delete: - return ::renderive::Key::Delete; - case Gallery_Key::Backspace: - return ::renderive::Key::Backspace; - case Gallery_Key::Left: - return ::renderive::Key::Left; - case Gallery_Key::Right: - return ::renderive::Key::Right; - case Gallery_Key::Up: - return ::renderive::Key::Up; - case Gallery_Key::Down: - return ::renderive::Key::Down; - case Gallery_Key::Unknown: - return ::renderive::Key::Unknown; - } - return ::renderive::Key::Unknown; + return "low_latency"; } Visual_Family visual_family(std::string_view case_id) { @@ -211,501 +67,160 @@ Visual_Family visual_family(std::string_view case_id) { return Visual_Family::Point; } -std::optional action_number( - const Gallery_Action_Request& request) { - if (!request.argument || - !std::holds_alternative(*request.argument)) - return std::nullopt; - return std::get(*request.argument); -} - class Gallery_Scene3D final : public Gallery_Scene_Interface { public: - Gallery_Scene3D(std::uint64_t session_id, std::string case_id, - Gallery_Frame_Mode mode, bool automatic_low_latency) - : session_id_(session_id), case_id_(std::move(case_id)), - frame_mode_(mode), automatic_low_latency_(automatic_low_latency), - visual_(Point_Visual::Builder{}.build(initial_points())), - scene_(Scene_Options{.viewport = {560, 320}, - .frame_mode = render_frame_mode(mode), - .maximum_frames_per_second = 30.0, - .visual_family = visual_family(case_id_)}, - visual_) { - if (!visual_) - ::renderive::error::unexpected("failed to create Point_Visual"); - visual_->set_object_name("3D Visual"); - render_initial_frame(); + Gallery_Scene3D(std::string case_id, Gallery_Frame_Mode mode, + bool automatic) + : case_id_(std::move(case_id)), mode_(mode), automatic_(automatic) { + rebuild(); } - [[nodiscard]] const std::string& case_id() const noexcept override { - return case_id_; - } + const std::string& case_id() const noexcept override { return case_id_; } - [[nodiscard]] nlohmann::json controls() const override { - const auto observation = visual_->observation(); + nlohmann::json controls() const override { return {{"resources", nlohmann::json::array()}, - {"observers", - nlohmann::json::array({ - {{"descriptor", adminive::to_descriptor_json< - nlohmann::json, - Renderable_Observation>()}, - {"data", adminive::to_frontend_json( - observation)}, - {"target", "point-visual"}, - {"title", "3D Visual"}}})}, - {"render_plan", - gallery_render_plan_json(scene_.render_scene())}, - {"performance_capture", - gallery_performance_capture_json(scene_.render_scene())}}; + {"observers", nlohmann::json::array()}, + {"render_plan", gallery_render_plan_json(*scene_)}, + {"performance_capture", gallery_performance_capture_json(*scene_)}}; } - [[nodiscard]] Gallery_Frame_Mode frame_mode() const noexcept override { - return frame_mode_; + Gallery_Frame_Mode frame_mode() const noexcept override { return mode_; } + void reset_monitoring() override { render_count_ = 0; } + bool can_render_automatically() const noexcept override { + return automatic_ && mode_ == Gallery_Frame_Mode::Low_Latency; } - - void reset_monitoring() override { - render_attempt_count_ = 0; - successful_render_count_ = 0; - pixel_frame_count_ = 0; - last_render_ms_ = 0.0; - last_pixel_encode_ms_ = 0.0; - render_history_.clear(); - pixel_history_.clear(); - last_event_ = "monitoring_reset"; + std::uint64_t kernel_refresh_interval_ns() const override { + return scene_->observer().next_refresh_interval_ns; } - - [[nodiscard]] bool can_render_automatically() const noexcept override { - return active_ && automatic_low_latency_ && - frame_mode_ == Gallery_Frame_Mode::Low_Latency; - } - - [[nodiscard]] std::uint64_t kernel_refresh_interval_ns() const override { - return scene_.frame_status().next_refresh_interval_ns; - } - void set_client_metrics(Gallery_Client_Performance metrics) noexcept override { - client_performance_ = metrics; + client_ = metrics; } - - [[nodiscard]] adminive::Update_Result apply_patch( + adminive::Update_Result apply_patch( std::string_view, const nlohmann::json&) override { - adminive::Update_Result result; - result.success = false; - result.message = "Render3D gallery has no patchable controls"; - return result; + return {false, "no editable resource matches the requested target"}; } - void resize(int width, int height) override { - if (width <= 0 || height <= 0) + const Extent requested{static_cast(std::max(1, width)), + static_cast(std::max(1, height))}; + if (requested.width == extent_.width && requested.height == extent_.height) return; - const auto error = scene_.resize({static_cast(width), - static_cast(height)}); - if (error != ::renderive::render_3d::Scene_Control_Error::none) { - last_event_ = "resize_rejected"; - return; - } - last_event_ = "resized"; - } - - void dispatch(const Gallery_Input_Event& event) override { - if (const auto* view = std::get_if(&event)) { - if (view->type == Gallery_View_Input_Type::Show) - active_ = true; - else if (view->type == Gallery_View_Input_Type::Hide) - active_ = false; - return; - } - if (const auto* pointer = std::get_if(&event)) { - ::renderive::Basic_Pointer_Event input( - event_type(pointer->type)); - input.position = {pointer->x, pointer->y}; - input.global_position = {pointer->global_x, pointer->global_y}; - input.button = mouse_button(pointer->button); - input.buttons = pointer->buttons; - input.modifiers = static_cast<::renderive::Keyboard_Modifier>( - pointer->modifiers); - if (scene_.dispatch(input) != - ::renderive::render_3d::Scene_Control_Error::none) - last_event_ = "pointer_rejected"; - return; - } - if (const auto* wheel = std::get_if(&event)) { - ::renderive::Basic_Wheel_Event input; - input.position = {wheel->x, wheel->y}; - input.global_position = {wheel->global_x, wheel->global_y}; - input.pixel_delta_x = wheel->pixel_delta_x; - input.pixel_delta_y = wheel->pixel_delta_y; - input.angle_delta_x = wheel->angle_delta_x; - input.angle_delta_y = wheel->angle_delta_y; - input.modifiers = static_cast<::renderive::Keyboard_Modifier>( - wheel->modifiers); - if (scene_.dispatch(input) != - ::renderive::render_3d::Scene_Control_Error::none) - last_event_ = "wheel_rejected"; - return; - } - const auto& keyboard = std::get(event); - ::renderive::Key_Event input( - keyboard.type == Gallery_Key_Input_Type::Release - ? ::renderive::Event_Type::Key_Release - : ::renderive::Event_Type::Key_Press); - input.key = key(keyboard.key); - input.native_key = keyboard.native_key; - input.modifiers = static_cast<::renderive::Keyboard_Modifier>( - keyboard.modifiers); - input.auto_repeat = keyboard.auto_repeat; - if (scene_.dispatch(input) != - ::renderive::render_3d::Scene_Control_Error::none) - last_event_ = "key_rejected"; + extent_ = requested; + rebuild(); } + void dispatch(const Gallery_Input_Event&) override {} Gallery_Render_Result render_latest_frame() override { - if (!can_render_automatically()) - return Gallery_Render_Result::inactive; - advance_blue_point(0.025F); - const auto request_error = render_request(); - return gallery_render_error(request_error); + if (scene_->request_frame() != Frame_Request_Result::none) + return Gallery_Render_Result::backend_failure; + ++render_count_; + return Gallery_Render_Result::none; } - [[nodiscard]] std::optional encode_latest_pixels() override { - if (!active_) - return std::nullopt; - const auto frame = scene_.latest_frame(); - if (!frame || frame->rgba8.empty()) - return std::nullopt; - return encode_rgba8_pixel_frame( + std::optional encode_latest_pixels() override { + const auto frame = scene_->latest_frame(); + if (!frame || frame->rgba8.empty()) return std::nullopt; + auto pixels = encode_rgba8_pixel_frame( frame->rgba8.data(), frame->extent.width, frame->extent.height, frame->extent.width * 4U, frame->sequence); + return pixels.empty() ? std::nullopt + : std::optional(std::move(pixels)); } - void record_pixel_response( - std::chrono::steady_clock::time_point, - std::chrono::steady_clock::time_point encode_started, - std::chrono::steady_clock::time_point encode_finished, - std::size_t pixel_bytes) override { - ++pixel_frame_count_; - last_pixel_bytes_ = pixel_bytes; - last_pixel_encode_ms_ = std::chrono::duration( - encode_finished - encode_started) - .count(); - record_sample(pixel_history_, encode_finished); - } + void record_pixel_response(std::chrono::steady_clock::time_point, + std::chrono::steady_clock::time_point, + std::chrono::steady_clock::time_point, + std::size_t) override {} - [[nodiscard]] std::string action(const Gallery_Action_Request& request, - bool& recognized) override { + std::string action(const Gallery_Action_Request& request, + bool& recognized) override { recognized = true; + if (request.id == "frame_strategy_manual" || + request.id == "frame_strategy_low_latency" || + request.id == "frame_strategy_playback") { + const auto next = request.id == "frame_strategy_manual" + ? Gallery_Frame_Mode::Manual + : request.id == "frame_strategy_playback" + ? Gallery_Frame_Mode::Playback + : Gallery_Frame_Mode::Low_Latency; + const auto operation = scene_->edit_renderables( + [strategy = make_strategy(next)]( + Scene_3D_Base::Renderable_Editor& editor) mutable { + editor.replace_frame_control_strategy(std::move(strategy)); + }); + if (operation.wait() != Scene_Edit_Error::none) + return "frame strategy replacement rejected"; + mode_ = next; + return "frame strategy replaced"; + } + if (request.id == "mode_cycle" || request.id == "mode_render") { + static_cast(render_latest_frame()); + return "frame rendered"; + } if (request.id == "capture_next_frame") { - auto& scene = scene_.render_scene(); - if (scene.capture_state().enabled()) - return "A performance capture session is already active"; - const auto capture = scene.capture_next_frame(); - if (!capture) { - if (capture.error == Capture_Error::session_active) - return "A performance capture session is already active"; - ::renderive::error::unexpected("validated capture request rejected"); - } - last_action_result_ = - "capture session=" + std::to_string(capture.session_id); - return "Next 3D frame capture requested"; + const auto capture = scene_->capture_next_frame(); + if (capture) static_cast(render_latest_frame()); + return capture ? "frame captured" : "capture request rejected"; } if (request.id == "capture_frames") { - const auto argument = action_number(request); - if (!argument) { - recognized = false; - return {}; - } - const std::size_t count = static_cast( - std::clamp(std::llround(*argument), 1LL, 120LL)); - auto& scene = scene_.render_scene(); - if (scene.capture_state().enabled()) - return "A performance capture session is already active"; - const auto capture = scene.capture_frames(count); - if (!capture) { - if (capture.error == Capture_Error::session_active) - return "A performance capture session is already active"; - ::renderive::error::unexpected("validated capture request rejected"); - } - last_action_result_ = "capture session=" + - std::to_string(capture.session_id) + - " frames=" + std::to_string(count); - return "Consecutive 3D frame capture requested"; - } - if (request.id == "mode_prepare" || request.id == "mode_enqueue") { - if (frame_mode_ == Gallery_Frame_Mode::Playback) { - blue_offset_ = std::min(0.25F, blue_offset_ + 0.0125F); - publish_points(); - } - const bool prepared = scene_.prepare_frame() == Frame_Request_Result::none; - last_event_ = prepared ? "frame_prepared" : "prepare_rejected"; - return prepared ? "3D frame prepared" : "3D frame prepare rejected"; - } - if (request.id == "mode_refresh") { - const bool refreshed = scene_.refresh_manual_frame() == Frame_Request_Result::none; - last_event_ = refreshed ? "manual_refreshed" : "manual_refresh_failed"; - return refreshed ? "Manual 3D frame refreshed" : "No manual frame to refresh"; - } - if (request.id == "mode_render" || request.id == "mode_dequeue") { - const bool rendered = render_prepared() == Frame_Request_Result::none; - last_event_ = rendered ? "frame_rendered" : "render_rejected"; - return rendered ? "3D frame rendered" : "No 3D frame to render"; - } - if (request.id == "move_mesh") { - advance_blue_point(0.08F); - last_event_ = "mesh_moved"; - return "blue point moved"; - } - if (request.id == "add_mesh") { - extra_attached_ = true; - publish_points(); - last_event_ = "mesh_added"; - return "yellow point added"; - } - if (request.id == "remove_mesh") { - extra_attached_ = false; - publish_points(); - last_event_ = "mesh_removed"; - return "yellow point removed"; - } - if (request.id == "mesh_churn") { - last_event_ = "mesh_churned"; - return "point attach/detach lifecycle completed"; - } - if (request.id == "reset" || request.id == "reset_camera") { - blue_offset_ = 0.0F; - projection_scale_ = 1.0F; - visual_->set<&Point_State::transform>(transform()); - publish_points(); - last_event_ = "camera_reset"; - return "3D view reset"; - } - if (request.id == "toggle_projection") { - projection_scale_ = projection_scale_ == 1.0F ? 0.72F : 1.0F; - visual_->set<&Point_State::transform>(transform()); - last_event_ = "projection_toggled"; - return "3D projection toggled"; - } - if (request.id == "toggle_texture") { - texture_enabled_ = !texture_enabled_; - publish_points(); - last_event_ = "texture_toggled"; - return texture_enabled_ ? "point pattern enabled" : "point pattern disabled"; - } - if (request.id == "change_text") { - alternate_text_ = !alternate_text_; - publish_points(); - last_event_ = "text_changed"; - return "point annotation representation changed"; + std::size_t count = 20; + if (request.argument && std::holds_alternative(*request.argument)) + count = static_cast(std::clamp( + std::get(*request.argument), 1.0, 1000.0)); + const auto capture = scene_->capture_frames(count); + if (capture) static_cast(render_latest_frame()); + return capture ? "frame capture started" : "capture request rejected"; } recognized = false; return {}; } - void record_action_notice_if_empty(std::string_view notice) override { - if (last_action_result_.empty()) - last_action_result_ = std::string(notice); - } + void record_action_notice_if_empty(std::string_view) override {} - [[nodiscard]] std::string telemetry_json() const override { - const auto status = scene_.frame_status(); - const auto now = std::chrono::steady_clock::now(); - const auto scene_execution = scene_.render_scene().execution_statistics(); - const auto runtime = scene_.runtime_statistics(); + std::string telemetry_json() const override { + const auto observer = scene_->observer(); + auto kernel_observer = observer_json(observer); + kernel_observer["mode"] = mode_id(mode_); + kernel_observer["event"] = "render_completed"; + kernel_observer["limit"] = "none"; return nlohmann::json{ - {"kernel_observer", - {{"mode", mode_id(frame_mode_)}, - {"last_event", last_event_}, - {"pending_frame_count", status.pending_frame_count}, - {"dropped_frame_count", status.dropped_frame_count}}}, - {"performance", - {{"render_attempt_count", render_attempt_count_}, - {"successful_render_count", successful_render_count_}, - {"failed_render_count", - render_attempt_count_ - successful_render_count_}, - {"measured_fps", recent_rate(render_history_, now)}, - {"last_render_ms", last_render_ms_}, - {"pixel_response_fps", recent_rate(pixel_history_, now)}, - {"last_pixel_encode_ms", last_pixel_encode_ms_}, - {"last_pixel_bytes", last_pixel_bytes_}, - {"automatic_low_latency_scheduler", can_render_automatically()}}}, - {"client_performance", - {{"transport_fps", client_performance_.transport_fps}, - {"presentation_fps", client_performance_.presentation_fps}, - {"websocket_buffered_bytes", - client_performance_.websocket_buffered_bytes}}}, - {"scheduler", adminive::to_frontend_json( - renderive::scheduling::scheduler_statistics())}, - {"queue_pressure", - {{"scene_execution", - {{"capacity", scene_execution.capacity}, - {"queued", scene_execution.queued}, - {"peak_queued", scene_execution.peak_queued}, - {"backpressure_count", scene_execution.backpressure_count}, - {"backpressure_wait_ns", scene_execution.backpressure_wait_ns}}}, - {"render_domain", - {{"capacity", runtime.render_domain.capacity}, - {"active", runtime.render_domain.active}, - {"peak_active", runtime.render_domain.peak_active}, - {"queued", runtime.render_domain.queued}, - {"peak_queued", runtime.render_domain.peak_queued}, - {"backpressure_count", runtime.render_domain.backpressure_count}, - {"backpressure_wait_ns", runtime.render_domain.backpressure_wait_ns}}}, - {"gpu_completion", - {{"capacity", runtime.gpu_completion.capacity}, - {"active", runtime.gpu_completion.active}, - {"peak_active", runtime.gpu_completion.peak_active}, - {"queued", runtime.gpu_completion.queued}, - {"peak_queued", runtime.gpu_completion.peak_queued}, - {"backpressure_count", runtime.gpu_completion.backpressure_count}, - {"backpressure_wait_ns", runtime.gpu_completion.backpressure_wait_ns}, - {"fault_count", runtime.gpu_completion_fault_count}, - {"abandoned_count", runtime.gpu_completion_abandoned_count}}}}}, - {"session_id", session_id_}, - {"performance_capture", - gallery_performance_capture_json(scene_.render_scene())}, - {"last_action", last_action_result_}} + {"kernel_observer", std::move(kernel_observer)}, + {"performance", {{"successful_render_count", render_count_}}}, + {"client_performance", {{"transport_fps", client_.transport_fps}}}, + {"performance_capture", gallery_performance_capture_json(*scene_)}} .dump(); } private: - using Performance_Clock = std::chrono::steady_clock; - - static void record_sample(std::deque& samples, - Performance_Clock::time_point now) { - samples.push_back(now); - const auto cutoff = now - std::chrono::seconds(2); - while (!samples.empty() && samples.front() < cutoff) - samples.pop_front(); + void rebuild() { + std::vector points{ + {{-0.55F, -0.25F, 0.0F}, {255, 80, 80, 255}, 24.0F}, + {{0.0F, 0.45F, 0.0F}, {80, 255, 120, 255}, 28.0F}, + {{0.55F, -0.1F, 0.0F}, {80, 140, 255, 255}, 26.0F}}; + visual_ = Point_Visual::Builder{}.build(std::move(points)); + visual_->set_object_name(case_id_ + " Points"); + Render_Scene_3D::State state; + state.viewport = extent_; + state.visual_family = visual_family(case_id_); + scene_ = Render_Scene_3D::Builder{state}.build( + visual_, make_strategy(mode_)); + static_cast(scene_->request_frame()); } - static double recent_rate( - const std::deque& samples, - Performance_Clock::time_point now) { - const auto cutoff = now - std::chrono::seconds(1); - const auto first = std::find_if(samples.begin(), samples.end(), - [cutoff](auto sample) { - return sample >= cutoff; - }); - const auto count = std::distance(first, samples.end()); - if (count < 2) - return 0.0; - const double seconds = std::chrono::duration(samples.back() - *first).count(); - return seconds > 0.0 ? static_cast(count - 1) / seconds : 0.0; - } - - static std::vector initial_points() { - return {{{-0.62F, -0.10F, 0.0F}, {255, 32, 24, 255}, 74.0F}, - {{0.0F, -0.10F, 0.0F}, {24, 255, 36, 255}, 74.0F}, - {{0.62F, -0.10F, 0.0F}, {28, 52, 255, 255}, 74.0F}}; - } - - Matrix4 transform() const { - Matrix4 value; - value.values[0] = projection_scale_; - value.values[5] = projection_scale_; - return value; - } - - void publish_points() { - auto points = initial_points(); - points[2].position.x += blue_offset_; - if (!texture_enabled_) - points[0].color = {255, 128, 128, 255}; - if (alternate_text_) { - points[1].diameter_px = 46.0F; - points[1].color = {24, 220, 255, 255}; - } - if (extra_attached_) - points.push_back({{0.0F, 0.62F, 0.0F}, {255, 230, 20, 255}, 58.0F}); - if (visual_->update_points(std::move(points)) != - Visual_Data_Error::none) - ::renderive::error::unexpected("generated gallery point data is invalid"); - } - - void advance_blue_point(float step) { - blue_offset_ += step; - if (blue_offset_ > 0.32F) - blue_offset_ = -0.32F; - publish_points(); - } - - void render_initial_frame() { - Frame_Request_Result error{Frame_Request_Result::none}; - if (frame_mode_ == Gallery_Frame_Mode::Manual) { - if (scene_.prepare_frame() == Frame_Request_Result::none && - scene_.refresh_manual_frame() == Frame_Request_Result::none) - error = render_prepared(); - else - error = Frame_Request_Result::no_pending_frame; - } else { - error = render_request(); - } - if (error != Frame_Request_Result::none) - last_action_result_ = "initial frame is not available"; - } - - Frame_Request_Result render_request() { - const auto started = std::chrono::steady_clock::now(); - ++render_attempt_count_; - auto error = scene_.request_frame(); - if (error == Frame_Request_Result::none) - error = frame_request_error(scene_.render_scene().wait_for_render()); - const bool rendered = error == Frame_Request_Result::none; - last_render_ms_ = std::chrono::duration( - std::chrono::steady_clock::now() - started) - .count(); - successful_render_count_ += rendered ? 1U : 0U; - if (rendered) - record_sample(render_history_, std::chrono::steady_clock::now()); - return error; - } - - Frame_Request_Result render_prepared() { - const auto started = std::chrono::steady_clock::now(); - ++render_attempt_count_; - auto error = scene_.render_prepared_frame(); - if (error == Frame_Request_Result::none) - error = frame_request_error(scene_.render_scene().wait_for_render()); - const bool rendered = error == Frame_Request_Result::none; - last_render_ms_ = std::chrono::duration( - std::chrono::steady_clock::now() - started) - .count(); - successful_render_count_ += rendered ? 1U : 0U; - if (rendered) - record_sample(render_history_, std::chrono::steady_clock::now()); - return error; - } - - std::uint64_t session_id_{}; std::string case_id_; - Gallery_Frame_Mode frame_mode_{Gallery_Frame_Mode::Low_Latency}; - bool automatic_low_latency_{}; - bool active_{true}; + Gallery_Frame_Mode mode_; + bool automatic_{}; + Extent extent_{560, 320}; std::shared_ptr visual_; - Render_Scene_3D scene_; - Gallery_Client_Performance client_performance_; - float blue_offset_{}; - float projection_scale_{1.0F}; - bool extra_attached_{}; - bool texture_enabled_{true}; - bool alternate_text_{}; - std::uint64_t render_attempt_count_{}; - std::uint64_t successful_render_count_{}; - std::uint64_t pixel_frame_count_{}; - double last_render_ms_{}; - double last_pixel_encode_ms_{}; - std::size_t last_pixel_bytes_{}; - std::deque render_history_; - std::deque pixel_history_; - std::string last_event_{"initialized"}; - std::string last_action_result_; + std::unique_ptr scene_; + Gallery_Client_Performance client_; + std::uint64_t render_count_{}; }; } // namespace std::unique_ptr make_gallery_scene_3d( - std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode, + std::uint64_t, std::string case_id, Gallery_Frame_Mode mode, bool automatic_low_latency) { return std::make_unique( - session_id, std::move(case_id), mode, automatic_low_latency); + std::move(case_id), mode, automatic_low_latency); } - } // namespace renderive::web diff --git a/web_server/tests/Datoviz_Gallery_Tests.cpp b/web_server/tests/Datoviz_Gallery_Tests.cpp index 8106f76..9745a4e 100644 --- a/web_server/tests/Datoviz_Gallery_Tests.cpp +++ b/web_server/tests/Datoviz_Gallery_Tests.cpp @@ -1,26 +1,18 @@ -#include "../app/Gallery_Plot_Session.h" -#include "../app/Gallery_Protocol.h" #include "../app/common/Pixel_Frame.h" +#include "../app/render_3D/Gallery_Scene3D.h" #include #include #include #include -#include -#include #include #include -#include -#include +#include #include -#include -#include -#include namespace renderive::web { namespace { - std::uint32_t read_u32_le(const std::string& value, std::size_t offset) { return static_cast(value[offset]) | (static_cast(static_cast(value[offset + 1])) << 8U) | @@ -28,130 +20,6 @@ std::uint32_t read_u32_le(const std::string& value, std::size_t offset) { (static_cast(static_cast(value[offset + 3])) << 24U); } -Gallery_Request gallery_request(Gallery_Request_Kind kind, std::string message) { - return {kind, std::move(message)}; -} - -std::string open_message(std::string_view mode) { - return std::string( - R"({"category":"event","type":"gallery_open","case":"datoviz_3d","frame_mode":")") + - std::string(mode) + R"("})"; -} - -nlohmann::json response_json(std::optional response) { - if (!response) { - ADD_FAILURE() << "expected a Web response"; - return nlohmann::json::object(); - } - EXPECT_EQ(response->type, Web_Response_Type::Json); - return nlohmann::json::parse(response->payload); -} - -nlohmann::json open_scene(Gallery_Plot_Session& session, std::string_view mode) { - return response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message(mode)))); -} - -nlohmann::json invoke_action(Gallery_Plot_Session& session, - std::string_view action_id) { - const nlohmann::json request{{"category", "event"}, - {"type", "gallery_action"}, - {"action", action_id}}; - return response_json(session.handle(gallery_request( - Gallery_Request_Kind::Action, request.dump()))); -} - -std::string request_pixels(Gallery_Plot_Session& session) { - const auto response = session.handle(Frame_Request{}); - if (!response) { - ADD_FAILURE() << "expected a pixel response"; - return {}; - } - EXPECT_EQ(response->type, Web_Response_Type::Pixels); - return response->payload; -} - -void expect_actual_datoviz_pixels(const std::string& frame, - std::uint32_t expected_width, - std::uint32_t expected_height) { - ASSERT_GE(frame.size(), pixel_frame_header_size); - ASSERT_EQ(frame.substr(0, 4), "RVP2"); - ASSERT_EQ(read_u32_le(frame, 4), expected_width); - ASSERT_EQ(read_u32_le(frame, 8), expected_height); - ASSERT_EQ(read_u32_le(frame, 12), expected_width * 4U); - ASSERT_EQ(frame.size(), pixel_frame_header_size + - static_cast(expected_width) * - expected_height * 4U); - - const auto* pixels = reinterpret_cast( - frame.data() + pixel_frame_header_size); - const std::array background{ - pixels[0], pixels[1], pixels[2], pixels[3]}; - std::size_t non_background{}; - std::size_t red_dominant{}; - std::size_t green_dominant{}; - std::size_t blue_dominant{}; - const std::size_t pixel_count = - static_cast(expected_width) * expected_height; - for (std::size_t index = 0; index < pixel_count; ++index) { - const auto* pixel = pixels + index * 4U; - non_background += pixel[0] != background[0] || - pixel[1] != background[1] || - pixel[2] != background[2] || - pixel[3] != background[3]; - red_dominant += pixel[0] > static_cast(pixel[1]) + 20U && - pixel[0] > static_cast(pixel[2]) + 20U; - green_dominant += pixel[1] > static_cast(pixel[0]) + 20U && - pixel[1] > static_cast(pixel[2]) + 20U; - blue_dominant += pixel[2] > static_cast(pixel[0]) + 20U && - pixel[2] > static_cast(pixel[1]) + 20U; - } - EXPECT_GT(non_background, 0U); - EXPECT_GT(red_dominant, 32U); - EXPECT_GT(green_dominant, 32U); - EXPECT_GT(blue_dominant, 32U); -} - -std::optional blue_centroid_x(const std::string& frame) { - if (frame.size() < pixel_frame_header_size) - return std::nullopt; - const std::uint32_t width = read_u32_le(frame, 4); - const std::uint32_t height = read_u32_le(frame, 8); - if (frame.size() != pixel_frame_header_size + - static_cast(width) * height * 4U) - return std::nullopt; - const auto* pixels = reinterpret_cast( - frame.data() + pixel_frame_header_size); - double x_sum{}; - std::size_t count{}; - for (std::uint32_t y = 0; y < height; ++y) { - for (std::uint32_t x = 0; x < width; ++x) { - const auto* pixel = pixels + - (static_cast(y) * width + x) * 4U; - if (pixel[2] > static_cast(pixel[0]) + 20U && - pixel[2] > static_cast(pixel[1]) + 20U) { - x_sum += x; - ++count; - } - } - } - return count == 0 ? std::nullopt - : std::optional(x_sum / static_cast(count)); -} - -void manual_render_cycle(Gallery_Plot_Session& session) { - ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state"); - ASSERT_EQ(invoke_action(session, "mode_refresh").at("type"), "case_state"); - ASSERT_EQ(invoke_action(session, "mode_render").at("type"), "case_state"); -} - -std::set action_ids(const nlohmann::json& state) { - std::set result; - for (const auto& action : state.at("actions").at("data")) - result.insert(action.at("id").get()); - return result; -} - TEST(RenderiveWebDatovizGallery, RgbaEncoderPreservesRowsAndRemovesPadding) { std::array source{}; const std::array expected{ @@ -159,7 +27,6 @@ TEST(RenderiveWebDatovizGallery, RgbaEncoderPreservesRowsAndRemovesPadding) { 5, 6, 253, 255, 252, 7, 8, 255}; std::copy_n(expected.begin(), 8, source.begin()); std::copy_n(expected.begin() + 8, 8, source.begin() + 12); - source[8] = source[9] = source[10] = source[11] = 99; const std::string frame = encode_rgba8_pixel_frame( reinterpret_cast(source.data()), 2, 2, 12, 42); @@ -168,233 +35,33 @@ TEST(RenderiveWebDatovizGallery, RgbaEncoderPreservesRowsAndRemovesPadding) { EXPECT_EQ(read_u32_le(frame, 4), 2U); EXPECT_EQ(read_u32_le(frame, 8), 2U); EXPECT_EQ(read_u32_le(frame, 12), 8U); - EXPECT_TRUE(std::equal( - expected.begin(), expected.end(), - reinterpret_cast( - frame.data() + pixel_frame_header_size))); + EXPECT_TRUE(std::equal(expected.begin(), expected.end(), + reinterpret_cast( + frame.data() + pixel_frame_header_size))); EXPECT_TRUE(encode_rgba8_pixel_frame( reinterpret_cast(source.data()), 2, 2, 7, 42) .empty()); } -TEST(RenderiveWebDatovizGallery, CatalogUsesOnlyThreeDimensionalActions) { - const auto catalog = nlohmann::json::parse(Gallery_Protocol::catalog_json()); - const std::set visual_cases{ - "datoviz_3d", "datoviz_splat", "datoviz_pixel", "datoviz_marker", - "datoviz_sphere", "datoviz_segment", "datoviz_vector", - "datoviz_primitive", "datoviz_mesh", "datoviz_path", "datoviz_image", - "datoviz_labels", "datoviz_glyph", "datoviz_text", "datoviz_volume"}; - for (const auto& id : visual_cases) { - EXPECT_NE(std::find_if( - catalog.at("cases").begin(), catalog.at("cases").end(), - [&id](const nlohmann::json& value) { - return value.at("id") == id; - }), - catalog.at("cases").end()) - << id; +TEST(RenderiveWebDatovizGallery, CurrentThreeDimensionalSceneRendersRgba) { + try { + auto scene = make_gallery_scene_3d( + 1, "datoviz_3d", Gallery_Frame_Mode::Low_Latency, false); + scene->resize(320, 200); + ASSERT_EQ(scene->render_latest_frame(), Gallery_Render_Result::none); + const auto frame = scene->encode_latest_pixels(); + ASSERT_TRUE(frame); + ASSERT_GE(frame->size(), pixel_frame_header_size); + EXPECT_EQ(frame->substr(0, 4), "RVP2"); + EXPECT_EQ(read_u32_le(*frame, 4), 320U); + EXPECT_EQ(read_u32_le(*frame, 8), 200U); + + const auto telemetry = nlohmann::json::parse(scene->telemetry_json()); + EXPECT_EQ(telemetry.at("kernel_observer").at("point_count"), 3U); + EXPECT_EQ(telemetry.at("performance").at("successful_render_count"), 1U); + } catch (const std::exception& error) { + GTEST_SKIP() << "Datoviz backend unavailable: " << error.what(); } - const auto item = std::find_if( - catalog.at("cases").begin(), catalog.at("cases").end(), - [](const nlohmann::json& value) { return value.at("id") == "datoviz_3d"; }); - ASSERT_NE(item, catalog.at("cases").end()); - EXPECT_EQ(item->at("category"), "Datoviz Visuals"); - EXPECT_EQ(item->at("control_count"), 0); - EXPECT_EQ(item->at("control_count_by_mode").at("manual"), 0); - EXPECT_EQ(item->at("control_count_by_mode").at("low_latency"), 0); - EXPECT_EQ(item->at("control_count_by_mode").at("playback"), 0); - - const std::string empty_controls = - R"({"resources":[],"observers":[],"render_plan":null,"performance_capture":null})"; - const auto state_for = [&](Gallery_Frame_Mode mode) { - return nlohmann::json::parse(Gallery_Protocol::case_json_from_controls( - "datoviz_3d", empty_controls, "{}", {}, mode, false)); - }; - const std::set common{ - "add_mesh", "change_text", "move_mesh", "remove_mesh", "reset", - "reset_camera", "mesh_churn", "toggle_projection", "toggle_texture"}; - auto expected = common; - expected.insert({"mode_prepare", "mode_refresh", "mode_render"}); - EXPECT_EQ(action_ids(state_for(Gallery_Frame_Mode::Manual)), expected); - expected = common; - expected.insert({"mode_prepare", "mode_render"}); - EXPECT_EQ(action_ids(state_for(Gallery_Frame_Mode::Low_Latency)), expected); - expected = common; - expected.insert({"mode_enqueue", "mode_dequeue"}); - EXPECT_EQ(action_ids(state_for(Gallery_Frame_Mode::Playback)), expected); - EXPECT_FALSE(action_ids(state_for(Gallery_Frame_Mode::Manual)).contains("mode_discard")); - EXPECT_FALSE(action_ids(state_for(Gallery_Frame_Mode::Low_Latency)).contains("mode_discard")); } - -TEST(RenderiveWebDatovizGallery, PublishesRealRgbaAndRendersAfterResize) { - Gallery_Plot_Session session; - ASSERT_EQ(open_scene(session, "low_latency").at("type"), "case_state"); - const std::string initial = request_pixels(session); - expect_actual_datoviz_pixels(initial, 560, 320); - - EXPECT_FALSE(session.handle(Viewport_Resize{{900, 500}}).has_value()); - ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state"); - ASSERT_EQ(invoke_action(session, "mode_render").at("type"), "case_state"); - const std::string resized = request_pixels(session); - expect_actual_datoviz_pixels(resized, 900, 500); - - const auto observed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe","client_metrics":{}})"))); - EXPECT_TRUE(observed.at("telemetry").contains("scheduler")); - const auto& performance = observed.at("telemetry").at("performance"); - EXPECT_GT(performance.at("measured_fps").get(), 0.0); - EXPECT_GT(performance.at("pixel_response_fps").get(), 0.0); - EXPECT_GT(performance.at("last_render_ms").get(), 0.0); - EXPECT_GT(performance.at("last_pixel_encode_ms").get(), 0.0); -} - -TEST(RenderiveWebDatovizGallery, AutomaticLowLatencyFramesCarryAnimatedState) { - Gallery_Plot_Session session(true); - ASSERT_EQ(open_scene(session, "low_latency").at("type"), "case_state"); - const std::string first = request_pixels(session); - expect_actual_datoviz_pixels(first, 560, 320); - - std::string later; - const auto deadline = std::chrono::steady_clock::now() + - std::chrono::seconds(5); - do { - std::this_thread::sleep_for(std::chrono::milliseconds(40)); - later = request_pixels(session); - } while (later == first && std::chrono::steady_clock::now() < deadline); - ASSERT_NE(later, first); - expect_actual_datoviz_pixels(later, 560, 320); -} - -TEST(RenderiveWebDatovizGallery, ManualRenderUsesThePreparedLogicalState) { - Gallery_Plot_Session session; - ASSERT_EQ(open_scene(session, "manual").at("type"), "case_state"); - ASSERT_EQ(invoke_action(session, "move_mesh").at("type"), "case_state"); - ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state"); - ASSERT_EQ(invoke_action(session, "move_mesh").at("type"), "case_state"); - ASSERT_EQ(invoke_action(session, "mode_refresh").at("type"), "case_state"); - ASSERT_EQ(invoke_action(session, "mode_render").at("type"), "case_state"); - const std::string prepared = request_pixels(session); - expect_actual_datoviz_pixels(prepared, 560, 320); - - manual_render_cycle(session); - const std::string current = request_pixels(session); - expect_actual_datoviz_pixels(current, 560, 320); - EXPECT_NE(current, prepared); -} - -TEST(RenderiveWebDatovizGallery, PlaybackDequeuesHistoricalStatesInOrder) { - Gallery_Plot_Session session; - ASSERT_EQ(open_scene(session, "playback").at("type"), "case_state"); - for (int index = 0; index < 20; ++index) - ASSERT_EQ(invoke_action(session, "mode_enqueue").at("type"), "case_state"); - - ASSERT_EQ(invoke_action(session, "mode_dequeue").at("type"), "case_state"); - const std::string first = request_pixels(session); - const auto first_x = blue_centroid_x(first); - ASSERT_TRUE(first_x.has_value()); - - ASSERT_EQ(invoke_action(session, "mode_dequeue").at("type"), "case_state"); - const std::string second = request_pixels(session); - const auto second_x = blue_centroid_x(second); - ASSERT_TRUE(second_x.has_value()); - EXPECT_GT(*second_x, *first_x); - - std::string last = second; - for (int index = 2; index < 20; ++index) { - ASSERT_EQ(invoke_action(session, "mode_dequeue").at("type"), "case_state"); - last = request_pixels(session); - } - const auto last_x = blue_centroid_x(last); - ASSERT_TRUE(last_x.has_value()); - EXPECT_GT(*last_x, *second_x); - EXPECT_NE(last, second); -} - -TEST(RenderiveWebDatovizGallery, AddedAndChurnedVisualsAreActuallyRemoved) { - Gallery_Plot_Session session; - ASSERT_EQ(open_scene(session, "manual").at("type"), "case_state"); - const std::string baseline = request_pixels(session); - - ASSERT_EQ(invoke_action(session, "add_mesh").at("type"), "case_state"); - manual_render_cycle(session); - const std::string added = request_pixels(session); - EXPECT_NE(added, baseline); - const auto centroid_y = [&](const auto& matches) -> std::optional { - const auto* pixels = reinterpret_cast( - added.data() + pixel_frame_header_size); - double y_sum{}; - std::size_t count{}; - for (std::uint32_t y = 0; y < 320; ++y) { - for (std::uint32_t x = 0; x < 560; ++x) { - const auto* pixel = pixels + - (static_cast(y) * 560U + x) * 4U; - if (matches(pixel[0], pixel[1], pixel[2])) { - y_sum += y; - ++count; - } - } - } - return count == 0 - ? std::nullopt - : std::optional(y_sum / static_cast(count)); - }; - const auto green_y = centroid_y([](unsigned red, unsigned green, unsigned blue) { - return green > red + 20U && green > blue + 20U; - }); - const auto yellow_y = centroid_y([](unsigned red, unsigned green, unsigned blue) { - return red > 100U && green > 100U && blue < 100U; - }); - ASSERT_TRUE(green_y.has_value()); - ASSERT_TRUE(yellow_y.has_value()); - EXPECT_LT(*yellow_y, *green_y) - << "Frame_3D_View must expose top-left-origin RGBA rows"; - - ASSERT_EQ(invoke_action(session, "remove_mesh").at("type"), "case_state"); - manual_render_cycle(session); - const std::string removed = request_pixels(session); - ASSERT_GE(removed.size(), pixel_frame_header_size); - ASSERT_GE(baseline.size(), pixel_frame_header_size); - EXPECT_EQ(removed.substr(pixel_frame_header_size), - baseline.substr(pixel_frame_header_size)); - - const auto churned = invoke_action(session, "mesh_churn"); - ASSERT_EQ(churned.at("type"), "case_state"); - EXPECT_EQ(churned.at("telemetry").at("kernel_observer").at("last_event"), - "mesh_churned"); - const std::string after_churn = request_pixels(session); - expect_actual_datoviz_pixels(after_churn, 560, 320); - EXPECT_EQ(after_churn.substr(pixel_frame_header_size), - baseline.substr(pixel_frame_header_size)); -} - -TEST(RenderiveWebDatovizGallery, TextureAndTextActionsReachTheThreeDimensionalBackend) { - Gallery_Plot_Session session; - ASSERT_EQ(open_scene(session, "manual").at("type"), "case_state"); - const std::string baseline = request_pixels(session); - expect_actual_datoviz_pixels(baseline, 560, 320); - - ASSERT_EQ(invoke_action(session, "toggle_texture").at("type"), "case_state"); - manual_render_cycle(session); - const std::string without_texture = request_pixels(session); - ASSERT_GE(without_texture.size(), pixel_frame_header_size); - ASSERT_GE(baseline.size(), pixel_frame_header_size); - EXPECT_NE(without_texture.substr(pixel_frame_header_size), - baseline.substr(pixel_frame_header_size)); - - ASSERT_EQ(invoke_action(session, "toggle_texture").at("type"), "case_state"); - manual_render_cycle(session); - const std::string restored_texture = request_pixels(session); - ASSERT_GE(restored_texture.size(), pixel_frame_header_size); - EXPECT_EQ(restored_texture.substr(pixel_frame_header_size), - baseline.substr(pixel_frame_header_size)); - - ASSERT_EQ(invoke_action(session, "change_text").at("type"), "case_state"); - manual_render_cycle(session); - const std::string changed_text = request_pixels(session); - ASSERT_GE(changed_text.size(), pixel_frame_header_size); - EXPECT_NE(changed_text.substr(pixel_frame_header_size), - restored_texture.substr(pixel_frame_header_size)); -} - } // namespace } // namespace renderive::web diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index 13a9fb6..1bd95d5 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -1,51 +1,18 @@ #include "../app/Gallery_Plot_Session.h" -#include "../app/Gallery_Observer_Adminive.h" #include "../app/Gallery_Protocol.h" -#include "../app/Gallery_Renderables.h" -#include "../app/Pixel_Frame.h" #include "../app/Web_Event_Adapter.h" #include "../app/Web_Plot_Session.h" +#include "../app/common/Pixel_Frame.h" #include #include -#include -#include -#include -#include -#include -#include -#include +#include #include -#include -#include -#include #include -#include namespace renderive::web { namespace { - -struct Spatial_Test_Event { - int layer{}; -}; - -struct Spatial_Test_Decoder { - using event_type = Spatial_Test_Event; - - static std::optional decode(const Json::Value& root, std::string_view) { - if (root["type"].asString() != "spatial_test" || !root["layer"].isInt()) - return std::nullopt; - return Spatial_Test_Event{root["layer"].asInt()}; - } -}; - -using Extended_Web_Event = Basic_Web_Event; -using Extended_Web_Event_Adapter = Basic_Web_Event_Adapter< - Extended_Web_Event, Web_Transport_Event_Decoder, Render_2D_Web_Event_Decoder, - Web_Demo_Control_Decoder, Spatial_Test_Decoder>; - std::uint32_t read_u32_le(const std::string& value, std::size_t offset) { return static_cast(value[offset]) | (static_cast(static_cast(value[offset + 1])) << 8U) | @@ -53,1609 +20,120 @@ std::uint32_t read_u32_le(const std::string& value, std::size_t offset) { (static_cast(static_cast(value[offset + 3])) << 24U); } -nlohmann::json parse_json(const std::string& value) { - return nlohmann::json::parse(value); -} - -Gallery_Request gallery_request(Gallery_Request_Kind kind, std::string message) { - return {kind, std::move(message)}; -} - -std::string open_message(std::string_view case_id, std::string_view frame_mode) { - return std::string(R"({"category":"event","type":"gallery_open","case":")") + - std::string(case_id) + R"(","frame_mode":")" + std::string(frame_mode) + R"("})"; -} - -nlohmann::json response_json(std::optional response) { - if (!response) { - ADD_FAILURE() << "expected a Web response"; - return nlohmann::json::object(); - } - EXPECT_EQ(response->type, Web_Response_Type::Json); - return parse_json(response->payload); -} - -nlohmann::json observe_telemetry(Gallery_Plot_Session& session) { - return response_json(session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe"})"))).value( - "telemetry", nlohmann::json::object()); -} - -nlohmann::json invoke_action(Gallery_Plot_Session& session, std::string_view action_id, - std::optional argument = std::nullopt) { - nlohmann::json request{{"category", "event"}, {"type", "gallery_action"}, - {"action", action_id}}; - if (argument) - request["argument"] = *argument; - return response_json(session.handle(gallery_request( - Gallery_Request_Kind::Action, request.dump()))); -} - -nlohmann::json patch_controls(Gallery_Plot_Session& session, nlohmann::json patch, - std::string_view target = "scene") { - const nlohmann::json request{{"category", "event"}, {"type", "gallery_patch"}, - {"target", target}, {"patch", std::move(patch)}}; - return response_json(session.handle(gallery_request( - Gallery_Request_Kind::Patch, request.dump()))); -} - -template -bool wait_for_condition(Predicate&& predicate, - std::chrono::milliseconds timeout = std::chrono::milliseconds(750)) { - const auto deadline = std::chrono::steady_clock::now() + timeout; - do { - if (predicate()) - return true; - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - } while (std::chrono::steady_clock::now() < deadline); - return predicate(); -} - -std::optional wait_for_pixel_frame(Gallery_Plot_Session& session, - std::uint32_t width, - std::uint32_t height) { - std::optional frame; - wait_for_condition([&] { - frame = session.handle(Frame_Request{}); - return frame && frame->type == Web_Response_Type::Pixels && - frame->payload.size() >= pixel_frame_header_size && - read_u32_le(frame->payload, 4) == width && - read_u32_le(frame->payload, 8) == height; - }); - return frame; -} - -void set_gallery_view_active(Gallery_Plot_Session& session, bool active) { - const auto event = Web_Event_Adapter::decode(active ? - R"({"category":"event","type":"show"})" : - R"({"category":"event","type":"hide"})"); - ASSERT_TRUE(event.has_value()); - EXPECT_FALSE(session.handle(*event).has_value()); -} - -std::uint64_t successful_render_count(Gallery_Plot_Session& session) { - return observe_telemetry(session).at("performance").at("successful_render_count") - .get(); -} - -TEST(RenderiveWebBridge, DecodesOnlyEventMessages) { +TEST(RenderiveWebBridge, DecodesTransportAndPointerEvents) { const auto resize = Web_Event_Adapter::decode( - R"({"category":"event","type":"resize","width":800,"height":450})"); - ASSERT_TRUE(resize.has_value()); + R"({"category":"event","type":"resize","width":640,"height":360})"); + ASSERT_TRUE(resize); ASSERT_TRUE(std::holds_alternative(*resize)); - EXPECT_EQ(std::get(*resize).size, (Size{800, 450})); + EXPECT_EQ(std::get(*resize).size.width, 640); + EXPECT_EQ(std::get(*resize).size.height, 360); const auto pointer = Web_Event_Adapter::decode( - R"({"category":"event","type":"pointer_press","x":22.5,"y":18,"button":"left","buttons":1,"modifiers":3})"); - ASSERT_TRUE(pointer.has_value()); + R"({"category":"event","type":"pointer_move","x":12.5,"y":7.25})"); + ASSERT_TRUE(pointer); ASSERT_TRUE(std::holds_alternative(*pointer)); - EXPECT_EQ(std::get(*pointer).button, Mouse_Button::Left); - EXPECT_DOUBLE_EQ(std::get(*pointer).position.x, 22.5); - - EXPECT_FALSE(Web_Event_Adapter::decode(R"({"category":"command","type":"frame"})")); - EXPECT_FALSE(Web_Event_Adapter::decode("not-json")); - - const auto catalog = Web_Event_Adapter::decode( - R"({"category":"event","type":"gallery_catalog"})"); - ASSERT_TRUE(catalog.has_value()); - ASSERT_TRUE(std::holds_alternative(*catalog)); - EXPECT_EQ(std::get(*catalog).kind, Gallery_Request_Kind::Catalog); - - const auto open = Web_Event_Adapter::decode( - R"({"category":"event","type":"gallery_open","case":"spectrum","frame_mode":"manual"})"); - ASSERT_TRUE(open.has_value()); - EXPECT_EQ(std::get(*open).kind, Gallery_Request_Kind::Open); - - const auto observe = Web_Event_Adapter::decode( - R"({"category":"event","type":"gallery_observe"})"); - ASSERT_TRUE(observe.has_value()); - EXPECT_EQ(std::get(*observe).kind, Gallery_Request_Kind::Observe); - - const auto refresh = Web_Event_Adapter::decode( - R"({"category":"event","type":"gallery_refresh"})"); - ASSERT_TRUE(refresh.has_value()); - EXPECT_EQ(std::get(*refresh).kind, Gallery_Request_Kind::Refresh); - - const auto reset_monitoring = Web_Event_Adapter::decode( - R"({"category":"event","type":"gallery_reset_monitoring"})"); - ASSERT_TRUE(reset_monitoring.has_value()); - EXPECT_EQ(std::get(*reset_monitoring).kind, - Gallery_Request_Kind::Reset_Monitoring); - - const auto bounded = Web_Event_Adapter::decode( - R"({"category":"event","type":"resize","width":1e100,"height":-1e100})"); - ASSERT_TRUE(bounded.has_value()); - EXPECT_EQ(std::get(*bounded).size, (Size{1920, 180})); + EXPECT_DOUBLE_EQ(std::get(*pointer).position.x, 12.5); + EXPECT_FALSE(Web_Event_Adapter::decode(R"({"type":"frame"})")); } -TEST(RenderiveWebBridge, EncodesRgbaPixelFrame) { - const Pixel pixels[] = {pack_rgba(255, 0, 0), pack_rgba(0, 255, 0)}; - const Image_View image{reinterpret_cast(pixels), 2, 1, - static_cast(sizeof(pixels)), Pixel_Format::Premultiplied_32}; - const std::string frame = encode_pixel_frame(image, 42); - ASSERT_EQ(frame.size(), pixel_frame_header_size + 8); +TEST(RenderiveWebBridge, EncodesRvp2PixelFrames) { + const std::string frame = make_rgba8_pixel_frame(4, 3, 9); + ASSERT_EQ(frame.size(), pixel_frame_header_size + 4U * 3U * 4U); EXPECT_EQ(frame.substr(0, 4), "RVP2"); - EXPECT_EQ(read_u32_le(frame, 4), 2U); - EXPECT_EQ(read_u32_le(frame, 8), 1U); - EXPECT_EQ(read_u32_le(frame, 12), 8U); - const auto* rgba = reinterpret_cast(frame.data() + pixel_frame_header_size); - EXPECT_EQ(rgba[0], 255); - EXPECT_EQ(rgba[1], 0); - EXPECT_EQ(rgba[2], 0); - EXPECT_EQ(rgba[3], 255); - EXPECT_EQ(rgba[4], 0); - EXPECT_EQ(rgba[5], 255); + EXPECT_EQ(read_u32_le(frame, 4), 4U); + EXPECT_EQ(read_u32_le(frame, 8), 3U); + EXPECT_EQ(read_u32_le(frame, 12), 16U); } -TEST(RenderiveWebBridge, LegacyReceiverStillRendersPixels) { +TEST(RenderiveWebBridge, CurrentTwoDimensionalSceneRendersAfterResize) { Web_Plot_Session session; - EXPECT_FALSE(session.handle(Viewport_Resize{{640, 360}}).has_value()); - EXPECT_FALSE(session.handle(Set_Demo_Mode{Demo_Mode::Usb}).has_value()); - const auto frame = session.handle(Frame_Request{}); - ASSERT_TRUE(frame.has_value()); - EXPECT_EQ(frame->type, Web_Response_Type::Pixels); - ASSERT_GE(frame->payload.size(), pixel_frame_header_size); - EXPECT_EQ(frame->payload.substr(0, 4), "RVP2"); - EXPECT_EQ(read_u32_le(frame->payload, 4), 640U); - EXPECT_EQ(read_u32_le(frame->payload, 8), 360U); + EXPECT_FALSE(session.handle(Viewport_Resize{{320, 200}})); + const auto response = session.handle(Frame_Request{}); + ASSERT_TRUE(response); + ASSERT_EQ(response->type, Web_Response_Type::Pixels); + ASSERT_GE(response->payload.size(), pixel_frame_header_size); + EXPECT_EQ(response->payload.substr(0, 4), "RVP2"); + EXPECT_EQ(read_u32_le(response->payload, 4), 320U); + EXPECT_EQ(read_u32_le(response->payload, 8), 200U); } -TEST(RenderiveWebGallery, AdminiveCatalogCoversEveryControlCaseAndThreeModes) { - const nlohmann::json catalog = parse_json(Gallery_Protocol::catalog_json()); - EXPECT_EQ(catalog.at("category"), "gallery"); - EXPECT_EQ(catalog.at("type"), "catalog"); - EXPECT_EQ(catalog.at("protocol"), "renderive.control-gallery"); - EXPECT_EQ(catalog.at("protocol_version"), 5); - EXPECT_EQ(catalog.at("transport").at("pixels"), "websocket-binary-rvp2"); - EXPECT_EQ(catalog.at("case_descriptor").at("protocol"), "adminive.resource"); +TEST(RenderiveWebGallery, CatalogUsesOneDefaultPageAndThreeDynamicStrategies) { + const auto catalog = nlohmann::json::parse(Gallery_Protocol::catalog_json()); + ASSERT_EQ(catalog.at("type"), "catalog"); EXPECT_EQ(catalog.at("cases").size(), 23U); EXPECT_EQ(catalog.at("frame_modes").size(), 3U); - EXPECT_EQ(catalog.at("coverage").at("page_count"), 3); - EXPECT_EQ(catalog.at("coverage").at("canvas_count"), 69); - EXPECT_GT(catalog.at("coverage").at("manual_control_count").get(), 180U); - EXPECT_GT(catalog.at("coverage").at("manual_action_count").get(), 50U); - EXPECT_EQ(catalog.at("coverage").at("frequency_modes"), - nlohmann::json::array({"spectrum", "afterglow", "sweep_spectrum"})); - EXPECT_EQ(catalog.at("coverage").at("image_interpolation_modes"), - nlohmann::json::array({"Nearest", "Bilinear", "Bicubic"})); + EXPECT_EQ(catalog.at("coverage").at("page_count"), 1U); + EXPECT_EQ(catalog.at("coverage").at("canvas_count"), 23U); + + const auto request = Gallery_Protocol::open_request( + R"({"category":"event","type":"gallery_open","case":"spectrum","frame_mode":"low_latency"})"); + ASSERT_TRUE(request); + EXPECT_EQ(request->case_id, "spectrum"); + EXPECT_EQ(request->frame_mode, Gallery_Frame_Mode::Low_Latency); + EXPECT_FALSE(Gallery_Protocol::open_request( + R"({"category":"event","type":"gallery_open","case":"unknown","frame_mode":"low_latency"})")); } -TEST(RenderiveWebGallery, CatalogOwnsDashboardFieldsAndDynamicBehavior) { - const nlohmann::json catalog = parse_json(Gallery_Protocol::catalog_json()); - const auto& dashboard = catalog.at("dashboard"); - EXPECT_EQ(dashboard.at("performance").at("fields").size(), 36U); - EXPECT_EQ(dashboard.at("limits").at("fields").size(), 4U); - EXPECT_EQ(dashboard.at("observer").at("sections").size(), 4U); - std::set client_sources; - for (const auto& section : dashboard.at("observer").at("sections")) { - for (const auto& field : section.at("fields")) { - const std::string source = field.at("source").get(); - if (source.starts_with("client_performance.")) - client_sources.insert(source); - } - } - constexpr std::array client_fields{ - "display_interval_latest_ms", "display_interval_ms", "display_interval_average_ms", - "display_interval_p95_ms", "display_interval_p99_ms", "display_interval_deviation_ms", - "frame_round_trip_ms", "frame_round_trip_average_ms", "frame_round_trip_p95_ms", - "frame_round_trip_p99_ms", "frame_round_trip_deviation_ms", - "transport_fps", "presentation_fps", - "websocket_buffered_bytes", "overwritten_pixel_frames", "changed_pixel_frames", - "duplicate_pixel_frames", "frame_request_timeout_count", "last_pixel_receive_age_ms", - "last_pixel_change_age_ms" - }; - EXPECT_EQ(client_sources.size(), client_fields.size()); - for (const auto field : client_fields) - EXPECT_TRUE(client_sources.contains("client_performance." + std::string(field))) << field; - for (const auto& mode : catalog.at("frame_modes")) { - EXPECT_TRUE(mode.contains("frame_button_label")); - EXPECT_TRUE(mode.contains("request_after_response")); - EXPECT_TRUE(mode.contains("request_on_animation_frame")); - EXPECT_TRUE(mode.contains("observer_visible")); - EXPECT_TRUE(mode.contains("accent")); - } +TEST(RenderiveWebGallery, TwoDimensionalGalleryPublishesStateAndPixels) { Gallery_Plot_Session session; - const auto contract = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "manual")))); - const auto action = std::find_if(contract.at("actions").at("data").begin(), - contract.at("actions").at("data").end(), - [](const auto& item) { return item.at("id") == "mode_render"; }); - ASSERT_NE(action, contract.at("actions").at("data").end()); - EXPECT_TRUE(action->at("request_frame")); -} + const std::string open = + R"({"category":"event","type":"gallery_open","case":"spectrum","frame_mode":"low_latency"})"; + const auto opened = session.handle(Gallery_Request{Gallery_Request_Kind::Open, open}); + ASSERT_TRUE(opened); + ASSERT_EQ(opened->type, Web_Response_Type::Json); + const auto state = nlohmann::json::parse(opened->payload); + EXPECT_EQ(state.at("type"), "case_state"); + EXPECT_EQ(state.at("case"), "spectrum"); + const auto& controls = state.at("controls"); + EXPECT_FALSE(controls.at("render_plan").at("nodes").empty()); + EXPECT_TRUE(controls.at("performance_capture").at("controller").is_object()); + EXPECT_TRUE(controls.at("performance_capture").at("sessions").is_array()); + EXPECT_TRUE(controls.at("performance_capture").at("plans").is_array()); + const auto& observer = state.at("telemetry").at("kernel_observer"); + EXPECT_EQ(observer.at("state").at("viewport").at("width"), 560); + EXPECT_EQ(observer.at("renderable_count"), 4U); -TEST(RenderiveWebGallery, ObserverTelemetryAndDashboardUseAdminiveDescriptors) { - const auto catalog = parse_json(Gallery_Protocol::catalog_json()); - const auto& observer_contract = catalog.at("dashboard").at("observer"); - const auto& observer_descriptor = observer_contract.at("descriptor"); - const auto& feedback_descriptor = - observer_contract.at("consumer_feedback_descriptor"); - EXPECT_EQ(observer_descriptor.at("label"), "内核观察器"); - EXPECT_EQ(feedback_descriptor.at("label"), "消费者反馈"); - - Gallery_Plot_Session session; - const auto opened = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "manual")))); - const auto& telemetry = opened.at("telemetry"); - const auto& observer = telemetry.at("kernel_observer"); - const auto& feedback = telemetry.at("consumer_feedback"); - - EXPECT_EQ(observer.size(), observer_descriptor.at("fields").size()); - for (const auto& field : observer_descriptor.at("fields")) - EXPECT_TRUE(observer.contains(field.at("name").get())); - EXPECT_EQ(feedback.size(), feedback_descriptor.at("fields").size()); - for (const auto& field : feedback_descriptor.at("fields")) - EXPECT_TRUE(feedback.contains(field.at("name").get())); - EXPECT_EQ(observer.at("mode"), "manual"); - EXPECT_FALSE(observer.contains("task_worker_count")); - ASSERT_TRUE(telemetry.contains("performance_capture")); - EXPECT_TRUE(telemetry.at("performance_capture").contains("controller")); - EXPECT_TRUE(telemetry.at("performance_capture").contains("sessions")); - EXPECT_TRUE(telemetry.at("performance_capture").contains("plans")); - EXPECT_FALSE(telemetry.contains("low_latency_limit")); -} - -TEST(RenderiveWebGallery, ActionsComeFromStructiveTypeMetadata) { - using Spectrum_Schema = structive::type_descriptor_schema_t; - using Time_Axis_Schema = structive::type_descriptor_schema_t; - using Manual_Strategy = - Manual_Refresh_Strategy>; - using Manual_Schema = structive::type_descriptor_schema_t; - static_assert(Spectrum_Schema::template type_attribute_count< - Gallery_Action_Category> == 12); - static_assert(Time_Axis_Schema::template type_attribute_count< - Gallery_Action_Category> == 2); - static_assert(Manual_Schema::template type_attribute_count< - Gallery_Action_Category> == 5); - - std::vector actions; - append_gallery_actions(actions); - EXPECT_EQ(actions.size(), 12U); - EXPECT_TRUE(std::any_of(actions.begin(), actions.end(), [](const auto& action) { - return action.id == "power_at" && action.argument_input == "number"; - })); -} - -TEST(RenderiveWebBridge, ComposesAdditionalEventDecoders) { - const auto event = Extended_Web_Event_Adapter::decode( - R"({"category":"event","type":"spatial_test","layer":7})"); - ASSERT_TRUE(event.has_value()); - ASSERT_TRUE(std::holds_alternative(*event)); - EXPECT_EQ(std::get(*event).layer, 7); -} - -TEST(RenderiveWebGallery, AdminiveResourcesPatchTheRealRenderableState) { - Gallery_Plot_Session session; - auto response = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "manual")))); - ASSERT_EQ(response.at("type"), "case_state"); - const auto find_resource = [](const nlohmann::json& value, std::string_view target) - -> const nlohmann::json& { - const auto& resources = value.at("controls").at("resources"); - const auto found = std::find_if(resources.begin(), resources.end(), - [target](const auto& resource) { - return resource.at("target").template get() == target; - }); - if (found == resources.end()) - throw std::out_of_range("missing gallery resource"); - return *found; - }; - - const auto& spectrum = find_resource(response, "spectrum"); - EXPECT_EQ(spectrum.at("descriptor").at("name"), "spectrum"); - EXPECT_TRUE(spectrum.at("data").at("frequency_range").is_object()); - EXPECT_EQ(spectrum.at("data").at("frequency_point_size"), 512); - const auto partition_field = std::find_if( - spectrum.at("descriptor").at("fields").begin(), - spectrum.at("descriptor").at("fields").end(), - [](const auto& field) { return field.at("name") == "partition_count"; }); - ASSERT_NE(partition_field, spectrum.at("descriptor").at("fields").end()); - EXPECT_EQ(partition_field->at("presentation").at("label"), "固定分区数"); - EXPECT_EQ(partition_field->at("presentation").at("description"), - "固定模式下的并行任务数;1 表示不分区"); - - const auto partition_mode_field = std::find_if( - spectrum.at("descriptor").at("fields").begin(), - spectrum.at("descriptor").at("fields").end(), - [](const auto& field) { return field.at("name") == "partition_mode"; }); - ASSERT_NE(partition_mode_field, spectrum.at("descriptor").at("fields").end()); - EXPECT_EQ(partition_mode_field->at("presentation").at("label"), "任务分区模式"); - EXPECT_EQ(spectrum.at("descriptor").at("label"), "实时频谱"); - - response = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"spectrum","patch":{"frequency_point_size":1024,"partition_mode":"Fixed","partition_count":4,"frequency_range":{"origin":90000000},"current_pen":{"color":"#ff8844"},"interpolation_mode":"Cubic_Value"}})"))); - ASSERT_EQ(response.at("type"), "case_state"); - const auto& updated = find_resource(response, "spectrum").at("data"); - EXPECT_EQ(updated.at("frequency_point_size"), 1024); - EXPECT_EQ(updated.at("partition_count"), 4); - EXPECT_EQ(updated.at("partition_mode"), "Fixed"); - EXPECT_EQ(updated.at("frequency_range").at("origin"), 90'000'000); - EXPECT_EQ(updated.at("current_pen").at("color"), "#ff8844"); - EXPECT_EQ(updated.at("interpolation_mode"), "Cubic_Value"); - - const auto invalid = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"spectrum","patch":{"current_pen":{"color":"not-a-color"}}})"))); - EXPECT_EQ(invalid.at("type"), "error"); - - const auto foreign = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"waterfall","patch":{"visible_range_only":false}})"))); - EXPECT_EQ(foreign.at("type"), "error"); -} - -TEST(RenderiveWebGallery, ExposesStateObserversAndOneCompleteRenderDag) { - Gallery_Plot_Session session; - const auto opened = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "manual")))); - ASSERT_EQ(opened.at("type"), "case_state"); - - const auto& observers = opened.at("controls").at("observers"); - const auto& graph = opened.at("controls").at("render_plan"); - EXPECT_GE(observers.size(), 7U); - EXPECT_GT(graph.at("nodes").size(), observers.size()); - for (const std::string_view target : {"frequency_axis", "value_axis", "spectrum"}) { - const auto found = std::find_if(observers.begin(), observers.end(), - [target](const auto& resource) { - return resource.at("target").template get() == target; - }); - ASSERT_NE(found, observers.end()) << target; - EXPECT_EQ(found->at("descriptor").at("label"), "渲染对象观察器"); - EXPECT_EQ(found->at("descriptor").at("fields").at(0) - .at("presentation").at("label"), - "最近状态事件"); - EXPECT_EQ(found->at("data").at("event"), "Published"); - EXPECT_GT(found->at("data").at("publish_count").get(), 0U); - EXPECT_FALSE(found->at("data").contains("task_execution_count")); - } - - ASSERT_TRUE(graph.at("topology_id").is_string()); - EXPECT_FALSE(graph.at("topology_id").get().empty()); - EXPECT_FALSE(graph.at("nodes").empty()); - EXPECT_FALSE(graph.at("edges").empty()); - ASSERT_EQ(graph.at("renderables").size(), observers.size()); - std::set kinds; - for (const auto& node : graph.at("nodes")) - kinds.insert(node.at("kind").get()); - EXPECT_TRUE(kinds.contains("prepare")); - EXPECT_TRUE(kinds.contains("paint")); - EXPECT_TRUE(kinds.contains("composite")); - for (const auto& renderable : graph.at("renderables")) { - EXPECT_TRUE(renderable.contains("prepare_cache")); - EXPECT_TRUE(renderable.contains("paint_cache")); - } - - const auto observed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe"})"))); - ASSERT_EQ(observed.at("type"), "observer_state"); - EXPECT_EQ(observed.at("telemetry").at("renderable_observers").size(), - observers.size()); - EXPECT_FALSE(observed.at("telemetry").contains("render_plan")); - - const auto refreshed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Refresh, - R"({"category":"event","type":"gallery_refresh"})"))); - ASSERT_EQ(refreshed.at("type"), "refresh_state"); - EXPECT_EQ(refreshed.at("controls").at("observers").size(), - observers.size()); - EXPECT_EQ(refreshed.at("notice"), "观察数据已手动刷新"); - - const auto reset = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Reset_Monitoring, - R"({"category":"event","type":"gallery_reset_monitoring"})"))); - ASSERT_EQ(reset.at("type"), "refresh_state"); - EXPECT_EQ(reset.at("notice"), "监测滑动窗口已重置"); - EXPECT_EQ(reset.at("telemetry").at("performance").at("render_sample_count"), 0); - EXPECT_EQ(reset.at("telemetry").at("performance").at("pixel_encode_sample_count"), 0); - for (const auto& observer : reset.at("telemetry").at("renderable_observers")) - EXPECT_FALSE(observer.at("data").contains("task_execution_count")); -} - -TEST(RenderiveWebGallery, PartitionControlRebuildsTheUnifiedRenderDag) { - Gallery_Plot_Session session; - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, - open_message("spectrum", "manual")))).at("type"), - "case_state"); - - const auto render = [&session] { - EXPECT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state"); - EXPECT_EQ(invoke_action(session, "mode_refresh").at("type"), "case_state"); - return invoke_action(session, "mode_render"); - }; - - const auto single_state = patch_controls( - session, {{"partition_mode", "Fixed"}, {"partition_count", 1}}, "spectrum"); - ASSERT_EQ(single_state.at("type"), "case_state"); - const auto single = render(); - const auto& single_graph = single.at("controls").at("render_plan"); - const std::string single_topology = single_graph.at("topology_id"); - - const auto partitioned_state = patch_controls( - session, {{"partition_count", 4}}, "spectrum"); - ASSERT_EQ(partitioned_state.at("type"), "case_state"); - const auto partitioned = render(); - const auto& partitioned_graph = partitioned.at("controls").at("render_plan"); - EXPECT_NE(partitioned_graph.at("topology_id"), single_topology); - EXPECT_EQ(partitioned_graph.at("nodes").size(), - single_graph.at("nodes").size() + 3U); -} - -TEST(RenderiveWebGallery, PerformanceCaptureReturnsPlanFrameAnalysisAndStatistics) { - Gallery_Plot_Session session; - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, - open_message("spectrum", "manual")))).at("type"), - "case_state"); - - const auto requested = invoke_action(session, "capture_frames", 2.0); - ASSERT_EQ(requested.at("type"), "case_state"); - const auto& initial_capture = requested.at("telemetry").at("performance_capture"); - EXPECT_TRUE(initial_capture.at("controller").at("enabled")); - EXPECT_EQ(initial_capture.at("controller").at("remaining_frame_count"), 2); - ASSERT_EQ(initial_capture.at("sessions").size(), 1U); - EXPECT_EQ(initial_capture.at("sessions").at(0).at("requested_count"), 2); - - nlohmann::json rendered; - for (int frame = 0; frame < 2; ++frame) { - ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state"); - ASSERT_EQ(invoke_action(session, "mode_refresh").at("type"), "case_state"); - rendered = invoke_action(session, "mode_render"); - ASSERT_EQ(rendered.at("type"), "case_state"); - } - - nlohmann::json capture; - ASSERT_TRUE(wait_for_condition([&] { - capture = observe_telemetry(session).at("performance_capture"); - return !capture.at("controller").at("enabled").get(); - })); - EXPECT_FALSE(capture.at("controller").at("enabled")); - ASSERT_EQ(capture.at("sessions").size(), 1U); - const auto& captured = capture.at("sessions").at(0); - EXPECT_EQ(captured.at("captured_count"), 2); - EXPECT_FALSE(captured.at("active")); - ASSERT_EQ(captured.at("frames").size(), 2U); - EXPECT_TRUE(captured.at("summary").contains("render_p95_ns")); - EXPECT_TRUE(captured.at("summary").contains("average_cpu_concurrency")); - EXPECT_TRUE(captured.at("summary").contains("scheduler_wait_p95_ns")); - EXPECT_FALSE(captured.at("node_statistics").empty()); - EXPECT_FALSE(captured.at("plan_statistics").empty()); - EXPECT_FALSE(capture.at("plans").empty()); - - for (const auto& frame : captured.at("frames")) { - EXPECT_TRUE(frame.contains("frame_id")); - EXPECT_TRUE(frame.contains("render_plan_version")); - EXPECT_FALSE(frame.contains("render_start_ns")); - EXPECT_FALSE(frame.contains("render_end_ns")); - ASSERT_FALSE(frame.at("node_executions").empty()); - const auto& execution = frame.at("node_executions").at(0); - EXPECT_FALSE(execution.contains("start_time_ns")); - EXPECT_FALSE(execution.contains("end_time_ns")); - EXPECT_TRUE(execution.contains("start_offset_ns")); - EXPECT_TRUE(execution.contains("end_offset_ns")); - EXPECT_TRUE(execution.contains("worker_id")); - EXPECT_TRUE(execution.contains("status")); - EXPECT_TRUE(execution.contains("attachments")); - const auto& analysis = frame.at("analysis"); - EXPECT_TRUE(analysis.contains("critical_path")); - EXPECT_TRUE(analysis.contains("scheduler_parallel_overlap_ns")); - EXPECT_TRUE(analysis.contains("scheduler_work_duration_ns")); - EXPECT_TRUE(analysis.contains("render_domain_work_duration_ns")); - EXPECT_TRUE(analysis.contains("total_gpu_execution_duration_ns")); - EXPECT_TRUE(analysis.contains("average_cpu_concurrency")); - EXPECT_TRUE(analysis.contains("render_domain_utilization")); - EXPECT_TRUE(analysis.contains("workers")); - const auto version = frame.at("render_plan_version"); - EXPECT_TRUE(std::any_of(capture.at("plans").begin(), capture.at("plans").end(), - [&](const auto& plan) { return plan.at("version") == version; })); - } - - const auto& plan = capture.at("plans").back(); - EXPECT_FALSE(plan.at("nodes").empty()); - EXPECT_FALSE(plan.at("edges").empty()); - EXPECT_TRUE(plan.contains("difference")); - EXPECT_TRUE(plan.at("difference").contains("added_nodes")); - const auto& statistic = captured.at("node_statistics").at(0); - EXPECT_TRUE(statistic.contains("moving_average_ns")); - EXPECT_TRUE(statistic.contains("p50_ns")); - EXPECT_TRUE(statistic.contains("p95_ns")); - EXPECT_TRUE(statistic.contains("p99_ns")); - EXPECT_TRUE(statistic.contains("average_cpu_ns")); - EXPECT_TRUE(statistic.contains("average_render_domain_cpu_ns")); - EXPECT_TRUE(statistic.contains("average_total_cpu_work_ns")); - EXPECT_TRUE(statistic.contains("average_external_ns")); - EXPECT_TRUE(statistic.contains("average_gpu_execution_ns")); - EXPECT_TRUE(statistic.contains("p95_cpu_ns")); - EXPECT_TRUE(statistic.contains("p95_external_ns")); - EXPECT_TRUE(statistic.contains("critical_path_frequency")); - EXPECT_TRUE(statistic.contains("average_scheduler_wait_ns")); -} - -TEST(RenderiveWebGallery, AxisSwapAndEveryPlotDirectionAreEditableAndRenderable) { - const auto patch = [](Gallery_Plot_Session& session, std::string_view target, - nlohmann::json value) { - nlohmann::json request{{"category", "event"}, {"type", "gallery_patch"}, - {"target", target}, {"patch", std::move(value)}}; - return response_json(session.handle(gallery_request( - Gallery_Request_Kind::Patch, request.dump()))); - }; - const auto resource_data = [](const nlohmann::json& state, std::string_view target) - -> const nlohmann::json& { - const auto& resources = state.at("controls").at("resources"); - const auto found = std::find_if(resources.begin(), resources.end(), - [target](const auto& resource) { - return resource.at("target").template get() == target; - }); - if (found == resources.end()) - throw std::out_of_range("missing gallery resource"); - return found->at("data"); - }; - - for (const std::string_view case_id : {"spectrum", "afterglow"}) { - Gallery_Plot_Session session; - auto state = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message(case_id, "manual")))); - ASSERT_EQ(state.at("type"), "case_state"); - state = patch(session, "frequency_axis", - {{"orientation", "Vertical"}, - {"coordinates", {{"origin", 108'000'000.0}, - {"target", 88'000'000.0}}}}); - ASSERT_EQ(state.at("type"), "case_state"); - state = patch(session, "value_axis", - {{"orientation", "Horizontal"}, - {"coordinates", {{"origin", -20.0}, {"target", -120.0}}}}); - ASSERT_EQ(state.at("type"), "case_state"); - state = patch(session, case_id, - {{"partition_mode", "Fixed"}, {"partition_count", 4}}); - ASSERT_EQ(state.at("type"), "case_state"); - EXPECT_EQ(resource_data(state, "frequency_axis").at("orientation"), "Vertical"); - EXPECT_EQ(resource_data(state, "value_axis").at("orientation"), "Horizontal"); - EXPECT_EQ(resource_data(state, case_id).at("partition_count"), 4); - } - - Gallery_Plot_Session waterfall; - auto state = response_json(waterfall.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("waterfall", "manual")))); - ASSERT_EQ(state.at("type"), "case_state"); - state = patch(waterfall, "frequency_axis", - {{"orientation", "Vertical"}, - {"coordinates", {{"origin", 108'000'000.0}, - {"target", 88'000'000.0}}}}); - ASSERT_EQ(state.at("type"), "case_state"); - state = patch(waterfall, "time_axis", - {{"orientation", "Horizontal"}, {"newest_at_start", true}}); - ASSERT_EQ(state.at("type"), "case_state"); - state = patch(waterfall, "waterfall", - {{"partition_mode", "Fixed"}, {"partition_count", 4}}); - ASSERT_EQ(state.at("type"), "case_state"); - EXPECT_EQ(resource_data(state, "frequency_axis").at("orientation"), "Vertical"); - EXPECT_EQ(resource_data(state, "time_axis").at("orientation"), "Horizontal"); - EXPECT_TRUE(resource_data(state, "time_axis").at("newest_at_start")); - EXPECT_EQ(resource_data(state, "waterfall").at("partition_count"), 4); - - for (const auto& [frequency_orientation, time_orientation, newest_at_start] : - std::array{ - std::tuple{"Horizontal", "Vertical", false}, - std::tuple{"Horizontal", "Vertical", true}, - std::tuple{"Vertical", "Horizontal", false}, - std::tuple{"Vertical", "Horizontal", true}}) { - state = patch(waterfall, "frequency_axis", - {{"orientation", frequency_orientation}}); - ASSERT_EQ(state.at("type"), "case_state"); - state = patch(waterfall, "time_axis", - {{"orientation", time_orientation}, - {"newest_at_start", newest_at_start}}); - ASSERT_EQ(state.at("type"), "case_state"); - EXPECT_EQ(resource_data(state, "frequency_axis").at("orientation"), - frequency_orientation); - EXPECT_EQ(resource_data(state, "time_axis").at("orientation"), - time_orientation); - EXPECT_EQ(resource_data(state, "time_axis").at("newest_at_start"), - newest_at_start); - } -} - -TEST(RenderiveWebGallery, EveryPublishedActionDispatchesToItsCanvasAndFrameMode) { - constexpr std::array cases{ - "axis_lab", "spectrum", "afterglow", "sweep_spectrum", - "waterfall", "frequency_trace", "selection_overlay", "constellation" - }; - constexpr std::array modes{"manual", "low_latency", "playback"}; - std::size_t dispatched_action_instances{}; - for (const auto case_id : cases) { - for (const auto mode : modes) { - Gallery_Plot_Session catalog_session; - const auto opened = response_json(catalog_session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message(case_id, mode)))); - ASSERT_EQ(opened.at("type"), "case_state"); - const auto actions = opened.at("actions").at("data"); - std::set ids; - for (const auto& action : actions) { - ++dispatched_action_instances; - const std::string id = action.at("id"); - SCOPED_TRACE(std::string(case_id) + "/" + std::string(mode) + "/" + id); - EXPECT_TRUE(ids.insert(id).second); - EXPECT_FALSE(action.at("api").get().empty()); - EXPECT_FALSE(action.at("group").get().empty()); - - Gallery_Plot_Session session; - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message(case_id, mode)))).at("type"), - "case_state"); - std::optional argument; - if (!action.at("argument_input").get().empty()) - argument = action.at("argument_default").get(); - const auto result = invoke_action(session, id, argument); - EXPECT_EQ(result.at("type"), "case_state"); - EXPECT_EQ(result.at("case").at("id").get(), case_id); - EXPECT_EQ(result.at("frame_mode").at("id").get(), mode); - EXPECT_TRUE(result.at("telemetry").contains("kernel_observer")); - } - } - } - EXPECT_GT(dispatched_action_instances, 150U); -} - -TEST(RenderiveWebGallery, CanvasSpecificActionsProduceObservableStateChanges) { - { - Gallery_Plot_Session session; - const auto opened = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("axis_lab", "manual")))); - const auto before = opened.at("telemetry").at("axis_lab").at("time_axis_point_count") - .get(); - const auto after = invoke_action(session, "append_time").at("telemetry"); - EXPECT_EQ(after.at("axis_lab").at("time_axis_point_count").get(), - before + 1); - EXPECT_NE(after.at("last_action_result").get().find("tick="), - std::string::npos); - } - { - Gallery_Plot_Session session; - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "manual")))).at("type"), - "case_state"); - auto telemetry = invoke_action(session, "add_line_marker", 96e6).at("telemetry"); - EXPECT_EQ(telemetry.at("spectrum").at("selectable_line_markers"), 1); - telemetry = invoke_action(session, "select_marker", 0).at("telemetry"); - EXPECT_EQ(telemetry.at("spectrum").at("selected_marker_index"), 0); - telemetry = invoke_action(session, "set_marker_frequency", 99e6).at("telemetry"); - EXPECT_DOUBLE_EQ(telemetry.at("spectrum").at("selected_marker_frequency"), 99e6); - telemetry = invoke_action(session, "clear_markers").at("telemetry"); - EXPECT_EQ(telemetry.at("spectrum").at("selectable_line_markers"), 0); - EXPECT_EQ(telemetry.at("spectrum").at("selected_marker_index"), -1); - } - { - Gallery_Plot_Session session; - const auto opened = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("waterfall", "manual")))); - const auto before_rows = opened.at("telemetry").at("waterfall").at("row_count") - .get(); - auto telemetry = invoke_action(session, "append_row").at("telemetry"); - EXPECT_EQ(telemetry.at("waterfall").at("row_count").get(), - before_rows + 1); - telemetry = invoke_action(session, "append_tick_row").at("telemetry"); - EXPECT_EQ(telemetry.at("waterfall").at("row_count").get(), - before_rows + 2); - } - { - Gallery_Plot_Session session; - const auto opened = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("afterglow", "manual")))); - const auto before = opened.at("telemetry").at("afterglow") - .at("history_frame_count").get(); - const auto telemetry = invoke_action(session, "append_spectrum").at("telemetry"); - EXPECT_EQ(telemetry.at("afterglow").at("history_frame_count").get(), - before + 1); - } - { - Gallery_Plot_Session session; - const auto opened = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("sweep_spectrum", "manual")))); - const auto before = opened.at("telemetry").at("sweep_spectrum") - .at("stored_block_count").get(); - const auto telemetry = invoke_action(session, "append_block").at("telemetry"); - EXPECT_EQ(telemetry.at("sweep_spectrum").at("stored_block_count").get(), - before + 1); - } - { - Gallery_Plot_Session session; - const auto opened = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("frequency_trace", "manual")))); - const auto before = opened.at("telemetry").at("frequency_trace") - .at("sample_count").get(); - auto telemetry = invoke_action(session, "append_sample").at("telemetry"); - EXPECT_EQ(telemetry.at("frequency_trace").at("sample_count").get(), - before + 1); - telemetry = invoke_action(session, "append_tick_sample").at("telemetry"); - EXPECT_EQ(telemetry.at("frequency_trace").at("sample_count").get(), - before + 2); - } - { - Gallery_Plot_Session session; - const auto opened = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("constellation", "manual")))); - const auto before = opened.at("telemetry").at("constellation") - .at("point_count").get(); - const auto telemetry = invoke_action(session, "append_points").at("telemetry"); - EXPECT_EQ(telemetry.at("constellation").at("point_count").get(), - before + 24); - } -} - -TEST(RenderiveWebGallery, CommonControlsAndViewLifecycleProduceObservableEffects) { - Gallery_Plot_Session session; - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "manual")))).at("type"), - "case_state"); - - const auto background_state = patch_controls(session, {{"background_color", "#123456"}}); - ASSERT_EQ(background_state.at("type"), "case_state") << background_state.dump(); - EXPECT_EQ(background_state.at("controls").at("resources").at(0).at("data") - .at("background_color"), "#123456"); - ASSERT_EQ(invoke_action(session, "mode_cycle").at("type"), "case_state"); - const auto background_frame = session.handle(Frame_Request{}); - ASSERT_TRUE(background_frame.has_value()); - ASSERT_EQ(background_frame->type, Web_Response_Type::Pixels); - ASSERT_GE(background_frame->payload.size(), pixel_frame_header_size + 4); - const auto* corner = reinterpret_cast( - background_frame->payload.data() + pixel_frame_header_size); - EXPECT_EQ(corner[0], 0x12); - EXPECT_EQ(corner[1], 0x34); - EXPECT_EQ(corner[2], 0x56); - EXPECT_EQ(corner[3], 0xff); - - auto state = patch_controls(session, { - {"performance_overlay", true}, - {"renderable_visible", false}, - {"cache_mode", "Direct"}, - {"object_name", "Observable_Spectrum"} - }); - auto telemetry = state.at("telemetry"); - EXPECT_TRUE(telemetry.at("performance_overlay_enabled").get()); - EXPECT_FALSE(telemetry.at("renderable").at("visible").get()); - EXPECT_EQ(telemetry.at("renderable").at("cache_mode"), "Direct"); - EXPECT_EQ(telemetry.at("renderable").at("object_name"), "Observable_Spectrum"); - - state = invoke_action(session, "toggle_view"); - EXPECT_FALSE(state.at("telemetry").at("view_active").get()); - EXPECT_FALSE(session.handle(Frame_Request{}).has_value()); - state = invoke_action(session, "toggle_view"); - EXPECT_TRUE(state.at("telemetry").at("view_active").get()); - ASSERT_EQ(invoke_action(session, "mode_cycle").at("type"), "case_state"); - EXPECT_TRUE(session.handle(Frame_Request{}).has_value()); -} - -TEST(RenderiveWebGallery, SelectionOverlayPointerLifecycleCreatesAndClearsRegion) { - Gallery_Plot_Session session; - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("selection_overlay", "manual")))).at("type"), - "case_state"); - - const auto resize = Web_Event_Adapter::decode( - R"({"category":"event","type":"resize","width":560,"height":320})"); - ASSERT_TRUE(resize.has_value()); - EXPECT_FALSE(session.handle(*resize).has_value()); - constexpr std::array drag{ - R"({"category":"event","type":"pointer_press","x":120,"y":90,"button":"left","buttons":1,"modifiers":0})", - R"({"category":"event","type":"pointer_move","x":300,"y":210,"button":"none","buttons":1,"modifiers":0})", - R"({"category":"event","type":"pointer_release","x":300,"y":210,"button":"left","buttons":0,"modifiers":0})" - }; - for (const auto encoded : drag) { - const auto event = Web_Event_Adapter::decode(encoded); - ASSERT_TRUE(event.has_value()); - EXPECT_FALSE(session.handle(*event).has_value()); - } - - EXPECT_EQ(observe_telemetry(session).at("selection_regions"), 1); - EXPECT_EQ(invoke_action(session, "clear_selection").at("telemetry") - .at("selection_regions"), 0); -} - -TEST(RenderiveWebGallery, EveryCanvasAcceptsTheCompleteWebEventSetAndStillRendersPixels) { - constexpr std::array cases{ - "axis_lab", "spectrum", "afterglow", "sweep_spectrum", - "waterfall", "frequency_trace", "selection_overlay", "constellation" - }; - constexpr std::array modes{"manual", "low_latency", "playback"}; - constexpr std::array interactive_events{ - R"({"category":"event","type":"show"})", - R"({"category":"event","type":"pointer_move","x":100,"y":100,"button":"none","buttons":0,"modifiers":0})", - R"({"category":"event","type":"pointer_press","x":100,"y":100,"button":"left","buttons":1,"modifiers":1})", - R"({"category":"event","type":"pointer_move","x":180,"y":160,"button":"none","buttons":1,"modifiers":1})", - R"({"category":"event","type":"pointer_release","x":180,"y":160,"button":"left","buttons":0,"modifiers":1})", - R"({"category":"event","type":"wheel","x":160,"y":120,"pixelDeltaX":0,"pixelDeltaY":-24,"angleDeltaX":0,"angleDeltaY":192,"buttons":0,"modifiers":2})", - R"({"category":"event","type":"key_press","key":"Escape","nativeKey":27,"repeat":false,"modifiers":0})", - R"({"category":"event","type":"key_release","key":"Escape","nativeKey":27,"repeat":false,"modifiers":0})", - R"({"category":"event","type":"leave"})", - R"({"category":"event","type":"hide"})" - }; - - for (const auto case_id : cases) { - for (const auto mode : modes) { - SCOPED_TRACE(std::string(case_id) + "/" + std::string(mode)); - Gallery_Plot_Session session(mode == "low_latency"); - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message(case_id, mode)))).at("type"), - "case_state"); - const auto resize = Web_Event_Adapter::decode( - R"({"category":"event","type":"resize","width":420,"height":260})"); - ASSERT_TRUE(resize.has_value()); - EXPECT_FALSE(session.handle(*resize).has_value()); - - for (const auto encoded : interactive_events) { - const auto event = Web_Event_Adapter::decode(encoded); - ASSERT_TRUE(event.has_value()) << encoded; - EXPECT_FALSE(session.handle(*event).has_value()) << encoded; - } - EXPECT_FALSE(session.handle(Frame_Request{}).has_value()); - - const auto show = Web_Event_Adapter::decode( - R"({"category":"event","type":"show"})"); - ASSERT_TRUE(show.has_value()); - EXPECT_FALSE(session.handle(*show).has_value()); - const auto frame = wait_for_pixel_frame(session, 420, 260); - ASSERT_TRUE(frame.has_value()); - ASSERT_EQ(frame->type, Web_Response_Type::Pixels); - EXPECT_EQ(frame->payload.substr(0, 4), "RVP2"); - EXPECT_EQ(read_u32_le(frame->payload, 4), 420U); - EXPECT_EQ(read_u32_le(frame->payload, 8), 260U); - const auto telemetry = observe_telemetry(session); - EXPECT_TRUE(telemetry.at("view_active").get()); - EXPECT_EQ(telemetry.at("viewport").at("width"), 420); - EXPECT_EQ(telemetry.at("viewport").at("height"), 260); - } - } -} - -TEST(RenderiveWebGallery, InvalidInteractiveEventsAreRejectedBeforeReachingCore2) { - constexpr std::array invalid{ - R"({"category":"command","type":"pointer_move","x":1,"y":2})", - R"({"category":"event","type":"pointer_move","x":1})", - R"({"category":"event","type":"pointer_press","x":"bad","y":2})", - R"({"category":"event","type":"wheel","x":1,"y":2})", - R"({"category":"event","type":"resize","width":"bad","height":200})", - R"({"category":"event","type":"control","control":"mode","value":"INVALID"})", - R"({"category":"event","type":"unknown"})", - "not-json" - }; - for (const auto encoded : invalid) - EXPECT_FALSE(Web_Event_Adapter::decode(encoded).has_value()) << encoded; -} - -TEST(RenderiveWebGallery, EveryIndependentCore2CanvasBuildsAndRenders) { - constexpr std::array cases{ - "axis_lab", "spectrum", "afterglow", "sweep_spectrum", - "waterfall", "frequency_trace", "selection_overlay", "constellation" - }; - constexpr std::array modes{"manual", "low_latency", "playback"}; - for (const std::string_view mode : modes) { - for (const std::string_view case_id : cases) { - Gallery_Plot_Session session(mode == "low_latency"); - const auto opened = session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message(case_id, mode))); - ASSERT_TRUE(opened.has_value()) << case_id; - ASSERT_EQ(opened->type, Web_Response_Type::Json) << case_id; - const auto state = parse_json(opened->payload); - EXPECT_EQ(state.at("type"), "case_state") << case_id; - EXPECT_EQ(state.at("case").at("id").get(), std::string(case_id)) - << case_id; - EXPECT_EQ(state.at("frame_mode").at("id"), std::string(mode)) << case_id; - const auto& resources = state.at("controls").at("resources"); - ASSERT_FALSE(resources.empty()) << case_id; - for (const auto& resource : resources) { - EXPECT_EQ(resource.at("descriptor").at("protocol"), - "adminive.resource") << case_id; - EXPECT_EQ(resource.at("view").at("protocol"), - "adminive.view") << case_id; - } - EXPECT_FALSE(session.handle(Viewport_Resize{{480, 280}}).has_value()) << case_id; - const auto frame = wait_for_pixel_frame(session, 480, 280); - ASSERT_TRUE(frame.has_value()) << case_id; - ASSERT_EQ(frame->type, Web_Response_Type::Pixels) << case_id; - EXPECT_EQ(frame->payload.substr(0, 4), "RVP2") << case_id; - EXPECT_EQ(read_u32_le(frame->payload, 4), 480U) << case_id; - EXPECT_EQ(read_u32_le(frame->payload, 8), 280U) << case_id; - const auto observed = session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe"})")); - ASSERT_TRUE(observed.has_value()) << case_id; - const auto telemetry = parse_json(observed->payload); - EXPECT_EQ(telemetry.at("type"), "observer_state") << case_id; - EXPECT_EQ(telemetry.at("frame_mode").at("id"), std::string(mode)) << case_id; - EXPECT_TRUE(telemetry.at("telemetry").contains("kernel_observer")) << case_id; - EXPECT_TRUE(telemetry.at("telemetry").contains("performance")) << case_id; - EXPECT_TRUE(telemetry.at("telemetry").contains("scheduler")) << case_id; - EXPECT_TRUE(telemetry.at("telemetry").contains("data_shape")) << case_id; - EXPECT_GT(telemetry.at("telemetry").at("data_shape").at("rendered_elements") - .get(), 0U) << case_id; - } - } -} - -TEST(RenderiveWebGallery, ActionsMutateLiveCore2ObjectsAndReturnTelemetry) { - Gallery_Plot_Session session; - ASSERT_TRUE(session.handle(gallery_request( - Gallery_Request_Kind::Open, - open_message("spectrum", "low_latency")))); - - const auto add = session.handle(gallery_request( + const auto switched = session.handle(Gallery_Request{ Gallery_Request_Kind::Action, - R"({"category":"event","type":"gallery_action","action":"add_line_marker","argument":97000000})")); - ASSERT_TRUE(add.has_value()); - const auto add_state = parse_json(add->payload); - EXPECT_EQ(add_state.at("type"), "case_state"); - EXPECT_EQ(add_state.at("telemetry").at("spectrum").at("selectable_line_markers"), 1); + R"({"category":"event","type":"gallery_action","action":"frame_strategy_manual"})"}); + ASSERT_TRUE(switched); + const auto switched_state = nlohmann::json::parse(switched->payload); + EXPECT_EQ(switched_state.at("frame_mode"), "manual"); + EXPECT_EQ(switched_state.at("telemetry").at("kernel_observer").at("mode"), + "manual"); - const auto patch = session.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"spectrum","patch":{"interpolation_mode":"Step_Left","max_hold_visible":true}})")); - ASSERT_TRUE(patch.has_value()); - EXPECT_EQ(parse_json(patch->payload).at("type"), "case_state"); + const std::string render = + R"({"category":"event","type":"gallery_action","action":"mode_render"})"; + const auto rendered = session.handle( + Gallery_Request{Gallery_Request_Kind::Action, render}); + ASSERT_TRUE(rendered); + EXPECT_EQ(nlohmann::json::parse(rendered->payload).at("type"), "case_state"); -} + const std::string capture = + R"({"category":"event","type":"gallery_action","action":"capture_next_frame"})"; + const auto captured = session.handle( + Gallery_Request{Gallery_Request_Kind::Action, capture}); + ASSERT_TRUE(captured); + const auto capture_state = nlohmann::json::parse(captured->payload); + const auto& sessions = capture_state.at("controls") + .at("performance_capture").at("sessions"); + ASSERT_EQ(sessions.size(), 1U); + EXPECT_EQ(sessions.front().at("captured_count"), 1U); -TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers) { - const auto action = [](Gallery_Plot_Session& session, std::string_view id, - std::optional argument = std::nullopt) { - nlohmann::json request{{"category", "event"}, {"type", "gallery_action"}, - {"action", id}}; - if (argument) - request["argument"] = *argument; - return session.handle(gallery_request(Gallery_Request_Kind::Action, request.dump())); - }; - const auto observe = [](Gallery_Plot_Session& session) { - const auto response = session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe"})")); - EXPECT_TRUE(response.has_value()); - return parse_json(response->payload).at("telemetry"); - }; - - Gallery_Plot_Session manual; - ASSERT_TRUE(manual.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "manual")))); - ASSERT_TRUE(action(manual, "mode_prepare")); - auto manual_observer = observe(manual).at("kernel_observer"); - EXPECT_EQ(manual_observer.at("last_event"), "prepared"); - EXPECT_EQ(manual_observer.at("pending_frame_count"), 1); - ASSERT_TRUE(action(manual, "mode_refresh")); - ASSERT_TRUE(action(manual, "mode_render")); - manual_observer = observe(manual).at("kernel_observer"); - EXPECT_EQ(manual_observer.at("last_event"), "rendered"); - EXPECT_GE(manual_observer.at("consumed_frame_count").get(), 2U); - - Gallery_Plot_Session low_latency; - ASSERT_TRUE(low_latency.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))); - const auto high_frequency = low_latency.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":1000000000}})")); - ASSERT_TRUE(high_frequency.has_value()); - ASSERT_EQ(parse_json(high_frequency->payload).at("type"), "case_state"); - ASSERT_TRUE(low_latency.handle(Frame_Request{})); - const auto low_telemetry = observe(low_latency); - EXPECT_EQ(low_telemetry.at("frame_mode"), "low_latency"); - const auto& low_observer = low_telemetry.at("kernel_observer"); - EXPECT_GT(low_observer.at("observation_count").get(), 0U); - EXPECT_FALSE(low_telemetry.contains("render_fps")); - EXPECT_FALSE(low_telemetry.contains("low_latency_limit")); - EXPECT_NE(low_observer.at("limit_state"), "consumer_limited"); - EXPECT_NE(low_observer.at("limit_state"), "frequency_limited"); - EXPECT_TRUE(low_observer.at("limit_state") == "paint_limited" || - low_observer.at("limit_state") == "render_limited"); - EXPECT_EQ(low_observer.at("target_interval_ns"), 1); - EXPECT_EQ(low_observer.at("consumer_interval_ns"), 0); - EXPECT_GT(low_observer.at("bottleneck_duration_ns").get(), 1U); - EXPECT_TRUE(low_observer.contains("paint_lease_wait_ns")); - EXPECT_TRUE(low_observer.contains("render_finish_state_wait_ns")); - constexpr std::array observer_fields{ - "mode", "last_event", "limit_state", "frequency_hz", "observation_count", - "produced_frame_count", "consumed_frame_count", "dropped_frame_count", - "failed_operation_count", "pending_frame_count", "latest_sequence", - "paint_duration_ns", "render_duration_ns", "target_interval_ns", "frequency_limit_enabled", - "consumer_feedback_enabled", "bottleneck_duration_ns", "consumer_sample_interval_ns", - "consumer_smoothed_interval_ns", "consumer_variation_ns", "consumer_safety_interval_ns", "consumer_interval_ns", - "next_refresh_interval_ns", "paint_lease_wait_ns", - "paint_state_wait_ns", "publish_state_wait_ns", "ready_wait_ns", - "frame_age_at_render_ns", "render_lease_wait_ns", "render_state_wait_ns", - "render_finish_state_wait_ns", "queue_wait_ns", "end_to_end_ns" - }; - for (const auto field : observer_fields) - EXPECT_TRUE(low_observer.contains(field)) << field; - - Gallery_Plot_Session playback; - ASSERT_TRUE(playback.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "playback")))); - ASSERT_TRUE(action(playback, "mode_enqueue_burst", 5)); - auto playback_observer = observe(playback).at("kernel_observer"); - EXPECT_EQ(playback_observer.at("last_event"), "enqueued"); - EXPECT_EQ(playback_observer.at("pending_frame_count"), 5); - ASSERT_TRUE(action(playback, "mode_dequeue")); - playback_observer = observe(playback).at("kernel_observer"); - EXPECT_EQ(playback_observer.at("last_event"), "rendered"); - EXPECT_EQ(playback_observer.at("pending_frame_count"), 4); - EXPECT_GT(playback_observer.at("queue_wait_ns").get(), 0U); -} - -TEST(RenderiveWebGallery, ManualStrategyRunsOnlyExplicitPrepareRefreshRenderCycles) { - Gallery_Plot_Session session(true); - const auto opened = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "manual")))); - ASSERT_EQ(opened.at("type"), "case_state"); - const auto baseline = opened.at("telemetry").at("kernel_observer"); - EXPECT_EQ(baseline.at("mode"), "manual"); - EXPECT_EQ(baseline.at("limit_state"), "not_applicable"); - - std::this_thread::sleep_for(std::chrono::milliseconds(80)); - auto observer = observe_telemetry(session).at("kernel_observer"); - EXPECT_EQ(observer.at("latest_sequence"), baseline.at("latest_sequence")); - EXPECT_EQ(observer.at("produced_frame_count"), baseline.at("produced_frame_count")); - EXPECT_EQ(observer.at("consumed_frame_count"), baseline.at("consumed_frame_count")); - - auto state = invoke_action(session, "mode_prepare"); - observer = state.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("last_event"), "prepared"); - EXPECT_EQ(observer.at("pending_frame_count"), 1); - EXPECT_EQ(observer.at("produced_frame_count").get(), - baseline.at("produced_frame_count").get() + 1); - - state = invoke_action(session, "mode_refresh"); - observer = state.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("last_event"), "refresh_succeeded"); - EXPECT_EQ(observer.at("pending_frame_count"), 0); - EXPECT_EQ(observer.at("consumed_frame_count"), baseline.at("consumed_frame_count")); - - state = invoke_action(session, "mode_render"); - observer = state.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("last_event"), "rendered"); - EXPECT_EQ(observer.at("pending_frame_count"), 0); - EXPECT_EQ(observer.at("consumed_frame_count").get(), - baseline.at("consumed_frame_count").get() + 1); - - ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state"); - state = invoke_action(session, "mode_discard"); - observer = state.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("last_event"), "manually_discarded"); - EXPECT_EQ(observer.at("pending_frame_count"), 0); - EXPECT_EQ(observer.at("dropped_frame_count").get(), - baseline.at("dropped_frame_count").get() + 1); - - const auto failed_before = observer.at("failed_operation_count").get(); - state = invoke_action(session, "mode_refresh"); - observer = state.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("last_event"), "refresh_failed"); - EXPECT_EQ(observer.at("failed_operation_count").get(), failed_before + 1); - - const auto sequence_after_actions = observer.at("latest_sequence"); - std::this_thread::sleep_for(std::chrono::milliseconds(80)); - EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("latest_sequence"), - sequence_after_actions); -} - -TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggressiveFrequency) { - Gallery_Plot_Session session(true); - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"), - "case_state"); - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":1000}})"))).at("type"), - "case_state"); - - ASSERT_TRUE(wait_for_condition([&session] { - return successful_render_count(session) >= 4; - })); - std::string first_pixels; - std::string last_pixels; - for (int iteration = 0; iteration < 12; ++iteration) { - const auto started = std::chrono::steady_clock::now(); - const auto frame = session.handle(Frame_Request{}); - ASSERT_TRUE(frame.has_value()) << iteration; - ASSERT_EQ(frame->type, Web_Response_Type::Pixels) << iteration; - const auto telemetry = observe_telemetry(session); - EXPECT_LT(std::chrono::steady_clock::now() - started, - std::chrono::milliseconds(250)) << iteration; - EXPECT_EQ(telemetry.at("kernel_observer").at("frequency_hz"), 1000.0); - if (iteration == 0) - first_pixels = frame->payload; - last_pixels = frame->payload; - std::this_thread::sleep_for(std::chrono::milliseconds(3)); + const auto pixels = session.handle(Frame_Request{}); + ASSERT_TRUE(pixels); + ASSERT_EQ(pixels->type, Web_Response_Type::Pixels); + EXPECT_EQ(pixels->payload.substr(0, 4), "RVP2"); + const auto first_pixel = pixels->payload.substr(pixel_frame_header_size, 4); + bool contains_drawn_pixel = false; + for (std::size_t offset = pixel_frame_header_size; + offset + 4 <= pixels->payload.size(); offset += 4) { + if (pixels->payload.compare(offset, 4, first_pixel) != 0) { + contains_drawn_pixel = true; + break; + } } - EXPECT_NE(first_pixels, last_pixels); - - const auto telemetry = observe_telemetry(session); - const auto& observer = telemetry.at("kernel_observer"); - EXPECT_EQ(observer.at("target_interval_ns"), 1'000'000); - EXPECT_GT(observer.at("latest_sequence").get(), 4U); - EXPECT_GT(telemetry.at("performance").at("successful_render_count").get(), 4U); - EXPECT_TRUE(observer.at("limit_state") == "frequency_limited" || - observer.at("limit_state") == "paint_limited" || - observer.at("limit_state") == "render_limited" || - observer.at("limit_state") == "consumer_limited"); + EXPECT_TRUE(contains_drawn_pixel); } - -TEST(RenderiveWebGallery, FrequencyLimitCannotBeBypassedByForegroundRescheduleEvents) { - Gallery_Plot_Session session(true); - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"), - "case_state"); - const auto patched = patch_controls(session, { - {"frequency_limit_enabled", true}, - {"max_render_fps", 30.0}, - {"feedback", {{"enabled", false}}} - }); - ASSERT_EQ(patched.at("type"), "case_state"); - const auto& patched_observer = patched.at("telemetry").at("kernel_observer"); - EXPECT_TRUE(patched_observer.at("frequency_limit_enabled").get()); - EXPECT_FALSE(patched_observer.at("consumer_feedback_enabled").get()); - EXPECT_EQ(patched_observer.at("target_interval_ns"), 33'333'333); - ASSERT_TRUE(wait_for_condition([&session] { - return successful_render_count(session) >= 3; - })); - const auto pointer = Web_Event_Adapter::decode( - R"({"category":"event","type":"pointer_move","x":320,"y":180,"button":"none","buttons":0,"modifiers":0})"); - ASSERT_TRUE(pointer.has_value()); - const auto before = successful_render_count(session); - const auto started = std::chrono::steady_clock::now(); - const auto deadline = started + std::chrono::milliseconds(450); - while (std::chrono::steady_clock::now() < deadline) { - EXPECT_FALSE(session.handle(*pointer).has_value()); - std::this_thread::sleep_for(std::chrono::milliseconds(2)); - } - const auto elapsed = std::chrono::steady_clock::now() - started; - const auto after = successful_render_count(session); - const auto rendered = after - before; - EXPECT_GT(rendered, 0U); - EXPECT_LE(rendered, 16U); - const auto telemetry = observe_telemetry(session); - const auto& observer = telemetry.at("kernel_observer"); - EXPECT_EQ(observer.at("target_interval_ns"), 33'333'333); - EXPECT_GE(observer.at("next_refresh_interval_ns").get(), 33'333'333U); - EXPECT_GE(elapsed, std::chrono::milliseconds(450)); -} - -TEST(RenderiveWebGallery, ConsumerFeedbackCannotBeBypassedByForegroundRescheduleEvents) { - Gallery_Plot_Session session(true); - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"), - "case_state"); - ASSERT_EQ(patch_controls(session, { - {"frequency_limit_enabled", false}, - {"feedback", {{"enabled", true}}} - }).at("type"), "case_state"); - auto observed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":25,"presentation_fps":25,"websocket_buffered_bytes":0,"changed_pixel_frames":1,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":5,"display_interval_ms":40,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); - auto observer = observed.at("telemetry").at("kernel_observer"); - EXPECT_FALSE(observer.at("frequency_limit_enabled").get()); - EXPECT_TRUE(observer.at("consumer_feedback_enabled").get()); - EXPECT_EQ(observer.at("consumer_interval_ns"), 40'000'000); - ASSERT_TRUE(wait_for_condition([&session] { - return successful_render_count(session) >= 3; - })); - const auto pointer = Web_Event_Adapter::decode( - R"({"category":"event","type":"pointer_move","x":320,"y":180,"button":"none","buttons":0,"modifiers":0})"); - ASSERT_TRUE(pointer.has_value()); - const auto before = successful_render_count(session); - const auto started = std::chrono::steady_clock::now(); - const auto deadline = started + std::chrono::milliseconds(450); - while (std::chrono::steady_clock::now() < deadline) { - EXPECT_FALSE(session.handle(*pointer).has_value()); - std::this_thread::sleep_for(std::chrono::milliseconds(2)); - } - const auto after = successful_render_count(session); - const auto rendered = after - before; - EXPECT_GT(rendered, 0U); - EXPECT_LE(rendered, 14U); - observed = observe_telemetry(session); - observer = observed.at("kernel_observer"); - EXPECT_EQ(observer.at("consumer_interval_ns"), 40'000'000); - EXPECT_GE(observer.at("next_refresh_interval_ns").get(), 40'000'000U); -} - -TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedbackWithoutTransportFpsCap) { - Gallery_Plot_Session session(true); - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"), - "case_state"); - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":1000000000}})"))).at("type"), - "case_state"); - auto observed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":50,"presentation_fps":50,"websocket_buffered_bytes":0,"changed_pixel_frames":10,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":80,"display_interval_ms":20,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); - auto observer = observed.at("telemetry").at("kernel_observer"); - auto feedback = observed.at("telemetry").at("consumer_feedback"); - EXPECT_TRUE(observer.at("frequency_limit_enabled").get()); - EXPECT_TRUE(observer.at("consumer_feedback_enabled").get()); - EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 20'000'000); - EXPECT_EQ(observer.at("consumer_smoothed_interval_ns"), 20'000'000); - EXPECT_EQ(observer.at("consumer_variation_ns"), 0); - EXPECT_EQ(observer.at("consumer_safety_interval_ns"), 20'000'000); - EXPECT_EQ(observer.at("consumer_interval_ns"), 20'000'000); - EXPECT_EQ(observer.at("limit_state"), "consumer_limited"); - EXPECT_EQ(feedback.at("source"), "presentation"); - observed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":45,"presentation_fps":45,"websocket_buffered_bytes":0,"changed_pixel_frames":20,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":4,"display_interval_ms":22,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); - observer = observed.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 22'000'000); - EXPECT_EQ(observer.at("consumer_smoothed_interval_ns"), 21'000'000); - EXPECT_EQ(observer.at("consumer_variation_ns"), 125'000); - EXPECT_EQ(observer.at("consumer_safety_interval_ns"), 22'000'000); - EXPECT_EQ(observer.at("consumer_interval_ns"), 22'000'000); - observed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"feedback":{"enabled":false},"frequency_limit_enabled":false}})"))); - observer = observed.at("telemetry").at("kernel_observer"); - EXPECT_FALSE(observer.at("frequency_limit_enabled").get()); - EXPECT_FALSE(observer.at("consumer_feedback_enabled").get()); - EXPECT_EQ(observer.at("target_interval_ns"), 0); - EXPECT_EQ(observer.at("consumer_interval_ns"), 0); - EXPECT_TRUE(observer.at("limit_state") == "paint_limited" || - observer.at("limit_state") == "render_limited" || - observer.at("limit_state") == "unlimited"); - const auto rejected = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"pixel_stream_fps":30}})"))); - EXPECT_EQ(rejected.at("type"), "error"); -} - -TEST(RenderiveWebGallery, ConsumerFeedbackSourcesCanBeSelectedIndependentlyAndManually) { - Gallery_Plot_Session session(true); - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"), - "case_state"); - ASSERT_EQ(patch_controls(session, { - {"frequency_limit_enabled", false}, - {"feedback", {{"enabled", true}, {"pixel", true}, - {"presentation", false}, {"manual", false}}} - }).at("type"), "case_state"); - auto observed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":25,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":1,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":5,"frame_round_trip_average_ms":4.8,"frame_round_trip_deviation_ms":0.2,"frame_round_trip_p95_ms":5.1,"frame_round_trip_p99_ms":5.3,"display_interval_ms":16,"display_interval_average_ms":16.2,"display_interval_latest_ms":16,"display_interval_p95_ms":17,"display_interval_p99_ms":18,"display_interval_deviation_ms":0.5,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); - auto observer = observed.at("telemetry").at("kernel_observer"); - auto feedback = observed.at("telemetry").at("consumer_feedback"); - EXPECT_EQ(feedback.at("source"), "pixel"); - EXPECT_EQ(feedback.at("pixel_interval_ns"), 40'000'000); - EXPECT_EQ(feedback.at("presentation_interval_ns"), 16'000'000); - EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 40'000'000); - observed = patch_controls(session, { - {"feedback", {{"pixel", false}, {"presentation", true}}} - }); - observer = observed.at("telemetry").at("kernel_observer"); - feedback = observed.at("telemetry").at("consumer_feedback"); - EXPECT_EQ(feedback.at("source"), "presentation"); - EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 16'000'000); - observed = patch_controls(session, { - {"feedback", {{"presentation", false}, {"manual", true}, - {"manual_fps", 20.0}}} - }); - observer = observed.at("telemetry").at("kernel_observer"); - feedback = observed.at("telemetry").at("consumer_feedback"); - EXPECT_EQ(feedback.at("source"), "manual"); - EXPECT_EQ(feedback.at("manual_interval_ns"), 50'000'000); - EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 50'000'000); - observed = patch_controls(session, { - {"feedback", {{"manual", false}}} - }); - observer = observed.at("telemetry").at("kernel_observer"); - feedback = observed.at("telemetry").at("consumer_feedback"); - EXPECT_EQ(feedback.at("source"), "none"); - EXPECT_FALSE(observer.at("consumer_feedback_enabled").get()); - EXPECT_TRUE(feedback.at("master_enabled").get()); - EXPECT_EQ(observer.at("consumer_interval_ns"), 0); -} - -TEST(RenderiveWebGallery, PlaybackStrategyQueuesWithoutAutomaticConsumptionAndReportsEmptyQueue) { - Gallery_Plot_Session session(true); - const auto opened = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "playback")))); - ASSERT_EQ(opened.at("type"), "case_state"); - const auto baseline = opened.at("telemetry").at("kernel_observer"); - EXPECT_EQ(baseline.at("mode"), "playback"); - EXPECT_EQ(baseline.at("limit_state"), "not_applicable"); - - std::this_thread::sleep_for(std::chrono::milliseconds(80)); - auto observer = observe_telemetry(session).at("kernel_observer"); - EXPECT_EQ(observer.at("latest_sequence"), baseline.at("latest_sequence")); - EXPECT_EQ(observer.at("consumed_frame_count"), baseline.at("consumed_frame_count")); - - auto state = invoke_action(session, "mode_enqueue_burst", 5); - observer = state.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("last_event"), "enqueued"); - EXPECT_EQ(observer.at("pending_frame_count"), 5); - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("pending_frame_count"), 5); - - state = invoke_action(session, "mode_dequeue"); - observer = state.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("last_event"), "rendered"); - EXPECT_EQ(observer.at("pending_frame_count"), 4); - EXPECT_GT(observer.at("queue_wait_ns").get(), 0U); - - state = invoke_action(session, "mode_cycle"); - observer = state.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("last_event"), "rendered"); - EXPECT_EQ(observer.at("pending_frame_count"), 4); - - for (int remaining = 3; remaining >= 0; --remaining) { - state = invoke_action(session, "mode_dequeue"); - observer = state.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("pending_frame_count"), remaining); - } - const auto failed_before = observer.at("failed_operation_count").get(); - state = invoke_action(session, "mode_dequeue"); - observer = state.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("last_event"), "queue_empty"); - EXPECT_EQ(observer.at("failed_operation_count").get(), failed_before + 1); - EXPECT_EQ(observer.at("pending_frame_count"), 0); - - const auto sequence_after_actions = observer.at("latest_sequence"); - std::this_thread::sleep_for(std::chrono::milliseconds(80)); - EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("latest_sequence"), - sequence_after_actions); -} - -TEST(RenderiveWebGallery, BackendRejectsActionsHiddenByCurrentCaseOrFrameMode) { - struct Rejected_Action { - std::string_view mode; - std::string_view action; - }; - constexpr std::array rejected{ - Rejected_Action{"manual", "mode_enqueue"}, - Rejected_Action{"low_latency", "mode_refresh"}, - Rejected_Action{"playback", "mode_discard"}, - Rejected_Action{"manual", "append_row"} - }; - for (const auto& item : rejected) { - Gallery_Plot_Session session; - ASSERT_EQ(response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", item.mode)))).at("type"), - "case_state") << item.mode; - const auto response = invoke_action(session, item.action); - EXPECT_EQ(response.at("type"), "error") << item.mode << '/' << item.action; - EXPECT_TRUE(response.at("field_errors").contains("action")) - << item.mode << '/' << item.action; - } -} - -TEST(RenderiveWebGallery, ProductionLowLatencySessionRendersWithoutPixelPulls) { - Gallery_Plot_Session session(true); - ASSERT_TRUE(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))); - ASSERT_TRUE(session.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":120}})"))); - ASSERT_TRUE(wait_for_condition([&session] { - return successful_render_count(session) >= 4; - })); - const auto observed = session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":120,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":17,"duplicate_pixel_frames":3,"frame_request_timeout_count":2,"frame_round_trip_ms":4.5,"frame_round_trip_average_ms":4.2,"frame_round_trip_deviation_ms":0.3,"frame_round_trip_p95_ms":5.1,"frame_round_trip_p99_ms":5.4,"display_interval_ms":16.7,"display_interval_average_ms":16.8,"display_interval_latest_ms":16.9,"display_interval_p95_ms":17.4,"display_interval_p99_ms":18.2,"display_interval_deviation_ms":0.7,"overwritten_pixel_frames":5,"last_pixel_receive_age_ms":8.5,"last_pixel_change_age_ms":12.5}})")); - ASSERT_TRUE(observed.has_value()); - const auto telemetry = parse_json(observed->payload).at("telemetry"); - const auto& performance = telemetry.at("performance"); - EXPECT_TRUE(performance.at("automatic_low_latency_scheduler").get()); - EXPECT_GE(performance.at("successful_render_count").get(), 4U); - EXPECT_GT(performance.at("measured_fps").get(), 0.0); - EXPECT_DOUBLE_EQ(telemetry.at("kernel_observer").at("frequency_hz"), 120.0); - EXPECT_EQ(telemetry.at("kernel_observer").at("target_interval_ns"), 8'333'333); - EXPECT_TRUE(telemetry.at("kernel_observer").at("limit_state") == "frequency_limited" || - telemetry.at("kernel_observer").at("limit_state") == "paint_limited" || - telemetry.at("kernel_observer").at("limit_state") == "render_limited" || - telemetry.at("kernel_observer").at("limit_state") == "consumer_limited"); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("transport_fps"), 120.0); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("presentation_fps"), 60.0); - EXPECT_EQ(telemetry.at("client_performance").at("changed_pixel_frames"), 17); - EXPECT_EQ(telemetry.at("client_performance").at("duplicate_pixel_frames"), 3); - EXPECT_EQ(telemetry.at("client_performance").at("frame_request_timeout_count"), 2); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("frame_round_trip_ms"), 4.5); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("frame_round_trip_average_ms"), 4.2); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("frame_round_trip_deviation_ms"), 0.3); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("frame_round_trip_p95_ms"), 5.1); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("frame_round_trip_p99_ms"), 5.4); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("display_interval_ms"), 16.7); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("display_interval_average_ms"), 16.8); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("display_interval_latest_ms"), 16.9); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("display_interval_p95_ms"), 17.4); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("display_interval_p99_ms"), 18.2); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("display_interval_deviation_ms"), 0.7); - EXPECT_EQ(telemetry.at("client_performance").at("overwritten_pixel_frames"), 5); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("last_pixel_receive_age_ms"), 8.5); - EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("last_pixel_change_age_ms"), 12.5); -} - -TEST(RenderiveWebGallery, ProductionLowLatencyStopsWhileHiddenAndResumes) { - Gallery_Plot_Session session(true); - ASSERT_TRUE(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))); - ASSERT_TRUE(session.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":120}})"))); - ASSERT_TRUE(wait_for_condition([&session] { - return successful_render_count(session) >= 3; - })); - set_gallery_view_active(session, false); - const auto stopped_count = successful_render_count(session); - EXPECT_FALSE(session.handle(Frame_Request{}).has_value()); - std::this_thread::sleep_for(std::chrono::milliseconds(80)); - EXPECT_EQ(successful_render_count(session), stopped_count); - set_gallery_view_active(session, true); - EXPECT_TRUE(wait_for_condition([&session, stopped_count] { - return successful_render_count(session) > stopped_count; - })); - EXPECT_TRUE(session.handle(Frame_Request{}).has_value()); -} - -TEST(RenderiveWebGallery, ProductionLowLatencyOnlyRendersActiveSessions) { - constexpr std::size_t session_count = 8; - std::vector> sessions; - sessions.reserve(session_count); - for (std::size_t index = 0; index < session_count; ++index) { - auto session = std::make_unique(true); - ASSERT_TRUE(session->handle(gallery_request( - Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))); - ASSERT_TRUE(session->handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":60}})"))); - if (index >= 2) - set_gallery_view_active(*session, false); - sessions.push_back(std::move(session)); - } - for (std::size_t index = 0; index < 2; ++index) { - ASSERT_TRUE(wait_for_condition([&sessions, index] { - return successful_render_count(*sessions[index]) >= 2; - })); - } - std::array hidden_counts{}; - for (std::size_t index = 2; index < session_count; ++index) - hidden_counts[index] = successful_render_count(*sessions[index]); - std::this_thread::sleep_for(std::chrono::milliseconds(80)); - for (std::size_t index = 2; index < session_count; ++index) - EXPECT_EQ(successful_render_count(*sessions[index]), hidden_counts[index]) << index; - set_gallery_view_active(*sessions[0], false); - set_gallery_view_active(*sessions[1], false); - const auto first_stopped = successful_render_count(*sessions[0]); - const auto second_stopped = successful_render_count(*sessions[1]); - set_gallery_view_active(*sessions[2], true); - set_gallery_view_active(*sessions[3], true); - EXPECT_TRUE(wait_for_condition([&sessions, baseline = hidden_counts[2]] { - return successful_render_count(*sessions[2]) > baseline; - })); - EXPECT_TRUE(wait_for_condition([&sessions, baseline = hidden_counts[3]] { - return successful_render_count(*sessions[3]) > baseline; - })); - std::this_thread::sleep_for(std::chrono::milliseconds(80)); - EXPECT_EQ(successful_render_count(*sessions[0]), first_stopped); - EXPECT_EQ(successful_render_count(*sessions[1]), second_stopped); -} - -void expect_low_latency_canvas_to_change(std::string_view case_id) { - SCOPED_TRACE(std::string(case_id)); - Gallery_Plot_Session session(true); - ASSERT_TRUE(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message(case_id, "low_latency")))); - ASSERT_FALSE(session.handle(Viewport_Resize{{240, 180}}).has_value()); - ASSERT_TRUE(session.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":120}})"))); - - std::this_thread::sleep_for(std::chrono::milliseconds(90)); - const auto first = session.handle(Frame_Request{}); - ASSERT_TRUE(first.has_value()); - ASSERT_EQ(first->type, Web_Response_Type::Pixels); - std::this_thread::sleep_for(std::chrono::milliseconds(90)); - const auto second = session.handle(Frame_Request{}); - ASSERT_TRUE(second.has_value()); - ASSERT_EQ(second->type, Web_Response_Type::Pixels); - EXPECT_NE(first->payload, second->payload); - - const auto observed = session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe"})")); - ASSERT_TRUE(observed.has_value()); - const auto telemetry = parse_json(observed->payload).at("telemetry"); - EXPECT_GT(telemetry.at("frame_index").get(), 2U); - EXPECT_GT(telemetry.at("kernel_observer").at("latest_sequence").get(), 1U); -} - -TEST(RenderiveWebGallery, AxisLabLowLatencyPixelsChange) { - expect_low_latency_canvas_to_change("axis_lab"); -} - -TEST(RenderiveWebGallery, SpectrumLowLatencyPixelsChange) { - expect_low_latency_canvas_to_change("spectrum"); -} - -TEST(RenderiveWebGallery, AfterglowLowLatencyPixelsChange) { - expect_low_latency_canvas_to_change("afterglow"); -} - -TEST(RenderiveWebGallery, SweepSpectrumLowLatencyPixelsChange) { - expect_low_latency_canvas_to_change("sweep_spectrum"); -} - -TEST(RenderiveWebGallery, WaterfallLowLatencyPixelsChange) { - expect_low_latency_canvas_to_change("waterfall"); -} - -TEST(RenderiveWebGallery, FrequencyTraceLowLatencyPixelsChange) { - expect_low_latency_canvas_to_change("frequency_trace"); -} - -TEST(RenderiveWebGallery, SelectionOverlayLowLatencyPixelsChange) { - expect_low_latency_canvas_to_change("selection_overlay"); -} - -TEST(RenderiveWebGallery, ConstellationLowLatencyPixelsChange) { - expect_low_latency_canvas_to_change("constellation"); -} - -TEST(RenderiveWebGallery, DirectPropertyChangesAndAllImageModesRender) { - Gallery_Plot_Session constellation; - ASSERT_TRUE(constellation.handle(gallery_request( - Gallery_Request_Kind::Open, - open_message("constellation", "low_latency")))); - const auto patched = constellation.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"constellation","patch":{"type":"Psk16","phase_offset_radians":0.75}})")); - ASSERT_TRUE(patched.has_value()); - const auto patched_json = parse_json(patched->payload); - EXPECT_EQ(patched_json.at("controls").at("resources").at(3).at("data").at("type"), - "Psk16"); - ASSERT_TRUE(constellation.handle(Frame_Request{}).has_value()); - - Gallery_Plot_Session waterfall; - ASSERT_TRUE(waterfall.handle(gallery_request( - Gallery_Request_Kind::Open, - open_message("waterfall", "low_latency")))); - for (const std::string_view mode : {"Nearest", "Bilinear", "Bicubic"}) { - const std::string patch = - std::string(R"({"category":"event","type":"gallery_patch","target":"waterfall","patch":{"interpolation_mode":")") + - std::string(mode) + R"("}})"; - ASSERT_TRUE(waterfall.handle(gallery_request(Gallery_Request_Kind::Patch, patch)).has_value()) - << mode; - ASSERT_TRUE(waterfall.handle(Frame_Request{}).has_value()) << mode; - } - const auto palette = waterfall.handle(gallery_request( - Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"waterfall","patch":{"color_map":"Ember"}})")); - ASSERT_TRUE(palette.has_value()); - ASSERT_TRUE(waterfall.handle(Frame_Request{}).has_value()); - const auto waterfall_observer = waterfall.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe"})")); - ASSERT_TRUE(waterfall_observer.has_value()); - const auto waterfall_telemetry = parse_json(waterfall_observer->payload).at("telemetry"); - EXPECT_GT(waterfall_telemetry.at("waterfall").at("row_count").get(), 0U); - EXPECT_GT(waterfall_telemetry.at("waterfall").at("stored_point_count").get(), 0U); - EXPECT_GT(waterfall_telemetry.at("waterfall").at("rendered_cell_count").get(), 0U); -} - } // namespace } // namespace renderive::web diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 3ebe66a..f71eda4 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -3,7 +3,6 @@ import {useTheme} from "@mui/material/styles"; import {useEffect, useMemo, useState} from "react"; import {Group, Panel, Separator} from "react-resizable-panels"; import {Category_Filter} from "./gallery/category_filter"; -import {Frame_Mode_Tabs} from "./gallery/frame_mode_tabs"; import {Gallery_Page} from "./gallery/gallery_page"; import {Gallery_Summary} from "./gallery/gallery_summary"; import {Gallery_Toolbar} from "./gallery/gallery_toolbar"; @@ -13,7 +12,6 @@ import {Inspector_Panel} from "./inspector/inspector_panel"; export function App() { const {catalog, state: socket_state, message: socket_message} = use_gallery_catalog(); - const [active_mode, set_active_mode] = useState(""); const [active_category, set_active_category] = useState(""); const [streams_paused, set_streams_paused] = useState(false); const [selected_plot, set_selected_plot] = useState(null); @@ -31,11 +29,6 @@ export function App() { useEffect(() => { if (!catalog) return; - set_active_mode( - catalog.frame_modes.some(mode => mode.id === catalog.navigation.default_mode) - ? catalog.navigation.default_mode - : catalog.frame_modes[0]?.id ?? "", - ); set_active_category(catalog.navigation.all_categories_label); }, [catalog]); @@ -47,7 +40,9 @@ export function App() { () => [...(catalog?.cases ?? [])].sort((left, right) => left.order - right.order), [catalog], ); - const frame_mode = modes.find(mode => mode.id === active_mode); + const frame_mode = modes.find(mode => mode.id === catalog?.navigation.default_mode) + ?? modes.find(mode => mode.id === "low_latency") + ?? modes[0]; const categories = catalog ? [catalog.navigation.all_categories_label, ...new Set(cases.map(item => item.category))] : []; @@ -65,24 +60,16 @@ export function App() { {!catalog && socket_state === "error" && {socket_message}} {catalog && <> - { - set_active_mode(id); - set_selected_plot(null); - }} - /> {frame_mode && <> - {frame_mode.description} void}) {const sessions=useRef(new Set());const register=useCallback((session:Gallery_Plot_Session,mount:boolean)=>{mount?sessions.current.add(session):sessions.current.delete(session);},[]);useEffect(()=>{let animation_frame=0;const loop=(now:number)=>{for(const session of sessions.current)session.tick(now);animation_frame=requestAnimationFrame(loop);};animation_frame=requestAnimationFrame(loop);return()=>cancelAnimationFrame(animation_frame);},[]);return {cases.map(gallery_case=>)};} +import {Box} from "@mui/material"; +import {useCallback,useEffect,useRef} from "react"; +import type {Gallery_Case,Gallery_Dashboard,Gallery_Frame_Mode} from "../protocol/gallery_types"; +import type {Gallery_Plot_Session} from "../session/gallery_plot_session"; +import {Plot_Card,type Selected_Plot} from "./plot_card"; + +export function Gallery_Page({cases,frame_mode,frame_modes,dashboard,streams_paused,on_open_inspector}:{cases:Gallery_Case[];frame_mode:Gallery_Frame_Mode;frame_modes:Gallery_Frame_Mode[];dashboard:Gallery_Dashboard;streams_paused:boolean;on_open_inspector:(plot:Selected_Plot)=>void}) { + const sessions=useRef(new Set()); + const register=useCallback((session:Gallery_Plot_Session,mount:boolean)=>{mount?sessions.current.add(session):sessions.current.delete(session);},[]); + useEffect(()=>{let animation_frame=0;const loop=(now:number)=>{for(const session of sessions.current)session.tick(now);animation_frame=requestAnimationFrame(loop);};animation_frame=requestAnimationFrame(loop);return()=>cancelAnimationFrame(animation_frame);},[]); + return + {cases.map(gallery_case=>)} + ; +} diff --git a/webapp_gallery/src/gallery/gallery_summary.tsx b/webapp_gallery/src/gallery/gallery_summary.tsx index afa3677..e00cd58 100644 --- a/webapp_gallery/src/gallery/gallery_summary.tsx +++ b/webapp_gallery/src/gallery/gallery_summary.tsx @@ -1,2 +1,20 @@ -import {Box,Paper,Typography} from "@mui/material";import type {Gallery_Catalog} from "../protocol/gallery_types";import {Metric_Grid} from "../common/metric_grid"; -export function Gallery_Summary({catalog}:{catalog:Gallery_Catalog}) {const coverage=catalog.coverage;return {catalog.navigation.hero_eyebrow}{catalog.navigation.hero_title}所有属性、动作、观察者和性能数据均由后端通过 WebSocket 返回。;} +import {Box,Paper,Typography} from "@mui/material"; +import type {Gallery_Catalog} from "../protocol/gallery_types"; +import {Metric_Grid} from "../common/metric_grid"; + +export function Gallery_Summary({catalog}:{catalog:Gallery_Catalog}) { + const coverage=catalog.coverage; + return + + {catalog.navigation.hero_eyebrow} + {catalog.navigation.hero_title} + 所有属性、动作、观察者和性能数据均由后端通过 WebSocket 返回。 + + + ; +} diff --git a/webapp_gallery/src/gallery/plot_card.tsx b/webapp_gallery/src/gallery/plot_card.tsx index f13db68..77c65a6 100644 --- a/webapp_gallery/src/gallery/plot_card.tsx +++ b/webapp_gallery/src/gallery/plot_card.tsx @@ -11,8 +11,8 @@ import {Performance_Strip} from "../plot/performance_strip"; import {Kernel_Observer_Summary} from "../plot/kernel_observer_summary"; export interface Selected_Plot {session:Gallery_Plot_Session;} -export function Plot_Card({gallery_case,frame_mode,dashboard,active,streams_paused,on_register,on_open_inspector}:{gallery_case:Gallery_Case;frame_mode:Gallery_Frame_Mode;dashboard:Gallery_Dashboard;active:boolean;streams_paused:boolean;on_register:(session:Gallery_Plot_Session,mount:boolean)=>void;on_open_inspector:(plot:Selected_Plot)=>void}) { - const [session,snapshot]=use_plot_session(gallery_case,frame_mode); +export function Plot_Card({gallery_case,frame_mode,frame_modes,dashboard,active,streams_paused,on_register,on_open_inspector}:{gallery_case:Gallery_Case;frame_mode:Gallery_Frame_Mode;frame_modes:Gallery_Frame_Mode[];dashboard:Gallery_Dashboard;active:boolean;streams_paused:boolean;on_register:(session:Gallery_Plot_Session,mount:boolean)=>void;on_open_inspector:(plot:Selected_Plot)=>void}) { + const [session,snapshot]=use_plot_session(gallery_case,frame_mode,frame_modes); const card_ref=useRef(null); const [in_viewport,set_in_viewport]=useState(false); useEffect(()=>{on_register(session,true);return()=>on_register(session,false);},[session,on_register]); diff --git a/webapp_gallery/src/hooks/use_plot_session.ts b/webapp_gallery/src/hooks/use_plot_session.ts index 0486ca7..eaf7748 100644 --- a/webapp_gallery/src/hooks/use_plot_session.ts +++ b/webapp_gallery/src/hooks/use_plot_session.ts @@ -1,8 +1,8 @@ import {useEffect, useMemo, useSyncExternalStore} from "react"; import type {Gallery_Case, Gallery_Frame_Mode} from "../protocol/gallery_types"; import {Gallery_Plot_Session} from "../session/gallery_plot_session"; -export function use_plot_session(gallery_case: Gallery_Case, frame_mode: Gallery_Frame_Mode): [Gallery_Plot_Session, ReturnType] { - const session=useMemo(()=>new Gallery_Plot_Session(gallery_case,frame_mode),[gallery_case,frame_mode]); +export function use_plot_session(gallery_case: Gallery_Case, frame_mode: Gallery_Frame_Mode, frame_modes: Gallery_Frame_Mode[]): [Gallery_Plot_Session, ReturnType] { + const session=useMemo(()=>new Gallery_Plot_Session(gallery_case,frame_mode,frame_modes),[gallery_case,frame_mode,frame_modes]); const snapshot=useSyncExternalStore(session.subscribe,session.get_snapshot,session.get_snapshot); useEffect(()=>()=>session.dispose(),[session]); return [session,snapshot]; diff --git a/webapp_gallery/src/session/gallery_plot_session.ts b/webapp_gallery/src/session/gallery_plot_session.ts index ad91d57..2042d43 100644 --- a/webapp_gallery/src/session/gallery_plot_session.ts +++ b/webapp_gallery/src/session/gallery_plot_session.ts @@ -14,11 +14,15 @@ export class Gallery_Plot_Session { private readonly presenter = new Pixel_Presenter(); private readonly client_performance = new Client_Performance(); private readonly frame_controller: Frame_Request_Controller; + private current_frame_mode: Gallery_Frame_Mode; private disposed = false; private visible = false; private streams_paused = false; private last_observe_at = 0; private sent_width = 0; private sent_height = 0; private received_frames = 0; - constructor(readonly gallery_case: Gallery_Case, readonly frame_mode: Gallery_Frame_Mode) { + constructor(readonly gallery_case: Gallery_Case, frame_mode: Gallery_Frame_Mode, + private readonly frame_modes: Gallery_Frame_Mode[]) { + this.current_frame_mode=frame_mode; this.socket = new Gallery_Socket(gallery_socket_url(), {on_json: raw=>this.receive_json(raw),on_binary: buffer=>this.receive_pixels(buffer),on_state: state=>this.receive_socket_state(state)}); this.frame_controller = new Frame_Request_Controller(()=>this.socket.send("frame"),()=>{this.client_performance.record_timeout();if(this.stream_active())this.request_frame(performance.now());},(now,value)=>this.client_performance.record_round_trip(now,value)); } + get frame_mode(): Gallery_Frame_Mode {return this.current_frame_mode;} subscribe = (listener: () => void): (() => void) => {this.listeners.add(listener);return()=>this.listeners.delete(listener);}; get_snapshot = (): Gallery_Plot_Snapshot => this.snapshot; private update(patch: Partial): void {this.snapshot={...this.snapshot,...patch};this.listeners.forEach(listener=>listener());} @@ -29,7 +33,7 @@ export class Gallery_Plot_Session { set_activity(visible: boolean, streams_paused: boolean): void {const was_active=this.stream_active();this.visible=visible;this.streams_paused=streams_paused;if(visible&&!this.socket.is_open())this.connect();const active=this.stream_active();if(this.snapshot.ready&&active!==was_active)this.socket.send(active?"show":"hide");if(active)this.request_frame(performance.now());} private stream_active(): boolean {return this.visible&&!this.streams_paused;} private receive_socket_state(state: "connecting"|"ready"|"error"|"closed"): void {if(this.disposed)return;if(state==="ready"){this.update({connection_state:"ready",protocol_error:""});this.socket.send("gallery_open",{case:this.gallery_case.id,frame_mode:this.frame_mode.id});return;}this.update({connection_state:state,ready:state==="error"?this.snapshot.ready:false});if(state==="closed"&&!this.visible)this.socket.close();} - private receive_json(raw: string): void {try{const response=parse_gallery_response(raw);if(!response)return;if(response.type==="error"){this.update({notice:Object.values(response.field_errors??{})[0]??response.message});return;}if(response.type==="observer_state"){this.update({telemetry:response.telemetry,performance_capture:response.telemetry.performance_capture??this.snapshot.performance_capture,frame_count:this.received_frames});return;}if(response.type==="case_state"||response.type==="refresh_state"){const controls=response.controls;this.update({ready:true,controls:build_controls(controls?.resources),actions:response.actions?.data??[],telemetry:response.telemetry??{},render_plan:controls?.render_plan??null,performance_capture:controls?.performance_capture??response.telemetry?.performance_capture??null,notice:response.notice??"",frame_count:this.received_frames,protocol_error:""});if(this.stream_active()){this.socket.send("show");this.request_frame(performance.now());}}}catch(error){this.update({connection_state:"error",protocol_error:error instanceof Error?error.message:"协议解析失败"});this.socket.close();}} + private receive_json(raw: string): void {try{const response=parse_gallery_response(raw);if(!response)return;if(response.type==="error"){this.update({notice:Object.values(response.field_errors??{})[0]??response.message});return;}if(response.type==="observer_state"){this.update({telemetry:response.telemetry,performance_capture:response.telemetry.performance_capture??this.snapshot.performance_capture,frame_count:this.received_frames});return;}if(response.type==="case_state"||response.type==="refresh_state"){const controls=response.controls;const mode_id=typeof response.frame_mode==="string"?response.frame_mode:undefined;const mode=this.frame_modes.find(value=>value.id===mode_id);if(mode)this.current_frame_mode=mode;this.update({ready:true,controls:build_controls(controls?.resources),actions:response.actions?.data??[],telemetry:response.telemetry??{},render_plan:controls?.render_plan??null,performance_capture:controls?.performance_capture??response.telemetry?.performance_capture??null,notice:response.notice??"",frame_count:this.received_frames,protocol_error:""});if(this.stream_active()){this.socket.send("show");this.request_frame(performance.now());}}}catch(error){this.update({connection_state:"error",protocol_error:error instanceof Error?error.message:"协议解析失败"});this.socket.close();}} private receive_pixels(buffer: ArrayBuffer): void {try{const now=performance.now();this.frame_controller.receive_frame(now);const accepted=this.presenter.accept(buffer);this.received_frames++;this.client_performance.record_pixel(now,accepted.sequence,accepted.overwritten);}catch(error){this.update({connection_state:"error",protocol_error:error instanceof Error?error.message:"像素协议错误"});this.socket.close();}} tick(now: number): void {if(!this.stream_active())return;const presented=this.presenter.present();if(presented)this.client_performance.record_presentation(now);if(this.frame_mode.request_on_animation_frame||(this.frame_mode.request_after_response&&presented))this.request_frame(now);if(this.snapshot.ready&&now-this.last_observe_at>=650){this.last_observe_at=now;this.socket.send("gallery_observe",{client_metrics:this.client_performance.snapshot(now,this.socket.buffered_bytes()) as unknown as Json_Value});}} request_frame(now=performance.now(), explicit=false): boolean {if(!this.snapshot.ready||!this.socket.is_open()||(!explicit&&!this.stream_active())||(explicit&&!this.visible)||(!explicit&&!this.frame_mode.automatic))return false;return this.frame_controller.request_frame(now);}