1976 lines
99 KiB
C++
1976 lines
99 KiB
C++
#include "Gallery_Plot_Session.h"
|
|
#include "Gallery_Protocol.h"
|
|
#include "Pixel_Frame.h"
|
|
#include "Web_Performance_Log.h"
|
|
#include "render_2D/export.h"
|
|
#include <nlohmann/json.hpp>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <condition_variable>
|
|
#include <cstdint>
|
|
#include <deque>
|
|
#include <limits>
|
|
#include <mutex>
|
|
#include <numbers>
|
|
#include <random>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
namespace renderive::web {
|
|
namespace {
|
|
template <class T>
|
|
const T& state_value(const Gallery_State& state, std::string_view id) {
|
|
return std::get<T>(state.values.at(std::string(id)));
|
|
}
|
|
double number_value(const Gallery_State& state, std::string_view id) {
|
|
return state_value<double>(state, id);
|
|
}
|
|
std::uint64_t milliseconds_to_ns(double milliseconds) {
|
|
return milliseconds > 0.0 ? static_cast<std::uint64_t>(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::uint64_t>(std::llround(1'000'000'000.0 / frequency_hz)) : 0;
|
|
}
|
|
int integer_value(const Gallery_State& state, std::string_view id) {
|
|
return static_cast<int>(std::lround(number_value(state, id)));
|
|
}
|
|
bool bool_value(const Gallery_State& state, std::string_view id) {
|
|
return state_value<bool>(state, id);
|
|
}
|
|
const std::string& string_value(const Gallery_State& state, std::string_view id) {
|
|
return state_value<std::string>(state, id);
|
|
}
|
|
bool has_value(const Gallery_State& state, std::string_view id) {
|
|
return state.values.contains(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<std::uint8_t>(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::uint8_t>(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<Color, 5> 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<Pixel> 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::size_t>(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::milliseconds>(
|
|
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<double> action_number(const Gallery_Action_Request& request) {
|
|
if (!request.argument || !std::holds_alternative<double>(*request.argument))
|
|
return std::nullopt;
|
|
return std::get<double>(*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) {
|
|
const auto current = state_.values.find(id);
|
|
const auto next = candidate.values.find(id);
|
|
return current != state_.values.end() && next != candidate.values.end() &&
|
|
current->second != next->second;
|
|
};
|
|
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<Gallery_Pixel_Copy> 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<Gallery_Pixel_Copy>(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<int>(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<int>(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 (request.id == "rebind_axes") {
|
|
spectrum_->set_frequency_axis(spectrum_->frequency_axis());
|
|
spectrum_->set_power_axis(spectrum_->power_axis());
|
|
return "Spectrum 的 shared_ptr 轴已回读并重新绑定";
|
|
}
|
|
}
|
|
if (selection_) {
|
|
if (request.id == "clear_selection") {
|
|
selection_->clear_selected_regions();
|
|
return "框选区域已清空";
|
|
}
|
|
if (request.id == "rebind_selection_axes") {
|
|
selection_->set_horizontal_axis(numeric_domain_axis_);
|
|
selection_->set_vertical_axis(value_axis_);
|
|
return "Selection Overlay 的两根轴已重新绑定";
|
|
}
|
|
}
|
|
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_->frequency_bin_count(),
|
|
waterfall_->power_range()));
|
|
return "Waterfall 已通过 int tick 重载追加一行";
|
|
}
|
|
if (request.id == "rebind_axes") {
|
|
last_action_result_ = waterfall_->frequency_axis() && waterfall_->time_axis()
|
|
? "frequency_axis/time_axis valid"
|
|
: "axis invalid";
|
|
return "Waterfall 轴绑定已回读";
|
|
}
|
|
}
|
|
if (afterglow_) {
|
|
if (request.id == "append_spectrum") {
|
|
push_afterglow_spectrum();
|
|
return "Afterglow 频谱已手动追加";
|
|
}
|
|
if (request.id == "rebind_axes") {
|
|
last_action_result_ = afterglow_->frequency_axis() && afterglow_->power_axis()
|
|
? "frequency_axis/power_axis valid"
|
|
: "axis invalid";
|
|
return "Afterglow 轴绑定已回读";
|
|
}
|
|
}
|
|
if (sweep_) {
|
|
if (request.id == "append_block") {
|
|
push_sweep_block();
|
|
return "Sweep_Spectrum 扫频块已手动追加";
|
|
}
|
|
if (request.id == "rebind_axes") {
|
|
last_action_result_ = sweep_->frequency_axis() && sweep_->power_axis()
|
|
? "frequency_axis/power_axis valid"
|
|
: "axis invalid";
|
|
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 (request.id == "read_axes") {
|
|
last_action_result_ = trace_->time_axis() && trace_->value_axis()
|
|
? "time_axis/value_axis valid"
|
|
: "axis invalid";
|
|
return "Frequency_Trace 轴绑定已回读";
|
|
}
|
|
}
|
|
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 已按轴拟合正方形";
|
|
}
|
|
if (request.id == "read_axes") {
|
|
last_action_result_ = constellation_->i_axis() && constellation_->q_axis()
|
|
? "i_axis/q_axis valid"
|
|
: "axis invalid";
|
|
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<double>(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<double>(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<double>(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<double>(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<double>(observer.paint_duration_ns) / 1e6},
|
|
{"kernel_render_ms", static_cast<double>(observer.render_duration_ns) / 1e6},
|
|
{"kernel_bottleneck_ms", static_cast<double>(observer.bottleneck_duration_ns) / 1e6},
|
|
{"kernel_end_to_end_ms", static_cast<double>(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<double>(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_->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_->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_->frequency_point_size()},
|
|
{"configured_power_points", afterglow_->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_->bins_per_block()},
|
|
{"configured_block_count", sweep_->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<std::size_t>(anchor_count)
|
|
},
|
|
{"point_lifetime_ms", constellation_->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<Performance_Clock::time_point>& 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<Performance_Clock::time_point>& 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<double>(history.back() - history.front()).count();
|
|
return seconds > 0.0 ? static_cast<double>(history.size() - 1) / seconds : 0.0;
|
|
}
|
|
static void record_pixel_sample(std::deque<Pixel_Performance_Sample>& 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<Pixel_Performance_Sample>& 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<double>(history.back().time - history.front().time).count();
|
|
return seconds > 0.0 ? static_cast<double>(history.size() - 1) / seconds : 0.0;
|
|
}
|
|
static double recent_pixel_megabytes_per_second(
|
|
const std::deque<Pixel_Performance_Sample>& 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<double>(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<double>(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<double, std::milli>(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<double, std::milli>(encode_started - request_started).count();
|
|
last_pixel_encode_ms_ =
|
|
std::chrono::duration<double, std::milli>(encode_finished - encode_started).count();
|
|
last_pixel_request_ms_ =
|
|
std::chrono::duration<double, std::milli>(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::milliseconds>(
|
|
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<double>(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(data_node_,
|
|
std::static_pointer_cast<Frequency_Axis>(numeric_domain_axis_),
|
|
value_axis_)
|
|
.set_frequency_range({
|
|
number_value(state_, "frequency_origin"),
|
|
number_value(state_, "frequency_target")
|
|
})
|
|
.set_frequency_point_size(integer_value(state_, "frequency_point_size"))
|
|
.set_center_frequency(number_value(state_, "center_frequency"))
|
|
.set_sweep_frequency_range({
|
|
number_value(state_, "sweep_origin"),
|
|
number_value(state_, "sweep_target")
|
|
})
|
|
.build();
|
|
primary_ = spectrum_;
|
|
if (case_id_ == "selection_overlay") {
|
|
selection_ = Selection_Rectangle_Overlay::Builder(
|
|
overlay_node_, numeric_domain_axis_, value_axis_)
|
|
.build();
|
|
primary_ = selection_;
|
|
}
|
|
return;
|
|
}
|
|
if (case_id_ == "waterfall") {
|
|
waterfall_ = Waterfall::Builder(data_node_,
|
|
std::static_pointer_cast<Frequency_Axis>(numeric_domain_axis_),
|
|
time_axis_)
|
|
.set_frequency_range({
|
|
number_value(state_, "frequency_origin"),
|
|
number_value(state_, "frequency_target")
|
|
})
|
|
.set_power_range({
|
|
number_value(state_, "power_origin"),
|
|
number_value(state_, "power_target")
|
|
})
|
|
.set_frequency_bin_count(integer_value(state_, "frequency_bin_count"))
|
|
.set_color_map(gallery_color_map(string_value(state_, "color_map")))
|
|
.build();
|
|
primary_ = waterfall_;
|
|
return;
|
|
}
|
|
if (case_id_ == "afterglow") {
|
|
afterglow_ = Afterglow::Builder(data_node_,
|
|
std::static_pointer_cast<Frequency_Axis>(numeric_domain_axis_),
|
|
value_axis_)
|
|
.set_frequency_range({
|
|
number_value(state_, "frequency_origin"),
|
|
number_value(state_, "frequency_target")
|
|
})
|
|
.set_power_range({
|
|
number_value(state_, "power_origin"),
|
|
number_value(state_, "power_target")
|
|
})
|
|
.set_frequency_bin_count(integer_value(state_, "frequency_point_size"))
|
|
.set_power_bin_count(integer_value(state_, "power_point_size"))
|
|
.set_color_map(gallery_color_map(string_value(state_, "color_map")))
|
|
.build();
|
|
primary_ = afterglow_;
|
|
return;
|
|
}
|
|
if (case_id_ == "sweep_spectrum") {
|
|
sweep_ = Sweep_Spectrum::Builder(data_node_, numeric_domain_axis_, value_axis_)
|
|
.set_frequency_range({
|
|
number_value(state_, "frequency_origin"),
|
|
number_value(state_, "frequency_target")
|
|
})
|
|
.set_bins_per_block(integer_value(state_, "bins_per_block"))
|
|
.set_block_num(integer_value(state_, "block_count"))
|
|
.build();
|
|
primary_ = sweep_;
|
|
return;
|
|
}
|
|
if (case_id_ == "frequency_trace") {
|
|
trace_ = Frequency_Trace::Builder(data_node_, time_axis_, value_axis_)
|
|
.set_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();
|
|
primary_ = trace_;
|
|
return;
|
|
}
|
|
if (case_id_ == "constellation") {
|
|
constellation_ = Constellation_Diagram::Builder(data_node_, numeric_domain_axis_, value_axis_)
|
|
.set_i_range({
|
|
number_value(state_, "i_origin"),
|
|
number_value(state_, "i_target")
|
|
})
|
|
.set_q_range({
|
|
number_value(state_, "q_origin"),
|
|
number_value(state_, "q_target")
|
|
})
|
|
.set_type(constellation_mode(string_value(state_, "constellation_type")))
|
|
.set_phase_offset_radians(number_value(state_, "phase_offset"))
|
|
.build();
|
|
primary_ = constellation_;
|
|
}
|
|
}
|
|
std::shared_ptr<Abs_Axis> horizontal_axis() const {
|
|
return numeric_domain_axis_
|
|
? std::static_pointer_cast<Abs_Axis>(numeric_domain_axis_)
|
|
: std::static_pointer_cast<Abs_Axis>(time_axis_);
|
|
}
|
|
std::shared_ptr<Abs_Axis> 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::size_t>(
|
|
std::max(1, static_cast<int>(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::size_t>(
|
|
std::max(1, static_cast<int>(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<std::size_t>(width));
|
|
}
|
|
}
|
|
void apply_axis_style(const std::shared_ptr<Abs_Axis>& 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_frequency_point_size(integer_value(state_, "frequency_point_size"));
|
|
spectrum_->set_frequency_range({
|
|
number_value(state_, "frequency_origin"),
|
|
number_value(state_, "frequency_target")
|
|
});
|
|
spectrum_->set_center_frequency(number_value(state_, "center_frequency"));
|
|
spectrum_->set_sweep_frequency_range({
|
|
number_value(state_, "sweep_origin"),
|
|
number_value(state_, "sweep_target")
|
|
});
|
|
spectrum_->set_max_hold_visible(bool_value(state_, "max_hold_visible"));
|
|
spectrum_->set_min_hold_visible(bool_value(state_, "min_hold_visible"));
|
|
spectrum_->set_max_marker_visible(bool_value(state_, "max_marker_visible"));
|
|
spectrum_->set_use_min_marker(bool_value(state_, "use_min_marker"));
|
|
spectrum_->set_sweep_region_visible(bool_value(state_, "sweep_region_visible"));
|
|
spectrum_->set_visible_range_only(bool_value(state_, "visible_range_only"));
|
|
spectrum_->set_interpolation_mode(line_mode(string_value(state_, "line_interpolation")));
|
|
spectrum_->set_max_brush({color_from_hex(string_value(state_, "max_brush"), 76), Brush_Style::Solid});
|
|
spectrum_->set_current_brush({color_from_hex(string_value(state_, "current_brush"), 76), Brush_Style::Solid});
|
|
spectrum_->set_min_brush({color_from_hex(string_value(state_, "min_brush"), 76), Brush_Style::Solid});
|
|
spectrum_->set_max_pen({color_from_hex(string_value(state_, "max_pen")), 1.2});
|
|
spectrum_->set_current_pen({
|
|
color_from_hex(string_value(state_, "current_pen")), 2.0,
|
|
Line_Style::Solid, Line_Cap::Round, Line_Join::Round
|
|
});
|
|
spectrum_->set_min_pen({color_from_hex(string_value(state_, "min_pen")), 1.2});
|
|
spectrum_->set_selected_marker_pen({color_from_hex(string_value(state_, "selected_marker_pen")), 2.0});
|
|
spectrum_->set_marker_pen({color_from_hex(string_value(state_, "marker_pen")), 1.2});
|
|
spectrum_->set_middle_frequency_pen({
|
|
color_from_hex(string_value(state_, "middle_frequency_pen")),
|
|
1.0, Line_Style::Dash
|
|
});
|
|
spectrum_->set_sweep_region_brush({
|
|
color_from_hex(string_value(state_, "sweep_region_brush"), 80),
|
|
Brush_Style::Solid
|
|
});
|
|
spectrum_->set_use_hover_info(bool_value(state_, "hover_enabled"));
|
|
spectrum_->set_hover_tooltip_font({number_value(state_, "hover_font_size"), 500, false});
|
|
spectrum_->set_tooltip_text_pen({color_from_hex(string_value(state_, "hover_text_color"))});
|
|
spectrum_->set_hover_tooltip_background_brush(
|
|
{color_from_hex(string_value(state_, "hover_background"), 230), Brush_Style::Solid});
|
|
}
|
|
if (selection_) {
|
|
selection_->set_label_font({number_value(state_, "selection_font_size"), 500, false});
|
|
selection_->set_label_pen({color_from_hex(string_value(state_, "selection_label_pen"))});
|
|
selection_->set_selection_brush({
|
|
color_from_hex(string_value(state_, "selection_brush"), 70),
|
|
Brush_Style::Solid
|
|
});
|
|
selection_->set_selection_border_pen({
|
|
color_from_hex(string_value(state_, "selection_border")),
|
|
1.0, Line_Style::Dash
|
|
});
|
|
}
|
|
if (waterfall_) {
|
|
waterfall_->set_frequency_range({
|
|
number_value(state_, "frequency_origin"),
|
|
number_value(state_, "frequency_target")
|
|
});
|
|
waterfall_->set_power_range({
|
|
number_value(state_, "power_origin"),
|
|
number_value(state_, "power_target")
|
|
});
|
|
waterfall_->set_frequency_bin_count(integer_value(state_, "frequency_bin_count"));
|
|
waterfall_->set_visible_range_only(bool_value(state_, "visible_range_only"));
|
|
waterfall_->set_interpolation_mode(image_mode(string_value(state_, "image_interpolation")));
|
|
waterfall_->set_use_hover_info(bool_value(state_, "hover_enabled"));
|
|
waterfall_->set_hover_tooltip_font({number_value(state_, "hover_font_size"), 500, false});
|
|
waterfall_->set_tooltip_text_pen({color_from_hex(string_value(state_, "hover_text_color"))});
|
|
waterfall_->set_hover_tooltip_background_brush(
|
|
{color_from_hex(string_value(state_, "hover_background"), 230), Brush_Style::Solid});
|
|
}
|
|
if (afterglow_) {
|
|
afterglow_->set_frequency_range({
|
|
number_value(state_, "frequency_origin"),
|
|
number_value(state_, "frequency_target")
|
|
});
|
|
afterglow_->set_power_range({
|
|
number_value(state_, "power_origin"),
|
|
number_value(state_, "power_target")
|
|
});
|
|
afterglow_->set_frequency_point_size(integer_value(state_, "frequency_point_size"));
|
|
afterglow_->set_power_point_size(integer_value(state_, "power_point_size"));
|
|
afterglow_->set_interpolate(bool_value(state_, "interpolate_power"));
|
|
afterglow_->set_attenuation_rate(number_value(state_, "attenuation_rate"));
|
|
}
|
|
if (sweep_) {
|
|
sweep_->set_frequency_range({
|
|
number_value(state_, "frequency_origin"),
|
|
number_value(state_, "frequency_target")
|
|
});
|
|
sweep_->set_bins_per_block(integer_value(state_, "bins_per_block"));
|
|
sweep_->set_block_count(integer_value(state_, "block_count"));
|
|
sweep_->set_pen({color_from_hex(string_value(state_, "sweep_pen")), 1.8});
|
|
sweep_->set_cur_frequency_pen({color_from_hex(string_value(state_, "current_frequency_pen")), 2.0});
|
|
sweep_->set_visible_range_only(bool_value(state_, "visible_range_only"));
|
|
sweep_->set_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_i_range({number_value(state_, "i_origin"), number_value(state_, "i_target")});
|
|
constellation_->set_q_range({number_value(state_, "q_origin"), number_value(state_, "q_target")});
|
|
constellation_->set_point_color(color_from_hex(string_value(state_, "point_color")));
|
|
constellation_->set_anchor_color(color_from_hex(string_value(state_, "anchor_color")));
|
|
constellation_->set_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<double> spectrum_values(int count, Range power_range = {-120.0, -20.0}) const {
|
|
count = std::max(count, 2);
|
|
std::vector<double> values(static_cast<std::size_t>(count));
|
|
const double time = static_cast<double>(frame_index_) * 0.09;
|
|
for (int index = 0; index < count; ++index) {
|
|
const double x = static_cast<double>(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<std::size_t>(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_->frequency_point_size()));
|
|
}
|
|
void push_waterfall_row() {
|
|
waterfall_->append_row(current_time_of_day(),
|
|
spectrum_values(waterfall_->frequency_bin_count(), waterfall_->power_range()));
|
|
}
|
|
void push_afterglow_spectrum() {
|
|
afterglow_->append_spectrum(spectrum_values(afterglow_->frequency_point_size(),
|
|
afterglow_->power_range()));
|
|
}
|
|
void push_sweep_block() {
|
|
const int count = sweep_->bins_per_block();
|
|
std::vector<double> block(static_cast<std::size_t>(std::max(count, 1)));
|
|
for (int index = 0; index < count; ++index) {
|
|
const double x = static_cast<double>(index) / std::max(1, count - 1);
|
|
block[static_cast<std::size_t>(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<int>((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<Renderable> root_;
|
|
std::shared_ptr<Renderable> axes_node_;
|
|
std::shared_ptr<Renderable> data_node_;
|
|
std::shared_ptr<Renderable> overlay_node_;
|
|
std::shared_ptr<Axis> numeric_domain_axis_;
|
|
std::shared_ptr<Axis> value_axis_;
|
|
std::shared_ptr<Time_Axis> time_axis_;
|
|
std::shared_ptr<Renderable> primary_;
|
|
std::shared_ptr<Spectrum> spectrum_;
|
|
std::shared_ptr<Waterfall> waterfall_;
|
|
std::shared_ptr<Frequency_Trace> trace_;
|
|
std::shared_ptr<Afterglow> afterglow_;
|
|
std::shared_ptr<Sweep_Spectrum> sweep_;
|
|
std::shared_ptr<Selection_Rectangle_Overlay> selection_;
|
|
std::shared_ptr<Constellation_Diagram> 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<Performance_Clock::time_point> 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_Performance_Sample> 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<std::uint64_t> 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<double>();
|
|
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<std::uint64_t>();
|
|
}
|
|
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<std::int64_t>();
|
|
return value > 0 ? static_cast<std::uint64_t>(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<std::mutex> 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<Clock::time_point> last_render_started;
|
|
std::uint64_t active_generation = std::numeric_limits<std::uint64_t>::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<std::uint64_t>(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::nanoseconds>(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<decltype(value)>;
|
|
if constexpr (std::is_same_v<T, Frame_Request>) {
|
|
return false;
|
|
}
|
|
else if constexpr (std::is_same_v<T, Gallery_Request>) {
|
|
return value.kind != Gallery_Request_Kind::Catalog &&
|
|
value.kind != Gallery_Request_Kind::Observe;
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
},
|
|
event);
|
|
}
|
|
std::unique_ptr<Gallery_Scene> 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<std::uint32_t> foreground_waiters{};
|
|
std::jthread render_worker;
|
|
std::optional<Web_Response> 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<Gallery_Scene>(
|
|
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<Gallery_Scene>(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<Gallery_Scene>(
|
|
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<Gallery_Scene>(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 已移除并通过 Builder 重建", 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<Web_Response> handle_frame_request() {
|
|
const auto request_started = std::chrono::steady_clock::now();
|
|
std::optional<Gallery_Pixel_Copy> 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{
|
|
Web_Response_Type::Pixels, std::move(pixels)
|
|
});
|
|
}
|
|
std::optional<Web_Response> handle(const Web_Event& event) {
|
|
if (std::holds_alternative<Frame_Request>(event))
|
|
return handle_frame_request();
|
|
const bool reschedule = affects_render_schedule(event);
|
|
std::optional<Web_Response> response;
|
|
{
|
|
auto lock = acquire_foreground_lock();
|
|
response = std::visit(
|
|
[this](const auto& value) -> std::optional<Web_Response> {
|
|
using T = std::decay_t<decltype(value)>;
|
|
if constexpr (std::is_same_v<T, Gallery_Request>) {
|
|
return handle_gallery(value);
|
|
}
|
|
else if constexpr (std::is_same_v<T, Frame_Request>) {
|
|
return std::nullopt;
|
|
}
|
|
else if constexpr (std::is_same_v<T, Viewport_Resize>) {
|
|
if (scene)
|
|
scene->resize(value.size);
|
|
}
|
|
else if constexpr (std::is_same_v<T, Event>) {
|
|
if (scene)
|
|
scene->dispatch(value);
|
|
}
|
|
else if constexpr (std::is_base_of_v<Event, T>) {
|
|
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<Impl>(automatic_low_latency)) {}
|
|
Gallery_Plot_Session::~Gallery_Plot_Session() = default;
|
|
std::optional<Web_Response> Gallery_Plot_Session::handle(const Web_Event& event) {
|
|
return impl_->handle(event);
|
|
}
|
|
} // namespace renderive::web
|