#include "Gallery_Plot_Session.h" #include "Gallery_Protocol.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 namespace renderive::web { namespace { double number_value(const Gallery_State& state, std::string_view id) { return std::get(gallery_property_value(state, id)); } 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 integer_value(const Gallery_State& state, std::string_view id) { return static_cast(std::lround(number_value(state, id))); } bool bool_value(const Gallery_State& state, std::string_view id) { return std::get(gallery_property_value(state, id)); } std::string string_value(const Gallery_State& state, std::string_view id) { return std::get(gallery_property_value(state, id)); } bool has_value(const Gallery_State& state, std::string_view id) { return gallery_has_property(state, id); } 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}; } Color blend(Color first, Color second, double amount) { const auto channel = [amount](std::uint8_t a, std::uint8_t b) { return static_cast(std::clamp( std::lround(a + (b - a) * amount), 0L, 255L)); }; return { channel(first.r, second.r), channel(first.g, second.g), channel(first.b, second.b), channel(first.a, second.a) }; } Color_Map gallery_color_map(std::string_view palette) { std::array stops{ Color{5, 10, 25, 255}, Color{20, 52, 112, 255}, Color{39, 196, 181, 255}, Color{242, 201, 76, 255}, Color{255, 76, 106, 255} }; if (palette == "Ember") { stops = { Color{10, 5, 8, 255}, Color{62, 18, 30, 255}, Color{153, 47, 39, 255}, Color{244, 132, 48, 255}, Color{255, 238, 166, 255} }; } else if (palette == "Grayscale") { stops = { Color{0, 0, 0, 255}, Color{52, 52, 52, 255}, Color{112, 112, 112, 255}, Color{188, 188, 188, 255}, Color{255, 255, 255, 255} }; } std::vector colors; colors.reserve(256); for (int index = 0; index < 256; ++index) { const double position = index / 255.0 * (stops.size() - 1); const auto first = static_cast(std::floor(position)); const auto second = std::min(first + 1, stops.size() - 1); colors.push_back(premultiply(blend(stops[first], stops[second], position - first))); } Color_Map result; result.set_colors(std::move(colors)); return result; } 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); } Line_Interpolation_Mode line_mode(std::string_view value) { if (value == "Nearest_Sample") return Line_Interpolation_Mode::Nearest_Sample; if (value == "Linear_Power_Domain") return Line_Interpolation_Mode::Linear_Power_Domain; if (value == "Step_Left") return Line_Interpolation_Mode::Step_Left; if (value == "Step_Right") return Line_Interpolation_Mode::Step_Right; if (value == "Cubic_Value") return Line_Interpolation_Mode::Cubic_Value; return Line_Interpolation_Mode::Linear_Value; } Image_Interpolation_Mode image_mode(std::string_view value) { if (value == "Bilinear") return Image_Interpolation_Mode::Bilinear; if (value == "Bicubic") return Image_Interpolation_Mode::Bicubic; return Image_Interpolation_Mode::Nearest; } Constellation_Diagram_Type constellation_mode(std::string_view value) { if (value == "PSK4") return Constellation_Diagram_Type::Psk4; if (value == "PSK16") return Constellation_Diagram_Type::Psk16; return Constellation_Diagram_Type::Psk8; } 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); } Frame_Control_Mode core_frame_mode(Gallery_Frame_Mode mode) { switch (mode) { case Gallery_Frame_Mode::Manual: return Frame_Control_Mode::Manual; case Gallery_Frame_Mode::Playback: return Frame_Control_Mode::Playback; case Gallery_Frame_Mode::Low_Latency: return Frame_Control_Mode::Low_Latency; } return Frame_Control_Mode::Low_Latency; } std::string_view frame_mode_name(Gallery_Frame_Mode mode) { switch (mode) { case Gallery_Frame_Mode::Manual: return "manual"; case Gallery_Frame_Mode::Playback: return "playback"; case Gallery_Frame_Mode::Low_Latency: return "low_latency"; } return "low_latency"; } } // namespace struct Gallery_Pixel_Copy { Pixel_Frame_Copy image; Color background; }; class Gallery_Scene final { public: Gallery_Scene(std::uint64_t session_id, std::string case_id, Gallery_State state, Gallery_Frame_Mode frame_mode, bool automatic_low_latency) : session_id_(session_id), case_id_(std::move(case_id)), state_(std::move(state)), frame_mode_(frame_mode), automatic_low_latency_(automatic_low_latency), plot_(core_frame_mode(frame_mode)), performance_started_(std::chrono::steady_clock::now()) { plot_.init(); plot_.set_viewport_size({560, 320}); root_ = plot_.root_renderable(); axes_node_ = plot_.create_renderable_node(root_, "Gallery_Axes"); data_node_ = plot_.create_renderable_node(root_, "Gallery_Data"); overlay_node_ = plot_.create_renderable_node(root_, "Gallery_Overlay"); axes_node_->set_cache_mode(Renderable_Cache_Mode::Local_Pixel); attach_performance_overlay(plot_); build_axes(); build_primary(); apply_state(); 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]] const Gallery_State& state() const noexcept { return state_; } [[nodiscard]] Gallery_Frame_Mode frame_mode() const noexcept { return frame_mode_; } [[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(double transport_fps, double presentation_fps, std::uint64_t 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 display_interval_ms, double display_interval_latest_ms, double display_interval_p95_ms, double display_jitter_ms, std::uint64_t overwritten_pixel_frames, double last_pixel_receive_age_ms, double last_pixel_change_age_ms) noexcept { client_transport_fps_ = std::isfinite(transport_fps) ? std::clamp(transport_fps, 0.0, 100000.0) : 0.0; client_presentation_fps_ = std::isfinite(presentation_fps) ? std::clamp(presentation_fps, 0.0, 100000.0) : 0.0; client_buffered_bytes_ = buffered_bytes; client_changed_pixel_frames_ = changed_pixel_frames; client_duplicate_pixel_frames_ = duplicate_pixel_frames; client_frame_request_timeout_count_ = frame_request_timeout_count; client_frame_round_trip_ms_ = std::isfinite(frame_round_trip_ms) ? std::max(0.0, frame_round_trip_ms) : 0.0; client_display_interval_ms_ = std::isfinite(display_interval_ms) ? std::max(0.0, display_interval_ms) : 0.0; client_display_interval_latest_ms_ = std::isfinite(display_interval_latest_ms) ? std::max(0.0, display_interval_latest_ms) : 0.0; client_display_interval_p95_ms_ = std::isfinite(display_interval_p95_ms) ? std::max(0.0, display_interval_p95_ms) : 0.0; client_display_jitter_ms_ = std::isfinite(display_jitter_ms) ? std::max(0.0, display_jitter_ms) : 0.0; client_overwritten_pixel_frames_ = overwritten_pixel_frames; client_last_pixel_receive_age_ms_ = std::isfinite(last_pixel_receive_age_ms) ? std::max(0.0, last_pixel_receive_age_ms) : 0.0; client_last_pixel_change_age_ms_ = std::isfinite(last_pixel_change_age_ms) ? std::max(0.0, last_pixel_change_age_ms) : 0.0; apply_consumer_feedback(); } [[nodiscard]] bool requires_rebuild(const Gallery_State& candidate) const { const auto changed = [this, &candidate](std::string_view id) { return gallery_has_property(state_, id) && gallery_has_property(candidate, id) && gallery_property_value(state_, id) != gallery_property_value(candidate, id); }; if (case_id_ == "frequency_trace") return changed("trace_pen") || changed("trace_pen_width"); if (case_id_ == "constellation") return changed("constellation_type") || changed("phase_offset"); if (case_id_ == "waterfall" || case_id_ == "afterglow") return changed("color_map"); return false; } void set_state(Gallery_State state) { const bool render_schedule_changed = frame_mode_ == Gallery_Frame_Mode::Low_Latency && (bool_value(state_, "frequency_limit_enabled") != bool_value(state, "frequency_limit_enabled") || number_value(state_, "max_render_fps") != number_value(state, "max_render_fps") || bool_value(state_, "consumer_feedback_enabled") != bool_value(state, "consumer_feedback_enabled") || bool_value(state_, "consumer_pixel_feedback_enabled") != bool_value(state, "consumer_pixel_feedback_enabled") || bool_value(state_, "consumer_presentation_feedback_enabled") != bool_value(state, "consumer_presentation_feedback_enabled") || bool_value(state_, "consumer_manual_feedback_enabled") != bool_value(state, "consumer_manual_feedback_enabled") || number_value(state_, "consumer_manual_fps") != number_value(state, "consumer_manual_fps")); state_ = std::move(state); apply_state(); if (render_schedule_changed) render_history_.clear(); } 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 remove_primary() { if (primary_) plot_.remove_renderable(primary_); } 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 copy_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; Gallery_Pixel_Copy copy; copy.background = plot_.background_color(); plot_.with_frame([©](Image_View image) { copy.image = copy_pixel_frame(image); }); return copy.image.empty() ? std::nullopt : std::optional(std::move(copy)); } 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 == "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_set_start") { if (numeric_domain_axis_) numeric_domain_axis_->set_coord_start(number_value(state_, "coord_origin")); return "已通过 Axis::set_coord_start 应用当前起点"; } if (request.id == "axis_set_length") { if (numeric_domain_axis_) numeric_domain_axis_->set_coord_length(number_value(state_, "coord_target") - number_value(state_, "coord_origin")); return "已通过 Axis::set_coord_length 应用当前跨度"; } 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); nlohmann::json telemetry{ {"case", case_id_}, {"frame_mode", frame_mode_name(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", { {"render_attempt_count", render_attempt_count_}, {"successful_render_count", successful_render_count_}, {"failed_render_count", render_attempt_count_ - successful_render_count_}, {"measured_fps", render_fps}, {"lifetime_average_fps", static_cast(successful_render_count_) / elapsed_seconds}, {"last_render_ms", last_render_ms_}, {"average_render_ms", successful_render_count_ == 0 ? 0.0 : total_render_ms_ / static_cast(successful_render_count_)}, {"maximum_render_ms", maximum_render_ms_}, {"pixel_response_fps", pixel_fps}, {"last_pixel_snapshot_ms", last_pixel_snapshot_ms_}, {"last_pixel_encode_ms", last_pixel_encode_ms_}, {"average_pixel_encode_ms", pixel_frame_count_ == 0 ? 0.0 : total_pixel_encode_ms_ / static_cast(pixel_frame_count_)}, {"maximum_pixel_encode_ms", maximum_pixel_encode_ms_}, {"last_pixel_request_ms", last_pixel_request_ms_}, {"last_pixel_bytes", last_pixel_bytes_}, {"pixel_payload_megabytes_per_second", pixel_megabytes_per_second}, { "automatic_low_latency_scheduler", automatic_low_latency_ && frame_mode_ == Gallery_Frame_Mode::Low_Latency }, {"kernel_paint_ms", static_cast(observer.paint_duration_ns) / 1e6}, {"kernel_render_ms", static_cast(observer.render_duration_ns) / 1e6}, {"kernel_bottleneck_ms", static_cast(observer.bottleneck_duration_ns) / 1e6}, {"kernel_end_to_end_ms", static_cast(observer.end_to_end_ns) / 1e6} } }, { "kernel_observer", { {"mode", frame_mode_name(frame_mode_)}, {"last_event", observer.last_event}, {"limit_state", observer.limit_state}, {"configured_frequency_hz", observer.frequency_hz}, {"observation_count", observer.observation_count}, {"produced_frame_count", observer.produced_frame_count}, {"consumed_frame_count", observer.consumed_frame_count}, {"dropped_frame_count", observer.dropped_frame_count}, {"failed_operation_count", observer.failed_operation_count}, {"pending_frame_count", observer.pending_frame_count}, {"latest_sequence", observer.latest_sequence}, {"paint_duration_ns", observer.paint_duration_ns}, {"render_duration_ns", observer.render_duration_ns}, {"target_interval_ns", observer.target_interval_ns}, {"frequency_limit_enabled", observer.frequency_limit_enabled}, {"consumer_feedback_enabled", observer.consumer_feedback_enabled}, { "consumer_feedback_master_enabled", frame_mode_ == Gallery_Frame_Mode::Low_Latency && bool_value(state_, "consumer_feedback_enabled") }, { "consumer_pixel_feedback_enabled", frame_mode_ == Gallery_Frame_Mode::Low_Latency && bool_value(state_, "consumer_pixel_feedback_enabled") }, { "consumer_presentation_feedback_enabled", frame_mode_ == Gallery_Frame_Mode::Low_Latency && bool_value(state_, "consumer_presentation_feedback_enabled") }, { "consumer_manual_feedback_enabled", frame_mode_ == Gallery_Frame_Mode::Low_Latency && bool_value(state_, "consumer_manual_feedback_enabled") }, {"consumer_manual_fps", frame_mode_ == Gallery_Frame_Mode::Low_Latency ? number_value(state_, "consumer_manual_fps") : 0.0}, {"consumer_feedback_source", consumer_feedback_source_}, {"consumer_pixel_interval_ns", consumer_pixel_interval_ns_}, {"consumer_presentation_interval_ns", consumer_presentation_interval_ns_}, {"consumer_manual_interval_ns", consumer_manual_interval_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} } }, { "client_performance", { {"transport_fps", client_transport_fps_}, {"presentation_fps", client_presentation_fps_}, {"websocket_buffered_bytes", client_buffered_bytes_}, {"changed_pixel_frames", client_changed_pixel_frames_}, {"duplicate_pixel_frames", client_duplicate_pixel_frames_}, {"frame_request_timeout_count", client_frame_request_timeout_count_}, {"frame_round_trip_ms", client_frame_round_trip_ms_}, {"display_interval_ms", client_display_interval_ms_}, {"display_interval_latest_ms", client_display_interval_latest_ms_}, {"display_interval_p95_ms", client_display_interval_p95_ms_}, {"display_jitter_ms", client_display_jitter_ms_}, {"overwritten_pixel_frames", client_overwritten_pixel_frames_}, {"last_pixel_receive_age_ms", client_last_pixel_receive_age_ms_}, {"last_pixel_change_age_ms", client_last_pixel_change_age_ms_} } }, {"last_action_result", last_action_result_} }; if (frame_mode_ == Gallery_Frame_Mode::Low_Latency) { telemetry["render_fps"] = plot_.max_render_fps(); telemetry["refresh_feedback_hz"] = plot_.refresh_feedback_snapshot().frequency_hz; const auto scheduler_interval_ns = kernel_refresh_interval_ns(); telemetry["low_latency_limit"] = { {"current", observer.limit_state}, {"frequency_limited", observer.limit_state == "frequency_limited"}, {"paint_limited", observer.limit_state == "paint_limited"}, {"render_limited", observer.limit_state == "render_limited"}, {"consumer_limited", observer.limit_state == "consumer_limited"}, {"configured_frequency_hz", observer.frequency_hz}, {"frequency_limit_enabled", observer.frequency_limit_enabled}, {"consumer_feedback_enabled", observer.consumer_feedback_enabled}, {"consumer_feedback_master_enabled", bool_value(state_, "consumer_feedback_enabled")}, {"consumer_pixel_feedback_enabled", bool_value(state_, "consumer_pixel_feedback_enabled")}, {"consumer_presentation_feedback_enabled", bool_value(state_, "consumer_presentation_feedback_enabled")}, {"consumer_manual_feedback_enabled", bool_value(state_, "consumer_manual_feedback_enabled")}, {"consumer_manual_fps", number_value(state_, "consumer_manual_fps")}, {"consumer_feedback_source", consumer_feedback_source_}, {"consumer_pixel_interval_ns", consumer_pixel_interval_ns_}, {"consumer_presentation_interval_ns", consumer_presentation_interval_ns_}, {"consumer_manual_interval_ns", consumer_manual_interval_ns_}, {"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}, {"scheduler_interval_ns", scheduler_interval_ns}, {"scheduler_frequency_hz", scheduler_interval_ns == 0 ? 0.0 : 1'000'000'000.0 / static_cast(scheduler_interval_ns)} }; } if (primary_) { telemetry["renderable"] = { {"object_name", primary_->object_name()}, {"visible", primary_->is_visible()}, { "cache_mode", primary_->get_cache_mode() == Renderable_Cache_Mode::Direct ? "Direct" : "Local_Pixel" } }; } 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 (numeric_domain_axis_) { const Range range = numeric_domain_axis_->coord_range(); const double center = range.center(); telemetry["axis"] = { {"origin", range.origin}, {"target", range.target}, {"start_coord", numeric_domain_axis_->start_coord()}, {"end_coord", numeric_domain_axis_->end_coord()}, {"center_pixel", numeric_domain_axis_->coord_to_pixel(center)}, { "center_roundtrip", numeric_domain_axis_->pixel_to_coord( numeric_domain_axis_->coord_to_pixel(center)) }, {"pixel_samples", numeric_domain_axis_->pixel_sample_count()}, {"tick_step", numeric_domain_axis_->tick_step(range)}, { "sub_tick_count", numeric_domain_axis_->sub_tick_count( numeric_domain_axis_->tick_step(range)) }, {"center_label", numeric_domain_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 std::string& constellation_type = string_value(state_, "constellation_type"); const int anchor_count = constellation_type == "PSK4" ? 4 : constellation_type == "PSK16" ? 16 : 8; 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", numeric_domain_axis_ ? numeric_domain_axis_->pixel_sample_count() : 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::string& constellation_type = string_value(state_, "constellation_type"); const std::size_t anchors = constellation_type == "PSK4" ? 4U : constellation_type == "PSK16" ? 16U : 8U; 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", numeric_domain_axis_ ? numeric_domain_axis_->pixel_sample_count() : 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_transport_fps_); consumer_presentation_interval_ns_ = milliseconds_to_ns(client_display_interval_ms_); consumer_manual_interval_ns_ = frequency_to_ns(number_value(state_, "consumer_manual_fps")); consumer_feedback_source_ = "none"; if (!bool_value(state_, "consumer_feedback_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(bool_value(state_, "consumer_pixel_feedback_enabled"), consumer_pixel_interval_ns_, "pixel"); select_source(bool_value(state_, "consumer_presentation_feedback_enabled"), consumer_presentation_interval_ns_, "presentation"); select_source(bool_value(state_, "consumer_manual_feedback_enabled"), 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; maximum_render_ms_ = std::max(maximum_render_ms_, duration_ms); if (rendered) { ++successful_render_count_; total_render_ms_ += 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(); maximum_pixel_encode_ms_ = std::max(maximum_pixel_encode_ms_, last_pixel_encode_ms_); total_pixel_encode_ms_ += last_pixel_encode_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", frame_mode_name(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_transport_fps_}, {"client_presentation_fps", client_presentation_fps_}, {"client_changed_pixel_frames", client_changed_pixel_frames_}, {"client_duplicate_pixel_frames", client_duplicate_pixel_frames_}, {"client_frame_request_timeout_count", client_frame_request_timeout_count_}, {"client_frame_round_trip_ms", client_frame_round_trip_ms_}, {"client_display_interval_ms", client_display_interval_ms_}, {"client_display_interval_latest_ms", client_display_interval_latest_ms_}, {"client_display_interval_p95_ms", client_display_interval_p95_ms_}, {"client_display_jitter_ms", client_display_jitter_ms_}, {"client_overwritten_pixel_frames", client_overwritten_pixel_frames_}, {"client_last_pixel_receive_age_ms", client_last_pixel_receive_age_ms_}, {"client_last_pixel_change_age_ms", client_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_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() { constexpr Color axis_color{118, 145, 184, 255}; const Range coordinate_range{ number_value(state_, "coord_origin"), number_value(state_, "coord_target") }; if (case_id_ == "frequency_trace") { time_axis_ = Time_Axis::Builder(axes_node_, Orientation::Horizontal) .set_visible_time_point_count(integer_value(state_, "time_visible_count")) .set_tick_label_spacing_px(integer_value(state_, "time_label_spacing")) .set_time_format(string_value(state_, "time_format")) .set_color(axis_color) .build(); } else if (case_id_ == "sweep_spectrum" || case_id_ == "constellation") { numeric_domain_axis_ = Axis::Builder(axes_node_, Orientation::Horizontal) .set_coord_range(coordinate_range) .set_use_wheel(true) .set_use_drag(true) .set_color(axis_color) .build(); } else { numeric_domain_axis_ = Frequency_Axis::Builder(axes_node_, Orientation::Horizontal) .set_coord_range(coordinate_range) .set_use_wheel(true) .set_use_drag(true) .set_color(axis_color) .build(); } if (case_id_ == "waterfall") { time_axis_ = Time_Axis::Builder(axes_node_, Orientation::Vertical) .set_visible_time_point_count(integer_value(state_, "time_visible_count")) .set_tick_label_spacing_px(integer_value(state_, "time_label_spacing")) .set_time_format(string_value(state_, "time_format")) .set_color(axis_color) .build(); } else { Range value_range{-120.0, -20.0}; if (case_id_ == "frequency_trace") value_range = { number_value(state_, "trace_value_min"), number_value(state_, "trace_value_max") }; else if (case_id_ == "constellation") value_range = { number_value(state_, "q_origin"), number_value(state_, "q_target") }; value_axis_ = Axis::Builder(axes_node_, Orientation::Vertical) .set_coord_range(value_range) .set_label_precision(case_id_ == "constellation" ? 2 : 0) .set_color(axis_color) .build(); } if (case_id_ == "axis_lab") { time_axis_ = Time_Axis::Builder(axes_node_, Orientation::Horizontal) .set_visible_time_point_count(integer_value(state_, "time_visible_count")) .set_tick_label_spacing_px(integer_value(state_, "time_label_spacing")) .set_time_format(string_value(state_, "time_format")) .set_color({69, 221, 190, 255}) .build(); } } void build_primary() { if (case_id_ == "axis_lab") { primary_ = numeric_domain_axis_; return; } if (case_id_ == "spectrum" || case_id_ == "selection_overlay") { spectrum_ = Spectrum::Builder{} .set<&Spectrum::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }) .set<&Spectrum::Properties::frequency_point_size>(integer_value(state_, "frequency_point_size")) .set<&Spectrum::Properties::center_frequency>(number_value(state_, "center_frequency")) .set<&Spectrum::Properties::sweep_frequency_range>(Range{ number_value(state_, "sweep_origin"), number_value(state_, "sweep_target") }) .build(data_node_, std::static_pointer_cast(numeric_domain_axis_), value_axis_); primary_ = spectrum_; if (case_id_ == "selection_overlay") { selection_ = Selection_Rectangle_Overlay::Builder{}.build(overlay_node_, numeric_domain_axis_, value_axis_); primary_ = selection_; } return; } if (case_id_ == "waterfall") { waterfall_ = Waterfall::Builder{} .set<&Waterfall::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }) .set<&Waterfall::Properties::power_range>(Range{ number_value(state_, "power_origin"), number_value(state_, "power_target") }) .set<&Waterfall::Properties::frequency_bin_count>(integer_value(state_, "frequency_bin_count")) .set<&Waterfall::Properties::color_map>(gallery_color_map(string_value(state_, "color_map"))) .build(data_node_, std::static_pointer_cast(numeric_domain_axis_), time_axis_); primary_ = waterfall_; return; } if (case_id_ == "afterglow") { afterglow_ = Afterglow::Builder{} .set<&Afterglow::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }) .set<&Afterglow::Properties::power_range>(Range{ number_value(state_, "power_origin"), number_value(state_, "power_target") }) .set<&Afterglow::Properties::frequency_point_size>(integer_value(state_, "frequency_point_size")) .set<&Afterglow::Properties::power_point_size>(integer_value(state_, "power_point_size")) .set<&Afterglow::Properties::color_map>(gallery_color_map(string_value(state_, "color_map"))) .build(data_node_, std::static_pointer_cast(numeric_domain_axis_), value_axis_); primary_ = afterglow_; return; } if (case_id_ == "sweep_spectrum") { sweep_ = Sweep_Spectrum::Builder{} .set<&Sweep_Spectrum::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }) .set<&Sweep_Spectrum::Properties::bins_per_block>(integer_value(state_, "bins_per_block")) .set<&Sweep_Spectrum::Properties::block_count>(integer_value(state_, "block_count")) .build(data_node_, numeric_domain_axis_, value_axis_); primary_ = sweep_; return; } if (case_id_ == "frequency_trace") { trace_ = Frequency_Trace::Builder{} .set<&Frequency_Trace::Properties::pen>(Pen{ color_from_hex(string_value(state_, "trace_pen")), number_value(state_, "trace_pen_width"), Line_Style::Solid, Line_Cap::Round, Line_Join::Round }) .build(data_node_, time_axis_, value_axis_); primary_ = trace_; return; } if (case_id_ == "constellation") { constellation_ = Constellation_Diagram::Builder{} .set<&Constellation_Diagram::Properties::i_range>(Range{ number_value(state_, "i_origin"), number_value(state_, "i_target") }) .set<&Constellation_Diagram::Properties::q_range>(Range{ number_value(state_, "q_origin"), number_value(state_, "q_target") }) .set<&Constellation_Diagram::Properties::type>(constellation_mode(string_value(state_, "constellation_type"))) .set<&Constellation_Diagram::Properties::phase_offset_radians>(number_value(state_, "phase_offset")) .build(data_node_, numeric_domain_axis_, value_axis_); primary_ = constellation_; } } std::shared_ptr horizontal_axis() const { return numeric_domain_axis_ ? std::static_pointer_cast(numeric_domain_axis_) : std::static_pointer_cast(time_axis_); } std::shared_ptr 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 int x_offset = integer_value(state_, "axis_x_offset"); const int y_offset = integer_value(state_, "axis_y_offset"); const double length_rate = number_value(state_, "axis_length_percent") / 100.0; if (const auto horizontal = horizontal_axis()) { horizontal->set_x(left + x_offset); horizontal->set_y(top + height + y_offset); horizontal->set_pixel_length(static_cast( std::max(1, static_cast(std::lround(width * length_rate))))); } if (const auto vertical = vertical_axis()) { vertical->set_x(left + x_offset); vertical->set_y(top + y_offset); vertical->set_pixel_length(static_cast( std::max(1, static_cast(std::lround(height * length_rate))))); } if (case_id_ == "axis_lab" && time_axis_) { time_axis_->set_x(left); time_axis_->set_y(26); time_axis_->set_pixel_length(static_cast(width)); } } void apply_axis_style(const std::shared_ptr& axis) { if (!axis) return; axis->set_tick_length(integer_value(state_, "tick_length")); axis->set_sub_tick_length(integer_value(state_, "sub_tick_length")); axis->set_color(color_from_hex(string_value(state_, "axis_color"))); axis->set_locale({string_value(state_, "decimal_separator").front()}); axis->set_unit_text(string_value(state_, "unit_text")); axis->set_unit_text_font({ number_value(state_, "unit_font_size"), integer_value(state_, "unit_font_weight"), bool_value(state_, "unit_font_italic") }); axis->set_unit_text_pen({color_from_hex(string_value(state_, "unit_pen_color"))}); axis->set_unit_text_background_brush( {color_from_hex(string_value(state_, "unit_background_color")), Brush_Style::Solid}); axis->set_label_rotation_degrees(integer_value(state_, "label_rotation")); } void apply_axes() { const bool horizontal = string_value(state_, "axis_orientation") == "Horizontal"; if (const auto main = horizontal_axis()) main->set_orientation(horizontal ? Orientation::Horizontal : Orientation::Vertical); if (const auto other = vertical_axis()) other->set_orientation(horizontal ? Orientation::Vertical : Orientation::Horizontal); apply_axis_style(horizontal_axis()); apply_axis_style(vertical_axis()); if (case_id_ == "axis_lab") apply_axis_style(time_axis_); if (numeric_domain_axis_) { numeric_domain_axis_->set_coord_range({ number_value(state_, "coord_origin"), number_value(state_, "coord_target") }); numeric_domain_axis_->set_label_precision(integer_value(state_, "label_precision")); numeric_domain_axis_->set_use_wheel(bool_value(state_, "use_wheel")); numeric_domain_axis_->set_use_drag(bool_value(state_, "use_drag")); } if (time_axis_) { time_axis_->set_visible_time_point_count(integer_value(state_, "time_visible_count")); time_axis_->set_tick_label_spacing_px(integer_value(state_, "time_label_spacing")); time_axis_->set_time_format(string_value(state_, "time_format")); time_axis_->set_font({number_value(state_, "time_font_size"), 500, false}); time_axis_->set_newest_at_axis_start(bool_value(state_, "time_newest_at_start")); } apply_layout(); } void apply_state() { plot_.set_background_color(color_from_hex(string_value(state_, "background_color"))); if (frame_mode_ == Gallery_Frame_Mode::Low_Latency) { if (bool_value(state_, "frequency_limit_enabled")) plot_.set_max_render_fps(number_value(state_, "max_render_fps")); else plot_.clear_max_render_fps(); apply_consumer_feedback(); } set_performance_plot_name(plot_, case_id_ + "/" + std::string(frame_mode_name(frame_mode_))); set_performance_overlay_enabled(plot_, bool_value(state_, "performance_overlay")); if (primary_) { primary_->set_visible(bool_value(state_, "renderable_visible")); primary_->set_cache_mode(string_value(state_, "cache_mode") == "Direct" ? Renderable_Cache_Mode::Direct : Renderable_Cache_Mode::Local_Pixel); primary_->set_object_name(string_value(state_, "object_name")); } apply_axes(); apply_specific_state(); plot_.notify_model_dirty(); } void apply_specific_state() { if (spectrum_) { spectrum_->set<&Spectrum::Properties::frequency_point_size>(integer_value(state_, "frequency_point_size")); spectrum_->set<&Spectrum::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }); spectrum_->set<&Spectrum::Properties::center_frequency>(number_value(state_, "center_frequency")); spectrum_->set<&Spectrum::Properties::sweep_frequency_range>(Range{ number_value(state_, "sweep_origin"), number_value(state_, "sweep_target") }); spectrum_->set<&Spectrum::Properties::max_hold_visible>(bool_value(state_, "max_hold_visible")); spectrum_->set<&Spectrum::Properties::min_hold_visible>(bool_value(state_, "min_hold_visible")); spectrum_->set<&Spectrum::Properties::max_marker_visible>(bool_value(state_, "max_marker_visible")); spectrum_->set<&Spectrum::Properties::use_min_marker>(bool_value(state_, "use_min_marker")); spectrum_->set<&Spectrum::Properties::sweep_region_visible>(bool_value(state_, "sweep_region_visible")); spectrum_->set<&Spectrum::Properties::visible_range_only>(bool_value(state_, "visible_range_only")); spectrum_->set<&Spectrum::Properties::interpolation_mode>(line_mode(string_value(state_, "line_interpolation"))); spectrum_->set<&Spectrum::Properties::max_brush>(Brush{color_from_hex(string_value(state_, "max_brush"), 76), Brush_Style::Solid}); spectrum_->set<&Spectrum::Properties::current_brush>(Brush{color_from_hex(string_value(state_, "current_brush"), 76), Brush_Style::Solid}); spectrum_->set<&Spectrum::Properties::min_brush>(Brush{color_from_hex(string_value(state_, "min_brush"), 76), Brush_Style::Solid}); spectrum_->set<&Spectrum::Properties::max_pen>(Pen{color_from_hex(string_value(state_, "max_pen")), 1.2}); spectrum_->set<&Spectrum::Properties::current_pen>(Pen{ color_from_hex(string_value(state_, "current_pen")), 2.0, Line_Style::Solid, Line_Cap::Round, Line_Join::Round }); spectrum_->set<&Spectrum::Properties::min_pen>(Pen{color_from_hex(string_value(state_, "min_pen")), 1.2}); spectrum_->set<&Spectrum::Properties::selected_marker_pen>(Pen{color_from_hex(string_value(state_, "selected_marker_pen")), 2.0}); spectrum_->set<&Spectrum::Properties::marker_pen>(Pen{color_from_hex(string_value(state_, "marker_pen")), 1.2}); spectrum_->set<&Spectrum::Properties::middle_frequency_pen>(Pen{ color_from_hex(string_value(state_, "middle_frequency_pen")), 1.0, Line_Style::Dash }); spectrum_->set<&Spectrum::Properties::sweep_region_brush>(Brush{ color_from_hex(string_value(state_, "sweep_region_brush"), 80), Brush_Style::Solid }); spectrum_->set<&Spectrum::Properties::tooltip_enabled>(bool_value(state_, "hover_enabled")); spectrum_->set<&Spectrum::Properties::tooltip_font>(Font{number_value(state_, "hover_font_size"), 500, false}); spectrum_->set<&Spectrum::Properties::tooltip_text_pen>(Pen{color_from_hex(string_value(state_, "hover_text_color"))}); spectrum_->set<&Spectrum::Properties::tooltip_background_brush>(Brush{color_from_hex(string_value(state_, "hover_background"), 230), Brush_Style::Solid}); } if (selection_) { selection_->set<&Selection_Rectangle_Overlay::Properties::label_font>(Font{number_value(state_, "selection_font_size"), 500, false}); selection_->set<&Selection_Rectangle_Overlay::Properties::label_pen>(Pen{color_from_hex(string_value(state_, "selection_label_pen"))}); selection_->set<&Selection_Rectangle_Overlay::Properties::selection_brush>(Brush{ color_from_hex(string_value(state_, "selection_brush"), 70), Brush_Style::Solid }); selection_->set<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(Pen{ color_from_hex(string_value(state_, "selection_border")), 1.0, Line_Style::Dash }); } if (waterfall_) { waterfall_->set<&Waterfall::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }); waterfall_->set<&Waterfall::Properties::power_range>(Range{ number_value(state_, "power_origin"), number_value(state_, "power_target") }); waterfall_->set<&Waterfall::Properties::frequency_bin_count>(integer_value(state_, "frequency_bin_count")); waterfall_->set<&Waterfall::Properties::visible_range_only>(bool_value(state_, "visible_range_only")); waterfall_->set<&Waterfall::Properties::interpolation_mode>(image_mode(string_value(state_, "image_interpolation"))); waterfall_->set<&Waterfall::Properties::tooltip_enabled>(bool_value(state_, "hover_enabled")); waterfall_->set<&Waterfall::Properties::tooltip_font>(Font{number_value(state_, "hover_font_size"), 500, false}); waterfall_->set<&Waterfall::Properties::tooltip_text_pen>(Pen{color_from_hex(string_value(state_, "hover_text_color"))}); waterfall_->set<&Waterfall::Properties::tooltip_background_brush>(Brush{color_from_hex(string_value(state_, "hover_background"), 230), Brush_Style::Solid}); } if (afterglow_) { afterglow_->set<&Afterglow::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }); afterglow_->set<&Afterglow::Properties::power_range>(Range{ number_value(state_, "power_origin"), number_value(state_, "power_target") }); afterglow_->set<&Afterglow::Properties::frequency_point_size>(integer_value(state_, "frequency_point_size")); afterglow_->set<&Afterglow::Properties::power_point_size>(integer_value(state_, "power_point_size")); afterglow_->set<&Afterglow::Properties::interpolate>(bool_value(state_, "interpolate_power")); afterglow_->set<&Afterglow::Properties::attenuation_rate>(number_value(state_, "attenuation_rate")); } if (sweep_) { sweep_->set<&Sweep_Spectrum::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }); sweep_->set<&Sweep_Spectrum::Properties::bins_per_block>(integer_value(state_, "bins_per_block")); sweep_->set<&Sweep_Spectrum::Properties::block_count>(integer_value(state_, "block_count")); sweep_->set<&Sweep_Spectrum::Properties::pen>(Pen{color_from_hex(string_value(state_, "sweep_pen")), 1.8}); sweep_->set<&Sweep_Spectrum::Properties::current_frequency_pen>(Pen{color_from_hex(string_value(state_, "current_frequency_pen")), 2.0}); sweep_->set<&Sweep_Spectrum::Properties::visible_range_only>(bool_value(state_, "visible_range_only")); sweep_->set<&Sweep_Spectrum::Properties::interpolation_mode>(line_mode(string_value(state_, "line_interpolation"))); } if (trace_ && value_axis_) value_axis_->set_coord_range({ number_value(state_, "trace_value_min"), number_value(state_, "trace_value_max") }); if (constellation_) { constellation_->set<&Constellation_Diagram::Properties::i_range>(Range{number_value(state_, "i_origin"), number_value(state_, "i_target")}); constellation_->set<&Constellation_Diagram::Properties::q_range>(Range{number_value(state_, "q_origin"), number_value(state_, "q_target")}); constellation_->set<&Constellation_Diagram::Properties::point_color>(color_from_hex(string_value(state_, "point_color"))); constellation_->set<&Constellation_Diagram::Properties::anchor_color>(color_from_hex(string_value(state_, "anchor_color"))); constellation_->set<&Constellation_Diagram::Properties::point_lifetime_ms>(integer_value(state_, "point_lifetime_ms")); if (numeric_domain_axis_) numeric_domain_axis_->set_coord_range({ number_value(state_, "i_origin"), number_value(state_, "i_target") }); if (value_axis_) value_axis_->set_coord_range({ number_value(state_, "q_origin"), number_value(state_, "q_target") }); } } 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 double lower = number_value(state_, "trace_value_min"); const double upper = number_value(state_, "trace_value_max"); 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 std::string& mode = string_value(state_, "constellation_type"); const int count = mode == "PSK4" ? 4 : mode == "PSK16" ? 16 : 8; const double phase = number_value(state_, "phase_offset"); 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_State state_; Gallery_Frame_Mode frame_mode_ = Gallery_Frame_Mode::Low_Latency; bool automatic_low_latency_{}; Plot_Core plot_; std::chrono::steady_clock::time_point performance_started_; std::chrono::steady_clock::time_point last_performance_log_; std::shared_ptr root_; std::shared_ptr axes_node_; std::shared_ptr data_node_; std::shared_ptr overlay_node_; std::shared_ptr numeric_domain_axis_; std::shared_ptr value_axis_; std::shared_ptr time_axis_; std::shared_ptr primary_; std::shared_ptr spectrum_; std::shared_ptr waterfall_; std::shared_ptr trace_; std::shared_ptr afterglow_; std::shared_ptr sweep_; std::shared_ptr selection_; std::shared_ptr constellation_; std::uint64_t frame_index_{}; std::uint64_t render_attempt_count_{}; std::uint64_t successful_render_count_{}; double last_render_ms_{}; double total_render_ms_{}; double maximum_render_ms_{}; std::deque render_history_; std::uint64_t pixel_frame_count_{}; double last_pixel_snapshot_ms_{}; double last_pixel_encode_ms_{}; double total_pixel_encode_ms_{}; double maximum_pixel_encode_ms_{}; double last_pixel_request_ms_{}; std::size_t last_pixel_bytes_{}; std::deque pixel_history_; double client_transport_fps_{}; double client_presentation_fps_{}; std::uint64_t client_buffered_bytes_{}; std::uint64_t client_changed_pixel_frames_{}; std::uint64_t client_duplicate_pixel_frames_{}; std::uint64_t client_frame_request_timeout_count_{}; double client_frame_round_trip_ms_{}; double client_display_interval_ms_{}; double client_display_interval_latest_ms_{}; double client_display_interval_p95_ms_{}; double client_display_jitter_ms_{}; 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"}; std::uint64_t client_overwritten_pixel_frames_{}; double client_last_pixel_receive_age_ms_{}; double client_last_pixel_change_age_ms_{}; bool rendered_since_last_pixel_{}; std::string last_action_result_; }; struct Gallery_Plot_Session::Impl { explicit Impl(bool enable_automatic_low_latency) : automatic_low_latency(enable_automatic_low_latency), session_id(next_session_id()) {} ~Impl() { render_worker.request_stop(); scheduler_condition.notify_all(); } static std::uint64_t next_session_id() noexcept { static std::atomic next{1}; return next.fetch_add(1, std::memory_order_relaxed); } void ensure_render_worker() { if (!automatic_low_latency || render_worker.joinable()) return; render_worker = std::jthread( [this](std::stop_token stop) { run_automatic_renderer(stop); }); } bool update_client_metrics(std::string_view message) { if (!scene) return false; const auto root = nlohmann::json::parse(message, nullptr, false); if (root.is_discarded() || !root.is_object() || !root.contains("client_metrics") || !root["client_metrics"].is_object()) return false; const auto& metrics = root["client_metrics"]; const auto finite_metric = [&metrics](std::string_view name) { const auto iterator = metrics.find(std::string(name)); if (iterator == metrics.end() || !iterator->is_number()) return 0.0; const double value = iterator->get(); return std::isfinite(value) ? value : 0.0; }; std::uint64_t buffered_bytes{}; if (const auto iterator = metrics.find("websocket_buffered_bytes"); iterator != metrics.end() && iterator->is_number_unsigned()) { buffered_bytes = iterator->get(); } const auto unsigned_metric = [&metrics](std::string_view name) { const auto iterator = metrics.find(std::string(name)); if (iterator == metrics.end() || !iterator->is_number_integer()) return std::uint64_t{}; const auto value = iterator->get(); return value > 0 ? static_cast(value) : std::uint64_t{}; }; scene->set_client_metrics(finite_metric("transport_fps"), finite_metric("presentation_fps"), buffered_bytes, unsigned_metric("changed_pixel_frames"), unsigned_metric("duplicate_pixel_frames"), unsigned_metric("frame_request_timeout_count"), finite_metric("frame_round_trip_ms"), finite_metric("display_interval_ms"), finite_metric("display_interval_latest_ms"), finite_metric("display_interval_p95_ms"), finite_metric("display_jitter_ms"), unsigned_metric("overwritten_pixel_frames"), finite_metric("last_pixel_receive_age_ms"), finite_metric("last_pixel_change_age_ms")); return true; } [[nodiscard]] std::unique_lock acquire_foreground_lock() { foreground_waiters.fetch_add(1, std::memory_order_release); scheduler_condition.notify_all(); std::unique_lock lock(mutex); foreground_waiters.fetch_sub(1, std::memory_order_release); scheduler_condition.notify_all(); return lock; } void run_automatic_renderer(std::stop_token stop) { using Clock = std::chrono::steady_clock; std::unique_lock lock(mutex); std::optional last_render_started; std::uint64_t active_generation = std::numeric_limits::max(); while (!stop.stop_requested()) { scheduler_condition.wait(lock, [this, &stop] { return stop.stop_requested() || (scene && scene->can_render_automatically()); }); if (stop.stop_requested()) break; if (active_generation != scene_generation) { active_generation = scene_generation; last_render_started.reset(); } if (foreground_waiters.load(std::memory_order_acquire) != 0) { scheduler_condition.wait(lock, [this, &stop] { return stop.stop_requested() || foreground_waiters.load(std::memory_order_acquire) == 0; }); continue; } const auto revision = scheduler_revision; const auto interval = std::chrono::nanoseconds(std::max(1, scene->kernel_refresh_interval_ns())); const auto now = Clock::now(); const auto deadline = last_render_started ? *last_render_started + interval : now; if (now < deadline) { const auto spin_window = std::min(interval / 4, std::chrono::duration_cast(std::chrono::microseconds(250))); const auto coarse_deadline = deadline > now + spin_window ? deadline - spin_window : now; if (now < coarse_deadline) { scheduler_condition.wait_until(lock, coarse_deadline, [this, &stop, revision, active_generation] { return stop.stop_requested() || scheduler_revision != revision || scene_generation != active_generation || foreground_waiters.load(std::memory_order_acquire) != 0 || !scene || !scene->can_render_automatically(); }); continue; } lock.unlock(); while (!stop.stop_requested() && Clock::now() < deadline) std::this_thread::yield(); lock.lock(); continue; } const auto started = Clock::now(); (void)scene->render_latest_frame(); last_render_started = started; } } static bool affects_render_schedule(const Web_Event& event) { return std::visit( [](const auto& value) { using T = std::decay_t; if constexpr (std::is_same_v) { return false; } else if constexpr (std::is_same_v) { return value.kind != Gallery_Request_Kind::Catalog && value.kind != Gallery_Request_Kind::Observe; } else { return true; } }, event); } std::unique_ptr scene; std::mutex mutex; std::condition_variable scheduler_condition; std::uint64_t scheduler_revision{}; std::uint64_t scene_generation{}; bool automatic_low_latency{}; std::uint64_t session_id{}; std::atomic foreground_waiters{}; std::jthread render_worker; std::optional handle_gallery(const Gallery_Request& request) { if (request.kind == Gallery_Request_Kind::Catalog) return Web_Response{Web_Response_Type::Json, Gallery_Protocol::catalog_json()}; if (request.kind == Gallery_Request_Kind::Open) { const auto open = Gallery_Protocol::open_request(request.message); if (!open) return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::error_json("未知的 gallery case 或 frame_mode", "case") }; scene = std::make_unique( session_id, open->case_id, Gallery_Protocol::default_state(open->case_id, open->frame_mode), open->frame_mode, automatic_low_latency); ++scene_generation; if (scene->frame_mode() == Gallery_Frame_Mode::Low_Latency) ensure_render_worker(); return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::case_json(scene->case_id(), scene->state(), scene->telemetry_json(), "render_2D/Kernel 独立画布已创建", scene->frame_mode()) }; } if (!scene) return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::error_json("请先发送 gallery_open") }; if (request.kind == Gallery_Request_Kind::Observe) { if (update_client_metrics(request.message)) { ++scheduler_revision; scheduler_condition.notify_all(); } return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::observer_json( scene->case_id(), scene->frame_mode(), scene->telemetry_json()) }; } if (request.kind == Gallery_Request_Kind::Patch) { auto patch = Gallery_Protocol::apply_patch(scene->case_id(), scene->state(), request.message, scene->frame_mode()); if (!patch.candidate) return Web_Response{Web_Response_Type::Json, std::move(patch.response_json)}; if (scene->requires_rebuild(*patch.candidate)) { const std::string id = scene->case_id(); const auto mode = scene->frame_mode(); scene->remove_primary(); scene = std::make_unique(session_id, id, std::move(*patch.candidate), mode, automatic_low_latency); ++scene_generation; } else { scene->set_state(std::move(*patch.candidate)); } return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::case_json(scene->case_id(), scene->state(), scene->telemetry_json(), "控件 API 已由后端应用", scene->frame_mode()) }; } const auto action = Gallery_Protocol::action_request(request.message); if (!action) return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::error_json("gallery_action 格式无效") }; if (!Gallery_Protocol::action_available(scene->case_id(), scene->frame_mode(), action->id)) return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::error_json( "动作不属于当前画布或帧策略", "action") }; if (action->id == "reset") { const std::string id = scene->case_id(); const auto mode = scene->frame_mode(); scene = std::make_unique( session_id, id, Gallery_Protocol::default_state(id, mode), mode, automatic_low_latency); ++scene_generation; return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::case_json(id, scene->state(), scene->telemetry_json(), "本图已恢复后端默认值", mode) }; } if (action->id == "rebuild_renderable") { const std::string id = scene->case_id(); const auto mode = scene->frame_mode(); Gallery_State state = scene->state(); scene->remove_primary(); scene = std::make_unique(session_id, id, std::move(state), mode, automatic_low_latency); ++scene_generation; return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::case_json(id, scene->state(), scene->telemetry_json(), "Renderable 已移除并按属性重建", mode) }; } bool recognized{}; const std::string notice = scene->action(*action, recognized); if (!recognized) return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::error_json("动作不属于当前画布或参数无效", "action") }; scene->record_action_notice_if_empty(notice); return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::case_json(scene->case_id(), scene->state(), scene->telemetry_json(), notice, scene->frame_mode()) }; } std::optional handle_frame_request() { const auto request_started = std::chrono::steady_clock::now(); std::optional copy; std::uint64_t generation{}; { auto lock = acquire_foreground_lock(); if (!scene) return std::nullopt; generation = scene_generation; copy = scene->copy_latest_pixels(); } if (!copy) return std::nullopt; const auto encode_started = std::chrono::steady_clock::now(); std::string pixels = encode_pixel_frame(copy->image, copy->background); const auto encode_finished = std::chrono::steady_clock::now(); { auto lock = acquire_foreground_lock(); if (scene && scene_generation == generation) scene->record_pixel_response(request_started, encode_started, encode_finished, pixels.size()); } return pixels.empty() ? std::nullopt : std::optional(Web_Response{ Web_Response_Type::Pixels, std::move(pixels) }); } std::optional handle(const Web_Event& event) { if (std::holds_alternative(event)) return handle_frame_request(); const bool reschedule = affects_render_schedule(event); std::optional response; { auto lock = acquire_foreground_lock(); response = std::visit( [this](const auto& value) -> std::optional { using T = std::decay_t; if constexpr (std::is_same_v) { return handle_gallery(value); } else if constexpr (std::is_same_v) { return std::nullopt; } else if constexpr (std::is_same_v) { if (scene) scene->resize(value.size); } else if constexpr (std::is_same_v) { if (scene) scene->dispatch(value); } else if constexpr (std::is_base_of_v) { if (scene) scene->dispatch(value); } return std::nullopt; }, event); if (reschedule) ++scheduler_revision; } if (reschedule) scheduler_condition.notify_all(); return response; } }; Gallery_Plot_Session::Gallery_Plot_Session() : Gallery_Plot_Session(false) {} Gallery_Plot_Session::Gallery_Plot_Session(bool automatic_low_latency) : impl_(std::make_unique(automatic_low_latency)) {} Gallery_Plot_Session::~Gallery_Plot_Session() = default; std::optional Gallery_Plot_Session::handle(const Web_Event& event) { return impl_->handle(event); } } // namespace renderive::web