diff --git a/CMakeLists.txt b/CMakeLists.txt index bc4bd3e..9ef39ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,9 @@ endif () if (RENDERIVE_BUILD_WEB AND NOT RENDERIVE_BUILD_2D) message(FATAL_ERROR "RENDERIVE_BUILD_WEB requires RENDERIVE_BUILD_2D") endif () +if (RENDERIVE_BUILD_WEB AND NOT RENDERIVE_BUILD_3D) + message(FATAL_ERROR "RENDERIVE_BUILD_WEB requires RENDERIVE_BUILD_3D") +endif () include("${CMAKE_CURRENT_LIST_DIR}/third_party/build_infra/start.cmake") if (RENDERIVE_BUILD_TESTS) enable_testing() diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp index f3129d4..7f74f39 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp @@ -1,5 +1,7 @@ #pragma once #include +#include +#include #include #include #include @@ -29,6 +31,10 @@ public: }; struct Configuration { double frequency_hz{60.0}; + // true keeps the existing latest-frame-wins behavior. false keeps one pending frame and + // applies producer backpressure until the renderer consumes it. Explicit discard APIs + // retain their existing semantics in either mode. + bool replace_pending_frame{true}; }; struct Timing_Statistics { std::uint64_t paint_request_time_ns{}; @@ -161,6 +167,7 @@ public: Frame_Control_Strategy_Base::State frame_control_state() const override; State state() const; Counter_Statistics counter_statistics() const; + std::size_t pending_frame_count() const; void on_real_time_data_update(const Real_Time_Data_Observation& observation) noexcept override; bool discard_pending_frame(); bool discard_pending_frame_before(std::uint64_t real_time_data_update_sequence); @@ -183,9 +190,11 @@ private: std::uint64_t real_time_data_update_sequence_{}; Real_Time_Data_Update_State last_real_time_data_update_; Counter_Statistics counters_; + bool replace_pending_frame_{true}; bool cache_ready_{}; bool render_ready_{}; mutable Mutex state_mutex_; + std::condition_variable_any pending_frame_condition_; Mutex painter_mutex_; Mutex render_mutex_; }; diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl index c9c82bf..65eaa98 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl @@ -8,7 +8,12 @@ Low_Latency_Strategy::Painter_Lease::Painter_Lease const std::uint64_t lease_acquired_time_ns = strategy.now_ns(); const std::uint64_t state_request_time_ns = strategy.now_ns(); { - std::lock_guard lock(strategy.state_mutex_); + std::unique_lock lock(strategy.state_mutex_); + if (!strategy.replace_pending_frame_) { + strategy.pending_frame_condition_.wait(lock, [&strategy] { + return !strategy.cache_ready_; + }); + } const std::uint64_t state_acquired_time_ns = strategy.now_ns(); frame_ = strategy.paint_; frame_->statistics = {}; @@ -94,7 +99,6 @@ Low_Latency_Strategy::Render_Lease::Render_Lease(L const std::uint64_t state_request_time_ns = strategy.now_ns(); { std::lock_guard lock(strategy.state_mutex_); - const std::uint64_t state_acquired_time_ns = strategy.now_ns(); if (strategy.cache_ready_) { std::swap(strategy.cache_, strategy.render_); strategy.cache_ready_ = false; @@ -105,6 +109,10 @@ Low_Latency_Strategy::Render_Lease::Render_Lease(L swap_failed_ = true; } frame_ = strategy.render_ready_ ? strategy.render_ : nullptr; + } + { + std::lock_guard lock(strategy.state_mutex_); + const std::uint64_t state_acquired_time_ns = strategy.now_ns(); if (frame_) { ++frame_->statistics.frame_render_count; frame_->statistics.timing.render_request_time_ns = request_time_ns_; @@ -165,6 +173,9 @@ Low_Latency_Strategy::Render_Lease::~Render_Lease( } } lease_lock_.unlock(); + if (acquired_new_frame_) { + strategy_->pending_frame_condition_.notify_one(); + } if (swap_failed_) { strategy_->observe(swap_failed_observation_); } @@ -205,7 +216,7 @@ auto Low_Latency_Strategy::Render_Lease::get() con } template Low_Latency_Strategy::Low_Latency_Strategy(Observer observer, Configuration configuration) - : Frame_Control_Strategy_Base(checked_frequency_hz(configuration.frequency_hz), frequency_interval_ns(configuration.frequency_hz)), observer_(std::move(observer)), paint_(&frames_[0]), cache_(&frames_[1]), render_(&frames_[2]) { + : Frame_Control_Strategy_Base(checked_frequency_hz(configuration.frequency_hz), frequency_interval_ns(configuration.frequency_hz)), observer_(std::move(observer)), paint_(&frames_[0]), cache_(&frames_[1]), render_(&frames_[2]), replace_pending_frame_(configuration.replace_pending_frame) { state_.frequency_hz = configuration.frequency_hz; state_.target_interval_ns = frequency_interval_ns(configuration.frequency_hz); update_refresh_control(); @@ -279,6 +290,11 @@ auto Low_Latency_Strategy::counter_statistics() co return counters_; } template +std::size_t Low_Latency_Strategy::pending_frame_count() const { + std::lock_guard lock(state_mutex_); + return cache_ready_ ? 1U : 0U; +} +template void Low_Latency_Strategy::on_real_time_data_update(const Real_Time_Data_Observation& observation) noexcept { Observation strategy_observation; { @@ -302,6 +318,7 @@ bool Low_Latency_Strategy::discard_pending_frame() discarded = discard_pending_frame_locked(observation); } if (discarded) { + pending_frame_condition_.notify_one(); observe(observation); } return discarded; @@ -317,6 +334,7 @@ bool Low_Latency_Strategy::discard_pending_frame_b } } if (discarded) { + pending_frame_condition_.notify_one(); observe(observation); } return discarded; @@ -332,6 +350,7 @@ bool Low_Latency_Strategy::discard_stale_latest_da } } if (discarded) { + pending_frame_condition_.notify_one(); observe(observation); } return discarded; diff --git a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp index 1b05f10..a8a78d8 100644 --- a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp @@ -1,4 +1,7 @@ #include +#include +#include +#include #include #include #include "Low_Latency_Strategy_Test_Types.hpp" @@ -36,6 +39,69 @@ TEST(low_latency_strategy_test, abandons_unconsumed_cached_frame) { } EXPECT_EQ(strategy.counter_statistics().abandoned_frame_count, 1); } +TEST(low_latency_strategy_test, replacement_disabled_preserves_pending_frame_and_wakes_after_renderer_release) { + using namespace std::chrono_literals; + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + Low_Latency_Test_Strategy strategy{ + Low_Latency_Test_Observer_State(observer, time_source), {60.0, false}}; + { + auto frame = strategy.acquire_painter(); + frame->value = 1; + } + EXPECT_EQ(strategy.pending_frame_count(), 1U); + + std::promise producer_started; + auto producer_started_future = producer_started.get_future(); + auto producer = std::async(std::launch::async, [&] { + producer_started.set_value(); + auto frame = strategy.acquire_painter(); + frame->value = 2; + }); + producer_started_future.wait(); + EXPECT_EQ(producer.wait_for(50ms), std::future_status::timeout); + + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, 1); + } + ASSERT_EQ(producer.wait_for(1s), std::future_status::ready); + producer.get(); + EXPECT_EQ(strategy.pending_frame_count(), 1U); + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, 2); + } + EXPECT_EQ(strategy.counter_statistics().abandoned_frame_count, 0u); +} +TEST(low_latency_strategy_test, replacement_disabled_wakes_blocked_producer_after_explicit_discard) { + using namespace std::chrono_literals; + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + Low_Latency_Test_Strategy strategy{ + Low_Latency_Test_Observer_State(observer, time_source), {60.0, false}}; + { + auto frame = strategy.acquire_painter(); + frame->value = 1; + } + + std::promise producer_started; + auto producer_started_future = producer_started.get_future(); + auto producer = std::async(std::launch::async, [&] { + producer_started.set_value(); + auto frame = strategy.acquire_painter(); + frame->value = 2; + }); + producer_started_future.wait(); + EXPECT_EQ(producer.wait_for(50ms), std::future_status::timeout); + EXPECT_TRUE(strategy.discard_pending_frame()); + ASSERT_EQ(producer.wait_for(1s), std::future_status::ready); + producer.get(); + EXPECT_EQ(strategy.counter_statistics().abandoned_frame_count, 0u); + EXPECT_EQ(strategy.counter_statistics().manually_discarded_frame_count, 1u); +} TEST(low_latency_strategy_test, manually_discards_stale_latest_data_frame) { Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; diff --git a/render_3D/cmake/computer_graphics.cmake b/render_3D/cmake/computer_graphics.cmake index 57df644..0c2f807 100644 --- a/render_3D/cmake/computer_graphics.cmake +++ b/render_3D/cmake/computer_graphics.cmake @@ -201,8 +201,24 @@ block() ) rcl_get_effective_install_dir(render_3D::Vulkan-Hpp VULKAN_HPP_ROOT) rcl_cmake_library_set_cmake_options(render_3D::Vulkan-Hpp ${Vulkan-Hpp_option}) - set(ENV{VULKAN_SDK} "C:/VulkanSDK/1.4.357.0") - find_package(Vulkan REQUIRED) + find_package(Vulkan QUIET) + if (NOT Vulkan_FOUND AND WIN32) + if (DEFINED ENV{VK_SDK_PATH} AND NOT "$ENV{VK_SDK_PATH}" STREQUAL "") + set(ENV{VULKAN_SDK} "$ENV{VK_SDK_PATH}") + else () + file(GLOB _vulkan_sdk_roots LIST_DIRECTORIES true "C:/VulkanSDK/*") + list(SORT _vulkan_sdk_roots COMPARE NATURAL ORDER DESCENDING) + foreach (_vulkan_sdk_root IN LISTS _vulkan_sdk_roots) + if (EXISTS "${_vulkan_sdk_root}/Include/vulkan/vulkan.h" AND EXISTS "${_vulkan_sdk_root}/Lib/vulkan-1.lib") + set(ENV{VULKAN_SDK} "${_vulkan_sdk_root}") + break() + endif () + endforeach () + endif () + find_package(Vulkan REQUIRED) + elseif (NOT Vulkan_FOUND) + find_package(Vulkan REQUIRED) + endif () get_filename_component(VULKAN_SDK_ROOT "${Vulkan_INCLUDE_DIR}" DIRECTORY) set(volk_option ${base_options}) list(APPEND volk_option diff --git a/renderive_package.zip b/renderive_package.zip new file mode 100644 index 0000000..f994ad4 Binary files /dev/null and b/renderive_package.zip differ diff --git a/web_server/CMakeLists.txt b/web_server/CMakeLists.txt index 04a4481..3c16162 100644 --- a/web_server/CMakeLists.txt +++ b/web_server/CMakeLists.txt @@ -123,18 +123,59 @@ if (NOT TARGET Adminive::Nlohmann) set(BUILD_TESTING "${Renderive_Web_saved_build_testing}") unset(Renderive_Web_saved_build_testing) endif () -set(Renderive_Web_source_dir "${CMAKE_CURRENT_LIST_DIR}/app") -append_glob_source(Renderive_Web_sources "${Renderive_Web_source_dir}") +# 公共模块只放 2D/3D 共用的像素协议和 Web 性能基础设施。 +set(Renderive_Web_Common_sources + "${CMAKE_CURRENT_LIST_DIR}/app/common/Pixel_Frame.cpp" + "${CMAKE_CURRENT_LIST_DIR}/app/Web_Performance_Log.cpp") +add_library(Renderive_Web_Common STATIC ${Renderive_Web_Common_sources}) +set_target_properties(Renderive_Web_Common PROPERTIES FOLDER Renderive_Web/common) +target_include_directories(Renderive_Web_Common PUBLIC + "$") +target_compile_features(Renderive_Web_Common PUBLIC cxx_std_20) +target_compile_definitions(Renderive_Web_Common PRIVATE NOMINMAX) +target_link_libraries(Renderive_Web_Common PRIVATE spdlog::spdlog) +# 2D 模块保留现有 Render2D gallery、像素转换和旧 Web_Plot_Session。 +set(Renderive_Web_2D_sources + "${CMAKE_CURRENT_LIST_DIR}/app/Pixel_Frame.cpp" + "${CMAKE_CURRENT_LIST_DIR}/app/Web_Plot_Session.cpp" + "${CMAKE_CURRENT_LIST_DIR}/app/render_2D/Gallery_Scene2D.cpp") +add_library(Renderive_Web_2D STATIC ${Renderive_Web_2D_sources}) +set_target_properties(Renderive_Web_2D PROPERTIES FOLDER Renderive_Web/render_2D) +target_include_directories(Renderive_Web_2D PUBLIC + "$") +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) +# 3D 模块只负责 Scene3D/Datoviz 到公共 RVP1 像素协议的适配。 +set(Renderive_Web_3D_sources + "${CMAKE_CURRENT_LIST_DIR}/app/render_3D/Gallery_Scene3D.cpp") +add_library(Renderive_Web_3D STATIC ${Renderive_Web_3D_sources}) +set_target_properties(Renderive_Web_3D PROPERTIES FOLDER Renderive_Web/render_3D) +target_include_directories(Renderive_Web_3D PUBLIC + "$") +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) +set(Renderive_Web_sources + "${CMAKE_CURRENT_LIST_DIR}/app/Gallery_Plot_Session.cpp" + "${CMAKE_CURRENT_LIST_DIR}/app/Gallery_Protocol.cpp" + "${CMAKE_CURRENT_LIST_DIR}/app/Gallery_WebSocket_Controller.cpp" + "${CMAKE_CURRENT_LIST_DIR}/app/Renderive_WebSocket_Controller.cpp" + "${CMAKE_CURRENT_LIST_DIR}/app/Web_Event_Adapter.cpp" + "${CMAKE_CURRENT_LIST_DIR}/app/Web_Server.cpp") add_library(Renderive_Web STATIC ${Renderive_Web_sources}) +set_target_properties(Renderive_Web PROPERTIES FOLDER Renderive_Web) target_include_directories(Renderive_Web PUBLIC - "$" -) + "$") target_compile_features(Renderive_Web PUBLIC cxx_std_20) target_compile_definitions(Renderive_Web PRIVATE NOMINMAX) target_link_libraries(Renderive_Web - PUBLIC Renderive_render_2D Drogon::Drogon Adminive::MagicEnum magic_enum::magic_enum - PRIVATE Renderive_render_3D Adminive::Nlohmann spdlog::spdlog -) + PUBLIC Renderive_Web_Common Renderive_Web_2D Renderive_Web_3D Drogon::Drogon Adminive::MagicEnum magic_enum::magic_enum + PRIVATE Adminive::Nlohmann spdlog::spdlog) set(Renderive_Web_Server_source_dir "${CMAKE_CURRENT_LIST_DIR}/server") append_glob_source(Renderive_Web_Server_sources "${Renderive_Web_Server_source_dir}") add_executable(Renderive_Web_Server ${Renderive_Web_Server_sources}) @@ -143,6 +184,9 @@ target_link_libraries(Renderive_Web_Server PRIVATE Renderive_Web) renderive_stage_render_3D_runtime(Renderive_Web_Server) #add_dependencies(Renderive_Web_Server Renderive_Web_Assets) if (MSVC) + target_compile_options(Renderive_Web_Common PRIVATE /utf-8) + target_compile_options(Renderive_Web_2D PRIVATE /utf-8) + target_compile_options(Renderive_Web_3D PRIVATE /utf-8) target_compile_options(Renderive_Web PRIVATE /utf-8) target_compile_options(Renderive_Web_Server PRIVATE /utf-8) endif () diff --git a/web_server/app/Gallery_Actions.h b/web_server/app/Gallery_Actions.h index 3461a6e..a1c42ab 100644 --- a/web_server/app/Gallery_Actions.h +++ b/web_server/app/Gallery_Actions.h @@ -1,5 +1,6 @@ #pragma once +#include "common/Gallery_Types.h" #include "renderive/scene/Scene2D_Context.hpp" #include @@ -11,12 +12,6 @@ namespace renderive::web { -enum class Gallery_Frame_Mode : std::uint8_t { - Manual, - Low_Latency, - Playback -}; - struct Gallery_Action_Category {}; struct Gallery_Action_Attribute { diff --git a/web_server/app/Gallery_Observer_Adminive.h b/web_server/app/Gallery_Observer_Adminive.h index 31f6935..fdde95c 100644 --- a/web_server/app/Gallery_Observer_Adminive.h +++ b/web_server/app/Gallery_Observer_Adminive.h @@ -1,6 +1,7 @@ #pragma once #include "Gallery_Enum.h" +#include "common/Gallery_Performance_Types.h" #include "render_2D/scene/Scene.h" #include "render_2D/renderable/Renderable.h" @@ -59,28 +60,6 @@ struct Gallery_Render_Performance { bool automatic_low_latency_scheduler{}; }; -struct Gallery_Client_Performance { - double transport_fps{}; - double presentation_fps{}; - std::uint64_t websocket_buffered_bytes{}; - std::uint64_t changed_pixel_frames{}; - std::uint64_t duplicate_pixel_frames{}; - std::uint64_t frame_request_timeout_count{}; - double frame_round_trip_ms{}; - double frame_round_trip_average_ms{}; - double frame_round_trip_deviation_ms{}; - double frame_round_trip_p95_ms{}; - double frame_round_trip_p99_ms{}; - double display_interval_ms{}; - double display_interval_average_ms{}; - double display_interval_latest_ms{}; - double display_interval_p95_ms{}; - double display_interval_p99_ms{}; - double display_interval_deviation_ms{}; - std::uint64_t overwritten_pixel_frames{}; - double last_pixel_receive_age_ms{}; - double last_pixel_change_age_ms{}; -}; } // namespace renderive::web diff --git a/web_server/app/Gallery_Plot_Session.cpp b/web_server/app/Gallery_Plot_Session.cpp index f0b3430..f6b8b41 100644 --- a/web_server/app/Gallery_Plot_Session.cpp +++ b/web_server/app/Gallery_Plot_Session.cpp @@ -1,1682 +1,127 @@ #include "Gallery_Plot_Session.h" -#include "Gallery_Controls.h" -#include "Gallery_Enum.h" -#include "Gallery_Observer_Adminive.h" #include "Gallery_Protocol.h" -#include "Gallery_Session_Control_Adminive.h" -#include "Pixel_Frame.h" -#include "Web_Performance_Log.h" -#include "render_2D/export.h" -#include "render_3D/Point_Demo.h" -#include +#include "common/Gallery_Scene_Interface.h" +#include "render_2D/Gallery_Scene2D.h" +#include "render_3D/Gallery_Scene3D.h" #include #include -#include #include #include #include #include #include -#include -#include #include #include -#include -#include +#include #include +#include #include #include #include -#include namespace renderive::web { namespace { -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)); - }); +Gallery_View_Input gallery_input(const Event& event) { + Gallery_View_Input input; + switch (event.type) { + case Event_Type::Show: + input.type = Gallery_View_Input_Type::Show; + break; + case Event_Type::Hide: + input.type = Gallery_View_Input_Type::Hide; + break; + case Event_Type::Leave: + input.type = Gallery_View_Input_Type::Leave; + break; + default: + input.type = Gallery_View_Input_Type::Leave; + break; } + return input; } - -std::uint64_t milliseconds_to_ns(double milliseconds) { - return milliseconds > 0.0 ? static_cast(std::llround(milliseconds * 1'000'000.0)) : 0; +Gallery_Mouse_Button gallery_mouse_button(Mouse_Button button) { + switch (button) { + case Mouse_Button::None: + return Gallery_Mouse_Button::None; + case Mouse_Button::Left: + return Gallery_Mouse_Button::Left; + case Mouse_Button::Right: + return Gallery_Mouse_Button::Right; + case Mouse_Button::Middle: + return Gallery_Mouse_Button::Middle; + } + return Gallery_Mouse_Button::None; } -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; +Gallery_Key gallery_key(Key key) { + switch (key) { + case Key::Escape: + return Gallery_Key::Escape; + case Key::Enter: + return Gallery_Key::Enter; + case Key::Space: + return Gallery_Key::Space; + case Key::Delete: + return Gallery_Key::Delete; + case Key::Backspace: + return Gallery_Key::Backspace; + case Key::Left: + return Gallery_Key::Left; + case Key::Right: + return Gallery_Key::Right; + case Key::Up: + return Gallery_Key::Up; + case Key::Down: + return Gallery_Key::Down; + case Key::Unknown: + return Gallery_Key::Unknown; + } + return Gallery_Key::Unknown; } -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; +Gallery_Pointer_Input gallery_input(const Pointer_Event& event) { + Gallery_Pointer_Input input; + if (event.type == Event_Type::Pointer_Press) + input.type = Gallery_Pointer_Input_Type::Press; + else if (event.type == Event_Type::Pointer_Release) + input.type = Gallery_Pointer_Input_Type::Release; + else + input.type = Gallery_Pointer_Input_Type::Move; + input.x = event.position.x; + input.y = event.position.y; + input.global_x = event.global_position.x; + input.global_y = event.global_position.y; + input.button = gallery_mouse_button(event.button); + input.buttons = event.buttons; + input.modifiers = static_cast(event.modifiers); + return input; } -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}; +Gallery_Wheel_Input gallery_input(const Wheel_Event& event) { + Gallery_Wheel_Input input; + input.x = event.position.x; + input.y = event.position.y; + input.global_x = event.global_position.x; + input.global_y = event.global_position.y; + input.button = gallery_mouse_button(event.button); + input.buttons = event.buttons; + input.modifiers = static_cast(event.modifiers); + input.angle_delta_x = event.angle_delta_x; + input.angle_delta_y = event.angle_delta_y; + input.pixel_delta_x = event.pixel_delta_x; + input.pixel_delta_y = event.pixel_delta_y; + return input; } -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}; +Gallery_Key_Input gallery_input(const Key_Event& event) { + Gallery_Key_Input input; + input.type = event.type == Event_Type::Key_Press ? Gallery_Key_Input_Type::Press : Gallery_Key_Input_Type::Release; + input.key = gallery_key(event.key); + input.native_key = event.native_key; + input.modifiers = static_cast(event.modifiers); + input.auto_repeat = event.auto_repeat; + return input; } -double gaussian(double x, double center, double width) { - const double normalized = (x - center) / width; - return std::exp(-0.5 * normalized * normalized); -} -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); -} -} // namespace -class Gallery_Scene_Interface { -public: - virtual ~Gallery_Scene_Interface() = default; - [[nodiscard]] virtual const std::string& case_id() const noexcept = 0; - [[nodiscard]] virtual nlohmann::json controls() const = 0; - [[nodiscard]] virtual Gallery_Frame_Mode frame_mode() const noexcept = 0; - virtual void reset_monitoring() = 0; - [[nodiscard]] virtual bool can_render_automatically() const noexcept = 0; - [[nodiscard]] virtual std::uint64_t kernel_refresh_interval_ns() const = 0; - virtual void set_client_metrics(Gallery_Client_Performance metrics) noexcept = 0; - [[nodiscard]] virtual adminive::Update_Result apply_patch(std::string_view target, const nlohmann::json& patch) = 0; - virtual void resize(Size size) = 0; - virtual void dispatch(const Event& event) = 0; - virtual void dispatch(const Pointer_Event& event) = 0; - virtual void dispatch(const Wheel_Event& event) = 0; - virtual void dispatch(const Key_Event& event) = 0; - virtual bool render_latest_frame() = 0; - [[nodiscard]] virtual std::optional encode_latest_pixels() = 0; - virtual 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) = 0; - [[nodiscard]] virtual std::string action(const Gallery_Action_Request& request, bool& recognized) = 0; - virtual void record_action_notice_if_empty(std::string_view notice) = 0; - [[nodiscard]] virtual std::string telemetry_json() const = 0; -}; -template -class Gallery_Scene 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), 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)); - attach.attach(group); - attach.add_display_parent(group, root_); - attach.add_dependency_parent(group, root_); - 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); - plot_.activate_view(); - update_model(); - (void)plot_.render_frame(true); - } - ~Gallery_Scene() { - plot_.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_; - } - [[nodiscard]] nlohmann::json controls() const { - return { - {"resources", controls_.resources()}, - {"observers", controls_.observers(plot_)}, - {"render_plan", controls_.render_plan(plot_)}, - {"performance_capture", gallery_performance_capture_json(plot_)} - }; - } - [[nodiscard]] Gallery_Frame_Mode frame_mode() const noexcept { - return frame_mode_; - } - 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(); - } - [[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_.notify_model_dirty(); - return result; - } - void resize(Size size) { - const Size previous = plot_.viewport_size(); - plot_.set_viewport_size(size); - apply_layout(); - Resize_Event event; - event.old_size = previous; - event.new_size = size; - plot_.dispatch_event(event); - } - void dispatch(const Event& event) { - if (event.type == Event_Type::Show) - plot_.activate_view(); - else if (event.type == Event_Type::Hide) - plot_.deactivate_view(); - plot_.dispatch_event(event); - } - void dispatch(const Pointer_Event& event) { - plot_.dispatch_event(event); - } - void dispatch(const Wheel_Event& event) { - plot_.dispatch_event(event); - } - void dispatch(const Key_Event& event) { - plot_.dispatch_event(event); - } - bool render_latest_frame() { - if (!plot_.view_active()) - return false; - update_model(); - const auto started = std::chrono::steady_clock::now(); - const bool rendered = plot_.render_frame(true); - record_performance(started, rendered); - if (rendered) - rendered_since_last_pixel_ = true; - return rendered; - } - [[nodiscard]] std::optional encode_latest_pixels() { - if (!plot_.view_active()) - return std::nullopt; - if (!rendered_since_last_pixel_ && !can_render_automatically() && - !render_latest_frame()) - return std::nullopt; - rendered_since_last_pixel_ = false; - std::string pixels; - const Color background = plot_.background_color(); - plot_.with_frame([&pixels, background](Image_View image) { - pixels = encode_pixel_frame(image, background); - }); - return pixels.empty() ? std::nullopt : std::optional(std::move(pixels)); - } - 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(); - recognized = true; - if (request.id == "capture_next_frame") { - const auto state = plot_.capture_state(); - if (state.enabled()) - return "A performance capture session is already active"; - const auto session_id = plot_.capture_next_frame(); - last_action_result_ = "capture session=" + std::to_string(session_id); - return "Capture next render frame requested"; - } - 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_.capture_state(); - if (state.enabled()) - return "A performance capture session is already active"; - const auto session_id = plot_.capture_frames(count); - last_action_result_ = "capture session=" + std::to_string(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(); - 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() ? 1 : 0; - } - last_action_result_ = "enqueued=" + std::to_string(prepared); - return "回放帧已批量压入 Flow 队列"; - } - if (request.id == "mode_refresh") { - const bool refreshed = plot_.refresh_manual_frame(); - 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(); - 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 bool rendered = plot_.render_frame(true); - 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 已按轴拟合正方形"; - } - } - 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_observer_snapshot(); - 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_; - 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)}, - {"renderable_observers", controls_.observers(plot_)}, - {"performance_capture", gallery_performance_capture_json(plot_)}, - {"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(); - } -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(); - } - 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) { - plot_.clear_consumer_feedback(); - consumer_feedback_source_ = "disabled"; - return; - } - 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) { - plot_.clear_consumer_feedback(); - return; - } - plot_.set_consumer_feedback({interval_ns}); - } - 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_observer_snapshot(); - 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_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_Base::Attach_Builder& attach) { - if (case_id_ == "axis_lab") { - primary_ = renderive_static_owner_cast(frequency_domain_axis_); - return; - } - if (case_id_ == "spectrum" || case_id_ == "selection_overlay") { - 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_; - if (case_id_ == "selection_overlay") { - 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_; - } - return; - } - if (case_id_ == "waterfall") { - 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") { - 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") { - 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") { - 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") { - 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_; - } - } - 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) { - 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)); - } - 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; - } - 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 - }); - } - } - 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_{}; - Scene_Type 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_; -}; - -render_3d::Frame_Mode point_frame_mode(Gallery_Frame_Mode mode) { - switch (mode) { - case Gallery_Frame_Mode::Manual: - return render_3d::Frame_Mode::Manual; - case Gallery_Frame_Mode::Low_Latency: - return render_3d::Frame_Mode::Low_Latency; - case Gallery_Frame_Mode::Playback: - return render_3d::Frame_Mode::Playback; - } - throw std::invalid_argument("unknown 3D frame mode"); -} - -class Point_Gallery_Scene final : public Gallery_Scene_Interface { -public: - Point_Gallery_Scene(Gallery_Frame_Mode mode, bool automatic_low_latency) - : mode_(mode), automatic_low_latency_(automatic_low_latency), - performance_started_(std::chrono::steady_clock::now()), - demo_(render_3d::make_point_demo({ - {560, 320}, {}, point_frame_mode(mode), 60.0, 0, false})) { - if (mode_ == Gallery_Frame_Mode::Playback) { - for (int index = 0; index < 24; ++index) { - phase_ = -1.20F + static_cast(index) * 0.05F; - publish_demo_points(); - (void)demo_.scene->prepare_frame(); - } - (void)record_render([this] { return demo_.scene->render_prepared_frame(); }); - } else { - (void)record_render([this] { return demo_.scene->request_frame(); }); - } - } - - [[nodiscard]] const std::string& case_id() const noexcept override { - return case_id_; - } - [[nodiscard]] nlohmann::json controls() const override { - return {{"resources", nlohmann::json::array()}, - {"observers", nlohmann::json::array()}, - {"render_plan", nullptr}, - {"performance_capture", nullptr}}; - } - [[nodiscard]] Gallery_Frame_Mode frame_mode() const noexcept override { - return mode_; - } - void reset_monitoring() override { - performance_started_ = std::chrono::steady_clock::now(); - render_attempt_count_ = 0; - successful_render_count_ = 0; - render_duration_statistics_.clear(); - pixel_encode_statistics_.clear(); - pixel_request_statistics_.clear(); - last_render_ms_ = 0.0; - last_pixel_encode_ms_ = 0.0; - last_pixel_request_ms_ = 0.0; - last_pixel_bytes_ = 0; - client_performance_ = {}; - } - [[nodiscard]] bool can_render_automatically() const noexcept override { - return active_ && automatic_low_latency_ && - mode_ == Gallery_Frame_Mode::Low_Latency; - } - [[nodiscard]] std::uint64_t kernel_refresh_interval_ns() const override { - const auto interval = demo_.scene->frame_status().next_refresh_interval_ns; - return interval == 0 ? 16'666'667U : interval; - } - void set_client_metrics(Gallery_Client_Performance metrics) noexcept override { - client_performance_ = metrics; - } - [[nodiscard]] adminive::Update_Result apply_patch( - std::string_view target, const nlohmann::json&) override { - adminive::Update_Result result; - result.message = "point_3d has no patchable control: " + std::string(target); - return result; - } - void resize(Size size) override { - if (size.width <= 0 || size.height <= 0) - return; - demo_.scene->resize({static_cast(size.width), - static_cast(size.height)}); - rendered_since_last_pixel_ = false; - } - void dispatch(const Event& event) override { - if (event.type == Event_Type::Show) - active_ = true; - else if (event.type == Event_Type::Hide) - active_ = false; - demo_.scene->dispatch(event); - } - void dispatch(const Pointer_Event& event) override { - if (active_) - demo_.scene->dispatch(event); - } - void dispatch(const Wheel_Event& event) override { - if (active_) - demo_.scene->dispatch(event); - } - void dispatch(const Key_Event& event) override { - if (active_) - demo_.scene->dispatch(event); - } - bool render_latest_frame() override { - if (!active_) - return false; - if (can_render_automatically()) { - phase_ += 0.045F; - publish_demo_points(); - } - return record_render([this] { return demo_.scene->request_frame(); }); - } - [[nodiscard]] std::optional encode_latest_pixels() override { - if (!active_) - return std::nullopt; - if (!rendered_since_last_pixel_ && !can_render_automatically() && - !render_latest_frame()) - return std::nullopt; - const auto frame = demo_.scene->latest_frame(); - if (!frame) - return std::nullopt; - rendered_since_last_pixel_ = false; - auto encoded = encode_rgba8_pixel_frame( - frame->rgba8.data(), frame->extent.width, frame->extent.height, - static_cast(frame->extent.width) * 4U); - return encoded.empty() ? std::nullopt - : std::optional(std::move(encoded)); - } - 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) override { - last_pixel_encode_ms_ = std::chrono::duration( - encode_finished - encode_started) - .count(); - last_pixel_request_ms_ = std::chrono::duration( - encode_finished - request_started) - .count(); - last_pixel_bytes_ = pixel_bytes; - pixel_encode_statistics_.add(last_pixel_encode_ms_); - pixel_request_statistics_.add(last_pixel_request_ms_); - } - [[nodiscard]] std::string action(const Gallery_Action_Request& request, - bool& recognized) override { - recognized = true; - if (request.id == "mode_prepare") { - const bool prepared = demo_.scene->prepare_frame(); - last_action_result_ = prepared ? "prepared" : "prepare_rejected"; - return prepared ? "Point frame prepared" : "Point frame prepare rejected"; - } - if (request.id == "mode_refresh") { - const bool refreshed = demo_.scene->refresh_manual_frame(); - last_action_result_ = refreshed ? "refresh_succeeded" : "refresh_failed"; - return refreshed ? "Point manual frame refreshed" : "No manual frame to refresh"; - } - if (request.id == "mode_render" || request.id == "mode_dequeue") { - const bool rendered = record_render( - [this] { return demo_.scene->render_prepared_frame(); }); - last_action_result_ = rendered ? "rendered" : "render_queue_empty"; - return rendered ? "Point frame rendered" : "No Point frame available"; - } - if (request.id == "mode_discard") { - const bool discarded = demo_.scene->discard_pending_frame(); - last_action_result_ = discarded ? "manually_discarded" : "discard_failed"; - return discarded ? "Pending Point frame discarded" : "No Point frame to discard"; - } - if (request.id == "mode_enqueue") { - phase_ += 0.05F; - publish_demo_points(); - const bool enqueued = demo_.scene->prepare_frame(); - last_action_result_ = enqueued ? "enqueued" : "enqueue_failed"; - return enqueued ? "Point playback frame enqueued" : "Point enqueue failed"; - } - if (request.id == "orbit_points") { - phase_ += 0.25F; - publish_demo_points(); - last_action_result_ = "points_orbited"; - rendered_since_last_pixel_ = false; - return "Point positions updated through Point_Visual double buffering"; - } - if (request.id == "add_point") { - demo_.visual->edit_points([](auto& points) { - points.push_back( - {{0.12F, -0.72F, 0.58F}, {55, 235, 230, 255}, 46.0F}); - }); - last_action_result_ = "point_added"; - rendered_since_last_pixel_ = false; - return "Point added to the bulk payload"; - } - if (request.id == "remove_point") { - bool removed{}; - demo_.visual->edit_points([&](auto& points) { - if (points.size() > render_3d::point_demo_data().size()) { - points.pop_back(); - removed = true; - } - }); - if (removed) { - last_action_result_ = "point_removed"; - rendered_since_last_pixel_ = false; - return "Last added Point removed"; - } - last_action_result_ = "no_added_point"; - return "No added Point to remove"; - } - if (request.id == "point_churn") { - demo_.visual->edit_points([](auto& points) { - points.push_back( - {{0.0F, 0.0F, 0.8F}, {255, 255, 255, 255}, 72.0F}); - points.pop_back(); - }); - last_action_result_ = "point_churned"; - rendered_since_last_pixel_ = false; - return "Transient Point added and removed before the next frame snapshot"; - } - if (request.id == "reset_points") { - phase_ = 0.0F; - publish_demo_points(); - last_action_result_ = "points_reset"; - rendered_since_last_pixel_ = false; - return "Point demo payload reset"; - } - recognized = false; - return {}; - } - void record_action_notice_if_empty(std::string_view notice) override { - if (last_action_result_.empty()) - last_action_result_ = notice; - } - [[nodiscard]] std::string telemetry_json() const override { - const auto status = demo_.scene->frame_status(); - const auto frame = demo_.scene->latest_frame(); - const auto point_count = demo_.visual->point_count(); - const auto render_window = render_duration_statistics_.snapshot(); - const auto encode_window = pixel_encode_statistics_.snapshot(); - const auto request_window = pixel_request_statistics_.snapshot(); - const double elapsed = std::max( - std::chrono::duration(std::chrono::steady_clock::now() - - performance_started_) - .count(), - 1e-9); - Gallery_Render_Performance performance; - performance.render_attempt_count = render_attempt_count_; - performance.successful_render_count = successful_render_count_; - performance.failed_render_count = render_attempt_count_ - successful_render_count_; - performance.lifetime_average_fps = successful_render_count_ / elapsed; - performance.last_render_ms = last_render_ms_; - performance.average_render_ms = render_window.average; - performance.maximum_render_ms = render_window.maximum; - performance.render_deviation_ms = render_window.deviation; - performance.render_p50_ms = render_window.p50; - performance.render_p95_ms = render_window.p95; - performance.render_p99_ms = render_window.p99; - performance.render_sample_count = render_window.sample_count; - performance.last_pixel_encode_ms = last_pixel_encode_ms_; - performance.average_pixel_encode_ms = encode_window.average; - performance.maximum_pixel_encode_ms = encode_window.maximum; - performance.pixel_encode_deviation_ms = encode_window.deviation; - performance.pixel_encode_p50_ms = encode_window.p50; - performance.pixel_encode_p95_ms = encode_window.p95; - performance.pixel_encode_p99_ms = encode_window.p99; - performance.pixel_encode_sample_count = encode_window.sample_count; - performance.last_pixel_request_ms = last_pixel_request_ms_; - performance.average_pixel_request_ms = request_window.average; - performance.pixel_request_deviation_ms = request_window.deviation; - performance.pixel_request_p95_ms = request_window.p95; - performance.pixel_request_p99_ms = request_window.p99; - performance.last_pixel_bytes = last_pixel_bytes_; - performance.automatic_low_latency_scheduler = can_render_automatically(); - - const auto extent = frame ? frame->extent : render_3d::Extent{560, 320}; - nlohmann::json telemetry{ - {"case", case_id_}, - {"frame_mode", gallery_enum_id(mode_)}, - {"frame_index", status.latest_sequence}, - {"viewport", {{"width", extent.width}, {"height", extent.height}}}, - {"view_active", active_}, - {"kernel_frame_count", status.consumed_frame_count}, - {"performance", adminive::to_frontend_json(performance)}, - {"kernel_observer", - {{"mode", gallery_enum_id(mode_)}, - {"last_event", last_action_result_.empty() - ? (frame ? "rendered" : "none") - : last_action_result_}, - {"frequency_hz", status.frequency_hz}, - {"produced_frame_count", status.produced_frame_count}, - {"consumed_frame_count", status.consumed_frame_count}, - {"dropped_frame_count", status.dropped_frame_count}, - {"failed_operation_count", status.failed_operation_count}, - {"pending_frame_count", status.pending_frame_count}, - {"latest_sequence", status.latest_sequence}, - {"next_refresh_interval_ns", status.next_refresh_interval_ns}}}, - {"consumer_feedback", nlohmann::json::object()}, - {"client_performance", - adminive::to_frontend_json(client_performance_)}, - {"renderable_observers", nlohmann::json::array()}, - {"performance_capture", nullptr}, - {"last_action_result", last_action_result_}, - {"point_3d", {{"point_count", point_count}, - {"data_revision", demo_.visual->data_revision()}}}, - {"data_shape", {{"input_elements", point_count}, - {"rendered_elements", point_count}}}} - ; - return telemetry.dump(); - } - -private: - void publish_demo_points() { - demo_.visual->update_points(render_3d::point_demo_data(phase_)); - } - template - bool record_render(Render&& render) { - ++render_attempt_count_; - const auto started = std::chrono::steady_clock::now(); - const bool rendered = std::invoke(std::forward(render)); - last_render_ms_ = std::chrono::duration( - std::chrono::steady_clock::now() - started) - .count(); - render_duration_statistics_.add(last_render_ms_); - successful_render_count_ += rendered ? 1U : 0U; - rendered_since_last_pixel_ = rendered; - return rendered; - } - - std::string case_id_{"point_3d"}; - Gallery_Frame_Mode mode_; - bool automatic_low_latency_{}; - bool active_{true}; - std::chrono::steady_clock::time_point performance_started_; - render_3d::Point_Demo demo_; - float phase_{}; - std::uint64_t render_attempt_count_{}; - std::uint64_t successful_render_count_{}; - double last_render_ms_{}; - Rolling_Statistics render_duration_statistics_{256}; - double last_pixel_encode_ms_{}; - double last_pixel_request_ms_{}; - std::size_t last_pixel_bytes_{}; - Rolling_Statistics pixel_encode_statistics_{256}; - Rolling_Statistics pixel_request_statistics_{256}; - Gallery_Client_Performance client_performance_; - bool rendered_since_last_pixel_{}; - std::string last_action_result_; -}; - std::unique_ptr make_gallery_scene(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode, bool automatic_low_latency) { - if (case_id == "point_3d") - return std::make_unique(mode, automatic_low_latency); - switch (mode) { - case Gallery_Frame_Mode::Manual: - return std::make_unique>(session_id, std::move(case_id), mode, automatic_low_latency); - case Gallery_Frame_Mode::Low_Latency: - return std::make_unique>(session_id, std::move(case_id), mode, automatic_low_latency); - case Gallery_Frame_Mode::Playback: - return std::make_unique>(session_id, std::move(case_id), mode, automatic_low_latency); - } - throw std::invalid_argument("unknown gallery frame mode"); + if (case_id == "datoviz_3d") + return make_gallery_scene_3d(session_id, std::move(case_id), mode, automatic_low_latency); + return make_gallery_scene_2d(session_id, std::move(case_id), mode, automatic_low_latency); +} } struct Gallery_Plot_Session::Impl { explicit Impl(bool enable_automatic_low_latency) @@ -1858,7 +303,7 @@ struct Gallery_Plot_Session::Impl { Web_Response_Type::Json, Gallery_Protocol::case_json_from_controls(scene->case_id(), scene->controls().dump(), scene->telemetry_json(), - "render_2D/Kernel 独立画布已创建", + "Renderive/Kernel 独立画布已创建", scene->frame_mode(), false) }; } @@ -1996,15 +441,15 @@ struct Gallery_Plot_Session::Impl { } else if constexpr (std::is_same_v) { if (scene) - scene->resize(value.size); + scene->resize(value.size.width, value.size.height); } else if constexpr (std::is_same_v) { if (scene) - scene->dispatch(value); + scene->dispatch(Gallery_Input_Event{gallery_input(value)}); } else if constexpr (Event_Object) { if (scene) - scene->dispatch(value); + scene->dispatch(Gallery_Input_Event{gallery_input(value)}); } return std::nullopt; }, diff --git a/web_server/app/Gallery_Protocol.cpp b/web_server/app/Gallery_Protocol.cpp index f1ef91f..7a0afa0 100644 --- a/web_server/app/Gallery_Protocol.cpp +++ b/web_server/app/Gallery_Protocol.cpp @@ -1,4 +1,5 @@ #include "Gallery_Protocol.h" +#include "Gallery_Actions.h" #include "Gallery_Enum.h" #include "Gallery_Observer_Adminive.h" #include "Gallery_Renderables.h" @@ -52,9 +53,9 @@ const std::vector& cases() { {"selection_overlay", "框选叠层", "Selection_Rectangle_Overlay", "交互控件", "鼠标框选、多区域保留、标注样式和清空。", 70}, {"constellation", "星座图", "Constellation_Diagram", "点图控件", - "PSK4/PSK8/PSK16 模式、相位、寿命、坐标范围和方形拟合。", 80} - ,{"point_3d", "Datoviz 3D 点组件", "Point Visual", "3D", - "Kernel 状态双缓冲、实时批量点数据、单线程 Datoviz 渲染与 RGBA 回读。", 90} + "PSK4/PSK8/PSK16 模式、相位、寿命、坐标范围和方形拟合。", 80}, + {"datoviz_3d", "Datoviz 3D", "Mesh / Texture / MSDF Text", "3D", + "Render3D 通过 Kernel 帧策略驱动迁移后的 Datoviz Vulkan 后端;3D 的已发布帧禁止自动覆盖。", 90} }; return value; } @@ -81,7 +82,9 @@ void append_session_actions(Gallery_Frame_Mode frame_mode, } throw std::invalid_argument("unknown gallery frame mode"); } -std::size_t session_control_count(Gallery_Frame_Mode frame_mode) { +std::size_t session_control_count(std::string_view case_id, Gallery_Frame_Mode frame_mode) { + if (case_id == "datoviz_3d") + return 0; switch (frame_mode) { case Gallery_Frame_Mode::Manual: return gallery_control_count>(); @@ -92,40 +95,39 @@ std::size_t session_control_count(Gallery_Frame_Mode frame_mode) { } throw std::invalid_argument("unknown gallery frame mode"); } -std::vector registered_actions(std::string_view case_id, - Gallery_Frame_Mode 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 == "point_3d") { - const auto keep_frame_action = [frame_mode](std::string_view id) { - switch (frame_mode) { - case Gallery_Frame_Mode::Manual: - return id == "mode_prepare" || id == "mode_refresh" || - id == "mode_render" || id == "mode_discard"; - case Gallery_Frame_Mode::Low_Latency: - return id == "mode_prepare" || id == "mode_render" || - id == "mode_discard"; - case Gallery_Frame_Mode::Playback: - return id == "mode_enqueue" || id == "mode_dequeue"; - } - return false; - }; - for (auto& action : gallery_frame_actions(frame_mode)) { - if (keep_frame_action(action.id)) - result.push_back(std::move(action)); - } - const auto add = [&result](std::string id, std::string label, - std::string api, std::string description) { - result.push_back({std::move(id), std::move(label), std::move(api), - std::move(description), "Point Visual", {}, {}, 0.0, - true}); - }; - add("orbit_points", "移动点", "Point_Visual::update_points", - "通过 Kernel Multi_Double_Buffer_Strategy 发布一批新位置"); - add("add_point", "添加点", "Point_Visual::edit_points", "向批量点数据追加一个点"); - add("remove_point", "移除点", "Point_Visual::edit_points", "移除最后追加的点"); - add("point_churn", "点增删压力", "Point_Visual::edit_points", - "在帧快照前连续追加并移除临时点"); - add("reset_points", "重置点", "Point_Visual::update_points", "恢复 3D 点演示数据"); + if (case_id == "datoviz_3d") { + append_3d_actions(frame_mode, result); return result; } append_session_actions(frame_mode, result); @@ -151,7 +153,7 @@ struct Type_Descriptor { return object("renderive_gallery_case", ADMINIVE_FIELD_LABEL(T, id, "标识"), ADMINIVE_FIELD_LABEL(T, title, "标题"), - ADMINIVE_FIELD_LABEL(T, component, "Core2 控件"), + ADMINIVE_FIELD_LABEL(T, component, "Renderive 组件"), ADMINIVE_FIELD_LABEL(T, category, "分类"), ADMINIVE_FIELD_LABEL(T, description, "说明"), ADMINIVE_FIELD_LABEL(T, order, "顺序"), @@ -168,7 +170,7 @@ struct Type_Descriptor { return object("renderive_gallery_action", ADMINIVE_FIELD_LABEL(T, id, "动作"), ADMINIVE_FIELD_LABEL(T, label, "名称"), - ADMINIVE_FIELD_LABEL(T, api, "Core2 API"), + ADMINIVE_FIELD_LABEL(T, api, "Renderive API"), ADMINIVE_FIELD_LABEL(T, description, "说明"), ADMINIVE_FIELD_LABEL(T, group, "分组"), ADMINIVE_FIELD_LABEL(T, argument_input, "参数类型"), @@ -188,7 +190,7 @@ adminive::Table_View action_view() { return adminive::table_view( adminive::column<&T::label>("动作"), adminive::column<&T::group>("分组"), - adminive::column<&T::api>("Core2 API")) + adminive::column<&T::api>("Renderive API")) .titled("保留 API 动作"); } @@ -552,12 +554,11 @@ std::string Gallery_Protocol::catalog_json() { 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 = gallery_renderable_control_count(item.id); + const std::size_t renderable_controls = item.id == "datoviz_3d" ? 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 = item.id == "point_3d" - ? 0U - : gallery_detail::session_control_count(mode) + renderable_controls; + 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; @@ -565,10 +566,9 @@ std::string Gallery_Protocol::catalog_json() { controls_total += controls; actions_total += actions; } - entry["control_count"] = item.id == "point_3d" - ? 0U - : gallery_detail::session_control_count(Gallery_Frame_Mode::Low_Latency) + - renderable_controls; + 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(); diff --git a/web_server/app/Gallery_Protocol.h b/web_server/app/Gallery_Protocol.h index 47da90f..9d0bec7 100644 --- a/web_server/app/Gallery_Protocol.h +++ b/web_server/app/Gallery_Protocol.h @@ -1,5 +1,5 @@ #pragma once -#include "Gallery_Actions.h" +#include "common/Gallery_Types.h" #include #include @@ -10,11 +10,6 @@ struct Gallery_Open_Request { std::string case_id; Gallery_Frame_Mode frame_mode = Gallery_Frame_Mode::Low_Latency; }; -using Gallery_Value = std::variant; -struct Gallery_Action_Request { - std::string id; - std::optional argument; -}; struct Gallery_Control_Patch_Request { std::string target; std::string patch_json; diff --git a/web_server/app/Pixel_Frame.h b/web_server/app/Pixel_Frame.h index e4723f5..a9932b9 100644 --- a/web_server/app/Pixel_Frame.h +++ b/web_server/app/Pixel_Frame.h @@ -1,10 +1,10 @@ #pragma once +#include "common/Pixel_Frame.h" #include "render_2D/base/Types.h" #include #include #include namespace renderive::web { -inline constexpr std::size_t pixel_frame_header_size = 16; [[nodiscard]] std::string encode_pixel_frame(Image_View image, Color background = Color::black()); [[nodiscard]] std::string encode_rgba8_pixel_frame(const std::byte* pixels, diff --git a/web_server/app/common/Gallery_Input.h b/web_server/app/common/Gallery_Input.h new file mode 100644 index 0000000..6ea56c4 --- /dev/null +++ b/web_server/app/common/Gallery_Input.h @@ -0,0 +1,71 @@ +#pragma once +#include +#include +namespace renderive::web { +enum class Gallery_View_Input_Type : std::uint8_t { + Show, + Hide, + Leave +}; +struct Gallery_View_Input { + Gallery_View_Input_Type type{}; +}; +enum class Gallery_Pointer_Input_Type : std::uint8_t { + Move, + Press, + Release +}; +enum class Gallery_Mouse_Button : std::uint8_t { + None, + Left, + Right, + Middle +}; +struct Gallery_Pointer_Input { + Gallery_Pointer_Input_Type type{}; + double x{}; + double y{}; + double global_x{}; + double global_y{}; + Gallery_Mouse_Button button{}; + std::uint8_t buttons{}; + std::uint8_t modifiers{}; +}; +struct Gallery_Wheel_Input { + double x{}; + double y{}; + double global_x{}; + double global_y{}; + Gallery_Mouse_Button button{}; + std::uint8_t buttons{}; + std::uint8_t modifiers{}; + double angle_delta_x{}; + double angle_delta_y{}; + double pixel_delta_x{}; + double pixel_delta_y{}; +}; +enum class Gallery_Key_Input_Type : std::uint8_t { + Press, + Release +}; +enum class Gallery_Key : std::uint16_t { + Unknown, + Escape, + Enter, + Space, + Delete, + Backspace, + Left, + Right, + Up, + Down +}; +struct Gallery_Key_Input { + Gallery_Key_Input_Type type{}; + Gallery_Key key{}; + std::uint32_t native_key{}; + std::uint8_t modifiers{}; + bool auto_repeat{}; +}; +using Gallery_Input_Event = std::variant; +} diff --git a/web_server/app/common/Gallery_Performance_Types.h b/web_server/app/common/Gallery_Performance_Types.h new file mode 100644 index 0000000..8df95e5 --- /dev/null +++ b/web_server/app/common/Gallery_Performance_Types.h @@ -0,0 +1,26 @@ +#pragma once +#include +namespace renderive::web { +struct Gallery_Client_Performance { + double transport_fps{}; + double presentation_fps{}; + std::uint64_t websocket_buffered_bytes{}; + std::uint64_t changed_pixel_frames{}; + std::uint64_t duplicate_pixel_frames{}; + std::uint64_t frame_request_timeout_count{}; + double frame_round_trip_ms{}; + double frame_round_trip_average_ms{}; + double frame_round_trip_deviation_ms{}; + double frame_round_trip_p95_ms{}; + double frame_round_trip_p99_ms{}; + double display_interval_ms{}; + double display_interval_average_ms{}; + double display_interval_latest_ms{}; + double display_interval_p95_ms{}; + double display_interval_p99_ms{}; + double display_interval_deviation_ms{}; + std::uint64_t overwritten_pixel_frames{}; + double last_pixel_receive_age_ms{}; + double last_pixel_change_age_ms{}; +}; +} diff --git a/web_server/app/common/Gallery_Scene_Interface.h b/web_server/app/common/Gallery_Scene_Interface.h new file mode 100644 index 0000000..a313cc1 --- /dev/null +++ b/web_server/app/common/Gallery_Scene_Interface.h @@ -0,0 +1,34 @@ +#pragma once +#include "Gallery_Input.h" +#include "Gallery_Performance_Types.h" +#include "Gallery_Types.h" +#include "adminive/adminive.hpp" +#include +#include +#include +#include +#include +#include +#include +namespace renderive::web { +class Gallery_Scene_Interface { +public: + virtual ~Gallery_Scene_Interface() = default; + [[nodiscard]] virtual const std::string& case_id() const noexcept = 0; + [[nodiscard]] virtual nlohmann::json controls() const = 0; + [[nodiscard]] virtual Gallery_Frame_Mode frame_mode() const noexcept = 0; + virtual void reset_monitoring() = 0; + [[nodiscard]] virtual bool can_render_automatically() const noexcept = 0; + [[nodiscard]] virtual std::uint64_t kernel_refresh_interval_ns() const = 0; + virtual void set_client_metrics(Gallery_Client_Performance metrics) noexcept = 0; + [[nodiscard]] virtual adminive::Update_Result apply_patch(std::string_view target, const nlohmann::json& patch) = 0; + virtual void resize(int width, int height) = 0; + virtual void dispatch(const Gallery_Input_Event& event) = 0; + virtual bool render_latest_frame() = 0; + [[nodiscard]] virtual std::optional encode_latest_pixels() = 0; + virtual 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) = 0; + [[nodiscard]] virtual std::string action(const Gallery_Action_Request& request, bool& recognized) = 0; + virtual void record_action_notice_if_empty(std::string_view notice) = 0; + [[nodiscard]] virtual std::string telemetry_json() const = 0; +}; +} diff --git a/web_server/app/common/Gallery_Types.h b/web_server/app/common/Gallery_Types.h new file mode 100644 index 0000000..09623ef --- /dev/null +++ b/web_server/app/common/Gallery_Types.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include +#include +namespace renderive::web { +enum class Gallery_Frame_Mode : std::uint8_t { + Manual, + Low_Latency, + Playback +}; +using Gallery_Value = std::variant; +struct Gallery_Action_Request { + std::string id; + std::optional argument; +}; +} diff --git a/web_server/app/common/Pixel_Frame.cpp b/web_server/app/common/Pixel_Frame.cpp new file mode 100644 index 0000000..323f3f3 --- /dev/null +++ b/web_server/app/common/Pixel_Frame.cpp @@ -0,0 +1,35 @@ +#include "Pixel_Frame.h" +#include +#include +namespace renderive::web { +namespace { +void write_u32_le(char* target, std::uint32_t value) { + target[0] = static_cast(value & 0xffU); + target[1] = static_cast((value >> 8U) & 0xffU); + target[2] = static_cast((value >> 16U) & 0xffU); + target[3] = static_cast((value >> 24U) & 0xffU); +} +} +std::string encode_rgba8_pixel_frame(const std::byte* data, std::uint32_t width, std::uint32_t height, std::uint32_t stride) { + if (data == nullptr || width == 0 || height == 0 || stride < width * 4U) + return {}; + const std::size_t row_size = static_cast(width) * 4U; + if (static_cast(height) > (std::numeric_limits::max() - pixel_frame_header_size) / row_size) + return {}; + const std::size_t pixel_bytes = row_size * height; + std::string frame(pixel_frame_header_size + pixel_bytes, '\0'); + frame[0] = 'R'; + frame[1] = 'V'; + frame[2] = 'P'; + frame[3] = '1'; + write_u32_le(frame.data() + 4, width); + write_u32_le(frame.data() + 8, height); + write_u32_le(frame.data() + 12, static_cast(row_size)); + char* output = frame.data() + pixel_frame_header_size; + for (std::uint32_t y = 0; y < height; ++y) { + const auto* row = data + static_cast(y) * stride; + std::memcpy(output + static_cast(y) * row_size, row, row_size); + } + return frame; +} +} diff --git a/web_server/app/common/Pixel_Frame.h b/web_server/app/common/Pixel_Frame.h new file mode 100644 index 0000000..6f85e1c --- /dev/null +++ b/web_server/app/common/Pixel_Frame.h @@ -0,0 +1,8 @@ +#pragma once +#include +#include +#include +namespace renderive::web { +inline constexpr std::size_t pixel_frame_header_size = 16; +[[nodiscard]] std::string encode_rgba8_pixel_frame(const std::byte* data, std::uint32_t width, std::uint32_t height, std::uint32_t stride); +} diff --git a/web_server/app/render_2D/Gallery_Scene2D.cpp b/web_server/app/render_2D/Gallery_Scene2D.cpp new file mode 100644 index 0000000..db7da70 --- /dev/null +++ b/web_server/app/render_2D/Gallery_Scene2D.cpp @@ -0,0 +1,1402 @@ +#include "Gallery_Scene2D.h" +#include "../Gallery_Controls.h" +#include "../Gallery_Enum.h" +#include "../Gallery_Observer_Adminive.h" +#include "../Gallery_Session_Control_Adminive.h" +#include "../Pixel_Frame.h" +#include "../Web_Performance_Log.h" +#include "render_2D/export.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace renderive::web { +namespace { +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::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}; +} +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); +} + +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); +} +} // namespace +template +class Gallery_Scene 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), 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)); + attach.attach(group); + attach.add_display_parent(group, root_); + attach.add_dependency_parent(group, root_); + 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); + plot_.activate_view(); + update_model(); + (void)plot_.render_frame(true); + } + ~Gallery_Scene() { + plot_.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_; + } + [[nodiscard]] nlohmann::json controls() const { + return { + {"resources", controls_.resources()}, + {"observers", controls_.observers(plot_)}, + {"render_plan", controls_.render_plan(plot_)}, + {"performance_capture", gallery_performance_capture_json(plot_)} + }; + } + [[nodiscard]] Gallery_Frame_Mode frame_mode() const noexcept { + return frame_mode_; + } + 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(); + } + [[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_.notify_model_dirty(); + return result; + } + 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); + } + 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); + } + }, event); + } + bool render_latest_frame() { + if (!plot_.view_active()) + return false; + update_model(); + const auto started = std::chrono::steady_clock::now(); + const bool rendered = plot_.render_frame(true); + record_performance(started, rendered); + if (rendered) + rendered_since_last_pixel_ = true; + return rendered; + } + [[nodiscard]] std::optional encode_latest_pixels() { + if (!plot_.view_active()) + return std::nullopt; + if (!rendered_since_last_pixel_ && !can_render_automatically() && + !render_latest_frame()) + return std::nullopt; + rendered_since_last_pixel_ = false; + std::string pixels; + const Color background = plot_.background_color(); + plot_.with_frame([&pixels, background](Image_View image) { + pixels = encode_pixel_frame(image, background); + }); + return pixels.empty() ? std::nullopt : std::optional(std::move(pixels)); + } + 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(); + recognized = true; + if (request.id == "capture_next_frame") { + const auto state = plot_.capture_state(); + if (state.enabled()) + return "A performance capture session is already active"; + const auto session_id = plot_.capture_next_frame(); + last_action_result_ = "capture session=" + std::to_string(session_id); + return "Capture next render frame requested"; + } + 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_.capture_state(); + if (state.enabled()) + return "A performance capture session is already active"; + const auto session_id = plot_.capture_frames(count); + last_action_result_ = "capture session=" + std::to_string(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(); + 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() ? 1 : 0; + } + last_action_result_ = "enqueued=" + std::to_string(prepared); + return "回放帧已批量压入 Flow 队列"; + } + if (request.id == "mode_refresh") { + const bool refreshed = plot_.refresh_manual_frame(); + 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(); + 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 bool rendered = plot_.render_frame(true); + 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 已按轴拟合正方形"; + } + } + 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_observer_snapshot(); + 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_; + 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)}, + {"renderable_observers", controls_.observers(plot_)}, + {"performance_capture", gallery_performance_capture_json(plot_)}, + {"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(); + } +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(); + } + 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) { + plot_.clear_consumer_feedback(); + consumer_feedback_source_ = "disabled"; + return; + } + 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) { + plot_.clear_consumer_feedback(); + return; + } + plot_.set_consumer_feedback({interval_ns}); + } + 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_observer_snapshot(); + 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_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_Base::Attach_Builder& attach) { + if (case_id_ == "axis_lab") { + primary_ = renderive_static_owner_cast(frequency_domain_axis_); + return; + } + 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_; + 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_; + } + return; + } + 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_; + } + } + 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) { + 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)); + } + 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; + } + 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 + }); + } + } + 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_{}; + Scene_Type 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_; +}; +std::unique_ptr make_gallery_scene_2d(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode, bool automatic_low_latency) { + switch (mode) { + case Gallery_Frame_Mode::Manual: + return std::make_unique>(session_id, std::move(case_id), mode, automatic_low_latency); + case Gallery_Frame_Mode::Low_Latency: + return std::make_unique>(session_id, std::move(case_id), mode, automatic_low_latency); + case Gallery_Frame_Mode::Playback: + return std::make_unique>(session_id, std::move(case_id), mode, automatic_low_latency); + } + throw std::invalid_argument("unknown gallery frame mode"); +} +} diff --git a/web_server/app/render_2D/Gallery_Scene2D.h b/web_server/app/render_2D/Gallery_Scene2D.h new file mode 100644 index 0000000..5f86024 --- /dev/null +++ b/web_server/app/render_2D/Gallery_Scene2D.h @@ -0,0 +1,8 @@ +#pragma once +#include "../common/Gallery_Scene_Interface.h" +#include +#include +#include +namespace renderive::web { +[[nodiscard]] std::unique_ptr make_gallery_scene_2d(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode, bool automatic_low_latency); +} diff --git a/web_server/app/render_3D/Gallery_Scene3D.cpp b/web_server/app/render_3D/Gallery_Scene3D.cpp new file mode 100644 index 0000000..1b2d0db --- /dev/null +++ b/web_server/app/render_3D/Gallery_Scene3D.cpp @@ -0,0 +1,451 @@ +#include "Gallery_Scene3D.h" + +#include "../common/Pixel_Frame.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace renderive::web { +namespace { +using namespace renderive::render_3d; + +struct Gallery_Point { + double x{}; + double y{}; +}; + +Frame_Mode render_frame_mode(Gallery_Frame_Mode mode) { + switch (mode) { + case Gallery_Frame_Mode::Manual: + return Frame_Mode::Manual; + case Gallery_Frame_Mode::Low_Latency: + return Frame_Mode::Low_Latency; + case Gallery_Frame_Mode::Playback: + return Frame_Mode::Playback; + } + throw std::invalid_argument("unknown gallery frame mode"); +} + +std::string 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 "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; +} + +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_) { + if (!visual_) + throw std::runtime_error("failed to create Point_Visual"); + render_initial_frame(); + } + + [[nodiscard]] const std::string& case_id() const noexcept override { + return case_id_; + } + + [[nodiscard]] nlohmann::json controls() const override { + return {{"resources", nlohmann::json::array()}, + {"observers", nlohmann::json::array()}, + {"render_plan", nullptr}, + {"performance_capture", nullptr}}; + } + + [[nodiscard]] Gallery_Frame_Mode frame_mode() const noexcept override { + return frame_mode_; + } + + void reset_monitoring() override { + render_attempt_count_ = 0; + successful_render_count_ = 0; + pixel_frame_count_ = 0; + last_render_ms_ = 0.0; + last_event_ = "monitoring_reset"; + } + + [[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; + } + + [[nodiscard]] 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; + } + + void resize(int width, int height) override { + if (width <= 0 || height <= 0) + return; + scene_.resize({static_cast(width), + static_cast(height)}); + 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); + scene_.dispatch(input); + 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); + scene_.dispatch(input); + 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; + scene_.dispatch(input); + } + + bool render_latest_frame() override { + if (!can_render_automatically()) + return false; + advance_blue_point(0.025F); + return render_request(); + } + + [[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( + frame->rgba8.data(), frame->extent.width, frame->extent.height, + frame->extent.width * 4U); + } + + 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(); + } + + [[nodiscard]] std::string action(const Gallery_Action_Request& request, + bool& recognized) override { + recognized = true; + 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(); + 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(); + 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(); + 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"; + } + 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); + } + + [[nodiscard]] std::string telemetry_json() const override { + const auto status = scene_.frame_status(); + 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}}}, + {"render_performance", + {{"render_attempt_count", render_attempt_count_}, + {"successful_render_count", successful_render_count_}, + {"failed_render_count", + render_attempt_count_ - successful_render_count_}, + {"last_render_ms", last_render_ms_}, + {"pixel_response_fps", 0.0}, + {"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}}}, + {"session_id", session_id_}, + {"last_action", last_action_result_}} + .dump(); + } + +private: + 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}); + visual_->update_points(std::move(points)); + } + + void advance_blue_point(float step) { + blue_offset_ += step; + if (blue_offset_ > 0.32F) + blue_offset_ = -0.32F; + publish_points(); + } + + void render_initial_frame() { + if (frame_mode_ == Gallery_Frame_Mode::Manual) { + if (scene_.prepare_frame() && scene_.refresh_manual_frame()) + render_prepared(); + } else { + render_request(); + } + } + + bool render_request() { + const auto started = std::chrono::steady_clock::now(); + ++render_attempt_count_; + const bool rendered = scene_.request_frame(); + last_render_ms_ = std::chrono::duration( + std::chrono::steady_clock::now() - started) + .count(); + successful_render_count_ += rendered ? 1U : 0U; + return rendered; + } + + bool render_prepared() { + const auto started = std::chrono::steady_clock::now(); + ++render_attempt_count_; + const bool rendered = scene_.render_prepared_frame(); + last_render_ms_ = std::chrono::duration( + std::chrono::steady_clock::now() - started) + .count(); + successful_render_count_ += rendered ? 1U : 0U; + return rendered; + } + + 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}; + std::shared_ptr visual_; + Point_Scene 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::string last_event_{"initialized"}; + std::string last_action_result_; +}; +} // namespace + +std::unique_ptr make_gallery_scene_3d( + 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 renderive::web diff --git a/web_server/app/render_3D/Gallery_Scene3D.h b/web_server/app/render_3D/Gallery_Scene3D.h new file mode 100644 index 0000000..90d7889 --- /dev/null +++ b/web_server/app/render_3D/Gallery_Scene3D.h @@ -0,0 +1,8 @@ +#pragma once +#include "../common/Gallery_Scene_Interface.h" +#include +#include +#include +namespace renderive::web { +[[nodiscard]] std::unique_ptr make_gallery_scene_3d(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode, bool automatic_low_latency); +} diff --git a/web_server/tests/Datoviz_Gallery_Tests.cpp b/web_server/tests/Datoviz_Gallery_Tests.cpp index b9ce5d6..76fbe0a 100644 --- a/web_server/tests/Datoviz_Gallery_Tests.cpp +++ b/web_server/tests/Datoviz_Gallery_Tests.cpp @@ -1,6 +1,6 @@ #include "../app/Gallery_Plot_Session.h" #include "../app/Gallery_Protocol.h" -#include "../app/Pixel_Frame.h" +#include "../app/common/Pixel_Frame.h" #include #include @@ -34,7 +34,7 @@ Gallery_Request gallery_request(Gallery_Request_Kind kind, std::string message) std::string open_message(std::string_view mode) { return std::string( - R"({"category":"event","type":"gallery_open","case":"point_3d","frame_mode":")") + + R"({"category":"event","type":"gallery_open","case":"datoviz_3d","frame_mode":")") + std::string(mode) + R"("})"; } @@ -181,7 +181,7 @@ TEST(RenderiveWebDatovizGallery, CatalogUsesOnlyThreeDimensionalActions) { const auto catalog = nlohmann::json::parse(Gallery_Protocol::catalog_json()); const auto item = std::find_if( catalog.at("cases").begin(), catalog.at("cases").end(), - [](const nlohmann::json& value) { return value.at("id") == "point_3d"; }); + [](const nlohmann::json& value) { return value.at("id") == "datoviz_3d"; }); ASSERT_NE(item, catalog.at("cases").end()); EXPECT_EQ(item->at("category"), "3D"); EXPECT_EQ(item->at("control_count"), 0); @@ -193,20 +193,22 @@ TEST(RenderiveWebDatovizGallery, CatalogUsesOnlyThreeDimensionalActions) { 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( - "point_3d", empty_controls, "{}", {}, mode, false)); + "datoviz_3d", empty_controls, "{}", {}, mode, false)); }; const std::set common{ - "add_point", "orbit_points", "remove_point", "point_churn", - "reset_points"}; + "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", "mode_discard"}); + 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", "mode_discard"}); + 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) { @@ -239,39 +241,12 @@ TEST(RenderiveWebDatovizGallery, AutomaticLowLatencyFramesCarryAnimatedState) { expect_actual_datoviz_pixels(later, 560, 320); } -TEST(RenderiveWebDatovizGallery, WebPointerEventsReachTheDatovizController) { - Gallery_Plot_Session session; - ASSERT_EQ(open_scene(session, "low_latency").at("type"), "case_state"); - const std::string before = request_pixels(session); - - Pointer_Event press(Event_Type::Pointer_Press); - press.position = {210.0, 135.0}; - press.button = Mouse_Button::Left; - EXPECT_FALSE(session.handle(Web_Event{press}).has_value()); - - Pointer_Event move(Event_Type::Pointer_Move); - move.position = {330.0, 190.0}; - move.buttons = 1; - EXPECT_FALSE(session.handle(Web_Event{move}).has_value()); - move.position = {390.0, 225.0}; - EXPECT_FALSE(session.handle(Web_Event{move}).has_value()); - - Pointer_Event release(Event_Type::Pointer_Release); - release.position = move.position; - release.button = Mouse_Button::Left; - EXPECT_FALSE(session.handle(Web_Event{release}).has_value()); - - const std::string after = request_pixels(session); - expect_actual_datoviz_pixels(after, 560, 320); - EXPECT_NE(after, before); -} - TEST(RenderiveWebDatovizGallery, ManualRenderUsesThePreparedLogicalState) { Gallery_Plot_Session session; ASSERT_EQ(open_scene(session, "manual").at("type"), "case_state"); - ASSERT_EQ(invoke_action(session, "orbit_points").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, "orbit_points").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); @@ -286,7 +261,8 @@ TEST(RenderiveWebDatovizGallery, ManualRenderUsesThePreparedLogicalState) { TEST(RenderiveWebDatovizGallery, PlaybackDequeuesHistoricalStatesInOrder) { Gallery_Plot_Session session; ASSERT_EQ(open_scene(session, "playback").at("type"), "case_state"); - ASSERT_EQ(invoke_action(session, "mode_enqueue").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); @@ -315,7 +291,7 @@ TEST(RenderiveWebDatovizGallery, AddedAndChurnedVisualsAreActuallyRemoved) { ASSERT_EQ(open_scene(session, "manual").at("type"), "case_state"); const std::string baseline = request_pixels(session); - ASSERT_EQ(invoke_action(session, "add_point").at("type"), "case_state"); + 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); @@ -341,28 +317,49 @@ TEST(RenderiveWebDatovizGallery, AddedAndChurnedVisualsAreActuallyRemoved) { const auto green_y = centroid_y([](unsigned red, unsigned green, unsigned blue) { return green > red + 20U && green > blue + 20U; }); - const auto cyan_y = centroid_y([](unsigned red, unsigned green, unsigned blue) { - return green > red + 50U && blue > red + 50U && - std::abs(static_cast(green) - static_cast(blue)) < 30; + 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(cyan_y.has_value()); - EXPECT_GT(*cyan_y, *green_y) + 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_point").at("type"), "case_state"); + ASSERT_EQ(invoke_action(session, "remove_mesh").at("type"), "case_state"); manual_render_cycle(session); const std::string removed = request_pixels(session); EXPECT_EQ(removed, baseline); - const auto churned = invoke_action(session, "point_churn"); + 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"), - "point_churned"); + "mesh_churned"); const std::string after_churn = request_pixels(session); expect_actual_datoviz_pixels(after_churn, 560, 320); EXPECT_EQ(after_churn, baseline); } +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); + EXPECT_NE(without_texture, baseline); + + ASSERT_EQ(invoke_action(session, "toggle_texture").at("type"), "case_state"); + manual_render_cycle(session); + const std::string restored_texture = request_pixels(session); + EXPECT_EQ(restored_texture, baseline); + + ASSERT_EQ(invoke_action(session, "change_text").at("type"), "case_state"); + manual_render_cycle(session); + const std::string changed_text = request_pixels(session); + EXPECT_NE(changed_text, restored_texture); +} + } // namespace } // namespace renderive::web