优化cmake

This commit is contained in:
2026-08-13 23:10:04 +08:00
parent 432c3afe6c
commit aea74a219b
61 changed files with 12001 additions and 44 deletions
+161
View File
@@ -0,0 +1,161 @@
set(Renderive_Web_dependencies global::drogon global::spdlog global::magic_enum)
if (RENDERIVE_BUILD_TESTS)
list(APPEND Renderive_Web_dependencies global::GTest)
endif ()
rcl_add_dependency_action_targets(Renderive_Web_env ${Renderive_Web_dependencies})
set_target_properties(Renderive_Web_env PROPERTIES FOLDER Renderive_Web)
if (CMAKE_CONFIGURATION_TYPES)
set(Renderive_Web_assets_dir "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/webapp_gallery")
else ()
set(Renderive_Web_assets_dir "${CMAKE_CURRENT_BINARY_DIR}/webapp_gallery")
endif ()
set(Renderive_Web_node_search_paths)
if (WIN32 AND DEFINED ENV{APPDATA})
file(GLOB Renderive_Web_clion_node_versions LIST_DIRECTORIES true
"$ENV{APPDATA}/JetBrains/CLion*/node/versions/*")
list(SORT Renderive_Web_clion_node_versions COMPARE NATURAL ORDER DESCENDING)
foreach (Renderive_Web_clion_node_version IN LISTS Renderive_Web_clion_node_versions)
if (EXISTS "${Renderive_Web_clion_node_version}/node.exe"
AND EXISTS "${Renderive_Web_clion_node_version}/npm.cmd")
list(APPEND Renderive_Web_node_search_paths "${Renderive_Web_clion_node_version}")
endif ()
endforeach ()
endif ()
find_program(Renderive_Web_node_executable NAMES node node.exe
HINTS ${Renderive_Web_node_search_paths} REQUIRED)
get_filename_component(Renderive_Web_node_dir "${Renderive_Web_node_executable}" DIRECTORY)
if (WIN32)
set(Renderive_Web_npm_executable "${Renderive_Web_node_dir}/npm.cmd")
else ()
set(Renderive_Web_npm_executable "${Renderive_Web_node_dir}/npm")
endif ()
if (NOT EXISTS "${Renderive_Web_npm_executable}")
message(FATAL_ERROR "npm was not found next to Node.js: ${Renderive_Web_node_dir}")
endif ()
set(Renderive_Web_frontend_dir "${CMAKE_CURRENT_LIST_DIR}/../webapp_gallery")
set(Renderive_Web_frontend_install_stamp "${CMAKE_CURRENT_BINARY_DIR}/webapp_gallery_npm_ci.stamp")
set(Renderive_Web_frontend_build_stamp "${CMAKE_CURRENT_BINARY_DIR}/webapp_gallery_vite_build.stamp")
file(GLOB_RECURSE Renderive_Web_frontend_sources CONFIGURE_DEPENDS
"${Renderive_Web_frontend_dir}/src/*"
)
set(Renderive_Web_frontend_configuration
"${Renderive_Web_frontend_dir}/package.json"
"${Renderive_Web_frontend_dir}/package-lock.json"
"${Renderive_Web_frontend_dir}/vite.config.ts"
"${Renderive_Web_frontend_dir}/tsconfig.json"
"${Renderive_Web_frontend_dir}/tsconfig.app.json"
"${Renderive_Web_frontend_dir}/index.html"
)
if (WIN32)
set(Renderive_Web_node_path "${Renderive_Web_node_dir};$ENV{PATH}")
else ()
set(Renderive_Web_node_path "${Renderive_Web_node_dir}:$ENV{PATH}")
endif ()
add_custom_command(
OUTPUT "${Renderive_Web_frontend_install_stamp}"
COMMAND "${CMAKE_COMMAND}" -E env
"PATH=${Renderive_Web_node_path}"
"${Renderive_Web_npm_executable}" ci
COMMAND "${CMAKE_COMMAND}" -E touch "${Renderive_Web_frontend_install_stamp}"
DEPENDS
"${Renderive_Web_frontend_dir}/package.json"
"${Renderive_Web_frontend_dir}/package-lock.json"
WORKING_DIRECTORY "${Renderive_Web_frontend_dir}"
COMMENT "Installing Renderive Gallery frontend dependencies"
VERBATIM
)
add_custom_command(
OUTPUT "${Renderive_Web_frontend_build_stamp}"
COMMAND "${CMAKE_COMMAND}" -E env
"PATH=${Renderive_Web_node_path}"
"${Renderive_Web_npm_executable}" run build
COMMAND "${CMAKE_COMMAND}" -E touch "${Renderive_Web_frontend_build_stamp}"
DEPENDS "${Renderive_Web_frontend_install_stamp}"
${Renderive_Web_frontend_configuration}
${Renderive_Web_frontend_sources}
WORKING_DIRECTORY "${Renderive_Web_frontend_dir}"
COMMENT "Building Renderive Gallery frontend"
VERBATIM
)
add_custom_target(Renderive_Web_Assets
COMMAND "${CMAKE_COMMAND}" -E remove_directory "${Renderive_Web_assets_dir}"
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${Renderive_Web_frontend_dir}/dist"
"${Renderive_Web_assets_dir}"
DEPENDS "${Renderive_Web_frontend_build_stamp}"
COMMENT "Synchronizing Renderive Gallery frontend assets"
VERBATIM
)
library_get_missing_with_rely(Renderive_Web_dependencies_missing ${Renderive_Web_dependencies})
if (NOT Renderive_Web_dependencies_missing)
rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_Web dependencies\n${Renderive_Web_dependencies_missing}\n are not installed. Build Renderive_Web_env first")
return()
endif ()
function(renderive_find_web_dependencies)
if (POLICY CMP0144)
cmake_policy(SET CMP0144 NEW)
endif ()
rcl_load_dependency_environment(${Renderive_Web_dependencies})
find_package(Drogon CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(magic_enum CONFIG REQUIRED)
if (RENDERIVE_BUILD_TESTS)
find_package(GTest CONFIG REQUIRED)
endif ()
endfunction()
renderive_find_web_dependencies()
if (NOT TARGET Renderive_render_2D)
rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_Web requires Renderive_render_2D")
return()
endif ()
if (NOT TARGET Adminive::Nlohmann)
set(Renderive_Web_saved_build_testing "${BUILD_TESTING}")
set(BUILD_TESTING OFF)
add_subdirectory(
"${CMAKE_CURRENT_LIST_DIR}/../third_party/Adminive"
"${CMAKE_CURRENT_BINARY_DIR}/third_party/Adminive"
EXCLUDE_FROM_ALL
)
set(BUILD_TESTING "${Renderive_Web_saved_build_testing}")
unset(Renderive_Web_saved_build_testing)
endif ()
set(Renderive_Web_source_dir "${CMAKE_CURRENT_LIST_DIR}/app")
append_glob_source(Renderive_Web_sources "${Renderive_Web_source_dir}")
add_library(Renderive_Web STATIC ${Renderive_Web_sources})
target_include_directories(Renderive_Web PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/..>"
)
target_compile_features(Renderive_Web PUBLIC cxx_std_20)
target_compile_definitions(Renderive_Web PRIVATE NOMINMAX)
target_link_libraries(Renderive_Web
PUBLIC Renderive_render_2D Drogon::Drogon Adminive::MagicEnum magic_enum::magic_enum
PRIVATE Adminive::Nlohmann spdlog::spdlog
)
set(Renderive_Web_Server_source_dir "${CMAKE_CURRENT_LIST_DIR}/server")
append_glob_source(Renderive_Web_Server_sources "${Renderive_Web_Server_source_dir}")
add_executable(Renderive_Web_Server ${Renderive_Web_Server_sources})
target_compile_features(Renderive_Web_Server PRIVATE cxx_std_20)
target_link_libraries(Renderive_Web_Server PRIVATE Renderive_Web)
#add_dependencies(Renderive_Web_Server Renderive_Web_Assets)
if (MSVC)
target_compile_options(Renderive_Web PRIVATE /utf-8)
target_compile_options(Renderive_Web_Server PRIVATE /utf-8)
endif ()
if (RENDERIVE_BUILD_TESTS)
set(Renderive_Web_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests")
append_glob_source(Renderive_Web_test_sources "${Renderive_Web_test_dir}")
foreach (Renderive_Web_test_source IN LISTS Renderive_Web_test_sources)
if (NOT Renderive_Web_test_source MATCHES "\\.(c|cc|cpp|cxx)$")
continue()
endif ()
file(RELATIVE_PATH Renderive_Web_test_name "${Renderive_Web_test_dir}" "${Renderive_Web_test_source}")
string(MD5 Renderive_Web_test_hash "${Renderive_Web_test_name}")
string(SUBSTRING "${Renderive_Web_test_hash}" 0 8 Renderive_Web_test_hash)
get_filename_component(Renderive_Web_test_name "${Renderive_Web_test_source}" NAME_WE)
string(MAKE_C_IDENTIFIER "${Renderive_Web_test_name}" Renderive_Web_test_name)
set(Renderive_Web_test_target "Renderive_Web_${Renderive_Web_test_name}_${Renderive_Web_test_hash}")
add_executable("${Renderive_Web_test_target}" "${Renderive_Web_test_source}")
target_link_libraries("${Renderive_Web_test_target}" PRIVATE Renderive_Web Adminive::Nlohmann GTest::gtest_main)
add_test(NAME "${Renderive_Web_test_target}" COMMAND "${Renderive_Web_test_target}")
endforeach ()
endif ()
@@ -0,0 +1,172 @@
#pragma once
#include "renderive/scene/Scene2D_Context.hpp"
#include <structive/property/property.hpp>
#include <cstdint>
#include <algorithm>
#include <string>
#include <string_view>
#include <vector>
namespace renderive::web {
enum class Gallery_Frame_Mode : std::uint8_t {
Manual,
Low_Latency,
Playback
};
struct Gallery_Action_Category {};
struct Gallery_Action_Attribute {
using attribute_category = Gallery_Action_Category;
static constexpr bool single_valued = false;
static constexpr bool inheritable = false;
std::string_view id;
std::string_view label;
std::string_view api;
std::string_view group;
std::string_view description;
std::string_view argument_input;
std::string_view argument_label;
double argument_default{};
bool request_frame{};
};
constexpr Gallery_Action_Attribute gallery_action(
std::string_view id, std::string_view label, std::string_view api,
std::string_view group, std::string_view description = {},
std::string_view argument_input = {}, std::string_view argument_label = {},
double argument_default = 0.0, bool request_frame = false) {
return {id, label, api, group, description, argument_input, argument_label,
argument_default, request_frame};
}
struct Gallery_Action_Model {
std::string id;
std::string label;
std::string api;
std::string description;
std::string group;
std::string argument_input;
std::string argument_label;
double argument_default{};
bool request_frame{};
};
inline Gallery_Action_Model gallery_action_model(
const Gallery_Action_Attribute& action) {
return {std::string(action.id), std::string(action.label), std::string(action.api),
std::string(action.description), std::string(action.group),
std::string(action.argument_input), std::string(action.argument_label),
action.argument_default, action.request_frame};
}
template <structive::Property_Described_Object Object>
void append_gallery_actions(std::vector<Gallery_Action_Model>& target) {
structive::type_descriptor<Object>()
.template for_each_type_attribute<Gallery_Action_Category>(
[&target](const auto& action) {
const auto found = std::find_if(
target.begin(), target.end(),
[&action](const auto& existing) { return existing.id == action.id; });
if (found == target.end())
target.push_back(gallery_action_model(action));
});
}
} // namespace renderive::web
namespace structive {
template <class Scene_Frame, Mutex_Type Mutex, class Observer>
struct Type_Descriptor<Manual_Refresh_Strategy<Scene_Frame, Mutex, Observer>> {
using T = Manual_Refresh_Strategy<Scene_Frame, Mutex, Observer>;
static auto get() {
using renderive::web::gallery_action;
return object<T>(type_metadata(
gallery_action("mode_prepare", "准备帧",
"Manual_Refresh_Strategy::acquire_painter / Basic_Scene2D::prepare_frame",
"Manual_Refresh_Strategy"),
gallery_action("mode_refresh", "提交手动刷新",
"Manual_Refresh_Strategy::refresh / Basic_Scene2D::refresh_manual_frame",
"Manual_Refresh_Strategy"),
gallery_action("mode_render", "渲染已刷新帧",
"Manual_Refresh_Strategy::acquire_renderer / Basic_Scene2D::render_prepared_frame",
"Manual_Refresh_Strategy", {}, {}, {}, 0.0, true),
gallery_action("mode_discard", "丢弃待刷新帧",
"Basic_Scene2D::discard_pending_frame",
"Manual_Refresh_Strategy"),
gallery_action("mode_cycle", "执行完整手动帧周期",
"Basic_Scene2D::render_frame", "Manual_Refresh_Strategy",
{}, {}, {}, 0.0, true)));
}
};
template <class Scene_Frame, Mutex_Type Mutex, class Observer>
struct Type_Descriptor<Low_Latency_Strategy<Scene_Frame, Mutex, Observer>> {
using T = Low_Latency_Strategy<Scene_Frame, Mutex, Observer>;
static auto get() {
using renderive::web::gallery_action;
return object<T>(type_metadata(
gallery_action("mode_prepare", "发布低延迟帧",
"Low_Latency_Strategy::acquire_painter / Basic_Scene2D::prepare_frame",
"Low_Latency_Strategy"),
gallery_action("mode_render", "消费最新帧",
"Low_Latency_Strategy::acquire_renderer / Basic_Scene2D::render_prepared_frame",
"Low_Latency_Strategy", {}, {}, {}, 0.0, true),
gallery_action("mode_discard", "丢弃陈旧帧",
"Basic_Scene2D::discard_pending_frame", "Low_Latency_Strategy"),
gallery_action("mode_cycle", "执行低延迟帧周期",
"Basic_Scene2D::render_frame", "Low_Latency_Strategy",
{}, {}, {}, 0.0, true)));
}
};
template <class Scene_Frame, Mutex_Type Mutex, class Observer>
struct Type_Descriptor<Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>> {
using T = Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>;
static auto get() {
using renderive::web::gallery_action;
return object<T>(type_metadata(
gallery_action("mode_enqueue", "压入一帧",
"Flow_Refresh_Strategy::acquire_painter / Basic_Scene2D::prepare_frame",
"Flow_Refresh_Strategy"),
gallery_action("mode_enqueue_burst", "批量压入回放队列",
"Flow_Refresh_Strategy::acquire_painter / Basic_Scene2D::prepare_frame x N",
"Flow_Refresh_Strategy", {}, "number", "帧数", 8.0),
gallery_action("mode_dequeue", "消费队首帧",
"Flow_Refresh_Strategy::acquire_renderer / Basic_Scene2D::render_prepared_frame",
"Flow_Refresh_Strategy", {}, {}, {}, 0.0, true),
gallery_action("mode_cycle", "执行回放帧周期",
"Basic_Scene2D::render_frame", "Flow_Refresh_Strategy",
{}, {}, {}, 0.0, true)));
}
};
} // namespace structive
namespace renderive::web {
inline std::vector<Gallery_Action_Model> gallery_frame_actions(Gallery_Frame_Mode mode) {
std::vector<Gallery_Action_Model> result;
switch (mode) {
case Gallery_Frame_Mode::Manual:
append_gallery_actions<Manual_Refresh_Strategy<Scene2D_Frame_Data, std::mutex,
Observer_State<>>>(result);
break;
case Gallery_Frame_Mode::Low_Latency:
append_gallery_actions<Low_Latency_Strategy<Scene2D_Frame_Data, std::mutex,
Observer_State<>>>(result);
break;
case Gallery_Frame_Mode::Playback:
append_gallery_actions<Flow_Refresh_Strategy<Scene2D_Frame_Data, std::mutex,
Observer_State<>>>(result);
break;
}
return result;
}
} // namespace renderive::web
@@ -0,0 +1,363 @@
#pragma once
#include "render_2D/renderable/Renderable.h"
#include <renderive/scene/base/Scene_Base.hpp>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <map>
#include <numeric>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
namespace renderive::web {
namespace gallery_capture_detail {
using Json = nlohmann::json;
inline const char* node_kind(Render_Node_Kind kind) noexcept {
switch (kind) {
case Render_Node_Kind::prepare:
return "prepare";
case Render_Node_Kind::paint:
return "paint";
case Render_Node_Kind::composite:
return "composite";
}
return "prepare";
}
inline const char* execution_status(Node_Execution_Status status) noexcept {
switch (status) {
case Node_Execution_Status::pending:
return "pending";
case Node_Execution_Status::running:
return "running";
case Node_Execution_Status::complete:
return "complete";
case Node_Execution_Status::failed:
return "failed";
}
return "pending";
}
inline Json metrics_json(const Node_Execution_Metrics& metrics) {
Json result = Json::object();
constexpr std::array names{"input_count", "chunk_size", "prepared_cells",
"pixel_count", "primitive_count"};
for (std::size_t index = 0; index < names.size(); ++index) {
const auto kind = static_cast<Node_Metric_Kind>(index);
if (metrics.contains(kind))
result[names[index]] = metrics.get(kind);
}
return result;
}
inline std::unordered_map<std::uint64_t, std::string> owner_names(
const Scene_Base& scene) {
std::unordered_map<std::uint64_t, std::string> result;
const auto topology = scene.topology_snapshot();
result.reserve(topology.renderables.size());
for (const auto& base : topology.renderables) {
std::string name;
if (const auto* renderable = dynamic_cast<const renderive::Renderable*>(base.get()))
name = renderable->object_name();
if (name.empty())
name = "Renderable " + std::to_string(base->renderable_id());
result.emplace(base->renderable_id(), std::move(name));
}
return result;
}
inline Json plan_json(const Scene_Base& scene, const Render_Plan& plan) {
const auto names = owner_names(scene);
Json nodes = Json::array();
Json edges = Json::array();
std::map<std::uint64_t, std::pair<bool, bool>> active_stages;
std::set<std::uint64_t> owners;
for (const auto& node : plan.graph.nodes) {
const std::string owner = node.owner_id == 0
? "Scene"
: names.contains(node.owner_id)
? names.at(node.owner_id)
: "Renderable " + std::to_string(node.owner_id);
nodes.push_back({
{"id", std::to_string(node.node_id)},
{"node_id", node.node_id},
{"owner_id", node.owner_id},
{"owner", owner},
{"label", owner + " / " + node.name},
{"name", node.name},
{"kind", node_kind(node.kind)},
{"execution_index", node.execution_index}
});
if (node.owner_id != 0) {
owners.insert(node.owner_id);
auto& stages = active_stages[node.owner_id];
stages.first = stages.first || node.kind == Render_Node_Kind::prepare;
stages.second = stages.second || node.kind == Render_Node_Kind::paint;
}
}
for (const auto& edge : plan.graph.edges) {
edges.push_back({{"from", std::to_string(edge.from)},
{"to", std::to_string(edge.to)},
{"kind", "dependency"}});
}
Json renderables = Json::array();
for (const std::uint64_t owner_id : owners) {
const auto stages = active_stages.find(owner_id);
const bool has_prepare = stages != active_stages.end() && stages->second.first;
const bool has_paint = stages != active_stages.end() && stages->second.second;
renderables.push_back({
{"owner_id", owner_id},
{"name", names.contains(owner_id)
? names.at(owner_id)
: "Renderable " + std::to_string(owner_id)},
{"prepare_cache", has_prepare ? "MISS" : "HIT"},
{"paint_cache", has_paint ? "MISS" : "HIT"},
{"prepare_in_plan", has_prepare},
{"paint_in_plan", has_paint}
});
}
return {{"version", plan.version},
{"topology_id", "plan-" + std::to_string(plan.version)},
{"nodes", std::move(nodes)},
{"edges", std::move(edges)},
{"renderables", std::move(renderables)}};
}
inline Json execution_json(const Node_Execution& execution,
const Frame_Snapshot& frame) {
return {
{"node_id", execution.node_id},
{"start_time_ns", execution.start_time_ns},
{"end_time_ns", execution.end_time_ns},
{"start_offset_ns", execution.start_time_ns >= frame.render_start_ns
? execution.start_time_ns - frame.render_start_ns : 0},
{"end_offset_ns", execution.end_time_ns >= frame.render_start_ns
? execution.end_time_ns - frame.render_start_ns : 0},
{"duration_ns", execution.duration_ns()},
{"worker_id", execution.worker_id},
{"status", execution_status(execution.status)},
{"metrics", metrics_json(execution.metrics)}
};
}
inline Json node_analysis_json(const Node_Frame_Analysis& node) {
return {
{"node_id", node.node_id},
{"duration_ns", node.duration_ns},
{"start_offset_ns", node.start_offset_ns},
{"end_offset_ns", node.end_offset_ns},
{"dependency_ready_time_ns", node.dependency_ready_time_ns},
{"scheduler_wait_ns", node.scheduler_wait_ns},
{"work_contribution", node.work_contribution},
{"critical_path_contribution", node.critical_path_contribution},
{"on_critical_path", node.on_critical_path}
};
}
inline Json frame_json(const Captured_Frame& frame) {
Json executions = Json::array();
for (const auto& execution : frame.snapshot->node_executions)
executions.push_back(execution_json(execution, *frame.snapshot));
Json nodes = Json::array();
for (const auto& node : frame.analysis.nodes)
nodes.push_back(node_analysis_json(node));
Json workers = Json::array();
for (const auto& worker : frame.analysis.workers) {
workers.push_back({{"worker_id", worker.worker_id},
{"work_duration_ns", worker.work_duration_ns},
{"utilization", worker.utilization}});
}
return {
{"frame_id", frame.snapshot->frame_id},
{"render_plan_version", frame.snapshot->render_plan_version},
{"render_start_ns", frame.snapshot->render_start_ns},
{"render_end_ns", frame.snapshot->render_end_ns},
{"render_duration_ns", frame.snapshot->render_duration_ns()},
{"node_executions", std::move(executions)},
{"analysis", {
{"total_render_duration_ns", frame.analysis.total_render_duration_ns},
{"critical_path_duration_ns", frame.analysis.critical_path_duration_ns},
{"total_work_duration_ns", frame.analysis.total_work_duration_ns},
{"parallel_overlap_ns", frame.analysis.parallel_overlap_ns},
{"peak_parallelism", frame.analysis.peak_parallelism},
{"average_parallelism", frame.analysis.average_parallelism},
{"critical_path", frame.analysis.critical_path},
{"bottleneck_nodes", frame.analysis.bottleneck_nodes},
{"nodes", std::move(nodes)},
{"workers", std::move(workers)}
}}
};
}
inline Json node_statistics_json(const Node_Statistics& statistics) {
return {
{"node_id", statistics.node_id},
{"execution_count", statistics.execution_count},
{"average_ns", statistics.average_ns},
{"moving_average_ns", statistics.moving_average_ns},
{"p50_ns", statistics.p50_ns},
{"p95_ns", statistics.p95_ns},
{"p99_ns", statistics.p99_ns},
{"minimum_ns", statistics.minimum_ns},
{"maximum_ns", statistics.maximum_ns},
{"critical_path_frequency", statistics.critical_path_frequency},
{"average_scheduler_wait_ns", statistics.average_scheduler_wait_ns}
};
}
inline Json plan_statistics_json(const Plan_Version_Statistics& statistics) {
Json nodes = Json::array();
for (const auto& node : statistics.nodes)
nodes.push_back(node_statistics_json(node));
return {
{"render_plan_version", statistics.render_plan_version},
{"frame_count", statistics.frame_count},
{"render_average_ns", statistics.render_average_ns},
{"render_p50_ns", statistics.render_p50_ns},
{"render_p95_ns", statistics.render_p95_ns},
{"render_maximum_ns", statistics.render_maximum_ns},
{"average_parallelism", statistics.average_parallelism},
{"peak_parallelism", statistics.peak_parallelism},
{"scheduler_wait_average_ns", statistics.scheduler_wait_average_ns},
{"scheduler_wait_p95_ns", statistics.scheduler_wait_p95_ns},
{"nodes", std::move(nodes)}
};
}
inline double percentile(std::vector<std::uint64_t> values, double quantile) {
if (values.empty())
return 0.0;
std::sort(values.begin(), values.end());
const double position = quantile * static_cast<double>(values.size() - 1);
const auto lower = static_cast<std::size_t>(std::floor(position));
const auto upper = static_cast<std::size_t>(std::ceil(position));
const double fraction = position - static_cast<double>(lower);
return static_cast<double>(values[lower]) * (1.0 - fraction) +
static_cast<double>(values[upper]) * fraction;
}
inline Json session_summary_json(const Capture_Session& session) {
std::vector<std::uint64_t> durations;
std::vector<std::uint64_t> waits;
std::map<Render_Node_Id, std::size_t> critical_frequency;
double parallelism{};
std::size_t peak{};
for (const auto& frame : session.frames) {
durations.push_back(frame.analysis.total_render_duration_ns);
parallelism += frame.analysis.average_parallelism;
peak = std::max(peak, frame.analysis.peak_parallelism);
for (const auto& node : frame.analysis.nodes)
waits.push_back(node.scheduler_wait_ns);
for (Render_Node_Id node_id : frame.analysis.critical_path)
++critical_frequency[node_id];
}
const double duration_sum = std::accumulate(
durations.begin(), durations.end(), 0.0);
const double wait_sum = std::accumulate(waits.begin(), waits.end(), 0.0);
Json critical = Json::array();
for (const auto& [node_id, frequency] : critical_frequency)
critical.push_back({{"node_id", node_id}, {"frequency", frequency}});
return {
{"render_average_ns", durations.empty() ? 0.0 : duration_sum / durations.size()},
{"render_p50_ns", percentile(durations, 0.50)},
{"render_p95_ns", percentile(durations, 0.95)},
{"render_maximum_ns", durations.empty() ? 0 : *std::max_element(durations.begin(), durations.end())},
{"average_parallelism", session.frames.empty() ? 0.0 : parallelism / session.frames.size()},
{"peak_parallelism", peak},
{"scheduler_wait_average_ns", waits.empty() ? 0.0 : wait_sum / waits.size()},
{"scheduler_wait_p95_ns", percentile(waits, 0.95)},
{"critical_path_frequency", std::move(critical)}
};
}
inline Json plan_difference_json(const Render_Plan_Difference& difference) {
Json added_edges = Json::array();
Json removed_edges = Json::array();
for (const auto& edge : difference.added_edges)
added_edges.push_back({{"from", edge.from}, {"to", edge.to}});
for (const auto& edge : difference.removed_edges)
removed_edges.push_back({{"from", edge.from}, {"to", edge.to}});
return {{"added_nodes", difference.added_nodes},
{"removed_nodes", difference.removed_nodes},
{"added_edges", std::move(added_edges)},
{"removed_edges", std::move(removed_edges)}};
}
} // namespace gallery_capture_detail
inline nlohmann::json gallery_render_plan_json(const Scene_Base& scene) {
const auto plan = scene.render_plan_snapshot();
return plan ? gallery_capture_detail::plan_json(scene, *plan)
: nlohmann::json{{"version", 0}, {"nodes", nlohmann::json::array()},
{"edges", nlohmann::json::array()},
{"renderables", nlohmann::json::array()}};
}
inline nlohmann::json gallery_performance_capture_json(const Scene_Base& scene) {
using namespace gallery_capture_detail;
const auto controller = scene.capture_state();
Json sessions = Json::array();
std::set<Render_Plan_Version> referenced_plans;
for (Capture_Session_Id id : scene.capture_sessions()) {
const auto session = scene.capture_session(id);
if (!session)
continue;
Json frames = Json::array();
for (const auto& frame : session->frames) {
frames.push_back(frame_json(frame));
referenced_plans.insert(frame.snapshot->render_plan_version);
}
Json node_statistics = Json::array();
for (const auto& node : scene.node_statistics(id))
node_statistics.push_back(node_statistics_json(node));
Json plan_statistics = Json::array();
for (const auto& plan : scene.plan_version_statistics(id))
plan_statistics.push_back(plan_statistics_json(plan));
sessions.push_back({
{"session_id", session->session_id},
{"requested_count", session->requested_count},
{"captured_count", session->captured_count()},
{"active", session->active()},
{"frames", std::move(frames)},
{"summary", session_summary_json(*session)},
{"node_statistics", std::move(node_statistics)},
{"plan_statistics", std::move(plan_statistics)}
});
}
if (const auto current = scene.render_plan_snapshot())
referenced_plans.insert(current->version);
Json plans = Json::array();
std::shared_ptr<const Render_Plan> previous;
for (Render_Plan_Version version : referenced_plans) {
const auto plan = scene.find_render_plan(version);
if (!plan)
continue;
Json value = plan_json(scene, *plan);
value["difference"] = previous
? plan_difference_json(compare_render_plans(*previous, *plan))
: Json{{"added_nodes", Json::array()}, {"removed_nodes", Json::array()},
{"added_edges", Json::array()}, {"removed_edges", Json::array()}};
plans.push_back(std::move(value));
previous = plan;
}
return {
{"controller", {
{"enabled", controller.enabled()},
{"session_id", controller.session_id},
{"remaining_frame_count", controller.remaining_frame_count}
}},
{"sessions", std::move(sessions)},
{"plans", std::move(plans)}
};
}
} // namespace renderive::web
@@ -0,0 +1,136 @@
#pragma once
#include "Gallery_Capture_Json.h"
#include "Gallery_Observer_Adminive.h"
#include "Gallery_Renderables.h"
#include <renderive/scene/base/Scene_Base.hpp>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <array>
#include <charconv>
#include <concepts>
#include <functional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
namespace renderive::web {
class Gallery_Controls final {
public:
using Json = nlohmann::json;
template <class Object>
void add(Object& object) {
const auto descriptor = adminive::Type_Descriptor<Object>::get();
add(descriptor.name(), descriptor.label(), object);
}
template <class Object>
void add(std::string id, Object& object) {
add(std::move(id), adminive::Type_Descriptor<Object>::get().label(), object);
}
[[nodiscard]] Json resources() const {
Json result = Json::array();
for (const auto& entry : entries_) {
Json resource = entry.resource();
resource["target"] = entry.id;
resource["view"]["title"] = entry.title;
result.push_back(std::move(resource));
}
return result;
}
[[nodiscard]] Json observers(const Scene_Base& scene) const {
Json result = Json::array();
const auto topology = scene.topology_snapshot();
for (std::size_t index = 0; index < topology.renderables.size(); ++index) {
const auto* renderable =
dynamic_cast<const renderive::Renderable*>(topology.renderables[index].get());
if (!renderable)
continue;
const Entry* entry = find(renderable);
std::string title = entry ? entry->title : renderable->object_name();
if (title.empty())
title = "渲染节点 " + std::to_string(index + 1);
Json resource{
{"descriptor", adminive::to_descriptor_json<
Json, renderive::Renderable_Observation>()},
{"data", adminive::to_frontend_json<Json>(renderable->observation())},
{"target", entry ? entry->id : node_id(index)},
{"title", std::move(title)}
};
result.push_back(std::move(resource));
}
return result;
}
[[nodiscard]] Json render_plan(const Scene_Base& scene) const {
return gallery_render_plan_json(scene);
}
[[nodiscard]] adminive::Update_Result apply(std::string_view target,
const Json& patch) const {
for (const auto& entry : entries_) {
if (entry.id == target)
return entry.apply(patch);
}
adminive::Update_Result result;
result.success = false;
result.message = "unknown gallery control target: " + std::string(target);
return result;
}
private:
struct Entry {
std::string id;
std::string title;
std::function<Json()> resource;
std::function<adminive::Update_Result(const Json&)> apply;
renderive::Renderable* renderable{};
};
template <class Object>
void add(std::string id, std::string title, Object& object) {
using Model = adminive::Object_Model_Type<Object>;
auto* target = &object;
renderive::Renderable* renderable{};
if constexpr (std::derived_from<Object, renderive::Renderable>)
renderable = target;
entries_.push_back({
std::move(id), std::move(title),
[target] {
return Json{
{"descriptor", adminive::to_descriptor_json<Json, Object>()},
{"view", adminive::to_view_json<Json, Model>(
adminive::describe_edit_view<Model>())},
{"data", adminive::to_frontend_json<Json>(*target)}
};
},
[target](const Json& patch) {
return adminive::apply_frontend_patch<Json>(*target, patch);
},
renderable
});
}
[[nodiscard]] const Entry* find(const Renderable_Base* renderable) const noexcept {
const auto found = std::find_if(entries_.begin(), entries_.end(),
[renderable](const Entry& entry) { return entry.renderable == renderable; });
return found == entries_.end() ? nullptr : &*found;
}
static std::string node_id(std::size_t index) {
return "renderable-" + std::to_string(index);
}
std::vector<Entry> entries_;
};
} // namespace renderive::web
@@ -0,0 +1,34 @@
#pragma once
#include <magic_enum/magic_enum.hpp>
#include <algorithm>
#include <cctype>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
namespace renderive::web {
template <class Enum>
requires std::is_enum_v<Enum>
std::string gallery_enum_id(Enum value) {
std::string result(magic_enum::enum_name(value));
std::ranges::transform(result, result.begin(), [](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
return result;
}
template <class Enum>
requires std::is_enum_v<Enum>
std::optional<Enum> gallery_enum_cast(std::string_view identifier) {
for (const Enum value : magic_enum::enum_values<Enum>()) {
if (gallery_enum_id(value) == identifier)
return value;
}
return std::nullopt;
}
} // namespace renderive::web
@@ -0,0 +1,259 @@
#pragma once
#include "Gallery_Enum.h"
#include "render_2D/scene/Scene.h"
#include "render_2D/renderable/Renderable.h"
#include "adminive/adminive.hpp"
#include "adminive/adapters/magic_enum.hpp"
#include <cstdint>
#include <stdexcept>
#include <string>
namespace renderive::web {
struct Gallery_Consumer_Feedback_Snapshot {
bool master_enabled{};
bool pixel_enabled{};
bool presentation_enabled{};
bool manual_enabled{};
double manual_fps{};
std::string source;
std::uint64_t pixel_interval_ns{};
std::uint64_t presentation_interval_ns{};
std::uint64_t manual_interval_ns{};
};
struct Gallery_Render_Performance {
std::uint64_t render_attempt_count{};
std::uint64_t successful_render_count{};
std::uint64_t failed_render_count{};
double measured_fps{};
double lifetime_average_fps{};
double last_render_ms{};
double average_render_ms{};
double maximum_render_ms{};
double render_deviation_ms{};
double render_p50_ms{};
double render_p95_ms{};
double render_p99_ms{};
std::uint64_t render_sample_count{};
double pixel_response_fps{};
double last_pixel_snapshot_ms{};
double last_pixel_encode_ms{};
double average_pixel_encode_ms{};
double maximum_pixel_encode_ms{};
double pixel_encode_deviation_ms{};
double pixel_encode_p50_ms{};
double pixel_encode_p95_ms{};
double pixel_encode_p99_ms{};
std::uint64_t pixel_encode_sample_count{};
double last_pixel_request_ms{};
double average_pixel_request_ms{};
double pixel_request_deviation_ms{};
double pixel_request_p95_ms{};
double pixel_request_p99_ms{};
std::uint64_t last_pixel_bytes{};
double pixel_payload_megabytes_per_second{};
bool automatic_low_latency_scheduler{};
};
struct Gallery_Client_Performance {
double transport_fps{};
double presentation_fps{};
std::uint64_t websocket_buffered_bytes{};
std::uint64_t changed_pixel_frames{};
std::uint64_t duplicate_pixel_frames{};
std::uint64_t frame_request_timeout_count{};
double frame_round_trip_ms{};
double frame_round_trip_average_ms{};
double frame_round_trip_deviation_ms{};
double frame_round_trip_p95_ms{};
double frame_round_trip_p99_ms{};
double display_interval_ms{};
double display_interval_average_ms{};
double display_interval_latest_ms{};
double display_interval_p95_ms{};
double display_interval_p99_ms{};
double display_interval_deviation_ms{};
std::uint64_t overwritten_pixel_frames{};
double last_pixel_receive_age_ms{};
double last_pixel_change_age_ms{};
};
} // namespace renderive::web
namespace adminive {
template <class Json>
struct Value_Adapter<renderive::Frame_Control_Mode, Json> {
using value_type = std::string;
static std::string read(renderive::Frame_Control_Mode value) {
return renderive::web::gallery_enum_id(value);
}
static void write(renderive::Frame_Control_Mode& target, std::string value) {
const auto parsed =
renderive::web::gallery_enum_cast<renderive::Frame_Control_Mode>(value);
if (!parsed)
throw std::invalid_argument("unknown frame control mode: " + value);
target = *parsed;
}
};
template <>
struct Type_Descriptor<renderive::Frame_Observer_Snapshot> {
static auto get() {
using T = renderive::Frame_Observer_Snapshot;
return object<T>("kernel_observer",
ADMINIVE_FIELD_LABEL(T, mode, "模式")
.enum_label<renderive::Frame_Control_Mode::Manual>("手动刷新")
.enum_label<renderive::Frame_Control_Mode::Low_Latency>("低延迟")
.enum_label<renderive::Frame_Control_Mode::Playback>("回放队列"),
ADMINIVE_FIELD_LABEL(T, last_event, "最近事件"),
ADMINIVE_FIELD_LABEL(T, limit_state, "当前瓶颈"),
ADMINIVE_FIELD_LABEL(T, frequency_hz, "配置频率"),
ADMINIVE_FIELD_LABEL(T, observation_count, "观察次数"),
ADMINIVE_FIELD_LABEL(T, produced_frame_count, "发布"),
ADMINIVE_FIELD_LABEL(T, consumed_frame_count, "完成"),
ADMINIVE_FIELD_LABEL(T, dropped_frame_count, "丢弃"),
ADMINIVE_FIELD_LABEL(T, failed_operation_count, "失败"),
ADMINIVE_FIELD_LABEL(T, pending_frame_count, "待处理"),
ADMINIVE_FIELD_LABEL(T, latest_sequence, "最新序号"),
ADMINIVE_FIELD_LABEL(T, paint_duration_ns, "绘制事件耗时"),
ADMINIVE_FIELD_LABEL(T, render_duration_ns, "后台渲染"),
ADMINIVE_FIELD_LABEL(T, target_interval_ns, "目标间隔"),
ADMINIVE_FIELD_LABEL(T, frequency_limit_enabled, "频率限制"),
ADMINIVE_FIELD_LABEL(T, consumer_feedback_enabled, "内核反馈有效"),
ADMINIVE_FIELD_LABEL(T, bottleneck_duration_ns, "内部瓶颈"),
ADMINIVE_FIELD_LABEL(T, consumer_sample_interval_ns, "消费者原始采样"),
ADMINIVE_FIELD_LABEL(T, consumer_smoothed_interval_ns, "消费者平滑周期"),
ADMINIVE_FIELD_LABEL(T, consumer_variation_ns, "消费者抖动"),
ADMINIVE_FIELD_LABEL(T, consumer_safety_interval_ns, "消费者安全期限"),
ADMINIVE_FIELD_LABEL(T, consumer_interval_ns, "消费者限速周期"),
ADMINIVE_FIELD_LABEL(T, next_refresh_interval_ns, "下次刷新"),
ADMINIVE_FIELD_LABEL(T, paint_lease_wait_ns, "绘制租约等待"),
ADMINIVE_FIELD_LABEL(T, paint_state_wait_ns, "绘制状态等待"),
ADMINIVE_FIELD_LABEL(T, publish_state_wait_ns, "发布状态等待"),
ADMINIVE_FIELD_LABEL(T, ready_wait_ns, "就绪等待"),
ADMINIVE_FIELD_LABEL(T, frame_age_at_render_ns, "开始渲染时帧龄"),
ADMINIVE_FIELD_LABEL(T, render_lease_wait_ns, "渲染租约等待"),
ADMINIVE_FIELD_LABEL(T, render_state_wait_ns, "渲染状态等待"),
ADMINIVE_FIELD_LABEL(T, render_finish_state_wait_ns, "渲染完成等待"),
ADMINIVE_FIELD_LABEL(T, queue_wait_ns, "回放队列等待"),
ADMINIVE_FIELD_LABEL(T, end_to_end_ns, "端到端延迟"))
.label("内核观察器");
}
};
template <>
struct Type_Descriptor<renderive::web::Gallery_Consumer_Feedback_Snapshot> {
static auto get() {
using T = renderive::web::Gallery_Consumer_Feedback_Snapshot;
return object<T>("consumer_feedback",
ADMINIVE_FIELD_LABEL(T, master_enabled, "消费者反馈总开关"),
ADMINIVE_FIELD_LABEL(T, pixel_enabled, "像素响应反馈"),
ADMINIVE_FIELD_LABEL(T, presentation_enabled, "浏览器呈现反馈"),
ADMINIVE_FIELD_LABEL(T, manual_enabled, "手动消费者反馈"),
ADMINIVE_FIELD_LABEL(T, manual_fps, "手动消费者 FPS"),
ADMINIVE_FIELD_LABEL(T, source, "生效反馈来源"),
ADMINIVE_FIELD_LABEL(T, pixel_interval_ns, "像素响应周期"),
ADMINIVE_FIELD_LABEL(T, presentation_interval_ns, "浏览器呈现周期"),
ADMINIVE_FIELD_LABEL(T, manual_interval_ns, "手动消费者周期"))
.label("消费者反馈");
}
};
template <>
struct Type_Descriptor<renderive::web::Gallery_Render_Performance> {
static auto get() {
using T = renderive::web::Gallery_Render_Performance;
return object<T>("render_performance",
ADMINIVE_FIELD_LABEL(T, render_attempt_count, "渲染尝试"),
ADMINIVE_FIELD_LABEL(T, successful_render_count, "渲染成功"),
ADMINIVE_FIELD_LABEL(T, failed_render_count, "渲染失败"),
ADMINIVE_FIELD_LABEL(T, measured_fps, "最近渲染帧率"),
ADMINIVE_FIELD_LABEL(T, lifetime_average_fps, "平均渲染帧率"),
ADMINIVE_FIELD_LABEL(T, last_render_ms, "最近渲染耗时"),
ADMINIVE_FIELD_LABEL(T, average_render_ms, "平均渲染耗时"),
ADMINIVE_FIELD_LABEL(T, maximum_render_ms, "最大渲染耗时"),
ADMINIVE_FIELD_LABEL(T, render_deviation_ms, "渲染标准差"),
ADMINIVE_FIELD_LABEL(T, render_p50_ms, "渲染 P50"),
ADMINIVE_FIELD_LABEL(T, render_p95_ms, "渲染 P95"),
ADMINIVE_FIELD_LABEL(T, render_p99_ms, "渲染 P99"),
ADMINIVE_FIELD_LABEL(T, render_sample_count, "渲染窗口样本"),
ADMINIVE_FIELD_LABEL(T, pixel_response_fps, "像素响应帧率"),
ADMINIVE_FIELD_LABEL(T, last_pixel_snapshot_ms, "最近像素快照耗时"),
ADMINIVE_FIELD_LABEL(T, last_pixel_encode_ms, "最近像素编码耗时"),
ADMINIVE_FIELD_LABEL(T, average_pixel_encode_ms, "平均像素编码耗时"),
ADMINIVE_FIELD_LABEL(T, maximum_pixel_encode_ms, "最大像素编码耗时"),
ADMINIVE_FIELD_LABEL(T, pixel_encode_deviation_ms, "像素编码标准差"),
ADMINIVE_FIELD_LABEL(T, pixel_encode_p50_ms, "像素编码 P50"),
ADMINIVE_FIELD_LABEL(T, pixel_encode_p95_ms, "像素编码 P95"),
ADMINIVE_FIELD_LABEL(T, pixel_encode_p99_ms, "像素编码 P99"),
ADMINIVE_FIELD_LABEL(T, pixel_encode_sample_count, "像素编码窗口样本"),
ADMINIVE_FIELD_LABEL(T, last_pixel_request_ms, "最近像素请求耗时"),
ADMINIVE_FIELD_LABEL(T, average_pixel_request_ms, "像素请求滑动平均"),
ADMINIVE_FIELD_LABEL(T, pixel_request_deviation_ms, "像素请求标准差"),
ADMINIVE_FIELD_LABEL(T, pixel_request_p95_ms, "像素请求 P95"),
ADMINIVE_FIELD_LABEL(T, pixel_request_p99_ms, "像素请求 P99"),
ADMINIVE_FIELD_LABEL(T, last_pixel_bytes, "最近像素负载"),
ADMINIVE_FIELD_LABEL(T, pixel_payload_megabytes_per_second, "像素负载吞吐"),
ADMINIVE_FIELD_LABEL(T, automatic_low_latency_scheduler, "自动低延迟调度"))
.label("渲染性能");
}
};
template <>
struct Type_Descriptor<renderive::web::Gallery_Client_Performance> {
static auto get() {
using T = renderive::web::Gallery_Client_Performance;
return object<T>("client_performance",
ADMINIVE_FIELD_LABEL(T, transport_fps, "像素响应帧率"),
ADMINIVE_FIELD_LABEL(T, presentation_fps, "浏览器呈现帧率"),
ADMINIVE_FIELD_LABEL(T, websocket_buffered_bytes, "WebSocket 缓冲"),
ADMINIVE_FIELD_LABEL(T, changed_pixel_frames, "变化像素帧"),
ADMINIVE_FIELD_LABEL(T, duplicate_pixel_frames, "重复像素帧"),
ADMINIVE_FIELD_LABEL(T, frame_request_timeout_count, "像素请求超时"),
ADMINIVE_FIELD_LABEL(T, frame_round_trip_ms, "WebSocket 往返耗时"),
ADMINIVE_FIELD_LABEL(T, frame_round_trip_average_ms, "WebSocket 往返滑动平均"),
ADMINIVE_FIELD_LABEL(T, frame_round_trip_deviation_ms, "WebSocket 往返标准差"),
ADMINIVE_FIELD_LABEL(T, frame_round_trip_p95_ms, "WebSocket 往返 P95"),
ADMINIVE_FIELD_LABEL(T, frame_round_trip_p99_ms, "WebSocket 往返 P99"),
ADMINIVE_FIELD_LABEL(T, display_interval_ms, "呈现中位周期"),
ADMINIVE_FIELD_LABEL(T, display_interval_average_ms, "呈现滑动平均周期"),
ADMINIVE_FIELD_LABEL(T, display_interval_latest_ms, "最近呈现周期"),
ADMINIVE_FIELD_LABEL(T, display_interval_p95_ms, "呈现 P95 周期"),
ADMINIVE_FIELD_LABEL(T, display_interval_p99_ms, "呈现 P99 周期"),
ADMINIVE_FIELD_LABEL(T, display_interval_deviation_ms, "呈现周期标准差"),
ADMINIVE_FIELD_LABEL(T, overwritten_pixel_frames, "未呈现覆盖帧"),
ADMINIVE_FIELD_LABEL(T, last_pixel_receive_age_ms, "最近像素龄"),
ADMINIVE_FIELD_LABEL(T, last_pixel_change_age_ms, "最近变化龄"))
.label("浏览器性能");
}
};
template <>
struct Type_Descriptor<renderive::Renderable_Observation> {
static auto get() {
using T = renderive::Renderable_Observation;
return object<T>("renderable_observer",
ADMINIVE_FIELD_LABEL(T, event, "最近状态事件")
.enum_label<renderive::Renderable_Observer_Event::None>("尚无事件")
.enum_label<renderive::Renderable_Observer_Event::Cache_Updated>("缓存状态已更新")
.enum_label<renderive::Renderable_Observer_Event::Published>("渲染状态已发布"),
ADMINIVE_FIELD_LABEL(T, event_time_ns, "事件时间"),
ADMINIVE_FIELD_LABEL(T, cache_update_count, "缓存更新次数"),
ADMINIVE_FIELD_LABEL(T, publish_count, "状态发布次数"))
.label("渲染对象观察器");
}
};
static_assert(Described_Type<renderive::Frame_Observer_Snapshot>);
static_assert(Described_Type<renderive::web::Gallery_Consumer_Feedback_Snapshot>);
static_assert(Described_Type<renderive::web::Gallery_Render_Performance>);
static_assert(Described_Type<renderive::web::Gallery_Client_Performance>);
static_assert(Described_Type<renderive::Renderable_Observation>);
} // namespace adminive
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,25 @@
#pragma once
#include "Web_Plot_Session.h"
#include <memory>
#include <optional>
namespace renderive::web {
class Gallery_Plot_Session final {
public:
Gallery_Plot_Session();
explicit Gallery_Plot_Session(bool automatic_low_latency);
~Gallery_Plot_Session();
Gallery_Plot_Session(const Gallery_Plot_Session&) = delete;
Gallery_Plot_Session& operator=(const Gallery_Plot_Session&) = delete;
[[nodiscard]] std::optional<Web_Response> handle(const Web_Event& event);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace renderive::web
@@ -0,0 +1,694 @@
#include "Gallery_Protocol.h"
#include "Gallery_Enum.h"
#include "Gallery_Observer_Adminive.h"
#include "Gallery_Renderables.h"
#include "Gallery_Session_Control_Adminive.h"
#include "adminive/adminive.hpp"
#include "adminive/adapters/nlohmann_json.hpp"
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <tuple>
#include <type_traits>
#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;
};
using Action_Model = Gallery_Action_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 驱动的连续轨迹,覆盖时间格式和属性化笔刷 API。", 60},
{"selection_overlay", "框选叠层", "Selection_Rectangle_Overlay", "交互控件",
"鼠标框选、多区域保留、标注样式和清空。", 70},
{"constellation", "星座图", "Constellation_Diagram", "点图控件",
"PSK4/PSK8/PSK16 模式、相位、寿命、坐标范围和方形拟合。", 80}
};
return value;
}
const Case_Model* find_case(std::string_view id) {
const auto& all = cases();
const auto found = std::find_if(all.begin(), all.end(),
[id](const Case_Model& item) { return item.id == id; });
return found == all.end() ? nullptr : &*found;
}
void append_session_actions(Gallery_Frame_Mode frame_mode,
std::vector<Action_Model>& result) {
switch (frame_mode) {
case Gallery_Frame_Mode::Manual:
append_gallery_actions<Gallery_Session_Control<Manual_Scene2D>>(result);
return;
case Gallery_Frame_Mode::Low_Latency:
append_gallery_actions<Gallery_Session_Control<Scene2D>>(result);
return;
case Gallery_Frame_Mode::Playback:
append_gallery_actions<Gallery_Session_Control<Playback_Scene2D>>(result);
return;
}
throw std::invalid_argument("unknown gallery frame mode");
}
std::size_t session_control_count(Gallery_Frame_Mode frame_mode) {
switch (frame_mode) {
case Gallery_Frame_Mode::Manual:
return gallery_control_count<Gallery_Session_Control<Manual_Scene2D>>();
case Gallery_Frame_Mode::Low_Latency:
return gallery_control_count<Gallery_Session_Control<Scene2D>>();
case Gallery_Frame_Mode::Playback:
return gallery_control_count<Gallery_Session_Control<Playback_Scene2D>>();
}
throw std::invalid_argument("unknown gallery frame mode");
}
std::vector<Action_Model> registered_actions(std::string_view case_id,
Gallery_Frame_Mode frame_mode) {
std::vector<Action_Model> result;
append_session_actions(frame_mode, result);
for (auto& action : gallery_frame_actions(frame_mode))
result.push_back(std::move(action));
append_gallery_renderable_actions(case_id, result);
return result;
}
Json parse_request(std::string_view message) {
return Json::parse(message.begin(), message.end());
}
adminive::Table_View action_view();
} // namespace renderive::web::gallery_detail
namespace adminive {
template <>
struct Type_Descriptor<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::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 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();
}
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;
}
template <class Object>
const Json& described_field(std::string_view name) {
static const Json descriptor = adminive::to_descriptor_json<Json, Object>();
const auto& fields = descriptor.at("fields");
const auto found = std::find_if(fields.begin(), fields.end(), [name](const Json& field) {
return field.at("name").template get<std::string>() == name;
});
if (found == fields.end())
throw std::logic_error("missing Adminive dashboard field: " + std::string(name));
return *found;
}
template <class Object>
std::string described_source(std::string_view prefix, std::string_view name) {
static_cast<void>(described_field<Object>(name));
return std::string(prefix) + "." + std::string(name);
}
template <class Object>
Json described_dashboard_field(std::string_view prefix, std::string_view name) {
const Json& field = described_field<Object>(name);
const std::string value_type = field.at("value_type");
std::string format = "integer";
if (name.ends_with("_ns"))
format = "nanoseconds";
else if (name.ends_with("_fps"))
format = "fps";
else if (name.ends_with("frequency_hz"))
format = "frequency";
else if (name == "source")
format = "flags";
else if (value_type == "boolean" || name == "limit_state")
format = "enum";
else if (value_type == "string" || value_type == "enum")
format = "text";
Json result = dashboard_field(
field.at("presentation").at("label").template get<std::string>(),
described_source<Object>(prefix, name), format,
name.ends_with("_fps") ? 2 : -1);
if (value_type == "boolean")
result["value_map"] = "enabled";
else if (name == "limit_state")
result["value_map"] = "limit_state";
else if (name == "source")
result["value_map"] = "consumer_feedback_source";
return result;
}
bool observer_header_field(std::string_view name) {
return name == "mode" || name == "last_event" || name == "limit_state";
}
bool observer_counter_field(std::string_view name) {
return name == "frequency_hz" || name == "latest_sequence" ||
name.ends_with("_count");
}
bool observer_detail_field(std::string_view name) {
return name.ends_with("_wait_ns") || name == "frame_age_at_render_ns";
}
template <class Predicate>
Json observer_fields(Predicate&& predicate) {
const Json descriptor =
adminive::to_descriptor_json<Json, renderive::Frame_Observer_Snapshot>();
Json result = Json::array();
for (const Json& field : descriptor.at("fields")) {
const std::string name = field.at("name");
if (predicate(name)) {
Json field = described_dashboard_field<renderive::Frame_Observer_Snapshot>(
"kernel_observer", name);
if (name == "frequency_hz") {
field["enabled_source"] =
described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "frequency_limit_enabled");
field["disabled_label"] = "已关闭";
} else if (name == "end_to_end_ns") {
field["cell_class"] = "critical";
}
result.push_back(std::move(field));
}
}
return result;
}
Json consumer_feedback_fields() {
const Json descriptor =
adminive::to_descriptor_json<Json, Gallery_Consumer_Feedback_Snapshot>();
Json result = Json::array();
for (const Json& field : descriptor.at("fields")) {
const std::string name = field.at("name");
result.push_back(described_dashboard_field<Gallery_Consumer_Feedback_Snapshot>(
"consumer_feedback", name));
}
return result;
}
Json observer_summary_fields() {
Json result = observer_fields([](std::string_view name) {
return !observer_header_field(name) && !observer_counter_field(name) &&
!observer_detail_field(name);
});
for (Json& field : consumer_feedback_fields())
result.push_back(std::move(field));
return result;
}
Json dashboard_contract() {
Json point_pair{{"label", "输入→绘制"}, {"format", "pair"},
{"sources", Json::array({"data_shape.input_points", "data_shape.rendered_elements"})},
{"separator", ""}};
Json bottleneck = described_dashboard_field<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "limit_state");
bottleneck["format"] = "duration_enum";
bottleneck["duration_sources"] = {
{"frequency_limited", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "target_interval_ns")},
{"paint_limited", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "paint_duration_ns")},
{"render_limited", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "render_duration_ns")},
{"consumer_limited", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "consumer_interval_ns")}
};
Json dashboard{
{"value_maps", {
{"enabled", {{"true", "启用"}, {"false", "关闭"}}},
{"consumer_feedback_source", {
{"disabled", "总开关关闭"}, {"none", ""}, {"pixel", "像素响应"},
{"presentation", "浏览器呈现"}, {"manual", "手动"}
}},
{"limit_state", {
{"frequency_limited", "内核频率受限"}, {"paint_limited", "绘制事件受限"},
{"render_limited", "后台渲染受限"}, {"consumer_limited", "消费者反馈受限"},
{"unlimited", "无限制"}, {"not_applicable", "N/A"}
}}
}},
{"performance", {
{"fields", Json::array({
dashboard_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("Core 滑动平均 ms", "performance.average_render_ms", "fixed", 2),
dashboard_field("Core P95 ms", "performance.render_p95_ms", "fixed", 2),
dashboard_field("Core P99 ms", "performance.render_p99_ms", "fixed", 2),
dashboard_field("像素编码 ms", "performance.last_pixel_encode_ms", "fixed", 2),
dashboard_field("编码 P95 ms", "performance.pixel_encode_p95_ms", "fixed", 2),
dashboard_field("编码 P99 ms", "performance.pixel_encode_p99_ms", "fixed", 2),
dashboard_field("WS 平均 ms", "client_performance.frame_round_trip_average_ms", "fixed", 2),
dashboard_field("WS P95 ms", "client_performance.frame_round_trip_p95_ms", "fixed", 2),
dashboard_field("WS P99 ms", "client_performance.frame_round_trip_p99_ms", "fixed", 2),
dashboard_field("呈现平均 ms", "client_performance.display_interval_average_ms", "fixed", 2),
dashboard_field("呈现 P95 ms", "client_performance.display_interval_p95_ms", "fixed", 2),
dashboard_field("呈现 P99 ms", "client_performance.display_interval_p99_ms", "fixed", 2),
dashboard_field("响应负载 MB/s", "performance.pixel_payload_megabytes_per_second", "fixed", 1),
described_dashboard_field<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "pending_frame_count"),
described_dashboard_field<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "dropped_frame_count"),
std::move(point_pair), std::move(bottleneck),
described_dashboard_field<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "last_event"),
dashboard_field("像素超时", "client_performance.frame_request_timeout_count"),
dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 0)
})}
}},
{"menu_views", {
{"observer", {
{"kernel", {
{"title", "内核帧观察器"},
{"source", "kernel_observer"},
{"descriptor", adminive::to_descriptor_json<
Json, renderive::Frame_Observer_Snapshot>()}
}},
{"renderables_source", "renderable_observers"},
{"renderable_fields", Json::array({
"event", "event_time_ns", "cache_update_count", "publish_count"
})}
}},
{"performance", {
{"renderables_source", "renderable_observers"},
{"renderable_fields", Json::array({
"event", "event_time_ns", "cache_update_count", "publish_count"
})},
{"resources", Json::array({
{
{"title", "渲染性能"},
{"source", "performance"},
{"descriptor", adminive::to_descriptor_json<
Json, Gallery_Render_Performance>()}
},
{
{"title", "浏览器性能"},
{"source", "client_performance"},
{"descriptor", adminive::to_descriptor_json<
Json, Gallery_Client_Performance>()}
}
})}
}}
}},
{"limits", {
{"aria_label", "低延迟限速来源"}, {"title", "限速来源"},
{"current_source", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "limit_state")},
{"active_label", "当前瓶颈"}, {"inactive_label", "未受限"},
{"disabled_label", "已关闭"},
{"fields", Json::array({
{{"label", "内核用户频率"}, {"active_value", "frequency_limited"},
{"duration_source", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "target_interval_ns")},
{"enabled_source", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "frequency_limit_enabled")}},
{{"label", "内核绘制事件"}, {"active_value", "paint_limited"},
{"duration_source", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "paint_duration_ns")}},
{{"label", "内核后台渲染"}, {"active_value", "render_limited"},
{"duration_source", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "render_duration_ns")}},
{{"label", "消费者反馈"}, {"active_value", "consumer_limited"},
{"duration_source", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "consumer_interval_ns")},
{"enabled_source", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "consumer_feedback_enabled")}}
})}
}},
{"observer", {
{"aria_label", "内核低延迟全量统计"},
{"header", {
{"prefix", "内核"}, {"suffix", "观察器"}, {"event_label", "事件"},
{"mode", described_dashboard_field<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "mode")},
{"limit", described_dashboard_field<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "limit_state")},
{"event", described_dashboard_field<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "last_event")}
}},
{"sections", Json::array({
{{"class_name", "observer-counters"},
{"fields", observer_fields(observer_counter_field)}},
{{"class_name", "latency-summary"},
{"fields", observer_summary_fields()}},
{{"class_name", "client-summary"}, {"aria_label", "浏览器消费者反馈全量统计"},
{"fields", Json::array({
dashboard_field("RAF 最新周期", "client_performance.display_interval_latest_ms", "milliseconds", 3),
dashboard_field("RAF 中位周期", "client_performance.display_interval_ms", "milliseconds", 3),
dashboard_field("RAF 平均周期", "client_performance.display_interval_average_ms", "milliseconds", 3),
dashboard_field("RAF P95 周期", "client_performance.display_interval_p95_ms", "milliseconds", 3),
dashboard_field("RAF P99 周期", "client_performance.display_interval_p99_ms", "milliseconds", 3),
dashboard_field("RAF 周期标准差", "client_performance.display_interval_deviation_ms", "milliseconds", 3),
dashboard_field("WS 往返", "client_performance.frame_round_trip_ms", "milliseconds", 3),
dashboard_field("WS 往返平均", "client_performance.frame_round_trip_average_ms", "milliseconds", 3),
dashboard_field("WS 往返 P95", "client_performance.frame_round_trip_p95_ms", "milliseconds", 3),
dashboard_field("WS 往返 P99", "client_performance.frame_round_trip_p99_ms", "milliseconds", 3),
dashboard_field("WS 往返标准差", "client_performance.frame_round_trip_deviation_ms", "milliseconds", 3),
dashboard_field("像素响应 FPS", "client_performance.transport_fps", "fps", 2),
dashboard_field("浏览器呈现 FPS", "client_performance.presentation_fps", "fps", 2),
dashboard_field("WS 缓冲", "client_performance.websocket_buffered_bytes", "bytes"),
dashboard_field("未呈现覆盖", "client_performance.overwritten_pixel_frames"),
dashboard_field("变化像素帧", "client_performance.changed_pixel_frames"),
dashboard_field("重复像素帧", "client_performance.duplicate_pixel_frames"),
dashboard_field("像素请求超时", "client_performance.frame_request_timeout_count"),
dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 3),
dashboard_field("最近变化龄", "client_performance.last_pixel_change_age_ms", "milliseconds", 3)
})}},
{{"class_name", "latency-details"}, {"aria_label", "内核各阶段等待耗时"},
{"fields", observer_fields(observer_detail_field)}}
})}
}}
};
dashboard["observer"]["descriptor"] =
adminive::to_descriptor_json<Json, renderive::Frame_Observer_Snapshot>();
dashboard["observer"]["consumer_feedback_descriptor"] =
adminive::to_descriptor_json<Json, Gallery_Consumer_Feedback_Snapshot>();
return dashboard;
}
Json frame_mode_contract(Gallery_Frame_Mode mode) {
switch (mode) {
case Gallery_Frame_Mode::Manual:
return {{"id", gallery_enum_id(mode)}, {"title", "手动刷新"},
{"strategy", "Manual_Refresh_Strategy"},
{"description", "显式准备、刷新和渲染;页面不会自动拉取像素。"},
{"automatic", false}, {"request_after_response", false},
{"request_on_animation_frame", false}, {"observer_visible", false},
{"frame_button_label", "手动刷新一帧"}, {"accent", "#ffd166aa"}, {"order", 10}};
case Gallery_Frame_Mode::Low_Latency:
return {{"id", gallery_enum_id(mode)}, {"title", "低延迟"},
{"strategy", "Low_Latency_Strategy"},
{"description", "以最大 FPS 自动发布并消费最新帧,可观测丢帧与端到端延迟。"},
{"automatic", true}, {"request_after_response", true},
{"request_on_animation_frame", false}, {"observer_visible", true},
{"frame_button_label", "立即刷新"}, {"accent", "#45ddbeaa"}, {"order", 20}};
case Gallery_Frame_Mode::Playback:
return {{"id", gallery_enum_id(mode)}, {"title", "回放队列"},
{"strategy", "Flow_Refresh_Strategy"},
{"description", "所有帧按队列顺序入队和消费,可观测队深与排队时间。"},
{"automatic", true}, {"request_after_response", true},
{"request_on_animation_frame", true}, {"observer_visible", false},
{"frame_button_label", "消费下一帧"}, {"accent", "#6aa9ffaa"}, {"order", 30}};
}
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", gallery_enum_id(Gallery_Frame_Mode::Low_Latency)},
{"all_categories_label", "全部"},
{"catalog_loaded_text", "Kernel 策略目录已加载"},
{"hero_eyebrow", "REAL KERNEL STRATEGIES"},
{"hero_title", "对称策略页面,同一批控件,直接比较"}
};
result["dashboard"] = dashboard_contract();
result["case_descriptor"] =
adminive::to_descriptor_json<Json, gallery_detail::Case_Model>();
result["frame_modes"] = Json::array();
constexpr auto frame_modes = magic_enum::enum_values<Gallery_Frame_Mode>();
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();
const std::size_t renderable_controls = gallery_renderable_control_count(item.id);
for (const auto mode : frame_modes) {
const std::string key = gallery_enum_id(mode);
const std::size_t controls =
gallery_detail::session_control_count(mode) + renderable_controls;
const std::size_t actions =
gallery_detail::registered_actions(item.id, mode).size();
entry["control_count_by_mode"][key] = controls;
entry["action_count_by_mode"][key] = actions;
controls_total += controls;
actions_total += actions;
}
entry["control_count"] =
gallery_detail::session_control_count(Gallery_Frame_Mode::Low_Latency) +
renderable_controls;
entry["action_count"] =
gallery_detail::registered_actions(
item.id, Gallery_Frame_Mode::Low_Latency).size();
result["cases"].push_back(std::move(entry));
}
result["coverage"] = {{"case_count", gallery_detail::cases().size()},
{"page_count", std::size(frame_modes)},
{"canvas_count", gallery_detail::cases().size() * std::size(frame_modes)},
{"manual_control_count", controls_total},
{"manual_action_count", actions_total},
{"frequency_modes", Json::array({"spectrum", "afterglow", "sweep_spectrum"})},
{"image_interpolation_modes", magic_enum::enum_names<Image_Interpolation_Mode>()}};
return result.dump();
}
bool Gallery_Protocol::is_case(std::string_view case_id) {
return gallery_detail::find_case(case_id) != nullptr;
}
std::string Gallery_Protocol::case_json_from_controls(
std::string_view case_id,
std::string_view controls_json,
std::string_view telemetry_json,
std::string_view notice,
Gallery_Frame_Mode frame_mode,
bool manual_refresh) {
Json result = protocol_base();
result["type"] = manual_refresh ? "refresh_state" : "case_state";
result["case"] = case_contract(case_id);
result["frame_mode"] = frame_mode_contract(frame_mode);
try {
result["controls"] = Json::parse(controls_json.begin(), controls_json.end());
} catch (const std::exception&) {
result["controls"] = {
{"resources", Json::array()},
{"observers", Json::array()},
{"render_plan", Json::object()}
};
}
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& action : gallery_detail::registered_actions(case_id, frame_mode))
result["actions"]["data"].push_back(
adminive::to_frontend_json<Json>(action));
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::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();
}
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 = gallery_enum_cast<Gallery_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::optional<Gallery_Control_Patch_Request> Gallery_Protocol::control_patch_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_patch" ||
!request.contains("target") || !request.at("target").is_string() ||
!request.contains("patch") || !request.at("patch").is_object())
return std::nullopt;
return Gallery_Control_Patch_Request{
request.at("target").get<std::string>(), request.at("patch").dump()};
} catch (const std::exception&) {
return std::nullopt;
}
}
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::registered_actions(case_id, frame_mode);
return std::any_of(available.begin(), available.end(),
[action_id](const gallery_detail::Action_Model& item) {
return item.id == action_id;
});
}
} // namespace renderive::web
@@ -0,0 +1,44 @@
#pragma once
#include "Gallery_Actions.h"
#include <optional>
#include <string>
#include <string_view>
#include <variant>
namespace renderive::web {
struct Gallery_Open_Request {
std::string case_id;
Gallery_Frame_Mode frame_mode = Gallery_Frame_Mode::Low_Latency;
};
using Gallery_Value = std::variant<bool, double, std::string>;
struct Gallery_Action_Request {
std::string id;
std::optional<Gallery_Value> argument;
};
struct Gallery_Control_Patch_Request {
std::string target;
std::string patch_json;
};
class Gallery_Protocol final {
public:
[[nodiscard]] static std::string catalog_json();
[[nodiscard]] static bool is_case(std::string_view case_id);
[[nodiscard]] static std::string error_json(std::string_view message,
std::string_view field = {});
[[nodiscard]] static std::string observer_json(std::string_view case_id,
Gallery_Frame_Mode frame_mode,
std::string_view telemetry_json);
[[nodiscard]] static std::string case_json_from_controls(
std::string_view case_id,
std::string_view controls_json,
std::string_view telemetry_json,
std::string_view notice,
Gallery_Frame_Mode frame_mode,
bool manual_refresh);
[[nodiscard]] static std::optional<Gallery_Open_Request> open_request(std::string_view message);
[[nodiscard]] static std::optional<Gallery_Action_Request> action_request(std::string_view message);
[[nodiscard]] static std::optional<Gallery_Control_Patch_Request> control_patch_request(
std::string_view message);
[[nodiscard]] static bool action_available(std::string_view case_id, Gallery_Frame_Mode frame_mode, std::string_view action_id);
};
} // namespace renderive::web
@@ -0,0 +1,144 @@
#pragma once
#include "render_2D/axis/Axis.h"
#include "render_2D/axis/Frequency_Axis.h"
#include "render_2D/axis/Time_Axis.h"
#include "render_2D/plottable/Afterglow.h"
#include "render_2D/plottable/Constellation_Diagram.h"
#include "render_2D/plottable/Frequency_Trace.h"
#include "render_2D/plottable/Selection_Rectangle_Overlay.h"
#include "render_2D/plottable/Spectrum.h"
#include "render_2D/plottable/Sweep_Spectrum.h"
#include "render_2D/plottable/Waterfall.h"
#include <structive/property/accessor.hpp>
#include <type_traits>
#include <utility>
namespace renderive::web {
template <class Object, auto Member>
struct Gallery_Property_Accessor {
using object_type = Object;
using Properties = typename Object::Properties;
using value_type = std::remove_cvref_t<decltype(std::declval<Properties>().*Member)>;
using storage_identity = void;
using dependency_spec = structive::No_Property_Dependencies;
static constexpr bool readable = true;
static constexpr bool writable = true;
static constexpr bool synchronized_view_read = false;
static constexpr bool trusted_object_access = true;
value_type read(const Object& object) const {
return object.template adminive_read<Member>();
}
void write(Object& object, value_type value) const {
object.template adminive_write<Member>(std::move(value));
}
};
template <class Base>
class Gallery_Plottable : public Base {
public:
using Properties = typename Base::Properties;
using Base::Base;
private:
template <auto Member>
auto adminive_read() const {
return this->template property_value<Member>();
}
template <auto Member, class Value>
void adminive_write(Value&& value) {
this->template set<Member>(std::forward<Value>(value));
}
template <class Object, auto Member>
friend struct Gallery_Property_Accessor;
};
class Gallery_Spectrum final : public Gallery_Plottable<Spectrum> {
public:
using Gallery_Plottable::Gallery_Plottable;
using Builder = Renderable_Builder<Gallery_Spectrum, Properties>;
};
class Gallery_Waterfall final : public Gallery_Plottable<Waterfall> {
public:
using Gallery_Plottable::Gallery_Plottable;
using Builder = Renderable_Builder<Gallery_Waterfall, Properties>;
};
class Gallery_Afterglow final : public Gallery_Plottable<Afterglow> {
public:
using Gallery_Plottable::Gallery_Plottable;
using Builder = Renderable_Builder<Gallery_Afterglow, Properties>;
};
class Gallery_Sweep_Spectrum final : public Gallery_Plottable<Sweep_Spectrum> {
public:
using Gallery_Plottable::Gallery_Plottable;
using Builder = Renderable_Builder<Gallery_Sweep_Spectrum, Properties>;
};
class Gallery_Frequency_Trace final : public Gallery_Plottable<Frequency_Trace> {
public:
using Gallery_Plottable::Gallery_Plottable;
using Builder = Renderable_Builder<Gallery_Frequency_Trace, Properties>;
};
class Gallery_Selection_Rectangle_Overlay final
: public Gallery_Plottable<Selection_Rectangle_Overlay> {
public:
using Gallery_Plottable::Gallery_Plottable;
using Builder = Renderable_Builder<Gallery_Selection_Rectangle_Overlay, Properties>;
};
class Gallery_Constellation_Diagram final
: public Gallery_Plottable<Constellation_Diagram> {
public:
using Gallery_Plottable::Gallery_Plottable;
using Builder = Renderable_Builder<Gallery_Constellation_Diagram, Properties>;
};
template <class Base, class Properties_Type>
class Gallery_Axis_Base : public Base {
public:
using Properties = Properties_Type;
using Base::Base;
private:
template <auto Member>
auto adminive_read() const {
return this->read([](const auto& state) {
return state.*Member;
});
}
template <auto Member, class Value>
void adminive_write(Value&& value) {
this->template set<Member>(std::forward<Value>(value));
}
template <class Object, auto Member>
friend struct Gallery_Property_Accessor;
};
class Gallery_Axis final : public Gallery_Axis_Base<Axis, Axis_Properties> {
public:
using Gallery_Axis_Base::Gallery_Axis_Base;
using Builder = detail::Axis_Renderable_Builder<Gallery_Axis, Properties>;
};
class Gallery_Frequency_Axis final
: public Gallery_Axis_Base<Frequency_Axis, Axis_Properties> {
public:
using Gallery_Axis_Base::Gallery_Axis_Base;
using Builder = detail::Axis_Renderable_Builder<Gallery_Frequency_Axis, Properties>;
};
class Gallery_Time_Axis final
: public Gallery_Axis_Base<Time_Axis, Time_Axis_Properties> {
public:
using Gallery_Axis_Base::Gallery_Axis_Base;
using Builder = detail::Axis_Renderable_Builder<Gallery_Time_Axis, Properties>;
};
} // namespace renderive::web
@@ -0,0 +1,662 @@
#pragma once
#include "Gallery_Actions.h"
#include "Gallery_Renderable_Types.h"
#include "adminive/adminive.hpp"
#include "adminive/adapters/magic_enum.hpp"
#include "adminive/adapters/nlohmann_json.hpp"
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace renderive::web::gallery_adminive {
enum class Number_Locale_Choice { Point, Comma };
enum class Color_Map_Choice { Spectrum, Ember, Grayscale };
inline Color blend(Color first, Color second, double amount) {
const auto channel = [amount](std::uint8_t a, std::uint8_t b) {
return static_cast<std::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)};
}
inline Color_Map color_map(Color_Map_Choice 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 == Color_Map_Choice::Ember) {
stops = {Color{10, 5, 8, 255}, Color{62, 18, 30, 255}, Color{153, 47, 39, 255},
Color{244, 132, 48, 255}, Color{255, 238, 166, 255}};
} else if (palette == Color_Map_Choice::Grayscale) {
stops = {Color{0, 0, 0, 255}, Color{52, 52, 52, 255}, Color{112, 112, 112, 255},
Color{188, 188, 188, 255}, Color{255, 255, 255, 255}};
}
std::vector<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)));
}
return Color_Map(std::move(colors));
}
inline Color_Map_Choice color_map_choice(const Color_Map& value) {
for (const Color_Map_Choice choice : magic_enum::enum_values<Color_Map_Choice>()) {
if (value.colors() == color_map(choice).colors())
return choice;
}
return Color_Map_Choice::Spectrum;
}
template <auto Member>
auto editable(std::string name, std::string label, adminive::Field_Control control) {
return adminive::field<Member>(std::move(name), std::move(label))
.editable().control(control);
}
template <auto Member>
auto number(std::string name, std::string label) {
return editable<Member>(std::move(name), std::move(label), adminive::Field_Control::number);
}
template <auto Member>
auto boolean(std::string name, std::string label) {
return editable<Member>(std::move(name), std::move(label), adminive::Field_Control::boolean);
}
template <auto Member>
auto select(std::string name, std::string label) {
return editable<Member>(std::move(name), std::move(label), adminive::Field_Control::select);
}
template <auto Member>
auto color(std::string name, std::string label) {
return editable<Member>(std::move(name), std::move(label), adminive::Field_Control::color);
}
template <class Object, auto Member>
auto property(std::string name, std::string label, adminive::Field_Control control) {
return adminive::field(std::move(name), std::move(label),
Gallery_Property_Accessor<Object, Member>{})
.editable()
.unsynchronized()
.control(control);
}
template <class Object, auto Member>
auto readonly_property(std::string name, std::string label) {
return adminive::field(std::move(name), std::move(label),
Gallery_Property_Accessor<Object, Member>{});
}
template <class Object, auto Member>
auto property_number(std::string name, std::string label) {
return property<Object, Member>(std::move(name), std::move(label),
adminive::Field_Control::number);
}
template <class Object, auto Member>
auto property_boolean(std::string name, std::string label) {
return property<Object, Member>(std::move(name), std::move(label),
adminive::Field_Control::boolean);
}
template <class Object, auto Member>
auto property_select(std::string name, std::string label) {
return property<Object, Member>(std::move(name), std::move(label),
adminive::Field_Control::select);
}
template <class Object, auto Member>
auto property_color(std::string name, std::string label) {
return property<Object, Member>(std::move(name), std::move(label),
adminive::Field_Control::color);
}
template <class Object, auto Member>
auto property_object(std::string name, std::string label) {
return property<Object, Member>(std::move(name), std::move(label),
adminive::Field_Control::automatic);
}
template <class Object, auto Member>
auto property_text(std::string name, std::string label) {
return property<Object, Member>(std::move(name), std::move(label),
adminive::Field_Control::text);
}
} // namespace renderive::web::gallery_adminive
namespace adminive {
template <class Validator>
struct Renderive_Validator_Metadata {};
template <class T, T Minimum, T Maximum>
struct Renderive_Validator_Metadata<::Range_Validator<T, Minimum, Maximum>> {
static constexpr T minimum = Minimum;
static constexpr T maximum = Maximum;
};
template <class T, class Validator, class Json>
struct Value_Adapter<::Validated_Value<T, Validator>, Json>
: Renderive_Validator_Metadata<Validator> {
using Storage = ::Validated_Value<T, Validator>;
using value_type = T;
static const T& read(const Storage& value) noexcept { return value.get(); }
static void write(Storage& target, T value) { target = std::move(value); }
};
template <class Json>
struct Value_Adapter<renderive::Color, Json> {
using value_type = std::string;
static std::string read(renderive::Color value) {
char result[8]{};
std::snprintf(result, sizeof(result), "#%02x%02x%02x", value.r, value.g, value.b);
return result;
}
static void write(renderive::Color& target, const std::string& value) {
if (value.size() != 7 || value.front() != '#' ||
!std::all_of(value.begin() + 1, value.end(), [](unsigned char character) {
return std::isxdigit(character) != 0;
}))
throw std::invalid_argument("color must use #RRGGBB");
const auto channel = [&value](std::size_t offset) {
return static_cast<std::uint8_t>(std::stoul(value.substr(offset, 2), nullptr, 16));
};
target.r = channel(1);
target.g = channel(3);
target.b = channel(5);
}
};
template <class Json>
struct Value_Adapter<renderive::Number_Locale, Json> {
using Choice = renderive::web::gallery_adminive::Number_Locale_Choice;
using value_type = Choice;
static Choice read(renderive::Number_Locale value) {
return value.decimal_point == ',' ? Choice::Comma : Choice::Point;
}
static void write(renderive::Number_Locale& target, Choice value) {
target.decimal_point = value == Choice::Comma ? ',' : '.';
}
};
template <class T, T Minimum, T Maximum, T Default, class Json>
struct Value_Adapter<renderive::Clamped_Property<T, Minimum, Maximum, Default>, Json> {
using Storage = renderive::Clamped_Property<T, Minimum, Maximum, Default>;
using value_type = T;
static constexpr T minimum = Minimum;
static constexpr T maximum = Maximum;
static T read(const Storage& value) noexcept { return value.get(); }
static void write(Storage& target, T value) { target = value; }
};
template <class Json>
struct Value_Adapter<renderive::Unit_Interval, Json> {
using value_type = double;
static constexpr double minimum = 0.0;
static constexpr double maximum = 1.0;
static constexpr double multiple_of = 0.01;
static double read(const renderive::Unit_Interval& value) noexcept { return value.get(); }
static void write(renderive::Unit_Interval& target, double value) { target = value; }
};
template <class Json>
struct Value_Adapter<renderive::Color_Map, Json> {
using Choice = renderive::web::gallery_adminive::Color_Map_Choice;
using value_type = Choice;
static Choice read(const renderive::Color_Map& value) {
return renderive::web::gallery_adminive::color_map_choice(value);
}
static void write(renderive::Color_Map& target, Choice value) {
target = renderive::web::gallery_adminive::color_map(value);
}
};
template <>
struct Type_Descriptor<renderive::Range> {
static auto get() {
using T = renderive::Range;
using namespace renderive::web::gallery_adminive;
return adminive::object<T>("range", "范围",
number<&T::origin>("origin", "起点"),
number<&T::target>("target", "终点"));
}
};
template <>
struct Type_Descriptor<renderive::Pen> {
static auto get() {
using T = renderive::Pen;
using namespace renderive::web::gallery_adminive;
return adminive::object<T>("pen", "画笔",
color<&T::color>("color", "颜色"),
number<&T::width>("width", "宽度"),
select<&T::style>("style", "线型")
.enum_label<renderive::Line_Style::None>("")
.enum_label<renderive::Line_Style::Solid>("实线")
.enum_label<renderive::Line_Style::Dash>("虚线")
.enum_label<renderive::Line_Style::Dot>("点线"),
select<&T::cap>("cap", "端点样式")
.enum_label<renderive::Line_Cap::Butt>("平头")
.enum_label<renderive::Line_Cap::Square>("方头")
.enum_label<renderive::Line_Cap::Round>("圆头"),
select<&T::join>("join", "连接样式")
.enum_label<renderive::Line_Join::Miter>("尖角")
.enum_label<renderive::Line_Join::Bevel>("斜角")
.enum_label<renderive::Line_Join::Round>("圆角"));
}
};
template <>
struct Type_Descriptor<renderive::Brush> {
static auto get() {
using T = renderive::Brush;
using namespace renderive::web::gallery_adminive;
return adminive::object<T>("brush", "画刷",
color<&T::color>("color", "颜色"),
select<&T::style>("style", "填充样式")
.enum_label<renderive::Brush_Style::None>("")
.enum_label<renderive::Brush_Style::Solid>("纯色"));
}
};
template <>
struct Type_Descriptor<renderive::Font> {
static auto get() {
using T = renderive::Font;
using namespace renderive::web::gallery_adminive;
return adminive::object<T>("font", "字体",
number<&T::size>("size", "字号"),
number<&T::weight>("weight", "字重"),
boolean<&T::italic>("italic", "斜体"));
}
};
#define RENDERIVE_AXIS_DESCRIPTOR(GalleryType, Name, Label) \
template <> struct Type_Descriptor<renderive::web::GalleryType> { \
static auto get() { \
using T = renderive::web::GalleryType; \
using B = renderive::Axis_Base_Properties; \
using P = renderive::Axis_Properties; \
using namespace renderive::web::gallery_adminive; \
return adminive::object<T>(Name, Label, \
readonly_property<T, &B::x>("x", "X 位置"), \
readonly_property<T, &B::y>("y", "Y 位置"), \
property_select<T, &B::orientation>("orientation", "方向") \
.enum_label<renderive::Orientation::Horizontal>("水平") \
.enum_label<renderive::Orientation::Vertical>("垂直"), \
readonly_property<T, &B::pixel_length>("pixel_length", "像素长度"), \
property_number<T, &B::tick_length>("tick_length", "主刻度长度"), \
property_number<T, &B::sub_tick_length>("sub_tick_length", "次刻度长度"), \
property_color<T, &B::color>("color", "颜色"), \
property_select<T, &B::locale>("locale", "小数点符号") \
.enum_label<renderive::web::gallery_adminive::Number_Locale_Choice::Point>("点") \
.enum_label<renderive::web::gallery_adminive::Number_Locale_Choice::Comma>("逗号"), \
property_text<T, &B::unit_text>("unit_text", "单位文本"), \
property_object<T, &B::unit_text_font>("unit_text_font", "单位字体"), \
property_object<T, &B::unit_text_pen>("unit_text_pen", "单位画笔"), \
property_object<T, &B::unit_text_background_brush>("unit_text_background_brush", "单位背景"), \
property_number<T, &B::label_rotation_degrees>("label_rotation_degrees", "标签旋转角度"), \
property_object<T, &P::coordinates>("coordinates", "坐标范围"), \
property_number<T, &P::precision>("precision", "小数位数"), \
property_boolean<T, &P::wheel>("wheel", "滚轮缩放"), \
property_boolean<T, &P::drag>("drag", "拖拽平移")); \
} \
}
RENDERIVE_AXIS_DESCRIPTOR(Gallery_Axis, "axis", "数值轴");
RENDERIVE_AXIS_DESCRIPTOR(Gallery_Frequency_Axis, "frequency_axis", "频率轴");
#undef RENDERIVE_AXIS_DESCRIPTOR
template <>
struct Type_Descriptor<renderive::web::Gallery_Time_Axis> {
static auto get() {
using T = renderive::web::Gallery_Time_Axis;
using B = renderive::Axis_Base_Properties;
using P = renderive::Time_Axis_Properties;
using namespace renderive::web::gallery_adminive;
return adminive::object<T>("time_axis", "时间轴",
readonly_property<T, &B::x>("x", "X 位置"),
readonly_property<T, &B::y>("y", "Y 位置"),
property_select<T, &B::orientation>("orientation", "方向")
.enum_label<renderive::Orientation::Horizontal>("水平")
.enum_label<renderive::Orientation::Vertical>("垂直"),
readonly_property<T, &B::pixel_length>("pixel_length", "像素长度"),
property_number<T, &B::tick_length>("tick_length", "主刻度长度"),
property_number<T, &B::sub_tick_length>("sub_tick_length", "次刻度长度"),
property_color<T, &B::color>("color", "颜色"),
property_select<T, &B::locale>("locale", "小数点符号")
.enum_label<renderive::web::gallery_adminive::Number_Locale_Choice::Point>("")
.enum_label<renderive::web::gallery_adminive::Number_Locale_Choice::Comma>("逗号"),
property_text<T, &B::unit_text>("unit_text", "单位文本"),
property_object<T, &B::unit_text_font>("unit_text_font", "单位字体"),
property_object<T, &B::unit_text_pen>("unit_text_pen", "单位画笔"),
property_object<T, &B::unit_text_background_brush>("unit_text_background_brush", "单位背景"),
property_number<T, &B::label_rotation_degrees>("label_rotation_degrees", "标签旋转角度"),
property_number<T, &P::visible_count>("visible_count", "可见点数"),
property_number<T, &P::tick_label_spacing_px>("tick_label_spacing_px", "标签间距"),
property_text<T, &P::format>("format", "时间格式"),
property_boolean<T, &P::newest_at_start>("newest_at_start", "最新数据在起点"));
}
};
#define RENDERIVE_GALLERY_DESCRIPTOR(GalleryType, PropertiesType, Name, Label, ...) \
template <> struct Type_Descriptor<renderive::web::GalleryType> { \
static auto get() { \
using T = renderive::web::GalleryType; \
using P = renderive::PropertiesType; \
using namespace renderive::web::gallery_adminive; \
return adminive::object<T>(Name, Label, __VA_ARGS__); \
} \
}
RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Spectrum, Spectrum_Properties, "spectrum", "实时频谱",
property_number<T, &P::frequency_point_size>("frequency_point_size", "频率点数"),
property_select<T, &P::partition_mode>("partition_mode", "任务分区模式")
.enum_label<renderive::Render_Partition_Mode::Automatic>("自动瓶颈优化")
.enum_label<renderive::Render_Partition_Mode::Fixed>("固定分区"),
property_number<T, &P::partition_count>("partition_count", "固定分区数")
.description("固定模式下的并行任务数;1 表示不分区")
.visible_on("${$self.partition_mode == 'Fixed'}"),
property_object<T, &P::frequency_range>("frequency_range", "频率范围"),
property_number<T, &P::center_frequency>("center_frequency", "中心频率"),
property_object<T, &P::sweep_frequency_range>("sweep_frequency_range", "扫频范围"),
property_boolean<T, &P::max_hold_visible>("max_hold_visible", "显示最大保持"),
property_boolean<T, &P::min_hold_visible>("min_hold_visible", "显示最小保持"),
property_boolean<T, &P::max_marker_visible>("max_marker_visible", "显示最大值标记"),
property_boolean<T, &P::use_min_marker>("use_min_marker", "显示最小值标记"),
property_boolean<T, &P::sweep_region_visible>("sweep_region_visible", "显示扫频区域"),
property_boolean<T, &P::visible_range_only>("visible_range_only", "仅绘制可见范围"),
property_select<T, &P::interpolation_mode>("interpolation_mode", "曲线插值")
.enum_label<renderive::Line_Interpolation_Mode::Nearest_Sample>("最近样本")
.enum_label<renderive::Line_Interpolation_Mode::Linear_Value>("数值线性")
.enum_label<renderive::Line_Interpolation_Mode::Linear_Power_Domain>("功率域线性")
.enum_label<renderive::Line_Interpolation_Mode::Step_Left>("左阶梯")
.enum_label<renderive::Line_Interpolation_Mode::Step_Right>("右阶梯")
.enum_label<renderive::Line_Interpolation_Mode::Cubic_Value>("三次插值"),
property_object<T, &P::max_brush>("max_brush", "最大保持画刷"),
property_object<T, &P::current_brush>("current_brush", "当前曲线画刷"),
property_object<T, &P::min_brush>("min_brush", "最小保持画刷"),
property_object<T, &P::max_pen>("max_pen", "最大保持画笔"),
property_object<T, &P::current_pen>("current_pen", "当前曲线画笔"),
property_object<T, &P::min_pen>("min_pen", "最小保持画笔"),
property_object<T, &P::selected_marker_pen>("selected_marker_pen", "选中标记画笔"),
property_object<T, &P::marker_pen>("marker_pen", "标记画笔"),
property_object<T, &P::middle_frequency_pen>("middle_frequency_pen", "中心频率画笔"),
property_object<T, &P::sweep_region_brush>("sweep_region_brush", "扫频区域画刷"),
property_boolean<T, &P::tooltip_enabled>("tooltip_enabled", "启用提示框"),
property_object<T, &P::tooltip_font>("tooltip_font", "提示框字体"),
property_object<T, &P::tooltip_text_pen>("tooltip_text_pen", "提示框文字画笔"),
property_object<T, &P::tooltip_background_brush>("tooltip_background_brush", "提示框背景"));
RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Waterfall, Waterfall_Properties, "waterfall", "频谱瀑布",
property_object<T, &P::frequency_range>("frequency_range", "频率范围"),
property_object<T, &P::power_range>("power_range", "功率范围"),
property_number<T, &P::frequency_bin_count>("frequency_bin_count", "频率格数"),
property_select<T, &P::partition_mode>("partition_mode", "任务分区模式")
.enum_label<renderive::Render_Partition_Mode::Automatic>("自动瓶颈优化")
.enum_label<renderive::Render_Partition_Mode::Fixed>("固定分区"),
property_number<T, &P::partition_count>("partition_count", "固定分区数")
.description("固定模式下的并行任务数;1 表示不分区")
.visible_on("${$self.partition_mode == 'Fixed'}"),
property_boolean<T, &P::visible_range_only>("visible_range_only", "仅绘制可见范围"),
property_select<T, &P::interpolation_mode>("interpolation_mode", "图像插值")
.enum_label<renderive::Image_Interpolation_Mode::Nearest>("最近邻")
.enum_label<renderive::Image_Interpolation_Mode::Bilinear>("双线性")
.enum_label<renderive::Image_Interpolation_Mode::Bicubic>("双三次"),
property_select<T, &P::color_map>("color_map", "色图")
.enum_label<renderive::web::gallery_adminive::Color_Map_Choice::Spectrum>("频谱")
.enum_label<renderive::web::gallery_adminive::Color_Map_Choice::Ember>("火焰")
.enum_label<renderive::web::gallery_adminive::Color_Map_Choice::Grayscale>("灰度"),
property_boolean<T, &P::tooltip_enabled>("tooltip_enabled", "启用提示框"),
property_object<T, &P::tooltip_font>("tooltip_font", "提示框字体"),
property_object<T, &P::tooltip_text_pen>("tooltip_text_pen", "提示框文字画笔"),
property_object<T, &P::tooltip_background_brush>("tooltip_background_brush", "提示框背景"));
RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Afterglow, Afterglow_Properties, "afterglow", "余辉频谱",
property_object<T, &P::frequency_range>("frequency_range", "频率范围"),
property_object<T, &P::power_range>("power_range", "功率范围"),
property_number<T, &P::frequency_point_size>("frequency_point_size", "频率点数"),
property_number<T, &P::power_point_size>("power_point_size", "功率点数"),
property_select<T, &P::partition_mode>("partition_mode", "任务分区模式")
.enum_label<renderive::Render_Partition_Mode::Automatic>("自动瓶颈优化")
.enum_label<renderive::Render_Partition_Mode::Fixed>("固定分区"),
property_number<T, &P::partition_count>("partition_count", "固定分区数")
.description("固定模式下的并行任务数;1 表示不分区")
.visible_on("${$self.partition_mode == 'Fixed'}"),
property_boolean<T, &P::interpolate>("interpolate", "插值功率点"),
property_number<T, &P::attenuation_rate>("attenuation_rate", "衰减率"),
property_select<T, &P::color_map>("color_map", "色图")
.enum_label<renderive::web::gallery_adminive::Color_Map_Choice::Spectrum>("频谱")
.enum_label<renderive::web::gallery_adminive::Color_Map_Choice::Ember>("火焰")
.enum_label<renderive::web::gallery_adminive::Color_Map_Choice::Grayscale>("灰度"));
RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Sweep_Spectrum, Sweep_Spectrum_Properties, "sweep_spectrum", "扫频频谱",
property_object<T, &P::frequency_range>("frequency_range", "频率范围"),
property_number<T, &P::bins_per_block>("bins_per_block", "每块频率格数"),
property_number<T, &P::block_count>("block_count", "扫频块数"),
property_object<T, &P::pen>("pen", "扫频曲线画笔"),
property_object<T, &P::current_frequency_pen>("current_frequency_pen", "当前频率画笔"),
property_boolean<T, &P::visible_range_only>("visible_range_only", "仅绘制可见范围"),
property_select<T, &P::interpolation_mode>("interpolation_mode", "曲线插值")
.enum_label<renderive::Line_Interpolation_Mode::Nearest_Sample>("最近样本")
.enum_label<renderive::Line_Interpolation_Mode::Linear_Value>("数值线性")
.enum_label<renderive::Line_Interpolation_Mode::Linear_Power_Domain>("功率域线性")
.enum_label<renderive::Line_Interpolation_Mode::Step_Left>("左阶梯")
.enum_label<renderive::Line_Interpolation_Mode::Step_Right>("右阶梯")
.enum_label<renderive::Line_Interpolation_Mode::Cubic_Value>("三次插值"));
RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Frequency_Trace, Frequency_Trace_Properties, "frequency_trace", "频率轨迹",
property_object<T, &P::pen>("pen", "轨迹画笔"));
RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Selection_Rectangle_Overlay, Selection_Rectangle_Overlay_Properties, "selection_overlay", "框选区域",
property_object<T, &P::label_font>("label_font", "标签字体"),
property_object<T, &P::label_pen>("label_pen", "标签画笔"),
property_object<T, &P::selection_brush>("selection_brush", "选区画刷"),
property_object<T, &P::selection_border_pen>("selection_border_pen", "选区边框画笔"));
RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Constellation_Diagram, Constellation_Diagram_Properties, "constellation", "星座图",
property_object<T, &P::i_range>("i_range", "I 轴范围"),
property_object<T, &P::q_range>("q_range", "Q 轴范围"),
property_color<T, &P::point_color>("point_color", "点颜色"),
property_color<T, &P::anchor_color>("anchor_color", "锚点颜色"),
property_number<T, &P::point_lifetime_ms>("point_lifetime_ms", "点保留时间"),
property_select<T, &P::type>("type", "调制类型")
.enum_label<renderive::Constellation_Diagram_Type::Psk4>("4 PSK")
.enum_label<renderive::Constellation_Diagram_Type::Psk8>("8 PSK")
.enum_label<renderive::Constellation_Diagram_Type::Psk16>("16 PSK"),
property_number<T, &P::phase_offset_radians>("phase_offset_radians", "相位偏移"));
#undef RENDERIVE_GALLERY_DESCRIPTOR
} // namespace adminive
namespace renderive::web::gallery_action_registry {
constexpr auto axis_metadata() {
return structive::type_metadata(
gallery_action("axis_probe", "读取坐标映射与刻度",
"coord_to_pixel / pixel_to_coord / tick_step / sub_tick_count / tick_label",
"坐标轴"));
}
} // namespace renderive::web::gallery_action_registry
namespace structive {
#define RENDERIVE_TYPE_ACTIONS(Type, ...) \
template <> struct Type_Descriptor<renderive::web::Type> { \
static auto get() { \
using T = renderive::web::Type; \
using renderive::web::gallery_action; \
return object<T>(type_metadata(__VA_ARGS__)); \
} \
}
template <>
struct Type_Descriptor<renderive::web::Gallery_Axis> {
static auto get() {
return object<renderive::web::Gallery_Axis>(
renderive::web::gallery_action_registry::axis_metadata());
}
};
template <>
struct Type_Descriptor<renderive::web::Gallery_Frequency_Axis> {
static auto get() {
return object<renderive::web::Gallery_Frequency_Axis>(
renderive::web::gallery_action_registry::axis_metadata());
}
};
template <>
struct Type_Descriptor<renderive::web::Gallery_Time_Axis> {
static auto get() {
using T = renderive::web::Gallery_Time_Axis;
using renderive::web::gallery_action;
return object<T>(type_metadata(
gallery_action("axis_probe", "读取坐标映射与刻度",
"coord_to_pixel / pixel_to_coord / tick_step / sub_tick_count / tick_label",
"坐标轴"),
gallery_action("append_time", "追加时间点",
"Time_Axis::append_time / tick_to_time", "时间轴")));
}
};
RENDERIVE_TYPE_ACTIONS(Gallery_Spectrum,
gallery_action("push_samples", "推送一帧样本", "Spectrum::update_samples", "频谱"),
gallery_action("power_at", "查询指定频率功率", "Spectrum::power_at", "频谱",
{}, "number", "频率", 98e6),
gallery_action("add_marker", "添加点标记", "Spectrum::add_custom_marker", "标记",
{}, "number", "频率", 96e6),
gallery_action("add_line_marker", "添加线标记", "Spectrum::add_custom_line_marker", "标记",
{}, "number", "频率", 100e6),
gallery_action("remove_marker", "按频率删除标记", "Spectrum::remove_custom_marker", "标记",
{}, "number", "频率", 96e6),
gallery_action("remove_selected_marker", "删除选中标记",
"Spectrum::remove_selected_marker", "标记"),
gallery_action("clear_markers", "清空标记", "Spectrum::clear_custom_markers", "标记"),
gallery_action("select_marker", "选择标记索引", "Spectrum::set_selected_marker_index", "标记",
{}, "number", "索引", 0.0),
gallery_action("select_next_marker", "选择下一个标记", "Spectrum::select_next_marker", "标记"),
gallery_action("select_previous_marker", "选择上一个标记", "Spectrum::select_previous_marker", "标记"),
gallery_action("clear_marker_selection", "清除标记选择",
"Spectrum::clear_marker_selection", "标记"),
gallery_action("set_marker_frequency", "修改选中标记频率",
"Spectrum::set_marker_frequency / set_current_marker_frequency", "标记",
{}, "number", "频率", 99e6));
RENDERIVE_TYPE_ACTIONS(Gallery_Waterfall,
gallery_action("append_row", "追加一行", "Waterfall::append_row", "频谱瀑布"),
gallery_action("append_tick_row", "按 tick 追加一行",
"Time_Axis::append_time / Waterfall::append_row(int, span)", "频谱瀑布"));
RENDERIVE_TYPE_ACTIONS(Gallery_Afterglow,
gallery_action("append_spectrum", "追加一帧频谱", "Afterglow::append_spectrum", "余辉频谱"));
RENDERIVE_TYPE_ACTIONS(Gallery_Sweep_Spectrum,
gallery_action("append_block", "追加一个扫频块", "Sweep_Spectrum::append_block", "扫频频谱"));
RENDERIVE_TYPE_ACTIONS(Gallery_Frequency_Trace,
gallery_action("append_sample", "追加一个样本", "Frequency_Trace::append_sample", "频率轨迹"),
gallery_action("append_tick_sample", "按 tick 追加样本",
"Time_Axis::append_time / Frequency_Trace::append_sample(int, double)",
"频率轨迹"));
RENDERIVE_TYPE_ACTIONS(Gallery_Selection_Rectangle_Overlay,
gallery_action("clear_selection", "清空框选区域",
"Selection_Rectangle_Overlay::clear_selected_regions", "框选区域"));
RENDERIVE_TYPE_ACTIONS(Gallery_Constellation_Diagram,
gallery_action("append_points", "追加一组 IQ 点",
"Constellation_Diagram::append_point", "星座图"),
gallery_action("fit_square", "按轴拟合正方形",
"Constellation_Diagram::fit_square_to_axes", "星座图"));
#undef RENDERIVE_TYPE_ACTIONS
} // namespace structive
namespace renderive::web {
template <class Object>
std::size_t gallery_control_count() {
using Json = nlohmann::json;
const auto count_fields = [](const auto& self, const Json& fields) -> std::size_t {
std::size_t result{};
for (const auto& field : fields) {
if (field.value("editable", false) && !field.contains("children"))
++result;
if (field.contains("children"))
result += self(self, field.at("children"));
}
return result;
};
const Json descriptor = adminive::to_descriptor_json<Json, Object>();
return count_fields(count_fields, descriptor.at("fields"));
}
struct Gallery_Case_Type_Registration {
std::string_view case_id;
std::size_t (*control_count)();
void (*append_actions)(std::vector<Gallery_Action_Model>&);
};
template <class... Objects>
Gallery_Case_Type_Registration gallery_case_types(std::string_view case_id) {
return {
case_id,
[] { return (std::size_t{} + ... + gallery_control_count<Objects>()); },
[](std::vector<Gallery_Action_Model>& actions) {
(append_gallery_actions<Objects>(actions), ...);
}
};
}
inline const Gallery_Case_Type_Registration* gallery_case_registration(
std::string_view case_id) {
static const std::array registrations{
gallery_case_types<Gallery_Frequency_Axis, Gallery_Time_Axis>("axis_lab"),
gallery_case_types<Gallery_Frequency_Axis, Gallery_Axis, Gallery_Spectrum>("spectrum"),
gallery_case_types<Gallery_Frequency_Axis, Gallery_Axis, Gallery_Afterglow>("afterglow"),
gallery_case_types<Gallery_Axis, Gallery_Axis, Gallery_Sweep_Spectrum>("sweep_spectrum"),
gallery_case_types<Gallery_Frequency_Axis, Gallery_Time_Axis, Gallery_Waterfall>("waterfall"),
gallery_case_types<Gallery_Time_Axis, Gallery_Axis, Gallery_Frequency_Trace>("frequency_trace"),
gallery_case_types<Gallery_Frequency_Axis, Gallery_Axis, Gallery_Spectrum,
Gallery_Selection_Rectangle_Overlay>("selection_overlay"),
gallery_case_types<Gallery_Axis, Gallery_Axis, Gallery_Constellation_Diagram>("constellation")
};
const auto found = std::find_if(
registrations.begin(), registrations.end(),
[case_id](const auto& registration) { return registration.case_id == case_id; });
return found == registrations.end() ? nullptr : &*found;
}
inline std::size_t gallery_renderable_control_count(std::string_view case_id) {
const auto* registration = gallery_case_registration(case_id);
return registration ? registration->control_count() : 0;
}
inline void append_gallery_renderable_actions(
std::string_view case_id, std::vector<Gallery_Action_Model>& actions) {
if (const auto* registration = gallery_case_registration(case_id))
registration->append_actions(actions);
}
} // namespace renderive::web
@@ -0,0 +1,32 @@
#pragma once
#include "render_2D/plottable/Performance_Overlay.h"
#include "render_2D/renderable/Renderable.h"
#include "render_2D/scene/Scene.h"
#include <memory>
#include <string>
namespace adminive {
template <class T>
struct Type_Descriptor;
}
namespace renderive::web {
struct Gallery_Feedback_Policy {
bool enabled{true};
bool pixel{};
bool presentation{true};
bool manual{};
double manual_fps{60.0};
friend bool operator==(const Gallery_Feedback_Policy&, const Gallery_Feedback_Policy&) = default;
};
template <class Scene_Type>
class Gallery_Session_Control final {
public:
Gallery_Session_Control(Scene_Type& scene, Renderable& renderable, Gallery_Feedback_Policy& feedback) noexcept
: scene_(scene), renderable_(renderable), feedback_(feedback) {}
private:
Scene_Type& scene_;
Renderable& renderable_;
Gallery_Feedback_Policy& feedback_;
template <class T>
friend struct ::adminive::Type_Descriptor;
};
}
@@ -0,0 +1,147 @@
#pragma once
#include "Gallery_Session_Control.h"
#include "Gallery_Renderables.h"
#include <concepts>
#include <utility>
namespace renderive::web::gallery_adminive {
template <class Object, class Value, class Reader, class Writer>
auto callback_property(std::string name, std::string label,
adminive::Field_Control control, Reader reader, Writer writer) {
auto accessor = structive::trusted_callable_accessor<Object>(
std::move(reader), std::move(writer));
static_assert(std::same_as<typename decltype(accessor)::value_type, Value>);
return adminive::field(std::move(name), std::move(label), std::move(accessor))
.editable()
.unsynchronized()
.control(control);
}
} // namespace renderive::web::gallery_adminive
namespace adminive {
template <>
struct Type_Descriptor<renderive::web::Gallery_Feedback_Policy> {
static auto get() {
using T = renderive::web::Gallery_Feedback_Policy;
using namespace renderive::web::gallery_adminive;
return adminive::object<T>("feedback_policy", "消费者反馈",
boolean<&T::enabled>("enabled", "启用反馈"),
boolean<&T::pixel>("pixel", "像素响应反馈"),
boolean<&T::presentation>("presentation", "浏览器呈现反馈"),
boolean<&T::manual>("manual", "手动反馈"),
number<&T::manual_fps>("manual_fps", "手动反馈帧率"));
}
};
template <class Scene_Type>
struct Type_Descriptor<renderive::web::Gallery_Session_Control<Scene_Type>> {
static auto get() {
using T = renderive::web::Gallery_Session_Control<Scene_Type>;
using namespace renderive::web::gallery_adminive;
const auto dirty = [](T& value) { value.scene_.notify_model_dirty(); };
return adminive::object<T>("scene", "场景",
callback_property<T, renderive::Color>(
"background_color", "背景颜色", Field_Control::color,
[](const T& value) { return value.scene_.background_color(); },
[dirty](T& value, renderive::Color color) {
value.scene_.set_background_color(color);
dirty(value);
}),
callback_property<T, bool>(
"performance_overlay", "性能叠加层", Field_Control::boolean,
[](const T& value) {
const auto overlay = value.scene_.performance_overlay();
return overlay && overlay->enabled();
},
[dirty](T& value, bool enabled) {
renderive::set_performance_overlay_enabled(value.scene_, enabled);
dirty(value);
}),
callback_property<T, bool>(
"renderable_visible", "显示渲染元素", Field_Control::boolean,
[](const T& value) { return value.renderable_.is_visible(); },
[dirty](T& value, bool visible) {
value.renderable_.set_visible(visible);
dirty(value);
}),
callback_property<T, renderive::Renderable_Cache_Mode>(
"cache_mode", "缓存模式", Field_Control::select,
[](const T& value) { return value.renderable_.get_cache_mode(); },
[dirty](T& value, renderive::Renderable_Cache_Mode mode) {
value.renderable_.set_cache_mode(mode);
dirty(value);
})
.template enum_label<renderive::Renderable_Cache_Mode::Direct>("直接绘制")
.template enum_label<renderive::Renderable_Cache_Mode::Local_Pixel>("本地像素缓存"),
callback_property<T, std::string>(
"object_name", "对象名称", Field_Control::text,
[](const T& value) { return value.renderable_.object_name(); },
[dirty](T& value, std::string name) {
value.renderable_.set_object_name(std::move(name));
dirty(value);
}),
callback_property<T, bool>(
"frequency_limit_enabled", "启用帧率限制", Field_Control::boolean,
[](const T& value) { return value.scene_.max_render_fps() > 0.0; },
[dirty](T& value, bool enabled) {
if (enabled) {
if (value.scene_.max_render_fps() <= 0.0)
value.scene_.set_max_render_fps(30.0);
} else {
value.scene_.clear_max_render_fps();
}
dirty(value);
}),
callback_property<T, double>(
"max_render_fps", "最大渲染帧率", Field_Control::number,
[](const T& value) {
const double fps = value.scene_.max_render_fps();
return fps > 0.0 ? fps : 30.0;
},
[dirty](T& value, double fps) {
if (value.scene_.max_render_fps() > 0.0)
value.scene_.set_max_render_fps(fps);
dirty(value);
}),
callback_property<T, renderive::web::Gallery_Feedback_Policy>(
"feedback", "消费者反馈", Field_Control::automatic,
[](const T& value) { return value.feedback_; },
[dirty](T& value, renderive::web::Gallery_Feedback_Policy feedback) {
value.feedback_ = std::move(feedback);
dirty(value);
}));
}
};
} // namespace adminive
namespace structive {
template <class Scene_Type>
struct Type_Descriptor<renderive::web::Gallery_Session_Control<Scene_Type>> {
static auto get() {
using T = renderive::web::Gallery_Session_Control<Scene_Type>;
using renderive::web::gallery_action;
return object<T>(type_metadata(
gallery_action("reset", "恢复本图默认值", "Gallery_Scene::rebuild", "场景"),
gallery_action("toggle_view", "切换 View 生命周期",
"Basic_Scene2D::activate_view / deactivate_view", "场景"),
gallery_action("capture_next_frame", "Capture next frame",
"Scene_Base::capture_next_frame", "Performance Capture",
"Capture the next completed render frame", {}, {}, 0.0, true),
gallery_action("capture_frames", "Capture N frames",
"Scene_Base::capture_frames", "Performance Capture",
"Capture consecutive completed render frames", "number",
"Frame count", 20.0, true),
gallery_action("read_data_shape", "读取数据形状",
"Renderable data query / observer telemetry", "观察器")));
}
};
} // namespace structive
@@ -0,0 +1,75 @@
#include "Gallery_WebSocket_Controller.h"
#include "Gallery_Plot_Session.h"
#include "Web_Event_Adapter.h"
#include <trantor/utils/Logger.h>
#include <exception>
#include <memory>
namespace renderive::web {
void Gallery_WebSocket_Controller::handleNewConnection(
const drogon::HttpRequestPtr&,
const drogon::WebSocketConnectionPtr& connection) {
connection->setContext(std::make_shared<Gallery_Plot_Session>(true));
connection->setPingMessage("renderive-gallery", std::chrono::seconds(20));
LOG_INFO << "Renderive Gallery WebSocket connected: "
<< connection->peerAddr().toIpPort();
}
void Gallery_WebSocket_Controller::handleNewMessage(
const drogon::WebSocketConnectionPtr& connection,
std::string&& message,
const drogon::WebSocketMessageType& type) {
if (type == drogon::WebSocketMessageType::Ping ||
type == drogon::WebSocketMessageType::Pong ||
type == drogon::WebSocketMessageType::Close) {
return;
}
if (type != drogon::WebSocketMessageType::Text) {
connection->shutdown(drogon::CloseCode::kInvalidMessage,
"Renderive Gallery accepts text events only");
return;
}
if (message.size() > 64 * 1024) {
connection->shutdown(drogon::CloseCode::kMessageTooBig,
"Renderive Gallery event is too large");
return;
}
const auto event = Web_Event_Adapter::decode(message);
if (!event) {
connection->shutdown(drogon::CloseCode::kWrongMessageContent,
"Invalid Renderive Gallery event");
return;
}
const auto session = connection->getContext<Gallery_Plot_Session>();
if (!session) {
connection->shutdown(drogon::CloseCode::kUnexpectedCondition,
"Renderive Gallery session is unavailable");
return;
}
try {
if (auto response = session->handle(*event)) {
const auto message_type = response->type == Web_Response_Type::Pixels
? drogon::WebSocketMessageType::Binary
: drogon::WebSocketMessageType::Text;
connection->send(response->payload.data(), response->payload.size(),
message_type);
}
} catch (const std::exception& error) {
LOG_ERROR << "Renderive Gallery session failed: " << error.what();
connection->shutdown(drogon::CloseCode::kUnexpectedCondition,
"Renderive Gallery rendering failed");
}
}
void Gallery_WebSocket_Controller::handleConnectionClosed(
const drogon::WebSocketConnectionPtr& connection) {
LOG_INFO << "Renderive Gallery WebSocket closed: "
<< connection->peerAddr().toIpPort();
connection->clearContext();
}
} // namespace renderive::web
@@ -0,0 +1,14 @@
#pragma once
#include <drogon/WebSocketController.h>
namespace renderive::web {
class Gallery_WebSocket_Controller final
: public drogon::WebSocketController<Gallery_WebSocket_Controller, false> {
public:
void handleNewMessage(const drogon::WebSocketConnectionPtr& connection, std::string&& message, const drogon::WebSocketMessageType& type) override;
void handleNewConnection(const drogon::HttpRequestPtr& request, const drogon::WebSocketConnectionPtr& connection) override;
void handleConnectionClosed(const drogon::WebSocketConnectionPtr& connection) override;
WS_PATH_LIST_BEGIN
WS_PATH_ADD("/renderive/gallery");
WS_PATH_LIST_END
};
} // namespace renderive::web
@@ -0,0 +1,113 @@
#include "Pixel_Frame.h"
#include <algorithm>
#include <limits>
#if defined(_M_X64) || defined(__x86_64__)
#include <tmmintrin.h>
#endif
namespace renderive::web {
namespace {
void write_u32_le(char* target, std::uint32_t value) {
target[0] = static_cast<char>(value & 0xffU);
target[1] = static_cast<char>((value >> 8U) & 0xffU);
target[2] = static_cast<char>((value >> 16U) & 0xffU);
target[3] = static_cast<char>((value >> 24U) & 0xffU);
}
std::uint8_t flatten(std::uint8_t premultiplied, std::uint8_t alpha,
std::uint8_t background) {
return static_cast<std::uint8_t>(
std::min(255U, static_cast<unsigned>(premultiplied) +
(static_cast<unsigned>(background) * (255U - alpha) + 127U) / 255U));
}
} // namespace
std::string encode_pixel_frame(Image_View image, Color background) {
if (image.empty() || image.format != Pixel_Format::Premultiplied_32 ||
image.stride < image.width * static_cast<int>(sizeof(Pixel))) {
return {};
}
const auto width = static_cast<std::size_t>(image.width);
const auto height = static_cast<std::size_t>(image.height);
if (width > (std::numeric_limits<std::size_t>::max() - pixel_frame_header_size) /
(height * sizeof(Pixel))) {
return {};
}
const std::size_t pixel_bytes = width * height * sizeof(Pixel);
std::string frame(pixel_frame_header_size + pixel_bytes, '\0');
frame[0] = 'R';
frame[1] = 'V';
frame[2] = 'P';
frame[3] = '1';
write_u32_le(frame.data() + 4, static_cast<std::uint32_t>(image.width));
write_u32_le(frame.data() + 8, static_cast<std::uint32_t>(image.height));
write_u32_le(frame.data() + 12,
static_cast<std::uint32_t>(image.width * static_cast<int>(sizeof(Pixel))));
auto* output = reinterpret_cast<std::uint32_t*>(frame.data() + pixel_frame_header_size);
const std::uint32_t opaque_background =
static_cast<std::uint32_t>(background.r) |
(static_cast<std::uint32_t>(background.g) << 8U) |
(static_cast<std::uint32_t>(background.b) << 16U) |
0xff000000U;
const auto convert_pixel = [background, opaque_background](Pixel pixel) {
const auto alpha = static_cast<std::uint8_t>((pixel >> 24U) & 0xffU);
if (alpha == 0)
return opaque_background;
const auto red = static_cast<std::uint8_t>((pixel >> 16U) & 0xffU);
const auto green = static_cast<std::uint8_t>((pixel >> 8U) & 0xffU);
const auto blue = static_cast<std::uint8_t>(pixel & 0xffU);
if (alpha == 255) {
return static_cast<std::uint32_t>(red) |
(static_cast<std::uint32_t>(green) << 8U) |
(static_cast<std::uint32_t>(blue) << 16U) |
0xff000000U;
}
return static_cast<std::uint32_t>(flatten(red, alpha, background.r)) |
(static_cast<std::uint32_t>(flatten(green, alpha, background.g)) << 8U) |
(static_cast<std::uint32_t>(flatten(blue, alpha, background.b)) << 16U) |
0xff000000U;
};
#if defined(_M_X64) || defined(__x86_64__)
const __m128i rgba_shuffle = _mm_setr_epi8(
2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15);
const __m128i background_pixels = _mm_set1_epi32(static_cast<int>(opaque_background));
#endif
for (int y = 0; y < image.height; ++y) {
const auto* row = reinterpret_cast<const Pixel*>(
image.data + static_cast<std::ptrdiff_t>(y) * image.stride);
int x{};
#if defined(_M_X64) || defined(__x86_64__)
for (; x + 4 <= image.width; x += 4) {
const Pixel alpha_union = row[x] | row[x + 1] | row[x + 2] | row[x + 3];
if ((alpha_union & 0xff000000U) == 0) {
_mm_storeu_si128(reinterpret_cast<__m128i*>(output), background_pixels);
output += 4;
continue;
}
const Pixel alpha_intersection = row[x] & row[x + 1] & row[x + 2] & row[x + 3];
if ((alpha_intersection & 0xff000000U) == 0xff000000U) {
const __m128i bgra =
_mm_loadu_si128(reinterpret_cast<const __m128i*>(row + x));
_mm_storeu_si128(reinterpret_cast<__m128i*>(output),
_mm_shuffle_epi8(bgra, rgba_shuffle));
output += 4;
continue;
}
*output++ = convert_pixel(row[x]);
*output++ = convert_pixel(row[x + 1]);
*output++ = convert_pixel(row[x + 2]);
*output++ = convert_pixel(row[x + 3]);
}
#endif
for (; x < image.width; ++x)
*output++ = convert_pixel(row[x]);
}
return frame;
}
} // namespace renderive::web
@@ -0,0 +1,9 @@
#pragma once
#include "render_2D/base/Types.h"
#include <cstddef>
#include <string>
namespace renderive::web {
inline constexpr std::size_t pixel_frame_header_size = 16;
[[nodiscard]] std::string encode_pixel_frame(Image_View image,
Color background = Color::black());
} // namespace renderive::web
@@ -0,0 +1,73 @@
#include "Renderive_WebSocket_Controller.h"
#include "Web_Event_Adapter.h"
#include "Web_Plot_Session.h"
#include <trantor/utils/Logger.h>
#include <exception>
#include <memory>
namespace renderive::web {
void Renderive_WebSocket_Controller::handleNewConnection(
const drogon::HttpRequestPtr&,
const drogon::WebSocketConnectionPtr& connection) {
connection->setContext(std::make_shared<Web_Plot_Session>());
connection->setPingMessage("renderive", std::chrono::seconds(20));
LOG_INFO << "Renderive WebSocket connected: " << connection->peerAddr().toIpPort();
}
void Renderive_WebSocket_Controller::handleNewMessage(
const drogon::WebSocketConnectionPtr& connection,
std::string&& message,
const drogon::WebSocketMessageType& type) {
if (type == drogon::WebSocketMessageType::Ping ||
type == drogon::WebSocketMessageType::Pong ||
type == drogon::WebSocketMessageType::Close) {
return;
}
if (type != drogon::WebSocketMessageType::Text) {
connection->shutdown(drogon::CloseCode::kInvalidMessage,
"Renderive accepts text events only");
return;
}
if (message.size() > 16 * 1024) {
connection->shutdown(drogon::CloseCode::kMessageTooBig,
"Renderive event is too large");
return;
}
const auto event = Web_Event_Adapter::decode(message);
if (!event) {
connection->shutdown(drogon::CloseCode::kWrongMessageContent,
"Invalid Renderive event");
return;
}
const auto session = connection->getContext<Web_Plot_Session>();
if (!session) {
connection->shutdown(drogon::CloseCode::kUnexpectedCondition,
"Renderive session is unavailable");
return;
}
try {
if (auto response = session->handle(*event)) {
const auto message_type = response->type == Web_Response_Type::Pixels
? drogon::WebSocketMessageType::Binary
: drogon::WebSocketMessageType::Text;
connection->send(response->payload.data(), response->payload.size(),
message_type);
}
} catch (const std::exception& error) {
LOG_ERROR << "Renderive WebSocket session failed: " << error.what();
connection->shutdown(drogon::CloseCode::kUnexpectedCondition,
"Renderive rendering failed");
}
}
void Renderive_WebSocket_Controller::handleConnectionClosed(
const drogon::WebSocketConnectionPtr& connection) {
LOG_INFO << "Renderive WebSocket closed: " << connection->peerAddr().toIpPort();
connection->clearContext();
}
} // namespace renderive::web
@@ -0,0 +1,22 @@
#pragma once
#include <drogon/WebSocketController.h>
namespace renderive::web {
class Renderive_WebSocket_Controller final
: public drogon::WebSocketController<Renderive_WebSocket_Controller, false> {
public:
void handleNewMessage(const drogon::WebSocketConnectionPtr& connection,
std::string&& message,
const drogon::WebSocketMessageType& type) override;
void handleNewConnection(const drogon::HttpRequestPtr& request,
const drogon::WebSocketConnectionPtr& connection) override;
void handleConnectionClosed(const drogon::WebSocketConnectionPtr& connection) override;
WS_PATH_LIST_BEGIN
WS_PATH_ADD("/renderive");
WS_PATH_LIST_END
};
} // namespace renderive::web
@@ -0,0 +1,58 @@
#pragma once
#include "render_2D/event/Event.h"
#include <string>
#include <variant>
namespace renderive::web {
struct Frame_Request {};
struct Viewport_Resize {
Size size;
};
enum class Demo_Mode : std::uint8_t { Am, Fm, Usb, Lsb };
struct Set_Demo_Mode {
Demo_Mode mode = Demo_Mode::Fm;
};
struct Set_Center_Frequency {
double megahertz = 102.5;
};
struct Set_Bandwidth {
double kilohertz = 1200.0;
};
struct Set_Gain {
double decibels{};
};
struct Set_Max_Hold {
bool enabled{};
};
struct Set_Smoothing {
bool enabled{};
};
struct Clear_Selection {};
enum class Gallery_Request_Kind : std::uint8_t {
Catalog,
Open,
Patch,
Action,
Observe,
Refresh,
Reset_Monitoring
};
struct Gallery_Request {
Gallery_Request_Kind kind = Gallery_Request_Kind::Catalog;
std::string message;
};
template <class... Scene_Events>
using Basic_Web_Event = std::variant<Frame_Request,
Viewport_Resize,
Scene_Events...,
Set_Demo_Mode,
Set_Center_Frequency,
Set_Bandwidth,
Set_Gain,
Set_Max_Hold,
Set_Smoothing,
Clear_Selection,
Gallery_Request>;
using Web_Event = Basic_Web_Event<Event, Pointer_Event, Wheel_Event, Key_Event>;
} // namespace renderive::web
@@ -0,0 +1,205 @@
#include "Web_Event_Adapter.h"
#include "Gallery_Enum.h"
#include <algorithm>
#include <cmath>
#include <memory>
#include <string>
namespace renderive::web {
namespace {
std::optional<double> finite_number(const Json::Value& root, const char* name) {
const auto& value = root[name];
if (!value.isNumeric())
return std::nullopt;
const double result = value.asDouble();
return std::isfinite(result) ? std::optional<double>(result) : std::nullopt;
}
std::optional<bool> boolean(const Json::Value& root, const char* name) {
const auto& value = root[name];
return value.isBool() ? std::optional<bool>(value.asBool()) : std::nullopt;
}
Keyboard_Modifier modifiers(const Json::Value& root) {
const int value = root["modifiers"].isInt() ? root["modifiers"].asInt() : 0;
return static_cast<Keyboard_Modifier>(std::clamp(value, 0, 15));
}
Mouse_Button mouse_button(const Json::Value& root) {
const std::string value = root["button"].isString() ? root["button"].asString() : "none";
return gallery_enum_cast<Mouse_Button>(value).value_or(Mouse_Button::None);
}
Key key(const Json::Value& root) {
const std::string value = root["key"].isString() ? root["key"].asString() : "";
if (const auto direct = magic_enum::enum_cast<Key>(value))
return *direct;
if (value == "ArrowLeft")
return Key::Left;
if (value == "ArrowRight")
return Key::Right;
if (value == "ArrowUp")
return Key::Up;
if (value == "ArrowDown")
return Key::Down;
return Key::Unknown;
}
std::optional<Render_2D_Web_Events> pointer_event(const Json::Value& root,
Event_Type type) {
const auto x = finite_number(root, "x");
const auto y = finite_number(root, "y");
if (!x || !y)
return std::nullopt;
Pointer_Event event(type);
event.position = {*x, *y};
event.global_position = event.position;
event.button = mouse_button(root);
const int buttons = root["buttons"].isInt() ? root["buttons"].asInt() : 0;
event.buttons = static_cast<Mouse_Button_Mask>(std::clamp(buttons, 0, 7));
event.modifiers = modifiers(root);
return event;
}
std::optional<Render_2D_Web_Events> wheel_event(const Json::Value& root) {
const auto x = finite_number(root, "x");
const auto y = finite_number(root, "y");
const auto pixel_x = finite_number(root, "pixelDeltaX");
const auto pixel_y = finite_number(root, "pixelDeltaY");
const auto angle_x = finite_number(root, "angleDeltaX");
const auto angle_y = finite_number(root, "angleDeltaY");
if (!x || !y || !pixel_x || !pixel_y || !angle_x || !angle_y)
return std::nullopt;
Wheel_Event event;
event.position = {*x, *y};
event.global_position = event.position;
event.pixel_delta_x = *pixel_x;
event.pixel_delta_y = *pixel_y;
event.angle_delta_x = *angle_x;
event.angle_delta_y = *angle_y;
event.modifiers = modifiers(root);
const int buttons = root["buttons"].isInt() ? root["buttons"].asInt() : 0;
event.buttons = static_cast<Mouse_Button_Mask>(std::clamp(buttons, 0, 7));
return event;
}
std::optional<Render_2D_Web_Events> key_event(const Json::Value& root, Event_Type type) {
Key_Event event(type);
event.key = key(root);
event.native_key = root["nativeKey"].isUInt() ? root["nativeKey"].asUInt() : 0;
event.modifiers = modifiers(root);
event.auto_repeat = root["repeat"].isBool() && root["repeat"].asBool();
return event;
}
} // namespace
std::optional<Json::Value> parse_web_event(std::string_view message) {
Json::CharReaderBuilder builder;
builder["collectComments"] = false;
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
Json::Value root;
std::string errors;
if (!reader->parse(message.data(), message.data() + message.size(), &root, &errors) ||
!root.isObject() || root["category"].asString() != "event" ||
!root["type"].isString()) {
return std::nullopt;
}
return root;
}
std::optional<Web_Transport_Events> Web_Transport_Event_Decoder::decode(
const Json::Value& root, std::string_view source) {
const std::string type = root["type"].asString();
if (type == "frame")
return Frame_Request{};
if (type == "resize") {
const auto width = finite_number(root, "width");
const auto height = finite_number(root, "height");
if (!width || !height)
return std::nullopt;
return Viewport_Resize{{static_cast<int>(std::clamp(*width, 240.0, 1920.0)),
static_cast<int>(std::clamp(*height, 180.0, 1200.0))}};
}
constexpr std::string_view gallery_prefix = "gallery_";
if (!std::string_view(type).starts_with(gallery_prefix))
return std::nullopt;
const auto kind = gallery_enum_cast<Gallery_Request_Kind>(
std::string_view(type).substr(gallery_prefix.size()));
return kind ? std::optional<Web_Transport_Events>(
Gallery_Request{*kind, std::string(source)})
: std::nullopt;
}
std::optional<Render_2D_Web_Events> Render_2D_Web_Event_Decoder::decode(
const Json::Value& root, std::string_view) {
const std::string type = root["type"].asString();
if (type == "show")
return Event(Event_Type::Show);
if (type == "hide")
return Event(Event_Type::Hide);
if (type == "leave")
return Event(Event_Type::Leave);
if (type == "pointer_move")
return pointer_event(root, Event_Type::Pointer_Move);
if (type == "pointer_press")
return pointer_event(root, Event_Type::Pointer_Press);
if (type == "pointer_release")
return pointer_event(root, Event_Type::Pointer_Release);
if (type == "wheel")
return wheel_event(root);
if (type == "key_press")
return key_event(root, Event_Type::Key_Press);
if (type == "key_release")
return key_event(root, Event_Type::Key_Release);
return std::nullopt;
}
std::optional<Web_Demo_Control_Events> Web_Demo_Control_Decoder::decode(
const Json::Value& root, std::string_view) {
if (root["type"].asString() != "control" || !root["control"].isString())
return std::nullopt;
const std::string control = root["control"].asString();
if (control == "mode" && root["value"].isString()) {
const std::string value = root["value"].asString();
const auto mode = magic_enum::enum_cast<Demo_Mode>(
value, magic_enum::case_insensitive);
return mode ? std::optional<Web_Demo_Control_Events>(Set_Demo_Mode{*mode})
: std::nullopt;
}
if (control == "center_frequency_mhz") {
const auto value = finite_number(root, "value");
return value && *value >= 1.0 && *value <= 6000.0
? std::optional<Web_Demo_Control_Events>(Set_Center_Frequency{*value})
: std::nullopt;
}
if (control == "bandwidth_khz") {
const auto value = finite_number(root, "value");
return value && *value >= 10.0 && *value <= 50000.0
? std::optional<Web_Demo_Control_Events>(Set_Bandwidth{*value})
: std::nullopt;
}
if (control == "gain_db") {
const auto value = finite_number(root, "value");
return value && *value >= -30.0 && *value <= 80.0
? std::optional<Web_Demo_Control_Events>(Set_Gain{*value})
: std::nullopt;
}
if (control == "max_hold") {
const auto value = boolean(root, "value");
return value ? std::optional<Web_Demo_Control_Events>(Set_Max_Hold{*value})
: std::nullopt;
}
if (control == "smoothing") {
const auto value = boolean(root, "value");
return value ? std::optional<Web_Demo_Control_Events>(Set_Smoothing{*value})
: std::nullopt;
}
if (control == "clear_selection")
return Clear_Selection{};
return std::nullopt;
}
} // namespace renderive::web
@@ -0,0 +1,96 @@
#pragma once
#include "Web_Event.h"
#include <json/json.h>
#include <concepts>
#include <optional>
#include <string_view>
#include <type_traits>
#include <utility>
#include <variant>
namespace renderive::web {
using Web_Transport_Events = std::variant<Frame_Request, Viewport_Resize, Gallery_Request>;
using Render_2D_Web_Events = std::variant<Event, Pointer_Event, Wheel_Event, Key_Event>;
using Web_Demo_Control_Events = std::variant<Set_Demo_Mode,
Set_Center_Frequency,
Set_Bandwidth,
Set_Gain,
Set_Max_Hold,
Set_Smoothing,
Clear_Selection>;
struct Web_Transport_Event_Decoder {
using event_type = Web_Transport_Events;
[[nodiscard]] static std::optional<event_type> decode(const Json::Value& root,
std::string_view source);
};
struct Render_2D_Web_Event_Decoder {
using event_type = Render_2D_Web_Events;
[[nodiscard]] static std::optional<event_type> decode(const Json::Value& root,
std::string_view source);
};
struct Web_Demo_Control_Decoder {
using event_type = Web_Demo_Control_Events;
[[nodiscard]] static std::optional<event_type> decode(const Json::Value& root,
std::string_view source);
};
template <class Decoder>
concept Web_Event_Decoder = requires(const Json::Value& root, std::string_view source) {
typename Decoder::event_type;
{ Decoder::decode(root, source) } ->
std::same_as<std::optional<typename Decoder::event_type>>;
};
[[nodiscard]] std::optional<Json::Value> parse_web_event(std::string_view message);
template <class T>
struct Is_Variant : std::false_type {};
template <class... Values>
struct Is_Variant<std::variant<Values...>> : std::true_type {};
template <class Event_Variant, Web_Event_Decoder... Decoders>
class Basic_Web_Event_Adapter final {
public:
[[nodiscard]] static std::optional<Event_Variant> decode(std::string_view message) {
const auto root = parse_web_event(message);
if (!root)
return std::nullopt;
std::optional<Event_Variant> result;
([&] {
if (result)
return;
auto decoded = Decoders::decode(*root, message);
if (decoded)
result = widen(std::move(*decoded));
}(), ...);
return result;
}
private:
template <class Decoded>
static Event_Variant widen(Decoded decoded) {
if constexpr (Is_Variant<std::remove_cvref_t<Decoded>>::value) {
return std::visit([](auto&& value) -> Event_Variant {
return Event_Variant(std::forward<decltype(value)>(value));
}, std::move(decoded));
} else {
return Event_Variant(std::move(decoded));
}
}
};
using Web_Event_Adapter = Basic_Web_Event_Adapter<Web_Event,
Web_Transport_Event_Decoder,
Render_2D_Web_Event_Decoder,
Web_Demo_Control_Decoder>;
} // namespace renderive::web
@@ -0,0 +1,12 @@
#pragma once
#include <filesystem>
#include <string_view>
namespace renderive::web {
void initialize_web_performance_log(const std::filesystem::path& log_directory);
void write_web_performance_log(std::string_view json_line) noexcept;
[[nodiscard]] std::filesystem::path web_performance_log_path();
} // namespace renderive::web
@@ -0,0 +1,343 @@
#include "Web_Plot_Session.h"
#include "Pixel_Frame.h"
#include "render_2D/export.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <mutex>
#include <type_traits>
#include <utility>
#include <vector>
namespace renderive::web {
namespace {
Color blend(Color first, Color second, double amount) {
const auto channel = [amount](std::uint8_t a, std::uint8_t b) {
return static_cast<std::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 radio_color_map() {
constexpr Color stops[] = {
{3, 7, 18, 255}, {16, 52, 105, 255}, {23, 163, 184, 255},
{238, 210, 91, 255}, {239, 68, 68, 255}
};
std::vector<Pixel> colors;
colors.reserve(256);
for (int index = 0; index < 256; ++index) {
const double position = index / 255.0 * (std::size(stops) - 1);
const auto stop = static_cast<std::size_t>(std::floor(position));
const auto next = std::min(stop + 1, std::size(stops) - 1);
colors.push_back(premultiply(blend(stops[stop], stops[next], position - stop)));
}
return Color_Map(std::move(colors));
}
Time_Of_Day current_time_of_day() {
constexpr std::int64_t day_ms = 24LL * 60LL * 60LL * 1000LL;
const auto now = std::chrono::duration_cast<std::chrono::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);
}
} // namespace
struct Web_Plot_Session::Impl {
Scene2D plot;
renderive_Owner<Frequency_Axis> spectrum_frequency_axis;
renderive_Owner<Axis> spectrum_power_axis;
renderive_Owner<Frequency_Axis> waterfall_frequency_axis;
renderive_Owner<Time_Axis> waterfall_time_axis;
renderive_Owner<Spectrum> spectrum;
renderive_Owner<Waterfall> waterfall;
renderive_Owner<Selection_Rectangle_Overlay> selection;
Demo_Mode mode = Demo_Mode::Fm;
double gain_db = 8.0;
std::uint64_t frame_index{};
std::mutex mutex;
Impl() {
plot.init();
plot.set_background_color({3, 7, 18, 255});
plot.set_max_render_fps(30.0);
plot.set_viewport_size({960, 600});
const auto root = plot.root_renderable();
constexpr Color axis_color{93, 116, 151, 255};
const Range initial_frequency{101'300'000.0, 103'700'000.0};
{
auto attach = plot.attach_builder();
const auto make_group = [&](std::string name) {
auto group = detail::make_renderable_group(true);
group->set_object_name(std::move(name));
attach.attach(group);
attach.add_display_parent(group, root);
attach.add_dependency_parent(group, root);
return group;
};
const auto data = make_group("Web_Data");
const auto axes = make_group("Web_Axes");
const auto overlay = make_group("Web_Overlay");
axes->set_cache_mode(Renderable_Cache_Mode::Local_Pixel);
spectrum_frequency_axis =
Frequency_Axis::Builder(axes, Orientation::Horizontal, &attach)
.set_coord_range(initial_frequency)
.set_label_precision(2)
.set_tick_length(8)
.set_sub_tick_length(4)
.set_color(axis_color)
.set_use_wheel(true)
.set_use_drag(true)
.build();
spectrum_power_axis =
Axis::Builder(axes, Orientation::Vertical, &attach)
.set_coord_range({-20.0, -120.0})
.set_label_precision(0)
.set_tick_length(-8)
.set_sub_tick_length(-4)
.set_color(axis_color)
.set_unit_text("dBm")
.build();
waterfall_frequency_axis =
Frequency_Axis::Builder(axes, Orientation::Horizontal, &attach)
.set_coord_range(initial_frequency)
.set_label_precision(2)
.set_tick_length(8)
.set_sub_tick_length(4)
.set_color(axis_color)
.set_use_wheel(true)
.set_use_drag(true)
.build();
waterfall_time_axis =
Time_Axis::Builder(axes, Orientation::Vertical, &attach)
.set_visible_time_point_count(72)
.set_tick_label_spacing_px(34)
.set_time_format("mm:ss")
.set_tick_length(-8)
.set_sub_tick_length(-4)
.set_color(axis_color)
.build();
spectrum = Spectrum::Builder{&attach}
.set<&Spectrum::Properties::frequency_range>(initial_frequency)
.set<&Spectrum::Properties::frequency_point_size>(768)
.set<&Spectrum::Properties::center_frequency>(102'500'000.0)
.set<&Spectrum::Properties::sweep_frequency_range>(Range{102'200'000.0, 102'800'000.0})
.set<&Spectrum::Properties::max_marker_visible>(true)
.set<&Spectrum::Properties::sweep_region_visible>(true)
.set<&Spectrum::Properties::interpolation_mode>(Line_Interpolation_Mode::Cubic_Value)
.build(data, spectrum_frequency_axis, spectrum_power_axis);
spectrum->set<&Spectrum::Properties::current_pen>(Pen{
Color{57, 224, 177, 255}, 2.0, Line_Style::Solid,
Line_Cap::Round, Line_Join::Round
});
spectrum->set<&Spectrum::Properties::current_brush>(Brush{Color{31, 174, 145, 32}, Brush_Style::Solid});
spectrum->set<&Spectrum::Properties::max_pen>(Pen{Color{250, 204, 21, 210}, 1.0});
spectrum->set<&Spectrum::Properties::middle_frequency_pen>(Pen{
Color{56, 189, 248, 220}, 1.0,
Line_Style::Dash
});
waterfall = Waterfall::Builder{&attach}
.set<&Waterfall::Properties::frequency_range>(initial_frequency)
.set<&Waterfall::Properties::power_range>(Range{-120.0, -20.0})
.set<&Waterfall::Properties::frequency_bin_count>(768)
.set<&Waterfall::Properties::interpolation_mode>(Image_Interpolation_Mode::Bilinear)
.set<&Waterfall::Properties::color_map>(radio_color_map())
.build(data, waterfall_frequency_axis, waterfall_time_axis);
selection = Selection_Rectangle_Overlay::Builder{&attach}
.set<&Selection_Rectangle_Overlay::Properties::selection_brush>(Brush{Color{56, 189, 248, 36}, Brush_Style::Solid})
.set<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(Pen{Color{125, 211, 252, 230}, 1.0, Line_Style::Dash})
.build(overlay, spectrum_frequency_axis, spectrum_power_axis);
}
apply_layout(plot.viewport_size());
plot.activate_view();
update_model();
(void)plot.render_frame(true);
}
~Impl() {
plot.deactivate_view();
}
void apply_layout(Size viewport) {
const int left = viewport.width < 620 ? 54 : 72;
const int right = viewport.width < 620 ? 44 : 70;
const int top = 16;
const int bottom = viewport.height < 480 ? 24 : 32;
const int gap = viewport.height < 480 ? 36 : 48;
const int content_width = std::max(1, viewport.width - left - right);
const int available_height = std::max(2, viewport.height - top - bottom - gap);
const int spectrum_height = std::max(1, available_height * 43 / 100);
const int waterfall_y = top + spectrum_height + gap;
const int waterfall_height = std::max(1, viewport.height - waterfall_y - bottom);
spectrum_frequency_axis->update([&](Axis_Properties& state) {
state.x = left;
state.y = top + spectrum_height;
state.pixel_length = static_cast<std::size_t>(content_width);
});
spectrum_power_axis->update([&](Axis_Properties& state) {
state.x = left;
state.y = top;
state.pixel_length = static_cast<std::size_t>(spectrum_height);
});
waterfall_frequency_axis->update([&](Axis_Properties& state) {
state.x = left;
state.y = waterfall_y + waterfall_height;
state.pixel_length = static_cast<std::size_t>(content_width);
});
waterfall_time_axis->update([&](auto& state) {
state.x = left;
state.y = waterfall_y;
state.pixel_length = static_cast<std::size_t>(waterfall_height);
});
}
void update_model() {
constexpr int sample_count = 768;
const double time = static_cast<double>(frame_index) * 0.075;
std::vector<double> samples(sample_count);
for (int index = 0; index < sample_count; ++index) {
const double x = static_cast<double>(index) / (sample_count - 1);
const double noise = std::sin(index * 12.9898 + frame_index * 0.371) *
std::sin(index * 0.137 + frame_index * 0.071);
double signal{};
switch (mode) {
case Demo_Mode::Am:
signal = 66.0 * gaussian(x, 0.5, 0.008) +
39.0 * gaussian(x, 0.42, 0.018) +
39.0 * gaussian(x, 0.58, 0.018);
break;
case Demo_Mode::Fm: {
const double moving = 0.5 + std::sin(time) * 0.035;
signal = 58.0 * gaussian(x, moving, 0.055) +
25.0 * gaussian(x, moving - 0.11, 0.023) +
25.0 * gaussian(x, moving + 0.11, 0.023);
break;
}
case Demo_Mode::Usb:
signal = 61.0 * gaussian(x, 0.57, 0.045) +
28.0 * gaussian(x, 0.68, 0.025);
break;
case Demo_Mode::Lsb:
signal = 61.0 * gaussian(x, 0.43, 0.045) +
28.0 * gaussian(x, 0.32, 0.025);
break;
}
samples[index] = std::clamp(-110.0 + noise * 4.5 + signal + gain_db,
-120.0, -20.0);
}
spectrum->update_samples(samples);
if ((frame_index & 1U) == 0U)
waterfall->append_row(current_time_of_day(), samples);
++frame_index;
}
void set_center_frequency(double megahertz) {
const double center = megahertz * 1'000'000.0;
const double bandwidth = spectrum_frequency_axis->get<&Axis_Properties::coordinates>().size();
const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5};
spectrum_frequency_axis->set<&Axis_Properties::coordinates>(range);
waterfall_frequency_axis->set<&Axis_Properties::coordinates>(range);
spectrum->set<&Spectrum::Properties::frequency_range>(range);
spectrum->set<&Spectrum::Properties::center_frequency>(center);
spectrum->set<&Spectrum::Properties::sweep_frequency_range>(Range{
center - bandwidth * 0.125,
center + bandwidth * 0.125
});
waterfall->set<&Waterfall::Properties::frequency_range>(range);
}
void set_bandwidth(double kilohertz) {
const double center = spectrum->get<&Spectrum::Properties::center_frequency>();
const double bandwidth = kilohertz * 1000.0;
const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5};
spectrum_frequency_axis->set<&Axis_Properties::coordinates>(range);
waterfall_frequency_axis->set<&Axis_Properties::coordinates>(range);
spectrum->set<&Spectrum::Properties::frequency_range>(range);
spectrum->set<&Spectrum::Properties::sweep_frequency_range>(Range{
center - bandwidth * 0.125,
center + bandwidth * 0.125
});
waterfall->set<&Waterfall::Properties::frequency_range>(range);
}
std::optional<std::string> render_pixels() {
if (!plot.view_active())
return std::nullopt;
update_model();
if (!plot.render_frame(true))
return std::nullopt;
std::string pixels;
const Color background = plot.background_color();
plot.with_frame([&pixels, background](Image_View image) {
pixels = encode_pixel_frame(image, background);
});
return pixels.empty() ? std::nullopt : std::optional<std::string>(std::move(pixels));
}
std::optional<Web_Response> handle(const Web_Event& event) {
std::lock_guard lock(mutex);
return std::visit(
[this](const auto& value) -> std::optional<Web_Response> {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, Frame_Request>) {
auto pixels = render_pixels();
if (pixels)
return Web_Response{Web_Response_Type::Pixels, std::move(*pixels)};
}
else if constexpr (std::is_same_v<T, Viewport_Resize>) {
const Size previous = plot.viewport_size();
plot.set_viewport_size(value.size);
apply_layout(value.size);
Resize_Event resized;
resized.old_size = previous;
resized.new_size = value.size;
plot.dispatch_event(resized);
}
else if constexpr (std::is_same_v<T, Event>) {
if (value.type == Event_Type::Show)
plot.activate_view();
else if (value.type == Event_Type::Hide)
plot.deactivate_view();
plot.dispatch_event(value);
}
else if constexpr (Event_Object<T>) {
plot.dispatch_event(value);
}
else if constexpr (std::is_same_v<T, Set_Demo_Mode>) {
mode = value.mode;
plot.notify_model_dirty();
}
else if constexpr (std::is_same_v<T, Set_Center_Frequency>) {
set_center_frequency(value.megahertz);
}
else if constexpr (std::is_same_v<T, Set_Bandwidth>) {
set_bandwidth(value.kilohertz);
}
else if constexpr (std::is_same_v<T, Set_Gain>) {
gain_db = value.decibels;
plot.notify_model_dirty();
}
else if constexpr (std::is_same_v<T, Set_Max_Hold>) {
spectrum->set<&Spectrum::Properties::max_hold_visible>(value.enabled);
}
else if constexpr (std::is_same_v<T, Set_Smoothing>) {
spectrum->set<&Spectrum::Properties::interpolation_mode>(
value.enabled
? Line_Interpolation_Mode::Cubic_Value
: Line_Interpolation_Mode::Nearest_Sample);
waterfall->set<&Waterfall::Properties::interpolation_mode>(
value.enabled
? Image_Interpolation_Mode::Bilinear
: Image_Interpolation_Mode::Nearest);
}
else if constexpr (std::is_same_v<T, Clear_Selection>) {
selection->clear_selected_regions();
}
return std::nullopt;
},
event);
}
};
Web_Plot_Session::Web_Plot_Session() : impl_(std::make_unique<Impl>()) {}
Web_Plot_Session::~Web_Plot_Session() = default;
std::optional<Web_Response> Web_Plot_Session::handle(const Web_Event& event) {
return impl_->handle(event);
}
} // namespace renderive::web
@@ -0,0 +1,32 @@
#pragma once
#include "Web_Event.h"
#include <memory>
#include <optional>
#include <string>
namespace renderive::web {
enum class Web_Response_Type : std::uint8_t { Pixels, Json };
struct Web_Response {
Web_Response_Type type = Web_Response_Type::Pixels;
std::string payload;
};
class Web_Plot_Session final {
public:
Web_Plot_Session();
~Web_Plot_Session();
Web_Plot_Session(const Web_Plot_Session&) = delete;
Web_Plot_Session& operator=(const Web_Plot_Session&) = delete;
[[nodiscard]] std::optional<Web_Response> handle(const Web_Event& event);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace renderive::web
@@ -0,0 +1,51 @@
#include "Web_Server.h"
#include "Gallery_WebSocket_Controller.h"
#include "Renderive_WebSocket_Controller.h"
#include "Web_Performance_Log.h"
#include <drogon/drogon.h>
#include <algorithm>
#include <filesystem>
#include <functional>
#include <iostream>
#include <memory>
#include <thread>
namespace renderive::web {
int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) {
const std::filesystem::path gallery_root = asset_root / "webapp_gallery" / "dist";
const std::filesystem::path gallery_index = gallery_root / "index.html";
if (!std::filesystem::is_regular_file(gallery_index)) {
std::cerr << "Renderive Gallery assets not found: " << gallery_index << '\n';
return 3;
}
initialize_web_performance_log(asset_root / "logs");
const auto controller = std::make_shared<Renderive_WebSocket_Controller>();
const auto gallery_controller = std::make_shared<Gallery_WebSocket_Controller>();
const auto hardware_threads = std::max(2U, std::thread::hardware_concurrency());
std::cout << "Renderive WebSocket backend: ws://127.0.0.1:" << port
<< "/renderive\n"
<< "Renderive control gallery: ws://127.0.0.1:" << port
<< "/renderive/gallery\n"
<< "Renderive hosted gallery: http://127.0.0.1:" << port << "/\n"
<< "Renderive performance log: " << web_performance_log_path().string() << "\n"
<< "HTTP only serves static HTML/CSS/JS; events and pixels stay on WebSocket.\n";
auto& app = drogon::app();
const auto redirect_to_gallery = [](
const drogon::HttpRequestPtr&,
std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
callback(drogon::HttpResponse::newRedirectionResponse("/"));
};
app.registerHandler("/gallery", redirect_to_gallery, {drogon::Get});
app.registerHandler("/gallery/", redirect_to_gallery, {drogon::Get});
app
.registerController(controller)
.registerController(gallery_controller)
.setDocumentRoot(gallery_root.string())
.setHomePage("index.html")
.setStaticFileHeaders({{"Cache-Control", "no-store"}})
.addListener("127.0.0.1", port)
.setThreadNum(std::min(8U, hardware_threads))
.setIdleConnectionTimeout(90)
.run();
return 0;
}
} // namespace renderive::web
@@ -0,0 +1,10 @@
#pragma once
#include <cstdint>
#include <filesystem>
namespace renderive::web {
int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root);
} // namespace renderive::web
@@ -0,0 +1,29 @@
#include "web_server/app/Web_Server.h"
#include <charconv>
#include <cstdint>
#include <filesystem>
#include <iostream>
#include <string_view>
namespace {
std::uint16_t parse_port(int argc, char** argv) {
constexpr std::uint16_t default_port = 8848;
if (argc != 3 || std::string_view(argv[1]) != "--port")
return default_port;
unsigned value{};
const std::string_view text(argv[2]);
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value);
if (error != std::errc{} || end != text.data() + text.size() || value == 0 || value > 65535) {
std::cerr << "Invalid port: " << text << '\n';
return 0;
}
return static_cast<std::uint16_t>(value);
}
}
int main(int argc, char** argv) {
const std::uint16_t port = parse_port(argc, argv);
if (port == 0)
return 2;
std::cout << "http://127.0.0.1:8848" << std::endl;
const std::filesystem::path executable = std::filesystem::absolute(argv[0]);
return renderive::web::run_web_server(port, executable.parent_path().parent_path().parent_path());
}
File diff suppressed because it is too large Load Diff