优化cmake
This commit is contained in:
@@ -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
Reference in New Issue
Block a user