Files
Renderive/web_server/app/Gallery_Protocol.cpp
T
2026-08-11 16:34:24 +08:00

1180 lines
69 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Gallery_Protocol.h"
#include "adminive/adminive.hpp"
#include "adminive/adapters/nlohmann_json.hpp"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <utility>
#include <vector>
namespace renderive::web::gallery_detail {
using Json = nlohmann::json;
struct Case_Model {
std::string id;
std::string title;
std::string component;
std::string category;
std::string description;
int order{};
int preferred_width = 560;
int preferred_height = 320;
};
struct Control_Model {
std::string id;
std::string label;
std::string api;
std::string description;
std::string group;
std::string input;
bool integer{};
double minimum{};
double maximum{};
double step{};
};
struct Action_Model {
std::string id;
std::string label;
std::string api;
std::string description;
std::string group;
std::string argument_input;
std::string argument_label;
double argument_default{};
bool request_frame{};
};
struct Control_Definition {
Control_Model model;
Gallery_Value initial;
std::vector<std::string> options;
};
struct Action_Definition {
Action_Model model;
};
const std::vector<Case_Model>& cases() {
static const std::vector<Case_Model> value{
{"axis_lab", "坐标轴实验室", "Axis / Frequency_Axis / Time_Axis", "基础控件",
"普通轴、频率轴和时间轴的布局、格式化、滚轮与拖拽 API。", 10},
{"spectrum", "实时频谱", "Spectrum", "三种频率图",
"频率控制模式 1:曲线、保持线、扫频区、峰值和自定义 Marker。", 20},
{"afterglow", "余辉频谱", "Afterglow", "三种频率图",
"频率控制模式 2:二维功率密度、衰减和功率 bin 插值。", 30},
{"sweep_spectrum", "扫频频谱", "Sweep_Spectrum", "三种频率图",
"频率控制模式 3:分块扫频、当前频率游标和六种线插值。", 40},
{"waterfall", "瀑布图", "Waterfall", "热力图控件",
"逐行时频热力图;Nearest、Bilinear、Bicubic 三种图像模式均可切换。", 50},
{"frequency_trace", "时间轨迹", "Frequency_Trace", "曲线控件",
"Time_Axis 驱动的连续轨迹,覆盖时间格式和 Builder 笔刷 API。", 60},
{"selection_overlay", "框选叠层", "Selection_Rectangle_Overlay", "交互控件",
"鼠标框选、多区域保留、标注样式、清空与轴重新绑定。", 70},
{"constellation", "星座图", "Constellation_Diagram", "点图控件",
"PSK4/PSK8/PSK16 Builder 模式、相位、寿命、坐标范围和方形拟合。", 80}
};
return value;
}
Control_Definition flag(std::string id, std::string label, std::string api,
bool initial, std::string group, std::string description = {}) {
return {{std::move(id), std::move(label), std::move(api), std::move(description),
std::move(group), "boolean"}, initial, {}};
}
Control_Definition number(std::string id, std::string label, std::string api,
double initial, double minimum, double maximum, double step,
std::string group, bool integer = false,
std::string description = {}) {
return {{std::move(id), std::move(label), std::move(api), std::move(description),
std::move(group), "number", integer, minimum, maximum, step}, initial, {}};
}
Control_Definition text_control(std::string id, std::string label, std::string api,
std::string initial, std::string group,
std::string description = {}) {
return {{std::move(id), std::move(label), std::move(api), std::move(description),
std::move(group), "text"}, std::move(initial), {}};
}
Control_Definition color(std::string id, std::string label, std::string api,
std::string initial, std::string group,
std::string description = {}) {
auto result = text_control(std::move(id), std::move(label), std::move(api),
std::move(initial), std::move(group), std::move(description));
result.model.input = "color";
return result;
}
Control_Definition select(std::string id, std::string label, std::string api,
std::string initial, std::vector<std::string> options,
std::string group, std::string description = {}) {
return {{std::move(id), std::move(label), std::move(api), std::move(description),
std::move(group), "select"}, std::move(initial), std::move(options)};
}
void append_common(std::vector<Control_Definition>& result, Gallery_Frame_Mode frame_mode) {
result.push_back(color("background_color", "背景颜色", "Plot_Core::set_background_color",
"#07111f", "Plot_Core"));
if (frame_mode == Gallery_Frame_Mode::Low_Latency) {
result.push_back(flag("frequency_limit_enabled", "启用频率上限",
"Plot_Core::set_max_render_fps / clear_max_render_fps", true,
"Low_Latency_Strategy", "关闭后用户频率不参与调度。"));
result.push_back(number("max_render_fps", "最大渲染 FPS", "Plot_Core::set_max_render_fps",
30, 0.01, 1'000'000'000, 1, "Low_Latency_Strategy", false,
"仅在启用频率上限时参与调度。"));
result.push_back(flag("consumer_feedback_enabled", "启用消费者反馈",
"Plot_Core::set_consumer_feedback / clear_consumer_feedback", true,
"Low_Latency_Strategy", "消费者反馈总开关;关闭后所有消费者来源都不参与调度。"));
result.push_back(flag("consumer_pixel_feedback_enabled", "启用像素响应反馈",
"Gallery_Plot_Session::set_client_metrics", false,
"Low_Latency_Strategy", "启用后按浏览器实际收到像素帧的 FPS 计算消费者周期。"));
result.push_back(flag("consumer_presentation_feedback_enabled", "启用浏览器呈现反馈",
"Gallery_Plot_Session::set_client_metrics", true,
"Low_Latency_Strategy", "启用后按浏览器 RAF 中位周期计算消费者周期。"));
result.push_back(flag("consumer_manual_feedback_enabled", "启用手动消费者反馈",
"Gallery_Plot_Session::apply_consumer_feedback", false,
"Low_Latency_Strategy", "启用后手动消费者 FPS 参与消费者限速。"));
result.push_back(number("consumer_manual_fps", "手动消费者 FPS",
"Gallery_Plot_Session::apply_consumer_feedback",
60, 0.01, 1'000'000'000, 1, "Low_Latency_Strategy", false,
"仅在启用手动消费者反馈时参与调度。"));
}
result.push_back(flag("performance_overlay", "性能叠层", "set_performance_overlay_enabled",
false, "Plot_Core"));
result.push_back(flag("renderable_visible", "控件可见", "Renderable::set_visible",
true, "Renderable"));
result.push_back(select("cache_mode", "缓存模式", "Renderable::set_cache_mode", "Local_Pixel",
{"Direct", "Local_Pixel"}, "Renderable"));
result.push_back(text_control("object_name", "对象名称", "Renderable::set_object_name",
"Gallery_Renderable", "Renderable"));
}
void append_axis(std::vector<Control_Definition>& result) {
result.push_back(select("axis_orientation", "主轴方向", "Abs_Axis::set_orientation", "Horizontal",
{"Horizontal", "Vertical"}, "Abs_Axis"));
result.push_back(number("axis_x_offset", "X 偏移", "Abs_Axis::set_x", 0, -160, 160, 1,
"Abs_Axis", true));
result.push_back(number("axis_y_offset", "Y 偏移", "Abs_Axis::set_y", 0, -160, 160, 1,
"Abs_Axis", true));
result.push_back(number("axis_length_percent", "轴长百分比", "Abs_Axis::set_pixel_length", 100,
25, 125, 1, "Abs_Axis", true));
result.push_back(number("tick_length", "主刻度长度", "Abs_Axis::set_tick_length", 8,
-24, 24, 1, "Abs_Axis", true));
result.push_back(number("sub_tick_length", "次刻度长度", "Abs_Axis::set_sub_tick_length", 4,
-16, 16, 1, "Abs_Axis", true));
result.push_back(color("axis_color", "轴颜色", "Abs_Axis::set_color", "#7691b8", "Abs_Axis"));
result.push_back(select("decimal_separator", "小数点", "Abs_Axis::set_locale", ".",
{".", ","}, "Abs_Axis"));
result.push_back(text_control("unit_text", "单位文本", "Abs_Axis::set_unit_text", "Hz", "Abs_Axis"));
result.push_back(number("unit_font_size", "单位字号", "Abs_Axis::set_unit_text_font", 12,
6, 36, 1, "Abs_Axis"));
result.push_back(number("unit_font_weight", "单位字重", "Abs_Axis::set_unit_text_font", 500,
100, 900, 100, "Abs_Axis", true));
result.push_back(flag("unit_font_italic", "单位斜体", "Abs_Axis::set_unit_text_font", false,
"Abs_Axis"));
result.push_back(color("unit_pen_color", "单位文字颜色", "Abs_Axis::set_unit_text_pen",
"#dce9ff", "Abs_Axis"));
result.push_back(color("unit_background_color", "单位背景颜色",
"Abs_Axis::set_unit_text_background_brush", "#07111f", "Abs_Axis"));
result.push_back(number("label_rotation", "标签旋转角度", "Abs_Axis::set_label_rotation_degrees",
0, -90, 90, 1, "Abs_Axis", true));
result.push_back(number("coord_origin", "坐标起点", "Axis::set_coord_range", 88'000'000,
-1e10, 1e10, 1000, "Axis"));
result.push_back(number("coord_target", "坐标终点", "Axis::set_coord_range", 108'000'000,
-1e10, 1e10, 1000, "Axis"));
result.push_back(number("label_precision", "标签精度", "Axis::set_label_precision", 2,
0, 9, 1, "Axis", true));
result.push_back(flag("use_wheel", "启用滚轮缩放", "Axis::set_use_wheel", true, "Axis"));
result.push_back(flag("use_drag", "启用拖拽平移", "Axis::set_use_drag", true, "Axis"));
}
void append_time_axis(std::vector<Control_Definition>& result) {
result.push_back(number("time_visible_count", "可见时间点", "Time_Axis::set_visible_time_point_count",
80, 8, 400, 1, "Time_Axis", true));
result.push_back(number("time_label_spacing", "时间标签间距", "Time_Axis::set_tick_label_spacing_px",
28, 0, 120, 1, "Time_Axis", true));
result.push_back(text_control("time_format", "时间格式", "Time_Axis::set_time_format",
"mm:ss.zzz", "Time_Axis"));
result.push_back(number("time_font_size", "时间字体", "Time_Axis::set_font", 11,
6, 32, 1, "Time_Axis"));
result.push_back(flag("time_newest_at_start", "最新数据位于轴起点",
"Time_Axis::set_newest_at_axis_start", false, "Time_Axis"));
}
void append_hover(std::vector<Control_Definition>& result) {
result.push_back(flag("hover_enabled", "悬浮信息", "Hover_Tooltip_Mixin::set_use_hover_info",
true, "Hover Tooltip"));
result.push_back(number("hover_font_size", "悬浮字体", "Hover_Tooltip_Mixin::set_hover_tooltip_font",
12, 6, 32, 1, "Hover Tooltip"));
result.push_back(color("hover_text_color", "悬浮文字", "Hover_Tooltip_Mixin::set_tooltip_text_pen",
"#ffffff", "Hover Tooltip"));
result.push_back(color("hover_background", "悬浮背景",
"Hover_Tooltip_Mixin::set_hover_tooltip_background_brush",
"#111827", "Hover Tooltip"));
}
std::vector<Control_Definition> controls(std::string_view case_id,
Gallery_Frame_Mode frame_mode) {
std::vector<Control_Definition> result;
append_common(result, frame_mode);
append_axis(result);
const auto set_initial = [&result](std::string_view id, Gallery_Value value) {
const auto found = std::find_if(result.begin(), result.end(), [id](const auto& item) {
return item.model.id == id;
});
if (found != result.end())
found->initial = std::move(value);
};
if (case_id == "sweep_spectrum") {
set_initial("coord_origin", 0.0);
set_initial("coord_target", 300.0);
set_initial("unit_text", std::string("MHz"));
} else if (case_id == "frequency_trace") {
set_initial("coord_origin", -1.0);
set_initial("coord_target", 1.0);
set_initial("unit_text", std::string("value"));
} else if (case_id == "constellation") {
set_initial("coord_origin", -1.25);
set_initial("coord_target", 1.25);
set_initial("unit_text", std::string("I / Q"));
}
if (case_id == "axis_lab") {
append_time_axis(result);
return result;
}
if (case_id == "spectrum" || case_id == "selection_overlay") {
result.push_back(number("frequency_point_size", "频率点数", "Spectrum::set_frequency_point_size",
512, 16, 4096, 16, "Spectrum", true));
result.push_back(number("frequency_origin", "数据频率起点", "Spectrum::set_frequency_range",
88e6, -1e10, 1e10, 1000, "Spectrum"));
result.push_back(number("frequency_target", "数据频率终点", "Spectrum::set_frequency_range",
108e6, -1e10, 1e10, 1000, "Spectrum"));
result.push_back(number("center_frequency", "中心频率", "Spectrum::set_center_frequency",
98e6, -1e10, 1e10, 1000, "Spectrum"));
result.push_back(number("sweep_origin", "扫频区起点", "Spectrum::set_sweep_frequency_range",
94e6, -1e10, 1e10, 1000, "Spectrum"));
result.push_back(number("sweep_target", "扫频区终点", "Spectrum::set_sweep_frequency_range",
102e6, -1e10, 1e10, 1000, "Spectrum"));
result.push_back(flag("max_hold_visible", "最大保持线", "Spectrum::set_max_hold_visible", true, "Spectrum"));
result.push_back(flag("min_hold_visible", "最小保持线", "Spectrum::set_min_hold_visible", false, "Spectrum"));
result.push_back(flag("max_marker_visible", "峰值 Marker", "Spectrum::set_max_marker_visible", true, "Spectrum"));
result.push_back(flag("use_min_marker", "最小值 Marker", "Spectrum::set_use_min_marker", false, "Spectrum"));
result.push_back(flag("sweep_region_visible", "扫频区域", "Spectrum::set_sweep_region_visible", true, "Spectrum"));
result.push_back(flag("visible_range_only", "仅绘制可见范围", "Spectrum::set_visible_range_only", true, "Spectrum"));
result.push_back(select("line_interpolation", "线插值模式", "Spectrum::set_interpolation_mode",
"Linear_Value", {"Nearest_Sample", "Linear_Value", "Linear_Power_Domain",
"Step_Left", "Step_Right", "Cubic_Value"}, "Spectrum"));
result.push_back(color("max_brush", "最大保持填充", "Spectrum::set_max_brush", "#332810", "Spectrum"));
result.push_back(color("current_brush", "当前曲线填充", "Spectrum::set_current_brush", "#0e3a35", "Spectrum"));
result.push_back(color("min_brush", "最小保持填充", "Spectrum::set_min_brush", "#19213a", "Spectrum"));
result.push_back(color("max_pen", "最大保持线颜色", "Spectrum::set_max_pen", "#ffd166", "Spectrum"));
result.push_back(color("current_pen", "当前曲线颜色", "Spectrum::set_current_pen", "#35e6b2", "Spectrum"));
result.push_back(color("min_pen", "最小保持线颜色", "Spectrum::set_min_pen", "#7aa2ff", "Spectrum"));
result.push_back(color("selected_marker_pen", "选中 Marker", "Spectrum::set_selected_marker_pen", "#ff4d8d", "Spectrum"));
result.push_back(color("marker_pen", "Marker 颜色", "Spectrum::set_marker_pen", "#ff7a59", "Spectrum"));
result.push_back(color("middle_frequency_pen", "中心频率线", "Spectrum::set_middle_frequency_pen", "#6bc8ff", "Spectrum"));
result.push_back(color("sweep_region_brush", "扫频区填充", "Spectrum::set_sweep_region_brush", "#293257", "Spectrum"));
append_hover(result);
if (case_id == "selection_overlay") {
result.push_back(number("selection_font_size", "框选标签字体", "Selection_Rectangle_Overlay::set_label_font",
12, 6, 32, 1, "Selection Overlay"));
result.push_back(color("selection_label_pen", "框选标签颜色", "Selection_Rectangle_Overlay::set_label_pen",
"#ffffff", "Selection Overlay"));
result.push_back(color("selection_brush", "框选填充", "Selection_Rectangle_Overlay::set_selection_brush",
"#173b66", "Selection Overlay"));
result.push_back(color("selection_border", "框选边框", "Selection_Rectangle_Overlay::set_selection_border_pen",
"#7dd3fc", "Selection Overlay"));
}
return result;
}
if (case_id == "waterfall") {
append_time_axis(result);
result.push_back(number("frequency_origin", "频率起点", "Waterfall::set_frequency_range", 88e6,
-1e10, 1e10, 1000, "Waterfall"));
result.push_back(number("frequency_target", "频率终点", "Waterfall::set_frequency_range", 108e6,
-1e10, 1e10, 1000, "Waterfall"));
result.push_back(number("power_origin", "功率起点", "Waterfall::set_power_range", -120,
-240, 120, 1, "Waterfall"));
result.push_back(number("power_target", "功率终点", "Waterfall::set_power_range", -20,
-240, 120, 1, "Waterfall"));
result.push_back(number("frequency_bin_count", "频率 Bin", "Waterfall::set_frequency_bin_count",
256, 16, 2048, 16, "Waterfall", true));
result.push_back(flag("visible_range_only", "仅绘制可见范围", "Waterfall::set_visible_range_only", true, "Waterfall"));
result.push_back(select("image_interpolation", "图像插值(三模式)", "Waterfall::set_interpolation_mode",
"Nearest", {"Nearest", "Bilinear", "Bicubic"}, "Waterfall"));
result.push_back(select("color_map", "颜色映射(重建 Builder",
"Waterfall::Builder::set_color_map / Color_Map::set_colors",
"Spectrum", {"Spectrum", "Ember", "Grayscale"}, "Waterfall"));
append_hover(result);
return result;
}
if (case_id == "afterglow") {
result.push_back(number("frequency_origin", "频率起点", "Afterglow::set_frequency_range", 88e6,
-1e10, 1e10, 1000, "Afterglow"));
result.push_back(number("frequency_target", "频率终点", "Afterglow::set_frequency_range", 108e6,
-1e10, 1e10, 1000, "Afterglow"));
result.push_back(number("power_origin", "功率起点", "Afterglow::set_power_range", -120,
-240, 120, 1, "Afterglow"));
result.push_back(number("power_target", "功率终点", "Afterglow::set_power_range", -20,
-240, 120, 1, "Afterglow"));
result.push_back(number("frequency_point_size", "频率格点", "Afterglow::set_frequency_point_size",
192, 16, 1024, 8, "Afterglow", true));
result.push_back(number("power_point_size", "功率格点", "Afterglow::set_power_point_size",
96, 8, 512, 8, "Afterglow", true));
result.push_back(flag("interpolate_power", "功率 Bin 插值", "Afterglow::set_interpolate", true, "Afterglow"));
result.push_back(number("attenuation_rate", "衰减率", "Afterglow::set_attenuation_rate", 0.18,
0.01, 1, 0.01, "Afterglow"));
result.push_back(select("color_map", "颜色映射(重建 Builder",
"Afterglow::Builder::set_color_map / Color_Map::set_colors",
"Spectrum", {"Spectrum", "Ember", "Grayscale"}, "Afterglow"));
return result;
}
if (case_id == "sweep_spectrum") {
result.push_back(number("frequency_origin", "扫频起点", "Sweep_Spectrum::set_frequency_range", 0,
-1e10, 1e10, 1, "Sweep Spectrum"));
result.push_back(number("frequency_target", "扫频终点", "Sweep_Spectrum::set_frequency_range", 300,
-1e10, 1e10, 1, "Sweep Spectrum"));
result.push_back(number("bins_per_block", "每块 Bin", "Sweep_Spectrum::set_bins_per_block", 64,
2, 1024, 1, "Sweep Spectrum", true));
result.push_back(number("block_count", "块数量", "Sweep_Spectrum::set_block_count", 8,
1, 128, 1, "Sweep Spectrum", true));
result.push_back(color("sweep_pen", "扫频曲线", "Sweep_Spectrum::set_pen", "#ffd166", "Sweep Spectrum"));
result.push_back(color("current_frequency_pen", "当前频率游标", "Sweep_Spectrum::set_cur_frequency_pen",
"#ff5d73", "Sweep Spectrum"));
result.push_back(flag("visible_range_only", "仅绘制可见范围", "Sweep_Spectrum::set_visible_range_only", true, "Sweep Spectrum"));
result.push_back(select("line_interpolation", "线插值模式", "Sweep_Spectrum::set_interpolation_mode",
"Linear_Value", {"Nearest_Sample", "Linear_Value", "Linear_Power_Domain",
"Step_Left", "Step_Right", "Cubic_Value"}, "Sweep Spectrum"));
return result;
}
if (case_id == "frequency_trace") {
append_time_axis(result);
result.push_back(color("trace_pen", "轨迹颜色(重建 Builder", "Frequency_Trace::Builder::set_pen",
"#35e6b2", "Frequency Trace"));
result.push_back(number("trace_pen_width", "轨迹宽度(重建 Builder", "Frequency_Trace::Builder::set_pen",
2, 0.25, 12, 0.25, "Frequency Trace"));
result.push_back(number("trace_value_min", "数值下限", "Axis::set_coord_range", -1,
-1e6, 1e6, 0.1, "Frequency Trace"));
result.push_back(number("trace_value_max", "数值上限", "Axis::set_coord_range", 1,
-1e6, 1e6, 0.1, "Frequency Trace"));
return result;
}
if (case_id == "constellation") {
result.push_back(number("i_origin", "I 起点", "Constellation_Diagram::set_i_range", -1.25,
-1000, 1000, 0.05, "Constellation"));
result.push_back(number("i_target", "I 终点", "Constellation_Diagram::set_i_range", 1.25,
-1000, 1000, 0.05, "Constellation"));
result.push_back(number("q_origin", "Q 起点", "Constellation_Diagram::set_q_range", -1.25,
-1000, 1000, 0.05, "Constellation"));
result.push_back(number("q_target", "Q 终点", "Constellation_Diagram::set_q_range", 1.25,
-1000, 1000, 0.05, "Constellation"));
result.push_back(color("point_color", "采样点颜色", "Constellation_Diagram::set_point_color",
"#49e6c3", "Constellation"));
result.push_back(color("anchor_color", "锚点颜色", "Constellation_Diagram::set_anchor_color",
"#ffd166", "Constellation"));
result.push_back(number("point_lifetime_ms", "采样点寿命", "Constellation_Diagram::set_point_lifetime_ms",
1800, 50, 20000, 50, "Constellation", true));
result.push_back(select("constellation_type", "星座模式(重建 Builder",
"Constellation_Diagram::Builder::set_type", "PSK8",
{"PSK4", "PSK8", "PSK16"}, "Constellation"));
result.push_back(number("phase_offset", "相位偏移(重建 Builder",
"Constellation_Diagram::Builder::set_phase_offset_radians", 0,
-6.283185307, 6.283185307, 0.05, "Constellation"));
return result;
}
return result;
}
Action_Definition action(std::string id, std::string label, std::string api,
std::string group, std::string description = {},
std::string argument_input = {}, std::string argument_label = {},
double argument_default = 0) {
return {{std::move(id), std::move(label), std::move(api), std::move(description),
std::move(group), std::move(argument_input), std::move(argument_label),
argument_default}};
}
Action_Definition frame_action(std::string id, std::string label, std::string api,
std::string group) {
auto result = action(std::move(id), std::move(label), std::move(api), std::move(group));
result.model.request_frame = true;
return result;
}
std::vector<Action_Definition> actions(std::string_view case_id,
Gallery_Frame_Mode frame_mode) {
std::vector<Action_Definition> result{
action("reset", "恢复本图默认值", "Gallery_Scene::rebuild", "会话"),
action("rebuild_renderable", "移除并重建控件", "Plot_Core::remove_renderable / Builder::build", "会话"),
action("toggle_view", "切换 View 生命周期", "Plot_Core::activate_view / deactivate_view", "会话"),
action("axis_set_start", "通过 set_coord_start 应用起点", "Axis::set_coord_start", "Axis"),
action("axis_set_length", "通过 set_coord_length 应用跨度", "Axis::set_coord_length", "Axis"),
action("axis_probe", "读取坐标映射与刻度", "coord_to_pixel / pixel_to_coord / tick_step / sub_tick_count / tick_label", "Axis")
};
if (frame_mode == Gallery_Frame_Mode::Manual) {
result.push_back(action("mode_prepare", "准备帧", "Manual_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame",
"Manual_Refresh_Strategy"));
result.push_back(action("mode_refresh", "提交手动刷新", "Manual_Refresh_Strategy::refresh / Plot_Core::refresh_manual_frame",
"Manual_Refresh_Strategy"));
result.push_back(frame_action("mode_render", "渲染已刷新帧", "Manual_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame",
"Manual_Refresh_Strategy"));
result.push_back(action("mode_discard", "丢弃待刷新帧", "Plot_Core::discard_pending_frame",
"Manual_Refresh_Strategy"));
result.push_back(frame_action("mode_cycle", "执行完整手动帧周期", "Plot_Core::render_frame",
"Manual_Refresh_Strategy"));
} else if (frame_mode == Gallery_Frame_Mode::Low_Latency) {
result.push_back(action("mode_prepare", "发布低延迟帧", "Low_Latency_Strategy::acquire_painter / Plot_Core::prepare_frame",
"Low_Latency_Strategy"));
result.push_back(frame_action("mode_render", "消费最新帧", "Low_Latency_Strategy::acquire_renderer / Plot_Core::render_prepared_frame",
"Low_Latency_Strategy"));
result.push_back(action("mode_discard", "丢弃陈旧帧", "Plot_Core::discard_pending_frame",
"Low_Latency_Strategy"));
result.push_back(frame_action("mode_cycle", "执行低延迟帧周期", "Plot_Core::render_frame",
"Low_Latency_Strategy"));
} else {
result.push_back(action("mode_enqueue", "压入一帧", "Flow_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame",
"Flow_Refresh_Strategy"));
result.push_back(action("mode_enqueue_burst", "批量压入回放队列", "Flow_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame × N",
"Flow_Refresh_Strategy", {}, "number", "帧数", 8));
result.push_back(frame_action("mode_dequeue", "消费队首帧", "Flow_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame",
"Flow_Refresh_Strategy"));
result.push_back(frame_action("mode_cycle", "执行回放帧周期", "Plot_Core::render_frame",
"Flow_Refresh_Strategy"));
}
if (case_id == "axis_lab") {
result.push_back(action("append_time", "追加时间点", "Time_Axis::append_time / tick_to_time", "Time_Axis"));
result.push_back(action("read_data_shape", "读取轴采样点状态", "Abs_Axis::pixel_sample_count / Time_Axis::time_point_count", "观察者"));
} else if (case_id == "spectrum" || case_id == "selection_overlay") {
result.push_back(action("push_samples", "推送一帧样本", "Spectrum::update_samples", "Spectrum"));
result.push_back(action("power_at", "查询指定频率功率", "Spectrum::power_at", "Spectrum", {}, "number", "频率", 98e6));
result.push_back(action("add_marker", "添加点 Marker", "Spectrum::add_custom_marker", "Marker", {}, "number", "频率", 96e6));
result.push_back(action("add_line_marker", "添加线 Marker", "Spectrum::add_custom_line_marker", "Marker", {}, "number", "频率", 100e6));
result.push_back(action("remove_marker", "按频率删除 Marker", "Spectrum::remove_custom_marker", "Marker", {}, "number", "频率", 96e6));
result.push_back(action("remove_selected_marker", "删除选中 Marker", "Spectrum::remove_selected_marker", "Marker"));
result.push_back(action("clear_markers", "清空 Marker", "Spectrum::clear_custom_markers", "Marker"));
result.push_back(action("select_marker", "选择 Marker 索引", "Spectrum::set_selected_marker_index", "Marker", {}, "number", "索引", 0));
result.push_back(action("select_next_marker", "选择下一个 Marker", "Spectrum::select_next_marker", "Marker"));
result.push_back(action("select_previous_marker", "选择上一个 Marker", "Spectrum::select_previous_marker", "Marker"));
result.push_back(action("clear_marker_selection", "清除 Marker 选择", "Spectrum::clear_marker_selection", "Marker"));
result.push_back(action("set_marker_frequency", "修改选中 Marker 频率",
"Spectrum::set_marker_frequency / set_current_marker_frequency",
"Marker", {}, "number", "频率", 99e6));
result.push_back(action("rebind_axes", "重新绑定频率轴和功率轴", "Spectrum::set_frequency_axis / set_power_axis", "Spectrum"));
result.push_back(action("read_data_shape", "读取输入与绘制点数", "Spectrum::sample_count / rendered_point_count", "观察者"));
if (case_id == "selection_overlay") {
result.push_back(action("clear_selection", "清空框选区域", "Selection_Rectangle_Overlay::clear_selected_regions", "Selection Overlay"));
result.push_back(action("rebind_selection_axes", "重新绑定框选轴", "Selection_Rectangle_Overlay::set_horizontal_axis / set_vertical_axis", "Selection Overlay"));
}
} else if (case_id == "waterfall") {
result.push_back(action("append_row", "追加一行", "Waterfall::append_row", "Waterfall"));
result.push_back(action("append_tick_row", "按 tick 追加一行", "Time_Axis::append_time / Waterfall::append_row(int, span)", "Waterfall"));
result.push_back(action("rebind_axes", "读取并重新绑定轴", "Waterfall::frequency_axis / time_axis", "Waterfall"));
result.push_back(action("read_data_shape", "读取行、点与渲染单元", "Waterfall::row_count / stored_point_count / rendered_cell_count", "观察者"));
} else if (case_id == "afterglow") {
result.push_back(action("append_spectrum", "追加一帧频谱", "Afterglow::append_spectrum", "Afterglow"));
result.push_back(action("rebind_axes", "读取轴绑定", "Afterglow::frequency_axis / power_axis", "Afterglow"));
result.push_back(action("read_data_shape", "读取历史帧与渲染网格", "Afterglow::history_count / latest_spectrum_point_count / rendered_cell_count", "观察者"));
} else if (case_id == "sweep_spectrum") {
result.push_back(action("append_block", "追加一个扫频块", "Sweep_Spectrum::append_block", "Sweep Spectrum"));
result.push_back(action("rebind_axes", "读取轴绑定", "Sweep_Spectrum::frequency_axis / power_axis", "Sweep Spectrum"));
result.push_back(action("read_data_shape", "读取块与绘制点数", "Sweep_Spectrum::stored_block_count / stored_point_count / rendered_point_count", "观察者"));
} else if (case_id == "frequency_trace") {
result.push_back(action("append_sample", "追加一个样本", "Frequency_Trace::append_sample", "Frequency Trace"));
result.push_back(action("append_tick_sample", "按 tick 追加样本", "Time_Axis::append_time / Frequency_Trace::append_sample(int, double)", "Frequency Trace"));
result.push_back(action("read_axes", "读取时间轴与值轴", "Frequency_Trace::time_axis / value_axis", "Frequency Trace"));
result.push_back(action("read_data_shape", "读取轨迹点数", "Frequency_Trace::sample_count / rendered_point_count / Time_Axis::time_point_count", "观察者"));
} else if (case_id == "constellation") {
result.push_back(action("append_points", "追加一组 IQ 点", "Constellation_Diagram::append_point", "Constellation"));
result.push_back(action("fit_square", "按轴拟合正方形", "Constellation_Diagram::fit_square_to_axes", "Constellation"));
result.push_back(action("read_axes", "读取 I/Q 轴", "Constellation_Diagram::i_axis / q_axis", "Constellation"));
result.push_back(action("read_data_shape", "读取星座点与锚点数", "Constellation_Diagram::point_count", "观察者"));
}
return result;
}
const Case_Model* find_case(std::string_view id) {
const auto& all = cases();
const auto found = std::find_if(all.begin(), all.end(),
[id](const Case_Model& item) { return item.id == id; });
return found == all.end() ? nullptr : &*found;
}
const Control_Definition* find_control(const std::vector<Control_Definition>& all,
std::string_view id) {
const auto found = std::find_if(all.begin(), all.end(),
[id](const Control_Definition& item) {
return item.model.id == id;
});
return found == all.end() ? nullptr : &*found;
}
bool valid_color(std::string_view value) {
if (value.size() != 7 || value.front() != '#')
return false;
return std::all_of(value.begin() + 1, value.end(), [](char character) {
return std::isxdigit(static_cast<unsigned char>(character)) != 0;
});
}
Json value_json(const Gallery_Value& value) {
return std::visit([](const auto& item) { return Json(item); }, value);
}
std::optional<Gallery_Value> parse_value(const Json& value,
const Control_Definition& definition,
std::string& error) {
if (definition.model.input == "boolean") {
if (!value.is_boolean()) {
error = "必须是布尔值";
return std::nullopt;
}
return Gallery_Value(value.get<bool>());
}
if (definition.model.input == "number") {
if (!value.is_number()) {
error = "必须是数字";
return std::nullopt;
}
const double number_value = value.get<double>();
if (!std::isfinite(number_value) || number_value < definition.model.minimum ||
number_value > definition.model.maximum) {
error = "数值超出后端允许范围";
return std::nullopt;
}
if (definition.model.integer && std::floor(number_value) != number_value) {
error = "必须是整数";
return std::nullopt;
}
return Gallery_Value(number_value);
}
if (!value.is_string()) {
error = "必须是字符串";
return std::nullopt;
}
std::string string_value = value.get<std::string>();
if (definition.model.input == "color" && !valid_color(string_value)) {
error = "颜色必须使用 #RRGGBB";
return std::nullopt;
}
if (definition.model.input == "select" &&
std::find(definition.options.begin(), definition.options.end(), string_value) ==
definition.options.end()) {
error = "枚举值不在后端选项中";
return std::nullopt;
}
if (string_value.size() > 160) {
error = "文本过长";
return std::nullopt;
}
return Gallery_Value(std::move(string_value));
}
double numeric(const Gallery_State& state, std::string_view id) {
const auto found = state.values.find(id);
return found == state.values.end() ? 0.0 : std::get<double>(found->second);
}
std::optional<std::string> range_error(const Gallery_State& state) {
constexpr std::pair<std::string_view, std::string_view> pairs[] = {
{"coord_origin", "coord_target"}, {"frequency_origin", "frequency_target"},
{"power_origin", "power_target"}, {"sweep_origin", "sweep_target"},
{"trace_value_min", "trace_value_max"}, {"i_origin", "i_target"},
{"q_origin", "q_target"}
};
for (const auto& [origin, target] : pairs) {
if (state.values.contains(origin) && state.values.contains(target) &&
numeric(state, origin) == numeric(state, target)) {
return std::string(origin) + "" + std::string(target) + " 不能相等";
}
}
return std::nullopt;
}
Json parse_request(std::string_view message) {
return Json::parse(message.begin(), message.end());
}
adminive::Table_View control_view();
adminive::Table_View action_view();
} // namespace renderive::web::gallery_detail
namespace adminive {
template <>
struct Type_Descriptor<renderive::web::gallery_detail::Case_Model> {
static auto get() {
using T = renderive::web::gallery_detail::Case_Model;
return object<T>("renderive_gallery_case",
ADMINIVE_FIELD_LABEL(T, id, "标识"),
ADMINIVE_FIELD_LABEL(T, title, "标题"),
ADMINIVE_FIELD_LABEL(T, component, "Core2 控件"),
ADMINIVE_FIELD_LABEL(T, category, "分类"),
ADMINIVE_FIELD_LABEL(T, description, "说明"),
ADMINIVE_FIELD_LABEL(T, order, "顺序"),
ADMINIVE_FIELD_LABEL(T, preferred_width, "建议宽度"),
ADMINIVE_FIELD_LABEL(T, preferred_height, "建议高度"))
.label("Renderive 控件用例");
}
};
template <>
struct Type_Descriptor<renderive::web::gallery_detail::Control_Model> {
static auto get() {
using T = renderive::web::gallery_detail::Control_Model;
return object<T>("renderive_gallery_control",
ADMINIVE_FIELD_LABEL(T, id, "字段"),
ADMINIVE_FIELD_LABEL(T, label, "控件"),
ADMINIVE_FIELD_LABEL(T, api, "Core2 API"),
ADMINIVE_FIELD_LABEL(T, description, "说明"),
ADMINIVE_FIELD_LABEL(T, group, "分组"),
ADMINIVE_FIELD_LABEL(T, input, "输入类型"),
ADMINIVE_FIELD_LABEL(T, integer, "整数"),
ADMINIVE_FIELD_LABEL(T, minimum, "最小值"),
ADMINIVE_FIELD_LABEL(T, maximum, "最大值"),
ADMINIVE_FIELD_LABEL(T, step, "步长"))
.label("后端控制菜单字段");
}
};
template <>
struct Type_Descriptor<renderive::web::gallery_detail::Action_Model> {
static auto get() {
using T = renderive::web::gallery_detail::Action_Model;
return object<T>("renderive_gallery_action",
ADMINIVE_FIELD_LABEL(T, id, "动作"),
ADMINIVE_FIELD_LABEL(T, label, "名称"),
ADMINIVE_FIELD_LABEL(T, api, "Core2 API"),
ADMINIVE_FIELD_LABEL(T, description, "说明"),
ADMINIVE_FIELD_LABEL(T, group, "分组"),
ADMINIVE_FIELD_LABEL(T, argument_input, "参数类型"),
ADMINIVE_FIELD_LABEL(T, argument_label, "参数名称"),
ADMINIVE_FIELD_LABEL(T, argument_default, "参数默认值"),
ADMINIVE_FIELD_LABEL(T, request_frame, "执行后请求像素帧"))
.label("后端动作菜单");
}
};
} // namespace adminive
namespace renderive::web::gallery_detail {
adminive::Table_View control_view() {
using T = Control_Model;
return adminive::table_view<T>(
adminive::column<&T::label>("控件"),
adminive::column<&T::group>("分组"),
adminive::column<&T::api>("Core2 API"),
adminive::column<&T::input>("输入类型"))
.titled("后端控制菜单");
}
adminive::Table_View action_view() {
using T = Action_Model;
return adminive::table_view<T>(
adminive::column<&T::label>("动作"),
adminive::column<&T::group>("分组"),
adminive::column<&T::api>("Core2 API"))
.titled("保留 API 动作");
}
} // namespace renderive::web::gallery_detail
namespace renderive::web {
namespace {
using gallery_detail::Json;
Json protocol_base() {
return {{"category", "gallery"},
{"protocol", "renderive.control-gallery"},
{"protocol_version", 4}};
}
Json case_contract(std::string_view case_id) {
const auto* item = gallery_detail::find_case(case_id);
return item ? adminive::to_frontend_json<Json>(*item) : Json::object();
}
std::string_view frame_mode_id(Gallery_Frame_Mode mode) {
switch (mode) {
case Gallery_Frame_Mode::Manual:
return "manual";
case Gallery_Frame_Mode::Low_Latency:
return "low_latency";
case Gallery_Frame_Mode::Playback:
return "playback";
}
return "low_latency";
}
std::optional<Gallery_Frame_Mode> parse_frame_mode(std::string_view value) {
if (value == "manual")
return Gallery_Frame_Mode::Manual;
if (value == "low_latency")
return Gallery_Frame_Mode::Low_Latency;
if (value == "playback")
return Gallery_Frame_Mode::Playback;
return std::nullopt;
}
Json dashboard_field(std::string_view label, std::string_view source,
std::string_view format = "integer", int digits = -1) {
Json result{{"label", label}, {"source", source}, {"format", format}};
if (digits >= 0)
result["digits"] = digits;
return result;
}
Json mapped_dashboard_field(std::string_view label, std::string_view source,
std::string_view format, std::string_view value_map) {
Json result = dashboard_field(label, source, format);
result["value_map"] = value_map;
return result;
}
Json dashboard_contract() {
Json point_pair{{"label", "输入→绘制"}, {"format", "pair"},
{"sources", Json::array({"data_shape.input_points", "data_shape.rendered_elements"})},
{"separator", ""}};
Json bottleneck = mapped_dashboard_field("当前瓶颈", "low_latency_limit.current",
"duration_enum", "limit_state");
bottleneck["duration_sources"] = {
{"frequency_limited", "kernel_observer.target_interval_ns"},
{"paint_limited", "kernel_observer.paint_duration_ns"},
{"render_limited", "kernel_observer.render_duration_ns"},
{"consumer_limited", "kernel_observer.consumer_interval_ns"}
};
Json configured_frequency = dashboard_field("配置频率", "kernel_observer.configured_frequency_hz",
"frequency");
configured_frequency["enabled_source"] = "kernel_observer.frequency_limit_enabled";
configured_frequency["disabled_label"] = "已关闭";
Json dashboard{
{"value_maps", {
{"enabled", {{"true", "启用"}, {"false", "关闭"}}},
{"consumer_feedback_source", {
{"disabled", "总开关关闭"}, {"none", ""}, {"pixel", "像素响应"},
{"presentation", "浏览器呈现"}, {"manual", "手动"}
}},
{"limit_state", {
{"frequency_limited", "Kernel 频率受限"}, {"paint_limited", "PaintEvent 受限"},
{"render_limited", "后台渲染受限"}, {"consumer_limited", "消费者反馈受限"},
{"unlimited", "无限制"}, {"not_applicable", "N/A"}
}}
}},
{"performance", {
{"fields", Json::array({
dashboard_field("后端渲染 FPS", "performance.measured_fps", "fixed", 1),
dashboard_field("像素响应 FPS", "performance.pixel_response_fps", "fixed", 1),
dashboard_field("浏览器呈现 FPS", "client_performance.presentation_fps", "fixed", 1),
dashboard_field("WS 往返 ms", "client_performance.frame_round_trip_ms", "fixed", 2),
dashboard_field("未呈现覆盖", "client_performance.overwritten_pixel_frames"),
dashboard_field("Core 渲染 ms", "performance.last_render_ms", "fixed", 2),
dashboard_field("像素编码 ms", "performance.last_pixel_encode_ms", "fixed", 2),
dashboard_field("响应负载 MB/s", "performance.pixel_payload_megabytes_per_second", "fixed", 1),
dashboard_field("Kernel 待处理", "kernel_observer.pending_frame_count"),
dashboard_field("Kernel 丢弃", "kernel_observer.dropped_frame_count"),
std::move(point_pair), std::move(bottleneck),
dashboard_field("观察事件", "kernel_observer.last_event", "text"),
dashboard_field("像素超时", "client_performance.frame_request_timeout_count"),
dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 0)
})}
}},
{"menu_views", {
{"observer", {{"source", "kernel_observer"}}},
{"performance", Json::object()}
}},
{"limits", {
{"aria_label", "低延迟限速来源"}, {"title", "限速来源"},
{"current_source", "low_latency_limit.current"},
{"active_label", "当前瓶颈"}, {"inactive_label", "未受限"},
{"disabled_label", "已关闭"},
{"fields", Json::array({
{{"label", "Kernel 用户频率"}, {"active_value", "frequency_limited"},
{"duration_source", "kernel_observer.target_interval_ns"},
{"enabled_source", "kernel_observer.frequency_limit_enabled"}},
{{"label", "Kernel PaintEvent"}, {"active_value", "paint_limited"},
{"duration_source", "kernel_observer.paint_duration_ns"}},
{{"label", "Kernel 后台渲染"}, {"active_value", "render_limited"},
{"duration_source", "kernel_observer.render_duration_ns"}},
{{"label", "消费者反馈"}, {"active_value", "consumer_limited"},
{"duration_source", "kernel_observer.consumer_interval_ns"},
{"enabled_source", "kernel_observer.consumer_feedback_enabled"}}
})}
}},
{"observer", {
{"aria_label", "Kernel 低延迟全量统计"},
{"header", {
{"prefix", "KERNEL"}, {"suffix", "OBSERVER"}, {"event_label", "事件"},
{"mode", dashboard_field("", "kernel_observer.mode", "text")},
{"limit", mapped_dashboard_field("", "kernel_observer.limit_state", "enum", "limit_state")},
{"event", dashboard_field("", "kernel_observer.last_event", "text")}
}},
{"sections", Json::array({
{{"class_name", "observer-counters"}, {"fields", Json::array({
std::move(configured_frequency),
dashboard_field("观察次数", "kernel_observer.observation_count"),
dashboard_field("最新序号", "kernel_observer.latest_sequence"),
dashboard_field("发布", "kernel_observer.produced_frame_count"),
dashboard_field("完成", "kernel_observer.consumed_frame_count"),
dashboard_field("待处理", "kernel_observer.pending_frame_count"),
dashboard_field("丢弃", "kernel_observer.dropped_frame_count"),
dashboard_field("失败", "kernel_observer.failed_operation_count")
})}},
{{"class_name", "latency-summary"}, {"fields", Json::array({
dashboard_field("目标间隔", "kernel_observer.target_interval_ns", "nanoseconds"),
dashboard_field("PaintEvent", "kernel_observer.paint_duration_ns", "nanoseconds"),
dashboard_field("后台渲染", "kernel_observer.render_duration_ns", "nanoseconds"),
dashboard_field("内部瓶颈", "kernel_observer.bottleneck_duration_ns", "nanoseconds"),
mapped_dashboard_field("消费者总开关", "kernel_observer.consumer_feedback_master_enabled", "enum", "enabled"),
mapped_dashboard_field("Kernel 反馈有效", "kernel_observer.consumer_feedback_enabled", "enum", "enabled"),
mapped_dashboard_field("像素响应反馈", "kernel_observer.consumer_pixel_feedback_enabled", "enum", "enabled"),
mapped_dashboard_field("浏览器呈现反馈", "kernel_observer.consumer_presentation_feedback_enabled", "enum", "enabled"),
mapped_dashboard_field("手动消费者反馈", "kernel_observer.consumer_manual_feedback_enabled", "enum", "enabled"),
dashboard_field("手动消费者 FPS", "kernel_observer.consumer_manual_fps", "fps", 2),
mapped_dashboard_field("生效反馈来源", "kernel_observer.consumer_feedback_source", "flags", "consumer_feedback_source"),
dashboard_field("像素响应周期", "kernel_observer.consumer_pixel_interval_ns", "nanoseconds"),
dashboard_field("浏览器呈现周期", "kernel_observer.consumer_presentation_interval_ns", "nanoseconds"),
dashboard_field("手动消费者周期", "kernel_observer.consumer_manual_interval_ns", "nanoseconds"),
dashboard_field("消费者原始采样", "kernel_observer.consumer_sample_interval_ns", "nanoseconds"),
dashboard_field("消费者平滑周期", "kernel_observer.consumer_smoothed_interval_ns", "nanoseconds"),
dashboard_field("消费者抖动", "kernel_observer.consumer_variation_ns", "nanoseconds"),
dashboard_field("消费者安全期限", "kernel_observer.consumer_safety_interval_ns", "nanoseconds"),
dashboard_field("消费者限速周期", "kernel_observer.consumer_interval_ns", "nanoseconds"),
dashboard_field("消费者限速 FPS", "kernel_observer.consumer_interval_ns", "inverse_fps", 2),
dashboard_field("下次刷新", "kernel_observer.next_refresh_interval_ns", "nanoseconds"),
Json{{"label", "端到端延迟"}, {"source", "kernel_observer.end_to_end_ns"},
{"format", "nanoseconds"}, {"cell_class", "critical"}}
})}},
{{"class_name", "client-summary"}, {"aria_label", "浏览器消费者反馈全量统计"},
{"fields", Json::array({
dashboard_field("RAF 最新周期", "client_performance.display_interval_latest_ms", "milliseconds", 3),
dashboard_field("RAF 中位周期", "client_performance.display_interval_ms", "milliseconds", 3),
dashboard_field("RAF P95 周期", "client_performance.display_interval_p95_ms", "milliseconds", 3),
dashboard_field("RAF P95-P50 抖动", "client_performance.display_jitter_ms", "milliseconds", 3),
dashboard_field("WS 往返", "client_performance.frame_round_trip_ms", "milliseconds", 3),
dashboard_field("像素响应 FPS", "client_performance.transport_fps", "fps", 2),
dashboard_field("浏览器呈现 FPS", "client_performance.presentation_fps", "fps", 2),
dashboard_field("WS 缓冲", "client_performance.websocket_buffered_bytes", "bytes"),
dashboard_field("未呈现覆盖", "client_performance.overwritten_pixel_frames"),
dashboard_field("变化像素帧", "client_performance.changed_pixel_frames"),
dashboard_field("重复像素帧", "client_performance.duplicate_pixel_frames"),
dashboard_field("像素请求超时", "client_performance.frame_request_timeout_count"),
dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 3),
dashboard_field("最近变化龄", "client_performance.last_pixel_change_age_ms", "milliseconds", 3)
})}},
{{"class_name", "latency-details"}, {"aria_label", "Kernel 各阶段等待耗时"},
{"fields", Json::array({
dashboard_field("Painter Lease 等待", "kernel_observer.paint_lease_wait_ns", "nanoseconds"),
dashboard_field("Paint State 等待", "kernel_observer.paint_state_wait_ns", "nanoseconds"),
dashboard_field("Publish State 等待", "kernel_observer.publish_state_wait_ns", "nanoseconds"),
dashboard_field("Ready 等待", "kernel_observer.ready_wait_ns", "nanoseconds"),
dashboard_field("开始渲染时帧龄", "kernel_observer.frame_age_at_render_ns", "nanoseconds"),
dashboard_field("Render Lease 等待", "kernel_observer.render_lease_wait_ns", "nanoseconds"),
dashboard_field("Render State 等待", "kernel_observer.render_state_wait_ns", "nanoseconds"),
dashboard_field("Render Finish 等待", "kernel_observer.render_finish_state_wait_ns", "nanoseconds"),
dashboard_field("回放队列等待", "kernel_observer.queue_wait_ns", "nanoseconds")
})}}
})}
}}
};
return dashboard;
}
Json frame_mode_contract(Gallery_Frame_Mode mode) {
switch (mode) {
case Gallery_Frame_Mode::Manual:
return {{"id", "manual"}, {"title", "手动刷新"},
{"strategy", "Manual_Refresh_Strategy"},
{"description", "显式准备、刷新和渲染;页面不会自动拉取像素。"},
{"automatic", false}, {"request_after_response", false},
{"request_on_animation_frame", false}, {"observer_visible", false},
{"frame_button_label", "手动刷新一帧"}, {"accent", "#ffd166aa"}, {"order", 10}};
case Gallery_Frame_Mode::Low_Latency:
return {{"id", "low_latency"}, {"title", "低延迟"},
{"strategy", "Low_Latency_Strategy"},
{"description", "以最大 FPS 自动发布并消费最新帧,可观测丢帧与端到端延迟。"},
{"automatic", true}, {"request_after_response", true},
{"request_on_animation_frame", false}, {"observer_visible", true},
{"frame_button_label", "立即刷新"}, {"accent", "#45ddbeaa"}, {"order", 20}};
case Gallery_Frame_Mode::Playback:
return {{"id", "playback"}, {"title", "回放队列"},
{"strategy", "Flow_Refresh_Strategy"},
{"description", "所有帧按队列顺序入队和消费,可观测队深与排队时间。"},
{"automatic", true}, {"request_after_response", true},
{"request_on_animation_frame", true}, {"observer_visible", false},
{"frame_button_label", "消费下一帧"}, {"accent", "#6aa9ffaa"}, {"order", 30}};
}
return Json::object();
}
} // namespace
std::string Gallery_Protocol::catalog_json() {
Json result = protocol_base();
result["type"] = "catalog";
result["transport"] = {{"events", "websocket-text"}, {"pixels", "websocket-binary-rvp1"},
{"http_api", false}, {"socket_per_canvas", true}};
result["navigation"] = {
{"default_mode", "low_latency"}, {"all_categories_label", "全部"},
{"catalog_loaded_text", "Kernel 策略目录已加载"},
{"hero_eyebrow", "REAL KERNEL STRATEGIES"},
{"hero_title", "对称策略页面,同一批控件,直接比较"}
};
result["dashboard"] = dashboard_contract();
result["case_descriptor"] =
adminive::to_descriptor_json<Json, gallery_detail::Case_Model>();
result["frame_modes"] = Json::array();
constexpr Gallery_Frame_Mode frame_modes[]{Gallery_Frame_Mode::Manual,
Gallery_Frame_Mode::Low_Latency,
Gallery_Frame_Mode::Playback};
for (const auto mode : frame_modes)
result["frame_modes"].push_back(frame_mode_contract(mode));
result["cases"] = Json::array();
std::size_t controls_total{};
std::size_t actions_total{};
for (const auto& item : gallery_detail::cases()) {
Json entry = adminive::to_frontend_json<Json>(item);
entry["control_count_by_mode"] = Json::object();
entry["action_count_by_mode"] = Json::object();
for (const auto mode : frame_modes) {
const std::string key(frame_mode_id(mode));
const std::size_t mode_controls = control_count(item.id, mode);
const std::size_t mode_actions = action_count(item.id, mode);
entry["control_count_by_mode"][key] = mode_controls;
entry["action_count_by_mode"][key] = mode_actions;
controls_total += mode_controls;
actions_total += mode_actions;
}
entry["control_count"] = control_count(item.id, Gallery_Frame_Mode::Low_Latency);
entry["action_count"] = action_count(item.id, Gallery_Frame_Mode::Low_Latency);
result["cases"].push_back(std::move(entry));
}
result["coverage"] = {{"case_count", gallery_detail::cases().size()},
{"page_count", std::size(frame_modes)},
{"canvas_count", gallery_detail::cases().size() * std::size(frame_modes)},
{"manual_control_count", controls_total},
{"manual_action_count", actions_total},
{"frequency_modes", Json::array({"spectrum", "afterglow", "sweep_spectrum"})},
{"image_interpolation_modes", Json::array({"Nearest", "Bilinear", "Bicubic"})}};
return result.dump();
}
bool Gallery_Protocol::is_case(std::string_view case_id) {
return gallery_detail::find_case(case_id) != nullptr;
}
Gallery_State Gallery_Protocol::default_state(std::string_view case_id,
Gallery_Frame_Mode frame_mode) {
Gallery_State state;
for (const auto& definition : gallery_detail::controls(case_id, frame_mode))
state.values.emplace(definition.model.id, definition.initial);
return state;
}
Gallery_State Gallery_Protocol::default_state(std::string_view case_id) {
return default_state(case_id, Gallery_Frame_Mode::Low_Latency);
}
std::string Gallery_Protocol::case_json(std::string_view case_id,
const Gallery_State& state,
std::string_view telemetry_json,
std::string_view notice,
Gallery_Frame_Mode frame_mode) {
Json result = protocol_base();
result["type"] = "case_state";
result["case"] = case_contract(case_id);
result["frame_mode"] = frame_mode_contract(frame_mode);
result["controls"] = {
{"descriptor", adminive::to_descriptor_json<Json, gallery_detail::Control_Model>()},
{"view", adminive::to_view_json<Json, gallery_detail::Control_Model>(
gallery_detail::control_view())},
{"data", Json::array()}
};
for (const auto& definition : gallery_detail::controls(case_id, frame_mode)) {
Json item = adminive::to_frontend_json<Json>(definition.model);
const auto found = state.values.find(definition.model.id);
item["value"] = found == state.values.end()
? gallery_detail::value_json(definition.initial)
: gallery_detail::value_json(found->second);
item["options"] = definition.options;
result["controls"]["data"].push_back(std::move(item));
}
result["actions"] = {
{"descriptor", adminive::to_descriptor_json<Json, gallery_detail::Action_Model>()},
{"view", adminive::to_view_json<Json, gallery_detail::Action_Model>(
gallery_detail::action_view())},
{"data", Json::array()}
};
for (const auto& definition : gallery_detail::actions(case_id, frame_mode))
result["actions"]["data"].push_back(
adminive::to_frontend_json<Json>(definition.model));
try {
result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end());
} catch (const std::exception&) {
result["telemetry"] = Json::object();
}
if (!notice.empty())
result["notice"] = notice;
return result.dump();
}
std::string Gallery_Protocol::case_json(std::string_view case_id,
const Gallery_State& state,
std::string_view telemetry_json,
std::string_view notice) {
return case_json(case_id, state, telemetry_json, notice,
Gallery_Frame_Mode::Low_Latency);
}
std::string Gallery_Protocol::error_json(std::string_view message,
std::string_view field) {
Json result = protocol_base();
result["type"] = "error";
result["message"] = message;
result["field_errors"] = Json::object();
if (!field.empty())
result["field_errors"][std::string(field)] = message;
return result.dump();
}
std::string Gallery_Protocol::observer_json(std::string_view case_id,
Gallery_Frame_Mode frame_mode,
std::string_view telemetry_json) {
Json result = protocol_base();
result["type"] = "observer_state";
result["case_id"] = case_id;
result["frame_mode"] = frame_mode_contract(frame_mode);
try {
result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end());
} catch (const std::exception&) {
result["telemetry"] = Json::object();
}
return result.dump();
}
Gallery_Patch_Result Gallery_Protocol::apply_patch(std::string_view case_id,
const Gallery_State& current,
std::string_view message,
Gallery_Frame_Mode frame_mode) {
try {
const Json request = gallery_detail::parse_request(message);
if (!request.is_object() || request.value("category", "") != "event" ||
request.value("type", "") != "gallery_patch" ||
!request.contains("patch") || !request.at("patch").is_object()) {
return {std::nullopt, error_json("gallery_patch 格式无效")};
}
const auto definitions = gallery_detail::controls(case_id, frame_mode);
Gallery_State candidate = current;
for (const auto& [id, value] : request.at("patch").items()) {
const auto* definition = gallery_detail::find_control(definitions, id);
if (!definition)
return {std::nullopt, error_json("该控件不属于当前画布", id)};
std::string validation_error;
auto parsed = gallery_detail::parse_value(value, *definition, validation_error);
if (!parsed)
return {std::nullopt, error_json(validation_error, id)};
candidate.values[id] = std::move(*parsed);
}
if (const auto error = gallery_detail::range_error(candidate))
return {std::nullopt, error_json(*error)};
return {std::move(candidate), {}};
} catch (const std::exception& error) {
return {std::nullopt, error_json(error.what())};
}
}
Gallery_Patch_Result Gallery_Protocol::apply_patch(std::string_view case_id,
const Gallery_State& current,
std::string_view message) {
return apply_patch(case_id, current, message, Gallery_Frame_Mode::Low_Latency);
}
std::optional<Gallery_Open_Request> Gallery_Protocol::open_request(std::string_view message) {
try {
const Json request = gallery_detail::parse_request(message);
if (!request.is_object() || request.value("category", "") != "event" ||
request.value("type", "") != "gallery_open" ||
!request.contains("case") || !request.at("case").is_string())
return std::nullopt;
const std::string value = request.at("case").get<std::string>();
if (!is_case(value) || !request.contains("frame_mode") ||
!request.at("frame_mode").is_string())
return std::nullopt;
const auto mode = parse_frame_mode(request.at("frame_mode").get<std::string>());
return mode ? std::optional<Gallery_Open_Request>(Gallery_Open_Request{value, *mode})
: std::nullopt;
} catch (const std::exception&) {
return std::nullopt;
}
}
std::optional<Gallery_Action_Request> Gallery_Protocol::action_request(
std::string_view message) {
try {
const Json request = gallery_detail::parse_request(message);
if (!request.is_object() || request.value("category", "") != "event" ||
request.value("type", "") != "gallery_action" ||
!request.contains("action") || !request.at("action").is_string())
return std::nullopt;
Gallery_Action_Request result{request.at("action").get<std::string>()};
if (request.contains("argument")) {
const Json& argument = request.at("argument");
if (argument.is_boolean())
result.argument = argument.get<bool>();
else if (argument.is_number()) {
const double value = argument.get<double>();
if (!std::isfinite(value))
return std::nullopt;
result.argument = value;
} else if (argument.is_string())
result.argument = argument.get<std::string>();
else
return std::nullopt;
}
return result;
} catch (const std::exception&) {
return std::nullopt;
}
}
std::size_t Gallery_Protocol::control_count(std::string_view case_id,
Gallery_Frame_Mode frame_mode) {
return gallery_detail::controls(case_id, frame_mode).size();
}
std::size_t Gallery_Protocol::control_count(std::string_view case_id) {
return control_count(case_id, Gallery_Frame_Mode::Low_Latency);
}
std::size_t Gallery_Protocol::action_count(std::string_view case_id,
Gallery_Frame_Mode frame_mode) {
return gallery_detail::actions(case_id, frame_mode).size();
}
std::size_t Gallery_Protocol::action_count(std::string_view case_id) {
return action_count(case_id, Gallery_Frame_Mode::Low_Latency);
}
bool Gallery_Protocol::action_available(std::string_view case_id,
Gallery_Frame_Mode frame_mode,
std::string_view action_id) {
const auto available = gallery_detail::actions(case_id, frame_mode);
return std::any_of(available.begin(), available.end(),
[action_id](const gallery_detail::Action_Definition& item) {
return item.model.id == action_id;
});
}
} // namespace renderive::web