仍有bug
This commit is contained in:
@@ -447,6 +447,11 @@ Scene_Edit_Error Scene_Base::Renderable_Editor::clear_dependency_parent(
|
||||
scene_data.model_dirty_.store(true, std::memory_order_release);
|
||||
return Scene_Edit_Error::none;
|
||||
}
|
||||
void Scene_Base::Renderable_Editor::replace_frame_control_strategy(
|
||||
std::shared_ptr<Frame_Control_Strategy_Base> strategy) {
|
||||
scene_.d_func().frame_control().replace_frame_control_strategy(
|
||||
std::move(strategy));
|
||||
}
|
||||
Scene_Edit_Error Scene_Base::Edit_Operation::wait() const {
|
||||
if (!result_.valid()) ::renderive::error::unexpected<std::logic_error>("scene edit operation is invalid");
|
||||
return result_.get();
|
||||
|
||||
@@ -80,6 +80,8 @@ public:
|
||||
[[nodiscard]] Scene_Edit_Error set_dependency_parent(const Renderable& child, const Renderable& parent);
|
||||
[[nodiscard]] Scene_Edit_Error add_dependency_parent(const Renderable& child, const Renderable& parent);
|
||||
[[nodiscard]] Scene_Edit_Error clear_dependency_parent(const Renderable& child);
|
||||
void replace_frame_control_strategy(
|
||||
std::shared_ptr<Frame_Control_Strategy_Base> strategy);
|
||||
[[nodiscard]] Scene_Edit_Error error() const noexcept {
|
||||
return error_;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ public:
|
||||
virtual ~Frame_Control_Capability() = default;
|
||||
virtual std::shared_ptr<Frame_Control_Strategy_Base> frame_control_strategy() = 0;
|
||||
virtual std::shared_ptr<const Frame_Control_Strategy_Base> frame_control_strategy() const = 0;
|
||||
virtual void replace_frame_control_strategy(
|
||||
std::shared_ptr<Frame_Control_Strategy_Base> strategy) = 0;
|
||||
};
|
||||
struct Scene_State_Capability {
|
||||
virtual ~Scene_State_Capability() = default;
|
||||
|
||||
@@ -28,8 +28,6 @@ struct Afterglow_Prepare_Buffer {
|
||||
}
|
||||
struct Afterglow::Impl
|
||||
: Base::next_Impl<Impl>, ::Render_Frame_Completion {
|
||||
static_assert(inheritance::Inherits_Chain_Parent<
|
||||
Impl, Base::next_Impl<Impl>>);
|
||||
Impl(renderive_Owner<Frequency_Axis> frequency,
|
||||
renderive_Owner<Axis> power) : frequency_axis(std::move(frequency)),
|
||||
power_axis(std::move(power)) {}
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
#include <deque>
|
||||
#include <numbers>
|
||||
#include <optional>
|
||||
namespace renderive {
|
||||
namespace detail {
|
||||
namespace renderive::detail {
|
||||
namespace {
|
||||
struct Timed_Point {
|
||||
PointF point;
|
||||
@@ -20,8 +19,6 @@ using Constellation_History = Plottable_History_Real_Time_Data<Timed_Point, std:
|
||||
}
|
||||
struct Constellation_Diagram::Impl
|
||||
: Base::next_Impl<Impl> {
|
||||
static_assert(inheritance::Inherits_Chain_Parent<
|
||||
Impl, Base::next_Impl<Impl>>);
|
||||
struct Prepare_Buffer {
|
||||
std::vector<PointF> anchors;
|
||||
std::vector<PointF> points;
|
||||
@@ -64,9 +61,9 @@ Constellation_Diagram::Observer Constellation_Diagram::capture_observation() con
|
||||
void Constellation_Diagram::fit_square_to_axes() {
|
||||
const auto state = Renderable::state<State>();
|
||||
const double side = std::max(state.i_range.size(), state.q_range.size());
|
||||
d_func<Impl>().i_axis->set < &Axis_Properties::coordinates > (
|
||||
d_func<Impl>().i_axis->set<&Axis_Properties::coordinates>(
|
||||
Range{state.i_range.center() - side * 0.5, state.i_range.center() + side * 0.5});
|
||||
d_func<Impl>().q_axis->set < &Axis_Properties::coordinates > (
|
||||
d_func<Impl>().q_axis->set<&Axis_Properties::coordinates>(
|
||||
Range{state.q_range.center() + side * 0.5, state.q_range.center() - side * 0.5});
|
||||
}
|
||||
void Constellation_Diagram::Impl::prepare_frame(
|
||||
@@ -108,4 +105,3 @@ void Constellation_Diagram::Impl::paint(Painter& painter,
|
||||
Brush{state.point_color, Brush_Style::Solid});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,12 @@
|
||||
#include <deque>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
namespace renderive {
|
||||
namespace detail {
|
||||
namespace renderive::detail {
|
||||
namespace {
|
||||
using Frequency_Trace_History = Plottable_History_Real_Time_Data<std::pair<int, double>, std::deque<std::pair<int, double>>>;
|
||||
}
|
||||
struct Frequency_Trace::Impl
|
||||
: Base::next_Impl<Impl> {
|
||||
static_assert(inheritance::Inherits_Chain_Parent<
|
||||
Impl, Base::next_Impl<Impl>>);
|
||||
struct Prepare_Buffer {
|
||||
std::vector<PointF> points;
|
||||
};
|
||||
@@ -78,4 +75,3 @@ void Frequency_Trace::Impl::paint(Painter& painter,
|
||||
control.render_state<State>(view).pen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
#include <optional>
|
||||
namespace renderive {
|
||||
namespace detail {
|
||||
namespace renderive::detail {
|
||||
namespace {
|
||||
struct Selection_Interaction {
|
||||
bool selecting{};
|
||||
@@ -24,8 +23,6 @@ struct Selection_Rectangle_Overlay::Impl
|
||||
: Base::next_Impl<Impl>,
|
||||
Renderable_Event_Handler {
|
||||
using Observer = Selection_Rectangle_Overlay::Observer;
|
||||
static_assert(inheritance::Inherits_Chain_Parent<
|
||||
Impl, Base::next_Impl<Impl>>);
|
||||
struct Prepared_Region {
|
||||
RectF pixels;
|
||||
std::string label;
|
||||
@@ -175,4 +172,3 @@ void Selection_Rectangle_Overlay::Impl::paint(
|
||||
state.selection_brush);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
#include <cmath>
|
||||
#include <deque>
|
||||
#include <optional>
|
||||
namespace renderive {
|
||||
namespace detail {
|
||||
namespace renderive::detail {
|
||||
namespace {
|
||||
using Sweep_Spectrum_History = Plottable_History_Real_Time_Data<std::vector<double>, std::deque<std::vector<double>>>;
|
||||
std::vector<double> flatten(const std::deque<std::vector<double>>& blocks) {
|
||||
@@ -20,8 +19,6 @@ std::vector<double> flatten(const std::deque<std::vector<double>>& blocks) {
|
||||
struct Sweep_Spectrum::Impl
|
||||
: Base::next_Impl<Impl> {
|
||||
using Observer = Sweep_Spectrum::Observer;
|
||||
static_assert(inheritance::Inherits_Chain_Parent<
|
||||
Impl, Base::next_Impl<Impl>>);
|
||||
friend struct Sweep_Spectrum::Observer;
|
||||
struct Prepare_Buffer {
|
||||
std::vector<PointF> points;
|
||||
@@ -117,4 +114,3 @@ void Sweep_Spectrum::Impl::paint(Painter& painter,
|
||||
state.current_frequency_pen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,6 @@ struct Waterfall::Impl
|
||||
Renderable_Event_Handler,
|
||||
::Render_Frame_Completion {
|
||||
using Observer = Waterfall::Observer;
|
||||
static_assert(inheritance::Inherits_Chain_Parent<
|
||||
Impl, Base::next_Impl<Impl>>);
|
||||
Impl(renderive_Owner<Frequency_Axis> frequency,
|
||||
renderive_Owner<Time_Axis> time) : frequency_axis(std::move(frequency)), time_axis(std::move(time)) {}
|
||||
renderive_Owner<Frequency_Axis> frequency_axis;
|
||||
|
||||
@@ -31,8 +31,7 @@ struct LIB_DECL Waterfall : Renderable<Waterfall> {
|
||||
Render_Partition_Mode partition_mode{Render_Partition_Mode::Automatic};
|
||||
Positive_Count partition_count;
|
||||
bool visible_range_only{true};
|
||||
Image_Interpolation_Mode interpolation_mode =
|
||||
Image_Interpolation_Mode::Nearest;
|
||||
Image_Interpolation_Mode interpolation_mode = Image_Interpolation_Mode::Nearest;
|
||||
Color_Map color_map;
|
||||
};
|
||||
struct Observer : next_Observer<Observer> {
|
||||
|
||||
@@ -14,7 +14,7 @@ struct Blend2D_Color_Cache final : ::Color_Cache {
|
||||
[[nodiscard]] Size size() const noexcept;
|
||||
[[nodiscard]] Image_View view() const noexcept;
|
||||
private:
|
||||
friend class Painter;
|
||||
friend struct Painter;
|
||||
BLImage image_;
|
||||
};
|
||||
struct Painter {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
namespace renderive {
|
||||
class Abs_Axis;
|
||||
namespace detail {
|
||||
class Paint_Overlay;
|
||||
struct Paint_Overlay;
|
||||
template <class Renderable_Type, class Axis_Type> requires std::derived_from<Renderable_Type, Renderable> && std::derived_from<Axis_Type, Abs_Axis>
|
||||
void attach_renderable_dependency(::Scene_2D_Base::Attach_Builder& builder, const renderive_Owner<Renderable_Type>& renderable, const renderive_Owner<Axis_Type>& axis) {
|
||||
if (!axis) return;
|
||||
@@ -41,12 +41,12 @@ struct Renderable_Builder {
|
||||
Renderable_Builder(Properties properties, Validator validator, ::Scene_2D_Base::Attach_Builder* attach_builder = nullptr) : properties(std::move(properties)), validator(std::move(validator)), attach_builder_(attach_builder) {}
|
||||
template <auto Member, Property_Member_Assignable<Properties, Member> Value>
|
||||
Self& set(Value&& value) {
|
||||
properties.*Member = std::forward < Value > (value);
|
||||
properties.*Member = std::forward<Value>(value);
|
||||
return *this;
|
||||
}
|
||||
template <Property_Configurator<Properties> Configure>
|
||||
Self& configure(Configure&& configure) {
|
||||
std::invoke(std::forward < Configure > (configure), properties);
|
||||
std::invoke(std::forward<Configure>(configure), properties);
|
||||
return *this;
|
||||
}
|
||||
const Properties& properties_value() const noexcept {
|
||||
|
||||
@@ -51,6 +51,10 @@ struct detail::Render_Scene_2D::Impl
|
||||
std::shared_ptr<const Frame_Control_Strategy_Base> frame_control_strategy() const override {
|
||||
return frame_control->runtime();
|
||||
}
|
||||
void replace_frame_control_strategy(
|
||||
std::shared_ptr<Frame_Control_Strategy_Base> strategy) override {
|
||||
frame_control->replace(std::move(strategy));
|
||||
}
|
||||
std::shared_ptr<Color_Cache> make_renderable_color_cache() override {
|
||||
std::pmr::polymorphic_allocator<detail::Blend2D_Color_Cache>
|
||||
allocator(&memory_resource());
|
||||
|
||||
@@ -88,21 +88,24 @@ protected:
|
||||
using Editor_Base::Editor_Base;
|
||||
};
|
||||
public:
|
||||
struct State : Base::template next_State<State> {
|
||||
struct State_Fields {
|
||||
Size viewport;
|
||||
Color background = Color::black();
|
||||
};
|
||||
struct State : Base::template next_State<State>, State_Fields {
|
||||
[[nodiscard]] bool operator==(const State& other) const {
|
||||
return viewport == other.viewport &&
|
||||
background == other.background;
|
||||
}
|
||||
};
|
||||
struct Observer : Base::template next_Observer<Observer> {
|
||||
struct Observer_Fields {
|
||||
State state;
|
||||
std::size_t renderable_count{};
|
||||
double frequency_hz{};
|
||||
std::uint64_t next_refresh_interval_ns{};
|
||||
bool view_active{};
|
||||
};
|
||||
struct Observer : Base::template next_Observer<Observer>, Observer_Fields {};
|
||||
[[nodiscard]] ::renderive_Owner<::renderive::Renderable> root_renderable() const;
|
||||
void dispatch_event(const Event& event);
|
||||
[[nodiscard]] auto render_frame(bool force = false) -> Expected<Plot_Render_Status, Plot_Render_Frame_Error>;
|
||||
|
||||
@@ -31,5 +31,21 @@ TEST(RenderScene2DIntegration, FrameImageMatchesConfiguredViewport) {
|
||||
});
|
||||
}
|
||||
|
||||
TEST(RenderScene2DIntegration, FrameStrategyIsReplacedInsideRenderableEdit) {
|
||||
Render_Scene_2D::State state;
|
||||
state.viewport = {48, 27};
|
||||
auto scene = Render_Scene_2D::Builder{state}.build(
|
||||
std::make_shared<Low_Latency_Render_Scene_2D_Strategy>());
|
||||
const auto operation = scene->edit_renderables(
|
||||
[](Scene_2D_Base::Renderable_Editor& editor) {
|
||||
editor.replace_frame_control_strategy(
|
||||
std::make_shared<Manual_Render_Scene_2D_Strategy>());
|
||||
});
|
||||
ASSERT_EQ(operation.wait(), Scene_Edit_Error::none);
|
||||
EXPECT_NE(std::dynamic_pointer_cast<Manual_Render_Scene_2D_Strategy>(
|
||||
scene->frame_control_strategy()),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace renderive
|
||||
|
||||
@@ -408,6 +408,10 @@ struct detail::Render_Scene_3D::Impl
|
||||
std::shared_ptr<const Frame_Control_Strategy_Base> frame_control_strategy() const override {
|
||||
return frame_control->runtime();
|
||||
}
|
||||
void replace_frame_control_strategy(
|
||||
std::shared_ptr<Frame_Control_Strategy_Base> strategy) override {
|
||||
frame_control->replace(std::move(strategy));
|
||||
}
|
||||
std::uint64_t acquire_scene_state() override {
|
||||
return scene_state.acquire_render_state();
|
||||
}
|
||||
@@ -521,4 +525,22 @@ Scene_Control_Error detail::Render_Scene_3D::dispatch(
|
||||
if (const auto* key = dynamic_cast<const ::renderive::Key_Event*>(&event)) return d_func<Impl>().dispatch_key(*this, *key);
|
||||
return Scene_Control_Error::none;
|
||||
}
|
||||
Frame_Request_Result detail::Render_Scene_3D::request_frame() {
|
||||
const auto requested = d_func<Impl>().frame_control->request(*this);
|
||||
if (requested != Frame_Request_Result::none) return requested;
|
||||
switch (wait_for_render()) {
|
||||
case Scene_Render_Result::none: return Frame_Request_Result::none;
|
||||
case Scene_Render_Result::cancelled: return Frame_Request_Result::cancelled;
|
||||
case Scene_Render_Result::deadline_exceeded:
|
||||
return Frame_Request_Result::deadline_exceeded;
|
||||
case Scene_Render_Result::external_failure:
|
||||
return Frame_Request_Result::external_failure;
|
||||
case Scene_Render_Result::shutting_down:
|
||||
return Frame_Request_Result::scene_shutting_down;
|
||||
}
|
||||
return Frame_Request_Result::external_failure;
|
||||
}
|
||||
std::shared_ptr<const Pixel_Frame> detail::Render_Scene_3D::latest_frame() const {
|
||||
return d_func<Impl>().latest_frame();
|
||||
}
|
||||
} // namespace renderive::render_3d
|
||||
|
||||
@@ -119,10 +119,12 @@ protected:
|
||||
using Editor_Base::Editor_Base;
|
||||
};
|
||||
public:
|
||||
struct State : Base::template next_State<State> {
|
||||
struct State_Fields {
|
||||
Extent viewport{560, 320};
|
||||
Clear_Color clear_color;
|
||||
Visual_Family visual_family{Visual_Family::Point};
|
||||
};
|
||||
struct State : Base::template next_State<State>, State_Fields {
|
||||
[[nodiscard]] bool operator==(const State& other) const {
|
||||
return viewport == other.viewport &&
|
||||
clear_color == other.clear_color &&
|
||||
@@ -135,7 +137,7 @@ public:
|
||||
template <class Product, class Properties>
|
||||
using Business_Builder = ::renderive::scene::Scene_Builder<
|
||||
Product, Properties, State_Validator>;
|
||||
struct Observer : Base::template next_Observer<Observer> {
|
||||
struct Observer_Fields {
|
||||
State state;
|
||||
std::size_t renderable_count{};
|
||||
std::size_t point_count{};
|
||||
@@ -143,8 +145,11 @@ public:
|
||||
std::uint64_t next_refresh_interval_ns{};
|
||||
bool backend_available{};
|
||||
};
|
||||
struct Observer : Base::template next_Observer<Observer>, Observer_Fields {};
|
||||
[[nodiscard]] Scene_Control_Error dispatch(
|
||||
const ::renderive::Event& event);
|
||||
[[nodiscard]] Frame_Request_Result request_frame();
|
||||
[[nodiscard]] std::shared_ptr<const Pixel_Frame> latest_frame() const;
|
||||
~Render_Scene_3D() override;
|
||||
private:
|
||||
[[nodiscard]] Observer capture_observation() const;
|
||||
|
||||
@@ -50,5 +50,27 @@ TEST(PointRenderIntegration, ObserverPublishesSceneAndVisualState) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PointRenderIntegration, FrameStrategyIsReplacedInsideRenderableEdit) {
|
||||
auto visual = Point_Visual::Builder{}.build(std::vector<Point>{
|
||||
{{0.0F, 0.0F, 0.0F}, {255, 0, 0, 255}, 8.0F}});
|
||||
Render_Scene_3D::State state;
|
||||
state.viewport = {64, 48};
|
||||
try {
|
||||
auto scene = make_scene(state, visual);
|
||||
const auto operation = scene->edit_renderables(
|
||||
[](Scene_3D_Base::Renderable_Editor& editor) {
|
||||
editor.replace_frame_control_strategy(
|
||||
std::make_shared<Manual_Render_Scene_3D_Strategy>());
|
||||
});
|
||||
ASSERT_EQ(operation.wait(), Scene_Edit_Error::none);
|
||||
EXPECT_NE(std::dynamic_pointer_cast<Manual_Render_Scene_3D_Strategy>(
|
||||
scene->frame_control_strategy()),
|
||||
nullptr);
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace renderive::render_3d
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
set(Renderive_Web_dependencies global::drogon global::spdlog global::magic_enum)
|
||||
set(Renderive_Web_dependencies global::drogon global::spdlog global::magic_enum global::pfr)
|
||||
if (RENDERIVE_BUILD_TESTS)
|
||||
set(Renderive_Web_test_targets)
|
||||
list(APPEND Renderive_Web_dependencies global::GTest)
|
||||
@@ -105,6 +105,9 @@ function(renderive_find_web_dependencies)
|
||||
endif ()
|
||||
endfunction()
|
||||
renderive_find_web_dependencies()
|
||||
add_library(Renderive_Web_BoostPfr INTERFACE)
|
||||
target_include_directories(Renderive_Web_BoostPfr INTERFACE
|
||||
"$<BUILD_INTERFACE:${EXTERNAL_DIR}/source/pfr/include>")
|
||||
if (NOT TARGET Renderive_render_2D)
|
||||
rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_Web requires Renderive_render_2D")
|
||||
return()
|
||||
@@ -148,7 +151,7 @@ target_compile_features(Renderive_Web_2D PUBLIC cxx_std_20)
|
||||
target_compile_definitions(Renderive_Web_2D PRIVATE NOMINMAX)
|
||||
target_link_libraries(Renderive_Web_2D
|
||||
PUBLIC Renderive_Web_Common Renderive_render_2D
|
||||
PRIVATE Adminive::Nlohmann Adminive::MagicEnum magic_enum::magic_enum)
|
||||
PRIVATE Adminive::Nlohmann Adminive::MagicEnum magic_enum::magic_enum Renderive_Web_BoostPfr)
|
||||
# 3D 模块只负责 Scene3D/Datoviz 到公共 RVP2 像素协议的适配。
|
||||
set(Renderive_Web_3D_sources
|
||||
"${CMAKE_CURRENT_LIST_DIR}/app/render_3D/Gallery_Scene3D.cpp")
|
||||
@@ -160,7 +163,7 @@ target_compile_features(Renderive_Web_3D PUBLIC cxx_std_20)
|
||||
target_compile_definitions(Renderive_Web_3D PRIVATE NOMINMAX)
|
||||
target_link_libraries(Renderive_Web_3D
|
||||
PUBLIC Renderive_Web_Common Renderive_render_3D
|
||||
PRIVATE Adminive::Nlohmann)
|
||||
PRIVATE Adminive::Nlohmann Renderive_Web_BoostPfr)
|
||||
set(Renderive_Web_sources
|
||||
"${CMAKE_CURRENT_LIST_DIR}/app/Gallery_Plot_Session.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/app/Gallery_Protocol.cpp"
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/Gallery_Types.h"
|
||||
#include "render_2D/scene/Scene_Context.hpp"
|
||||
#include <structive/property/property.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace renderive::web {
|
||||
|
||||
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 / Plot_Scene::prepare_frame",
|
||||
"Manual_Refresh_Strategy"),
|
||||
gallery_action("mode_refresh", "提交手动刷新",
|
||||
"Manual_Refresh_Strategy::refresh / Plot_Scene::refresh_manual_frame",
|
||||
"Manual_Refresh_Strategy"),
|
||||
gallery_action("mode_render", "渲染已刷新帧",
|
||||
"Manual_Refresh_Strategy::acquire_renderer / Plot_Scene::render_prepared_frame",
|
||||
"Manual_Refresh_Strategy", {}, {}, {}, 0.0, true),
|
||||
gallery_action("mode_discard", "丢弃待刷新帧",
|
||||
"Plot_Scene::discard_pending_frame",
|
||||
"Manual_Refresh_Strategy"),
|
||||
gallery_action("mode_cycle", "执行完整手动帧周期",
|
||||
"Plot_Scene::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 / Plot_Scene::prepare_frame",
|
||||
"Low_Latency_Strategy"),
|
||||
gallery_action("mode_render", "消费最新帧",
|
||||
"Low_Latency_Strategy::acquire_renderer / Plot_Scene::render_prepared_frame",
|
||||
"Low_Latency_Strategy", {}, {}, {}, 0.0, true),
|
||||
gallery_action("mode_discard", "丢弃陈旧帧",
|
||||
"Plot_Scene::discard_pending_frame", "Low_Latency_Strategy"),
|
||||
gallery_action("mode_cycle", "执行低延迟帧周期",
|
||||
"Plot_Scene::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 / Plot_Scene::prepare_frame",
|
||||
"Flow_Refresh_Strategy"),
|
||||
gallery_action("mode_enqueue_burst", "批量压入回放队列",
|
||||
"Flow_Refresh_Strategy::acquire_painter / Plot_Scene::prepare_frame x N",
|
||||
"Flow_Refresh_Strategy", {}, "number", "帧数", 8.0),
|
||||
gallery_action("mode_dequeue", "消费队首帧",
|
||||
"Flow_Refresh_Strategy::acquire_renderer / Plot_Scene::render_prepared_frame",
|
||||
"Flow_Refresh_Strategy", {}, {}, {}, 0.0, true),
|
||||
gallery_action("mode_cycle", "执行回放帧周期",
|
||||
"Plot_Scene::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
|
||||
@@ -1,136 +0,0 @@
|
||||
#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
|
||||
@@ -1,239 +0,0 @@
|
||||
#pragma once
|
||||
#include <renderive/error/Error_Policy.hpp>
|
||||
|
||||
#include "Gallery_Enum.h"
|
||||
#include "common/Gallery_Performance_Types.h"
|
||||
#include "render_2D/scene/Render_Scene_2D.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{};
|
||||
};
|
||||
|
||||
|
||||
} // namespace renderive::web
|
||||
|
||||
namespace adminive {
|
||||
|
||||
template <class Json>
|
||||
struct Value_Adapter<renderive::Plot_Frame_Mode, Json> {
|
||||
using value_type = std::string;
|
||||
|
||||
static std::string read(renderive::Plot_Frame_Mode value) {
|
||||
return renderive::web::gallery_enum_id(value);
|
||||
}
|
||||
static void write(renderive::Plot_Frame_Mode& target, std::string value) {
|
||||
const auto parsed =
|
||||
renderive::web::gallery_enum_cast<renderive::Plot_Frame_Mode>(value);
|
||||
if (!parsed)
|
||||
::renderive::error::unexpected<std::invalid_argument>("unknown frame control mode: " + value);
|
||||
target = *parsed;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Type_Descriptor<renderive::Plot_Frame_Status> {
|
||||
static auto get() {
|
||||
using T = renderive::Plot_Frame_Status;
|
||||
return object<T>("kernel_observer",
|
||||
ADMINIVE_FIELD_LABEL(T, mode, "模式")
|
||||
.enum_label<renderive::Plot_Frame_Mode::Manual>("手动刷新")
|
||||
.enum_label<renderive::Plot_Frame_Mode::Low_Latency>("低延迟")
|
||||
.enum_label<renderive::Plot_Frame_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::Plot_Frame_Status>);
|
||||
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
|
||||
+200
-736
@@ -1,780 +1,244 @@
|
||||
#include <renderive/error/Error_Policy.hpp>
|
||||
#include "Gallery_Protocol.h"
|
||||
#include "Gallery_Actions.h"
|
||||
#include "Gallery_Enum.h"
|
||||
#include "Gallery_Observer_Adminive.h"
|
||||
#include "Gallery_Renderables.h"
|
||||
#include "Gallery_Session_Control_Adminive.h"
|
||||
#include "common/Gallery_Scheduler_Adminive.h"
|
||||
|
||||
#include "adminive/adminive.hpp"
|
||||
#include "adminive/adapters/nlohmann_json.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <string>
|
||||
#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},
|
||||
{"datoviz_3d", "Datoviz Visuals", "Point_Visual", "Datoviz Visuals",
|
||||
"Renderive 3D Renderable、双缓冲实时数据与 Datoviz Vulkan 后端。", 90},
|
||||
{"datoviz_splat", "Splat", "Splat_Visual", "Datoviz Visuals", "各向异性高斯 splat。", 91},
|
||||
{"datoviz_pixel", "Pixel", "Pixel_Visual", "Datoviz Visuals", "屏幕空间像素。", 92},
|
||||
{"datoviz_marker", "Marker", "Marker_Visual", "Datoviz Visuals", "可旋转的参数化标记。", 93},
|
||||
{"datoviz_sphere", "Sphere", "Sphere_Visual", "Datoviz Visuals", "带深度的球体。", 94},
|
||||
{"datoviz_segment", "Segment", "Segment_Visual", "Datoviz Visuals", "独立线段。", 95},
|
||||
{"datoviz_vector", "Vector", "Vector_Visual", "Datoviz Visuals", "方向向量。", 96},
|
||||
{"datoviz_primitive", "Primitive", "Primitive_Visual", "Datoviz Visuals", "可选拓扑的基础图元。", 97},
|
||||
{"datoviz_mesh", "Mesh", "Mesh_Visual", "Datoviz Visuals", "法线与纹理坐标网格。", 98},
|
||||
{"datoviz_path", "Path", "Path_Visual", "Datoviz Visuals", "连续描边路径。", 99},
|
||||
{"datoviz_image", "Image", "Image_Visual", "Datoviz Visuals", "采样图像实例。", 100},
|
||||
{"datoviz_labels", "Labels", "Labels_Visual", "Datoviz Visuals", "图集标签实例。", 101},
|
||||
{"datoviz_glyph", "Glyph", "Glyph_Visual", "Datoviz Visuals", "MSDF 字形。", 102},
|
||||
{"datoviz_text", "Text", "Text_Visual", "Datoviz Visuals", "基于 glyph 的高层文字注记。", 103},
|
||||
{"datoviz_volume", "Volume", "Volume_Visual", "Datoviz Visuals", "三维标量场。", 104}
|
||||
};
|
||||
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<Render_Scene_2D>>(result);
|
||||
return;
|
||||
case Gallery_Frame_Mode::Low_Latency:
|
||||
append_gallery_actions<Gallery_Session_Control<Render_Scene_2D>>(result);
|
||||
return;
|
||||
case Gallery_Frame_Mode::Playback:
|
||||
append_gallery_actions<Gallery_Session_Control<Render_Scene_2D>>(result);
|
||||
return;
|
||||
}
|
||||
::renderive::error::unexpected<std::invalid_argument>("unknown gallery frame mode");
|
||||
}
|
||||
std::size_t session_control_count(std::string_view case_id, Gallery_Frame_Mode frame_mode) {
|
||||
if (case_id.starts_with("datoviz_"))
|
||||
return 0;
|
||||
switch (frame_mode) {
|
||||
case Gallery_Frame_Mode::Manual:
|
||||
return gallery_control_count<Gallery_Session_Control<Render_Scene_2D>>();
|
||||
case Gallery_Frame_Mode::Low_Latency:
|
||||
return gallery_control_count<Gallery_Session_Control<Render_Scene_2D>>();
|
||||
case Gallery_Frame_Mode::Playback:
|
||||
return gallery_control_count<Gallery_Session_Control<Render_Scene_2D>>();
|
||||
}
|
||||
::renderive::error::unexpected<std::invalid_argument>("unknown gallery frame mode");
|
||||
}
|
||||
void append_3d_actions(Gallery_Frame_Mode frame_mode, std::vector<Action_Model>& result) {
|
||||
const auto append = [&result](const Gallery_Action_Attribute& action) {
|
||||
result.push_back(gallery_action_model(action));
|
||||
};
|
||||
switch (frame_mode) {
|
||||
case Gallery_Frame_Mode::Manual:
|
||||
append(gallery_action("mode_prepare", "准备 3D 帧", "Manual_Scene3D::prepare_frame", "3D 帧策略"));
|
||||
append(gallery_action("mode_refresh", "提交手动刷新", "Manual_Scene3D::refresh", "3D 帧策略"));
|
||||
append(gallery_action("mode_render", "渲染已刷新帧", "Manual_Scene3D::render_frame", "3D 帧策略", {}, {}, {}, 0.0, true));
|
||||
break;
|
||||
case Gallery_Frame_Mode::Low_Latency:
|
||||
append(gallery_action("mode_prepare", "发布 3D 帧", "Scene3D::prepare_frame", "3D 帧策略"));
|
||||
append(gallery_action("mode_render", "消费 3D 帧", "Scene3D::render_frame", "3D 帧策略", {}, {}, {}, 0.0, true));
|
||||
break;
|
||||
case Gallery_Frame_Mode::Playback:
|
||||
append(gallery_action("mode_enqueue", "压入 3D 帧", "Playback_Scene3D::prepare_frame", "3D 帧策略"));
|
||||
append(gallery_action("mode_dequeue", "消费队首 3D 帧", "Playback_Scene3D::render_frame", "3D 帧策略", {}, {}, {}, 0.0, true));
|
||||
break;
|
||||
}
|
||||
append(gallery_action("move_mesh", "移动蓝色对象", "Mesh::place", "3D 场景"));
|
||||
append(gallery_action("add_mesh", "添加黄色对象", "Scene_Edit::attach", "3D 场景"));
|
||||
append(gallery_action("remove_mesh", "移除黄色对象", "Scene_Edit::detach", "3D 场景"));
|
||||
append(gallery_action("mesh_churn", "Attach/Detach 验证", "Scene_Edit::attach/detach", "3D 场景"));
|
||||
append(gallery_action("reset_camera", "重置相机", "Camera::look_at/use_perspective", "3D 相机"));
|
||||
append(gallery_action("toggle_projection", "切换投影", "Camera::use_perspective/use_orthographic", "3D 相机"));
|
||||
append(gallery_action("toggle_texture", "切换纹理", "Material::use_texture", "3D 材质"));
|
||||
append(gallery_action("change_text", "修改 MSDF 文本", "Text::set_string", "3D 文本"));
|
||||
append(gallery_action("reset", "恢复 3D 场景", "Gallery_Scene3D factory", "3D 场景"));
|
||||
}
|
||||
std::vector<Action_Model> registered_actions(std::string_view case_id, Gallery_Frame_Mode frame_mode) {
|
||||
std::vector<Action_Model> result;
|
||||
if (case_id.starts_with("datoviz_")) {
|
||||
append_3d_actions(frame_mode, result);
|
||||
return 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, "Renderive 组件"),
|
||||
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, "Renderive 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>("Renderive API"))
|
||||
.titled("保留 API 动作");
|
||||
}
|
||||
|
||||
} // namespace renderive::web::gallery_detail
|
||||
|
||||
namespace renderive::web {
|
||||
namespace {
|
||||
using Json = nlohmann::json;
|
||||
struct Case_Info { std::string_view id, title, component, category; };
|
||||
constexpr std::array cases{
|
||||
Case_Info{"axis_lab", "Axis Lab", "Axis", "2D"},
|
||||
Case_Info{"spectrum", "Spectrum", "Spectrum", "2D"},
|
||||
Case_Info{"afterglow", "Afterglow", "Afterglow", "2D"},
|
||||
Case_Info{"sweep_spectrum", "Sweep Spectrum", "Sweep_Spectrum", "2D"},
|
||||
Case_Info{"waterfall", "Waterfall", "Waterfall", "2D"},
|
||||
Case_Info{"frequency_trace", "Frequency Trace", "Frequency_Trace", "2D"},
|
||||
Case_Info{"selection_overlay", "Selection Overlay", "Selection", "2D"},
|
||||
Case_Info{"constellation", "Constellation", "Constellation_Diagram", "2D"},
|
||||
Case_Info{"datoviz_3d", "Datoviz 3D", "Point", "3D"},
|
||||
Case_Info{"datoviz_splat", "Datoviz Splat", "Splat", "3D"},
|
||||
Case_Info{"datoviz_pixel", "Datoviz Pixel", "Pixel", "3D"},
|
||||
Case_Info{"datoviz_marker", "Datoviz Marker", "Marker", "3D"},
|
||||
Case_Info{"datoviz_sphere", "Datoviz Sphere", "Sphere", "3D"},
|
||||
Case_Info{"datoviz_segment", "Datoviz Segment", "Segment", "3D"},
|
||||
Case_Info{"datoviz_vector", "Datoviz Vector", "Vector", "3D"},
|
||||
Case_Info{"datoviz_primitive", "Datoviz Primitive", "Primitive", "3D"},
|
||||
Case_Info{"datoviz_mesh", "Datoviz Mesh", "Mesh", "3D"},
|
||||
Case_Info{"datoviz_path", "Datoviz Path", "Path", "3D"},
|
||||
Case_Info{"datoviz_image", "Datoviz Image", "Image", "3D"},
|
||||
Case_Info{"datoviz_labels", "Datoviz Labels", "Labels", "3D"},
|
||||
Case_Info{"datoviz_glyph", "Datoviz Glyph", "Glyph", "3D"},
|
||||
Case_Info{"datoviz_text", "Datoviz Text", "Text", "3D"},
|
||||
Case_Info{"datoviz_volume", "Datoviz Volume", "Volume", "3D"},
|
||||
};
|
||||
|
||||
using gallery_detail::Json;
|
||||
|
||||
Json protocol_base() {
|
||||
return {{"category", "gallery"},
|
||||
{"protocol", "renderive.control-gallery"},
|
||||
{"protocol_version", 5}};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Json dashboard_trend_field(std::string_view label, std::string_view source,
|
||||
std::string_view trend_group,
|
||||
std::string_view format = "integer", int digits = -1) {
|
||||
Json result = dashboard_field(label, source, format, digits);
|
||||
result["trend_group"] = trend_group;
|
||||
return result;
|
||||
}
|
||||
Json dashboard_summary_field(std::string_view label, std::string_view source,
|
||||
std::string_view trend_group,
|
||||
std::string_view format = "integer", int digits = -1) {
|
||||
Json result = dashboard_trend_field(label, source, trend_group, format, digits);
|
||||
result["summary"] = true;
|
||||
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())
|
||||
::renderive::error::unexpected<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::Plot_Frame_Status>();
|
||||
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::Plot_Frame_Status>(
|
||||
"kernel_observer", name);
|
||||
if (name == "frequency_hz") {
|
||||
field["enabled_source"] =
|
||||
described_source<renderive::Plot_Frame_Status>(
|
||||
"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::Plot_Frame_Status>(
|
||||
"kernel_observer", "limit_state");
|
||||
bottleneck["format"] = "duration_enum";
|
||||
bottleneck["duration_sources"] = {
|
||||
{"frequency_limited", described_source<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "target_interval_ns")},
|
||||
{"paint_limited", described_source<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "paint_duration_ns")},
|
||||
{"render_limited", described_source<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "render_duration_ns")},
|
||||
{"consumer_limited", described_source<renderive::Plot_Frame_Status>(
|
||||
"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_summary_field("后端渲染 FPS", "performance.measured_fps", "FPS", "fixed", 1),
|
||||
dashboard_summary_field("像素响应 FPS", "performance.pixel_response_fps", "FPS", "fixed", 1),
|
||||
dashboard_trend_field("浏览器呈现 FPS", "client_performance.presentation_fps", "FPS", "fixed", 1),
|
||||
dashboard_trend_field("WS 往返 ms", "client_performance.frame_round_trip_ms", "帧延迟", "fixed", 2),
|
||||
dashboard_field("未呈现覆盖", "client_performance.overwritten_pixel_frames"),
|
||||
dashboard_summary_field("帧完成延迟 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_summary_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),
|
||||
dashboard_field("TBB 并发度", "scheduler.concurrency"),
|
||||
dashboard_trend_field("活动 Worker", "scheduler.active_workers", "调度与队列"),
|
||||
dashboard_field("峰值 Worker", "scheduler.peak_workers"),
|
||||
dashboard_field("活动外部线程", "scheduler.active_external_threads"),
|
||||
dashboard_trend_field("Scene 排队", "queue_pressure.scene_execution.queued", "调度与队列"),
|
||||
dashboard_trend_field("Render Domain 排队", "queue_pressure.render_domain.queued", "调度与队列"),
|
||||
dashboard_trend_field("GPU Completion 活动", "queue_pressure.gpu_completion.active", "调度与队列"),
|
||||
dashboard_field("Scene Backpressure", "queue_pressure.scene_execution.backpressure_count"),
|
||||
dashboard_field("Render Backpressure", "queue_pressure.render_domain.backpressure_count"),
|
||||
dashboard_field("GPU Backpressure", "queue_pressure.gpu_completion.backpressure_count"),
|
||||
described_dashboard_field<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "pending_frame_count"),
|
||||
described_dashboard_field<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "dropped_frame_count"),
|
||||
std::move(point_pair), std::move(bottleneck),
|
||||
described_dashboard_field<renderive::Plot_Frame_Status>(
|
||||
"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::Plot_Frame_Status>()}
|
||||
}},
|
||||
{"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>()}
|
||||
},
|
||||
{
|
||||
{"title", "oneTBB 调度器"},
|
||||
{"source", "scheduler"},
|
||||
{"descriptor", adminive::to_descriptor_json<
|
||||
Json, renderive::scheduling::Scheduler_Statistics>()}
|
||||
}
|
||||
})}
|
||||
}}
|
||||
}},
|
||||
{"limits", {
|
||||
{"aria_label", "低延迟限速来源"}, {"title", "限速来源"},
|
||||
{"current_source", described_source<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "limit_state")},
|
||||
{"active_label", "当前瓶颈"}, {"inactive_label", "未受限"},
|
||||
{"disabled_label", "已关闭"},
|
||||
{"fields", Json::array({
|
||||
{{"label", "内核用户频率"}, {"active_value", "frequency_limited"},
|
||||
{"duration_source", described_source<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "target_interval_ns")},
|
||||
{"enabled_source", described_source<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "frequency_limit_enabled")}},
|
||||
{{"label", "内核绘制事件"}, {"active_value", "paint_limited"},
|
||||
{"duration_source", described_source<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "paint_duration_ns")}},
|
||||
{{"label", "内核后台渲染"}, {"active_value", "render_limited"},
|
||||
{"duration_source", described_source<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "render_duration_ns")}},
|
||||
{{"label", "消费者反馈"}, {"active_value", "consumer_limited"},
|
||||
{"duration_source", described_source<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "consumer_interval_ns")},
|
||||
{"enabled_source", described_source<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "consumer_feedback_enabled")}}
|
||||
})}
|
||||
}},
|
||||
{"observer", {
|
||||
{"aria_label", "内核低延迟全量统计"},
|
||||
{"header", {
|
||||
{"prefix", "内核"}, {"suffix", "观察器"}, {"event_label", "事件"},
|
||||
{"mode", described_dashboard_field<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "mode")},
|
||||
{"limit", described_dashboard_field<renderive::Plot_Frame_Status>(
|
||||
"kernel_observer", "limit_state")},
|
||||
{"event", described_dashboard_field<renderive::Plot_Frame_Status>(
|
||||
"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::Plot_Frame_Status>();
|
||||
dashboard["observer"]["consumer_feedback_descriptor"] =
|
||||
adminive::to_descriptor_json<Json, Gallery_Consumer_Feedback_Snapshot>();
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
Json frame_mode_contract(Gallery_Frame_Mode mode) {
|
||||
std::string_view mode_id(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}};
|
||||
case Gallery_Frame_Mode::Manual: return "manual";
|
||||
case Gallery_Frame_Mode::Low_Latency: return "low_latency";
|
||||
case Gallery_Frame_Mode::Playback: return "playback";
|
||||
}
|
||||
return Json::object();
|
||||
return "low_latency";
|
||||
}
|
||||
std::optional<Gallery_Frame_Mode> parse_mode(std::string_view value) {
|
||||
if (value == "manual") return Gallery_Frame_Mode::Manual;
|
||||
if (value == "low_latency") return Gallery_Frame_Mode::Low_Latency;
|
||||
if (value == "playback") return Gallery_Frame_Mode::Playback;
|
||||
return std::nullopt;
|
||||
}
|
||||
Json parse_object(std::string_view text) {
|
||||
Json value = Json::parse(text, nullptr, false);
|
||||
return value.is_object() ? std::move(value) : Json::object();
|
||||
}
|
||||
Json actions() {
|
||||
return Json::array({
|
||||
{{"id", "frame_strategy_low_latency"}, {"label", "Low latency"},
|
||||
{"api", "Renderable_Editor::replace_frame_control_strategy"},
|
||||
{"group", "Frame strategy"}, {"request_frame", true}},
|
||||
{{"id", "frame_strategy_manual"}, {"label", "Manual"},
|
||||
{"api", "Renderable_Editor::replace_frame_control_strategy"},
|
||||
{"group", "Frame strategy"}, {"request_frame", true}},
|
||||
{{"id", "frame_strategy_playback"}, {"label", "Playback"},
|
||||
{"api", "Renderable_Editor::replace_frame_control_strategy"},
|
||||
{"group", "Frame strategy"}, {"request_frame", true}},
|
||||
{{"id", "mode_render"}, {"label", "Render frame"},
|
||||
{"api", "Scene::render"}, {"group", "Frame"}, {"request_frame", true}},
|
||||
{{"id", "capture_next_frame"}, {"label", "Capture next frame"},
|
||||
{"api", "Scene_Base::capture_next_frame"}, {"group", "Performance Capture"},
|
||||
{"request_frame", true}},
|
||||
{{"id", "capture_frames"}, {"label", "Capture N frames"},
|
||||
{"api", "Scene_Base::capture_frames"}, {"group", "Performance Capture"},
|
||||
{"argument_input", "number"}, {"argument_default", 20}, {"request_frame", true}},
|
||||
{{"id", "reset"}, {"label", "Reset scene"},
|
||||
{"api", "Scene::Builder"}, {"group", "Scene"}, {"request_frame", true}}
|
||||
});
|
||||
}
|
||||
template <class T>
|
||||
Gallery_Request_Result<T> fail(Gallery_Request_Error error) {
|
||||
return {.value = std::nullopt, .error = error};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string Gallery_Protocol::catalog_json() {
|
||||
Json result = protocol_base();
|
||||
result["type"] = "catalog";
|
||||
result["transport"] = {{"events", "websocket-text"}, {"pixels", "websocket-binary-rvp2"},
|
||||
{"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 = item.id.starts_with("datoviz_") ? 0 : 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(item.id, 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(item.id, 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));
|
||||
Json published_cases = Json::array();
|
||||
for (std::size_t index = 0; index < cases.size(); ++index) {
|
||||
const auto& item = cases[index];
|
||||
published_cases.push_back({
|
||||
{"id", item.id}, {"title", item.title}, {"component", item.component},
|
||||
{"category", item.category}, {"description", "Live Renderive scene"},
|
||||
{"order", index},
|
||||
{"control_count_by_mode", {{"manual", 0}, {"low_latency", 0}, {"playback", 0}}},
|
||||
{"action_count_by_mode", {{"manual", 7}, {"low_latency", 7}, {"playback", 7}}}
|
||||
});
|
||||
}
|
||||
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();
|
||||
Json field_mode{{"label", "Mode"}, {"source", "kernel_observer.mode"}};
|
||||
Json field_limit{{"label", "Limit"}, {"source", "kernel_observer.limit"}};
|
||||
Json field_event{{"label", "Event"}, {"source", "kernel_observer.event"}};
|
||||
Json root{
|
||||
{"category", "gallery"}, {"type", "catalog"},
|
||||
{"protocol", "renderive.control-gallery"}, {"protocol_version", 5},
|
||||
{"transport", {{"pixels", "websocket-binary-rvp2"}, {"socket_per_canvas", true}}},
|
||||
{"navigation", {{"default_mode", "low_latency"}, {"all_categories_label", "All"},
|
||||
{"hero_eyebrow", "Renderive"}, {"hero_title", "Scene Gallery"},
|
||||
{"catalog_loaded_text", "Gallery ready"}}},
|
||||
{"frame_modes", Json::array({
|
||||
{{"id", "manual"}, {"title", "Manual"}, {"strategy", "Manual"},
|
||||
{"description", "Explicit frame requests"}, {"order", 0}, {"accent", "#ffbd69"},
|
||||
{"automatic", false}, {"request_after_response", false},
|
||||
{"request_on_animation_frame", false}, {"observer_visible", true},
|
||||
{"frame_button_label", "Render frame"}},
|
||||
{{"id", "low_latency"}, {"title", "Low latency"}, {"strategy", "Low_Latency"},
|
||||
{"description", "Latest-state rendering"}, {"order", 1}, {"accent", "#59d6c5"},
|
||||
{"automatic", true}, {"request_after_response", true},
|
||||
{"request_on_animation_frame", false}, {"observer_visible", true},
|
||||
{"frame_button_label", "Render frame"}},
|
||||
{{"id", "playback"}, {"title", "Playback"}, {"strategy", "Flow"},
|
||||
{"description", "Ordered frame playback"}, {"order", 2}, {"accent", "#84b3ce"},
|
||||
{"automatic", false}, {"request_after_response", false},
|
||||
{"request_on_animation_frame", true}, {"observer_visible", true},
|
||||
{"frame_button_label", "Render frame"}}
|
||||
})},
|
||||
{"cases", std::move(published_cases)},
|
||||
{"coverage", {{"case_count", cases.size()}, {"page_count", 1},
|
||||
{"canvas_count", cases.size()}, {"manual_control_count", 0},
|
||||
{"manual_action_count", cases.size() * 7}}},
|
||||
{"dashboard", {
|
||||
{"performance", {{"fields", Json::array({
|
||||
{{"label", "Rendered"}, {"source", "performance.successful_render_count"},
|
||||
{"format", "integer"}, {"summary", true}},
|
||||
{{"label", "Transport FPS"}, {"source", "client_performance.transport_fps"},
|
||||
{"digits", 1}, {"summary", true}}
|
||||
})}}},
|
||||
{"limits", {{"title", "Frame limits"}, {"aria_label", "Frame limits"},
|
||||
{"current_source", "kernel_observer.limit"}, {"active_label", "active"},
|
||||
{"inactive_label", "inactive"}, {"disabled_label", "disabled"},
|
||||
{"fields", Json::array()}}},
|
||||
{"observer", {{"aria_label", "Kernel observer"},
|
||||
{"header", {{"prefix", ""}, {"suffix", ""}, {"event_label", ""},
|
||||
{"mode", field_mode}, {"limit", field_limit},
|
||||
{"event", field_event}}},
|
||||
{"sections", Json::array()}}},
|
||||
{"menu_views", Json::object()}
|
||||
}}
|
||||
};
|
||||
return root.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);
|
||||
result["controls"] = Json::parse(controls_json.begin(), controls_json.end());
|
||||
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));
|
||||
result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end());
|
||||
if (!notice.empty())
|
||||
result["notice"] = notice;
|
||||
return result.dump();
|
||||
return std::ranges::any_of(cases, [case_id](const auto& item) {
|
||||
return item.id == case_id;
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
Json root{{"category", "gallery"}, {"type", "error"}, {"message", message}};
|
||||
if (!field.empty()) root["field_errors"] = {{field, message}};
|
||||
return root.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);
|
||||
result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end());
|
||||
return result.dump();
|
||||
return Json{{"category", "gallery"}, {"type", "observer_state"},
|
||||
{"case", case_id}, {"frame_mode", mode_id(frame_mode)},
|
||||
{"telemetry", parse_object(telemetry_json)}}.dump();
|
||||
}
|
||||
|
||||
Gallery_Request_Result<Gallery_Open_Request> Gallery_Protocol::open_request(std::string_view message) {
|
||||
Json request;
|
||||
try {
|
||||
request = gallery_detail::parse_request(message);
|
||||
} catch (const Json::exception&) {
|
||||
return {{}, Gallery_Request_Error::invalid_json};
|
||||
}
|
||||
if (!request.is_object() || !request.contains("category") ||
|
||||
!request.at("category").is_string() ||
|
||||
request.at("category").get_ref<const std::string&>() != "event" ||
|
||||
!request.contains("type") || !request.at("type").is_string() ||
|
||||
request.at("type").get_ref<const std::string&>() != "gallery_open")
|
||||
return {{}, Gallery_Request_Error::wrong_message_type};
|
||||
if (!request.contains("case") || !request.contains("frame_mode"))
|
||||
return {{}, Gallery_Request_Error::missing_field};
|
||||
if (!request.at("case").is_string() || !request.at("frame_mode").is_string())
|
||||
return {{}, Gallery_Request_Error::invalid_field};
|
||||
const std::string value = request.at("case").get<std::string>();
|
||||
const auto mode = gallery_enum_cast<Gallery_Frame_Mode>(
|
||||
request.at("frame_mode").get_ref<const std::string&>());
|
||||
if (!is_case(value) || !mode)
|
||||
return {{}, Gallery_Request_Error::invalid_field};
|
||||
return {Gallery_Open_Request{value, *mode}, Gallery_Request_Error::none};
|
||||
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) {
|
||||
return Json{{"category", "gallery"},
|
||||
{"type", manual_refresh ? "refresh_state" : "case_state"},
|
||||
{"case", case_id}, {"frame_mode", mode_id(frame_mode)},
|
||||
{"controls", parse_object(controls_json)},
|
||||
{"actions", {{"data", actions()}}},
|
||||
{"telemetry", parse_object(telemetry_json)}, {"notice", notice}}.dump();
|
||||
}
|
||||
|
||||
Gallery_Request_Result<Gallery_Open_Request> Gallery_Protocol::open_request(
|
||||
std::string_view message) {
|
||||
const Json root = Json::parse(message, nullptr, false);
|
||||
if (!root.is_object()) return fail<Gallery_Open_Request>(Gallery_Request_Error::invalid_json);
|
||||
if (root.value("category", "") != "event" || root.value("type", "") != "gallery_open")
|
||||
return fail<Gallery_Open_Request>(Gallery_Request_Error::wrong_message_type);
|
||||
if (!root.contains("case") || !root["case"].is_string() ||
|
||||
!root.contains("frame_mode") || !root["frame_mode"].is_string())
|
||||
return fail<Gallery_Open_Request>(Gallery_Request_Error::missing_field);
|
||||
const auto case_id = root["case"].get<std::string>();
|
||||
const auto mode = parse_mode(root["frame_mode"].get<std::string>());
|
||||
if (!is_case(case_id) || !mode)
|
||||
return fail<Gallery_Open_Request>(Gallery_Request_Error::invalid_field);
|
||||
return {.value = Gallery_Open_Request{case_id, *mode},
|
||||
.error = Gallery_Request_Error::none};
|
||||
}
|
||||
|
||||
Gallery_Request_Result<Gallery_Action_Request> Gallery_Protocol::action_request(
|
||||
std::string_view message) {
|
||||
Json request;
|
||||
try {
|
||||
request = gallery_detail::parse_request(message);
|
||||
} catch (const Json::exception&) {
|
||||
return {{}, Gallery_Request_Error::invalid_json};
|
||||
const Json root = Json::parse(message, nullptr, false);
|
||||
if (!root.is_object()) return fail<Gallery_Action_Request>(Gallery_Request_Error::invalid_json);
|
||||
if (root.value("category", "") != "event" || root.value("type", "") != "gallery_action")
|
||||
return fail<Gallery_Action_Request>(Gallery_Request_Error::wrong_message_type);
|
||||
if (!root.contains("action") || !root["action"].is_string())
|
||||
return fail<Gallery_Action_Request>(Gallery_Request_Error::missing_field);
|
||||
Gallery_Action_Request request{root["action"].get<std::string>(), std::nullopt};
|
||||
if (const auto argument = root.find("argument"); argument != root.end()) {
|
||||
if (argument->is_boolean()) request.argument = argument->get<bool>();
|
||||
else if (argument->is_number()) request.argument = argument->get<double>();
|
||||
else if (argument->is_string()) request.argument = argument->get<std::string>();
|
||||
else return fail<Gallery_Action_Request>(Gallery_Request_Error::invalid_field);
|
||||
}
|
||||
if (!request.is_object() || !request.contains("category") ||
|
||||
!request.at("category").is_string() ||
|
||||
request.at("category").get_ref<const std::string&>() != "event" ||
|
||||
!request.contains("type") || !request.at("type").is_string() ||
|
||||
request.at("type").get_ref<const std::string&>() != "gallery_action")
|
||||
return {{}, Gallery_Request_Error::wrong_message_type};
|
||||
if (!request.contains("action"))
|
||||
return {{}, Gallery_Request_Error::missing_field};
|
||||
if (!request.at("action").is_string())
|
||||
return {{}, Gallery_Request_Error::invalid_field};
|
||||
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 {{}, Gallery_Request_Error::invalid_field};
|
||||
result.argument = value;
|
||||
} else if (argument.is_string())
|
||||
result.argument = argument.get<std::string>();
|
||||
else
|
||||
return {{}, Gallery_Request_Error::invalid_field};
|
||||
}
|
||||
return {std::move(result), Gallery_Request_Error::none};
|
||||
return {.value = std::move(request), .error = Gallery_Request_Error::none};
|
||||
}
|
||||
|
||||
Gallery_Request_Result<Gallery_Control_Patch_Request> Gallery_Protocol::control_patch_request(
|
||||
std::string_view message) {
|
||||
Json request;
|
||||
try {
|
||||
request = gallery_detail::parse_request(message);
|
||||
} catch (const Json::exception&) {
|
||||
return {{}, Gallery_Request_Error::invalid_json};
|
||||
}
|
||||
if (!request.is_object() || !request.contains("category") ||
|
||||
!request.at("category").is_string() ||
|
||||
request.at("category").get_ref<const std::string&>() != "event" ||
|
||||
!request.contains("type") || !request.at("type").is_string() ||
|
||||
request.at("type").get_ref<const std::string&>() != "gallery_patch")
|
||||
return {{}, Gallery_Request_Error::wrong_message_type};
|
||||
if (!request.contains("target") || !request.contains("patch"))
|
||||
return {{}, Gallery_Request_Error::missing_field};
|
||||
if (!request.at("target").is_string() || !request.at("patch").is_object())
|
||||
return {{}, Gallery_Request_Error::invalid_field};
|
||||
return {Gallery_Control_Patch_Request{
|
||||
request.at("target").get<std::string>(), request.at("patch").dump()},
|
||||
Gallery_Request_Error::none};
|
||||
Gallery_Request_Result<Gallery_Control_Patch_Request>
|
||||
Gallery_Protocol::control_patch_request(std::string_view message) {
|
||||
const Json root = Json::parse(message, nullptr, false);
|
||||
if (!root.is_object()) return fail<Gallery_Control_Patch_Request>(Gallery_Request_Error::invalid_json);
|
||||
if (root.value("category", "") != "event" || root.value("type", "") != "gallery_patch")
|
||||
return fail<Gallery_Control_Patch_Request>(Gallery_Request_Error::wrong_message_type);
|
||||
if (!root.contains("target") || !root["target"].is_string() ||
|
||||
!root.contains("patch") || !root["patch"].is_object())
|
||||
return fail<Gallery_Control_Patch_Request>(Gallery_Request_Error::missing_field);
|
||||
return {.value = Gallery_Control_Patch_Request{
|
||||
root["target"].get<std::string>(), root["patch"].dump()},
|
||||
.error = Gallery_Request_Error::none};
|
||||
}
|
||||
|
||||
bool Gallery_Protocol::action_available(std::string_view case_id,
|
||||
Gallery_Frame_Mode frame_mode,
|
||||
Gallery_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;
|
||||
});
|
||||
return is_case(case_id) &&
|
||||
(action_id == "reset" || action_id == "mode_render" ||
|
||||
action_id == "capture_next_frame" || action_id == "capture_frames" ||
|
||||
action_id == "frame_strategy_low_latency" ||
|
||||
action_id == "frame_strategy_manual" ||
|
||||
action_id == "frame_strategy_playback");
|
||||
}
|
||||
|
||||
} // namespace renderive::web
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
#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>
|
||||
struct Gallery_Plottable : Base {
|
||||
using Properties = typename Base::Properties;
|
||||
using Base::Base;
|
||||
|
||||
private:
|
||||
template <auto Member>
|
||||
auto adminive_read() const {
|
||||
return this->template get<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>
|
||||
struct Gallery_Axis_Base : Base {
|
||||
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 = 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 = 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 = Renderable_Builder<Gallery_Time_Axis, Properties>;
|
||||
};
|
||||
|
||||
} // namespace renderive::web
|
||||
@@ -1,663 +0,0 @@
|
||||
#pragma once
|
||||
#include <renderive/error/Error_Policy.hpp>
|
||||
|
||||
#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;
|
||||
}))
|
||||
::renderive::error::unexpected<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
|
||||
@@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
#include "render_2D/plottable/Performance_Overlay.h"
|
||||
#include "render_2D/renderable/Renderable.h"
|
||||
#include "render_2D/scene/Render_Scene_2D.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;
|
||||
};
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
#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_.request_redraw(); };
|
||||
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))
|
||||
return;
|
||||
} else {
|
||||
if (!value.scene_.clear_max_render_fps())
|
||||
return;
|
||||
}
|
||||
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))
|
||||
return;
|
||||
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
|
||||
@@ -1,350 +1,45 @@
|
||||
#include "Web_Plot_Session.h"
|
||||
#include "Pixel_Frame.h"
|
||||
#include "render_2D/export.h"
|
||||
#include <renderive/error/Error_Policy.hpp>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include "render_2D/scene/Render_Scene_2D.h"
|
||||
#include <memory>
|
||||
#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 {
|
||||
Render_Scene_2D 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});
|
||||
if (!plot.set_max_render_fps(30.0))
|
||||
::renderive::error::unexpected<std::logic_error>(
|
||||
"validated web plot frame rate was rejected");
|
||||
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{&attach}
|
||||
.set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal)
|
||||
.set<&Frequency_Axis::Properties::coordinates>(initial_frequency)
|
||||
.set<&Frequency_Axis::Properties::precision>(2)
|
||||
.set<&Frequency_Axis::Properties::tick_length>(8)
|
||||
.set<&Frequency_Axis::Properties::sub_tick_length>(4)
|
||||
.set<&Frequency_Axis::Properties::color>(axis_color)
|
||||
.set<&Frequency_Axis::Properties::wheel>(true)
|
||||
.set<&Frequency_Axis::Properties::drag>(true)
|
||||
.build(axes);
|
||||
spectrum_power_axis =
|
||||
Axis::Builder{&attach}
|
||||
.set<&Axis::Properties::orientation>(Orientation::Vertical)
|
||||
.set<&Axis::Properties::coordinates>(Range{-20.0, -120.0})
|
||||
.set<&Axis::Properties::precision>(0)
|
||||
.set<&Axis::Properties::tick_length>(-8)
|
||||
.set<&Axis::Properties::sub_tick_length>(-4)
|
||||
.set<&Axis::Properties::color>(axis_color)
|
||||
.set<&Axis::Properties::unit_text>("dBm")
|
||||
.build(axes);
|
||||
waterfall_frequency_axis =
|
||||
Frequency_Axis::Builder{&attach}
|
||||
.set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal)
|
||||
.set<&Frequency_Axis::Properties::coordinates>(initial_frequency)
|
||||
.set<&Frequency_Axis::Properties::precision>(2)
|
||||
.set<&Frequency_Axis::Properties::tick_length>(8)
|
||||
.set<&Frequency_Axis::Properties::sub_tick_length>(4)
|
||||
.set<&Frequency_Axis::Properties::color>(axis_color)
|
||||
.set<&Frequency_Axis::Properties::wheel>(true)
|
||||
.set<&Frequency_Axis::Properties::drag>(true)
|
||||
.build(axes);
|
||||
waterfall_time_axis =
|
||||
Time_Axis::Builder{&attach}
|
||||
.set<&Time_Axis::Properties::orientation>(Orientation::Vertical)
|
||||
.set<&Time_Axis::Properties::visible_count>(72)
|
||||
.set<&Time_Axis::Properties::tick_label_spacing_px>(34)
|
||||
.set<&Time_Axis::Properties::format>("mm:ss")
|
||||
.set<&Time_Axis::Properties::tick_length>(-8)
|
||||
.set<&Time_Axis::Properties::sub_tick_length>(-4)
|
||||
.set<&Time_Axis::Properties::color>(axis_color)
|
||||
.build(axes);
|
||||
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();
|
||||
const auto initial_render = plot.render_frame(true);
|
||||
if (!initial_render || *initial_render != Plot_Render_Status::rendered)
|
||||
plot.request_redraw();
|
||||
}
|
||||
~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();
|
||||
const auto render = plot.render_frame(true);
|
||||
if (!render || *render != Plot_Render_Status::rendered)
|
||||
return std::nullopt;
|
||||
std::string pixels;
|
||||
const Color background = plot.background_color();
|
||||
const std::uint64_t sequence = plot.frame_status().latest_sequence;
|
||||
plot.with_frame([&pixels, background, sequence](Image_View image) {
|
||||
pixels = encode_pixel_frame(image, sequence, background);
|
||||
});
|
||||
return pixels.empty() ? std::nullopt : std::optional<std::string>(std::move(pixels));
|
||||
Size viewport{960, 600};
|
||||
std::uint64_t sequence{};
|
||||
std::unique_ptr<Render_Scene_2D> scene;
|
||||
|
||||
Impl() { rebuild(); }
|
||||
void rebuild() {
|
||||
Render_Scene_2D::State state;
|
||||
state.viewport = viewport;
|
||||
scene = Render_Scene_2D::Builder{state}.build(
|
||||
std::make_shared<Low_Latency_Render_Scene_2D_Strategy>());
|
||||
scene->activate_view();
|
||||
}
|
||||
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.request_redraw();
|
||||
}
|
||||
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.request_redraw();
|
||||
}
|
||||
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);
|
||||
return std::visit([this](const auto& value) -> std::optional<Web_Response> {
|
||||
using T = std::decay_t<decltype(value)>;
|
||||
if constexpr (std::same_as<T, Frame_Request>) {
|
||||
const auto result = scene->render_frame(true);
|
||||
if (!result || scene->wait_for_render() != Scene_Render_Result::none)
|
||||
return std::nullopt;
|
||||
std::string pixels;
|
||||
scene->with_frame([&](Image_View image) {
|
||||
pixels = encode_pixel_frame(image, ++sequence);
|
||||
});
|
||||
if (pixels.empty()) return std::nullopt;
|
||||
return Web_Response{Web_Response_Type::Pixels,
|
||||
std::move(pixels)};
|
||||
} else if constexpr (std::same_as<T, Viewport_Resize>) {
|
||||
viewport = value.size;
|
||||
rebuild();
|
||||
} else if constexpr (std::derived_from<T, Event>) {
|
||||
scene->dispatch_event(value);
|
||||
}
|
||||
return std::nullopt;
|
||||
}, event);
|
||||
}
|
||||
};
|
||||
Web_Plot_Session::Web_Plot_Session() : impl_(std::make_unique<Impl>()) {}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <adminive/adapters/boost_pfr.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
namespace renderive::web {
|
||||
template <class T>
|
||||
struct Observer_Pfr_Adapter {
|
||||
using Type = adminive::Boost_Pfr_Reflection_Adapter<T>;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
template <class T>
|
||||
nlohmann::json observer_value_json(const T& value);
|
||||
|
||||
template <class T, class Adapter, std::size_t... Indices>
|
||||
nlohmann::json observer_object_json(
|
||||
const T& value, std::index_sequence<Indices...>) {
|
||||
nlohmann::json result = nlohmann::json::object();
|
||||
((result[std::string(Adapter::template name<Indices>())] =
|
||||
observer_value_json(Adapter::template get<Indices>(value))),
|
||||
...);
|
||||
return result;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
nlohmann::json observer_value_json(const T& value) {
|
||||
using Value = std::remove_cvref_t<T>;
|
||||
if constexpr (std::same_as<Value, std::string>) {
|
||||
return value;
|
||||
} else if constexpr (std::same_as<Value, bool> ||
|
||||
std::is_arithmetic_v<Value>) {
|
||||
return value;
|
||||
} else if constexpr (std::is_enum_v<Value>) {
|
||||
return static_cast<std::underlying_type_t<Value>>(value);
|
||||
} else if constexpr (std::is_aggregate_v<Value>) {
|
||||
using Adapter = typename Observer_Pfr_Adapter<Value>::Type;
|
||||
return observer_object_json<Value, Adapter>(
|
||||
value, std::make_index_sequence<Adapter::field_count>{});
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
template <class Observer>
|
||||
nlohmann::json observer_json(const Observer& observer) {
|
||||
return detail::observer_value_json(observer);
|
||||
}
|
||||
} // namespace renderive::web
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,196 +1,52 @@
|
||||
#include <renderive/error/Error_Policy.hpp>
|
||||
#include "Gallery_Scene3D.h"
|
||||
|
||||
#include "../Gallery_Capture_Json.h"
|
||||
#include "../Gallery_Enum.h"
|
||||
#include "../common/Gallery_Scheduler_Adminive.h"
|
||||
#include "../common/Observer_Json.h"
|
||||
#include "../common/Pixel_Frame.h"
|
||||
#include <render_3D/Render_Scene_3D.h>
|
||||
|
||||
#include "adminive/adminive.hpp"
|
||||
#include "adminive/adapters/nlohmann_json.hpp"
|
||||
#include "render_3D/Render_Scene_3D.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace adminive {
|
||||
|
||||
template <class Json>
|
||||
struct Value_Adapter<
|
||||
renderive::render_3d::Renderable_Observer_Event, Json> {
|
||||
using value_type = std::string;
|
||||
static std::string read(
|
||||
renderive::render_3d::Renderable_Observer_Event value) {
|
||||
return renderive::web::gallery_enum_id(value);
|
||||
}
|
||||
static void write(
|
||||
renderive::render_3d::Renderable_Observer_Event& target,
|
||||
std::string value) {
|
||||
const auto parsed = renderive::web::gallery_enum_cast<
|
||||
renderive::render_3d::Renderable_Observer_Event>(value);
|
||||
if (!parsed)
|
||||
::renderive::error::unexpected<std::invalid_argument>("unknown 3D observer event");
|
||||
target = *parsed;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Type_Descriptor<renderive::render_3d::Renderable_Observation> {
|
||||
static auto get() {
|
||||
using T = renderive::render_3d::Renderable_Observation;
|
||||
return object<T>(
|
||||
"renderable_3d_observer",
|
||||
ADMINIVE_FIELD_LABEL(T, event, "事件"),
|
||||
ADMINIVE_FIELD_LABEL(T, event_time_ns, "事件时间"),
|
||||
ADMINIVE_FIELD_LABEL(T, cache_update_count, "缓存更新"),
|
||||
ADMINIVE_FIELD_LABEL(T, publish_count, "发布"),
|
||||
ADMINIVE_FIELD_LABEL(T, data_revision, "数据版本"),
|
||||
ADMINIVE_FIELD_LABEL(T, item_count, "项目数"));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace adminive
|
||||
|
||||
namespace renderive::web {
|
||||
template <>
|
||||
struct Observer_Pfr_Adapter<renderive::render_3d::Render_Scene_3D::State> {
|
||||
using Scene = renderive::render_3d::Render_Scene_3D;
|
||||
using Type = adminive::Boost_Pfr_Base_Reflection_Adapter<
|
||||
Scene::State, Scene::State_Fields>;
|
||||
};
|
||||
template <>
|
||||
struct Observer_Pfr_Adapter<renderive::render_3d::Render_Scene_3D::Observer> {
|
||||
using Scene = renderive::render_3d::Render_Scene_3D;
|
||||
using Type = adminive::Boost_Pfr_Base_Reflection_Adapter<
|
||||
Scene::Observer, Scene::Observer_Fields>;
|
||||
};
|
||||
namespace {
|
||||
using namespace renderive::render_3d;
|
||||
Gallery_Render_Result gallery_render_error(Frame_Request_Result error) {
|
||||
switch (error) {
|
||||
case Frame_Request_Result::none:
|
||||
return Gallery_Render_Result::none;
|
||||
case Frame_Request_Result::cancelled:
|
||||
return Gallery_Render_Result::cancelled;
|
||||
case Frame_Request_Result::deadline_exceeded:
|
||||
return Gallery_Render_Result::deadline_exceeded;
|
||||
case Frame_Request_Result::external_failure:
|
||||
return Gallery_Render_Result::backend_failure;
|
||||
case Frame_Request_Result::scene_shutting_down:
|
||||
return Gallery_Render_Result::shutting_down;
|
||||
default:
|
||||
return Gallery_Render_Result::frame_unavailable;
|
||||
}
|
||||
}
|
||||
Gallery_Render_Result gallery_render_error(Scene_Render_Result error) {
|
||||
switch (error) {
|
||||
case Scene_Render_Result::none:
|
||||
return Gallery_Render_Result::none;
|
||||
case Scene_Render_Result::cancelled:
|
||||
return Gallery_Render_Result::cancelled;
|
||||
case Scene_Render_Result::deadline_exceeded:
|
||||
return Gallery_Render_Result::deadline_exceeded;
|
||||
case Scene_Render_Result::external_failure:
|
||||
return Gallery_Render_Result::backend_failure;
|
||||
case Scene_Render_Result::shutting_down:
|
||||
return Gallery_Render_Result::shutting_down;
|
||||
}
|
||||
::renderive::error::unexpected<std::logic_error>("unknown scene render error");
|
||||
}
|
||||
Frame_Request_Result frame_request_error(Scene_Render_Result error) {
|
||||
switch (error) {
|
||||
case Scene_Render_Result::none:
|
||||
return Frame_Request_Result::none;
|
||||
case Scene_Render_Result::cancelled:
|
||||
return Frame_Request_Result::cancelled;
|
||||
case Scene_Render_Result::deadline_exceeded:
|
||||
return Frame_Request_Result::deadline_exceeded;
|
||||
case Scene_Render_Result::external_failure:
|
||||
return Frame_Request_Result::external_failure;
|
||||
case Scene_Render_Result::shutting_down:
|
||||
return Frame_Request_Result::scene_shutting_down;
|
||||
}
|
||||
::renderive::error::unexpected<std::logic_error>("unknown scene render error");
|
||||
}
|
||||
|
||||
struct Gallery_Point {
|
||||
double x{};
|
||||
double y{};
|
||||
};
|
||||
|
||||
Frame_Mode render_frame_mode(Gallery_Frame_Mode mode) {
|
||||
std::shared_ptr<Frame_Control_Strategy_Base> make_strategy(
|
||||
Gallery_Frame_Mode mode) {
|
||||
switch (mode) {
|
||||
case Gallery_Frame_Mode::Manual:
|
||||
return Frame_Mode::Manual;
|
||||
return std::make_shared<Manual_Render_Scene_3D_Strategy>();
|
||||
case Gallery_Frame_Mode::Low_Latency:
|
||||
return Frame_Mode::Low_Latency;
|
||||
return std::make_shared<Low_Latency_Render_Scene_3D_Strategy>();
|
||||
case Gallery_Frame_Mode::Playback:
|
||||
return Frame_Mode::Playback;
|
||||
return std::make_shared<Playback_Render_Scene_3D_Strategy>();
|
||||
}
|
||||
::renderive::error::unexpected<std::invalid_argument>("unknown gallery frame mode");
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string mode_id(Gallery_Frame_Mode mode) {
|
||||
std::string_view mode_id(Gallery_Frame_Mode mode) {
|
||||
switch (mode) {
|
||||
case Gallery_Frame_Mode::Manual:
|
||||
return "manual";
|
||||
case Gallery_Frame_Mode::Low_Latency:
|
||||
return "low_latency";
|
||||
case Gallery_Frame_Mode::Playback:
|
||||
return "playback";
|
||||
case Gallery_Frame_Mode::Manual: return "manual";
|
||||
case Gallery_Frame_Mode::Low_Latency: return "low_latency";
|
||||
case Gallery_Frame_Mode::Playback: return "playback";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
::renderive::Event_Type event_type(Gallery_Pointer_Input_Type type) {
|
||||
switch (type) {
|
||||
case Gallery_Pointer_Input_Type::Move:
|
||||
return ::renderive::Event_Type::Pointer_Move;
|
||||
case Gallery_Pointer_Input_Type::Press:
|
||||
return ::renderive::Event_Type::Pointer_Press;
|
||||
case Gallery_Pointer_Input_Type::Release:
|
||||
return ::renderive::Event_Type::Pointer_Release;
|
||||
}
|
||||
return ::renderive::Event_Type::Pointer_Move;
|
||||
}
|
||||
|
||||
::renderive::Mouse_Button mouse_button(Gallery_Mouse_Button button) {
|
||||
switch (button) {
|
||||
case Gallery_Mouse_Button::Left:
|
||||
return ::renderive::Mouse_Button::Left;
|
||||
case Gallery_Mouse_Button::Right:
|
||||
return ::renderive::Mouse_Button::Right;
|
||||
case Gallery_Mouse_Button::Middle:
|
||||
return ::renderive::Mouse_Button::Middle;
|
||||
case Gallery_Mouse_Button::None:
|
||||
return ::renderive::Mouse_Button::None;
|
||||
}
|
||||
return ::renderive::Mouse_Button::None;
|
||||
}
|
||||
|
||||
::renderive::Key key(Gallery_Key value) {
|
||||
switch (value) {
|
||||
case Gallery_Key::Escape:
|
||||
return ::renderive::Key::Escape;
|
||||
case Gallery_Key::Enter:
|
||||
return ::renderive::Key::Enter;
|
||||
case Gallery_Key::Space:
|
||||
return ::renderive::Key::Space;
|
||||
case Gallery_Key::Delete:
|
||||
return ::renderive::Key::Delete;
|
||||
case Gallery_Key::Backspace:
|
||||
return ::renderive::Key::Backspace;
|
||||
case Gallery_Key::Left:
|
||||
return ::renderive::Key::Left;
|
||||
case Gallery_Key::Right:
|
||||
return ::renderive::Key::Right;
|
||||
case Gallery_Key::Up:
|
||||
return ::renderive::Key::Up;
|
||||
case Gallery_Key::Down:
|
||||
return ::renderive::Key::Down;
|
||||
case Gallery_Key::Unknown:
|
||||
return ::renderive::Key::Unknown;
|
||||
}
|
||||
return ::renderive::Key::Unknown;
|
||||
return "low_latency";
|
||||
}
|
||||
|
||||
Visual_Family visual_family(std::string_view case_id) {
|
||||
@@ -211,501 +67,160 @@ Visual_Family visual_family(std::string_view case_id) {
|
||||
return Visual_Family::Point;
|
||||
}
|
||||
|
||||
std::optional<double> action_number(
|
||||
const Gallery_Action_Request& request) {
|
||||
if (!request.argument ||
|
||||
!std::holds_alternative<double>(*request.argument))
|
||||
return std::nullopt;
|
||||
return std::get<double>(*request.argument);
|
||||
}
|
||||
|
||||
class Gallery_Scene3D final : public Gallery_Scene_Interface {
|
||||
public:
|
||||
Gallery_Scene3D(std::uint64_t session_id, std::string case_id,
|
||||
Gallery_Frame_Mode mode, bool automatic_low_latency)
|
||||
: session_id_(session_id), case_id_(std::move(case_id)),
|
||||
frame_mode_(mode), automatic_low_latency_(automatic_low_latency),
|
||||
visual_(Point_Visual::Builder{}.build(initial_points())),
|
||||
scene_(Scene_Options{.viewport = {560, 320},
|
||||
.frame_mode = render_frame_mode(mode),
|
||||
.maximum_frames_per_second = 30.0,
|
||||
.visual_family = visual_family(case_id_)},
|
||||
visual_) {
|
||||
if (!visual_)
|
||||
::renderive::error::unexpected<std::runtime_error>("failed to create Point_Visual");
|
||||
visual_->set_object_name("3D Visual");
|
||||
render_initial_frame();
|
||||
Gallery_Scene3D(std::string case_id, Gallery_Frame_Mode mode,
|
||||
bool automatic)
|
||||
: case_id_(std::move(case_id)), mode_(mode), automatic_(automatic) {
|
||||
rebuild();
|
||||
}
|
||||
|
||||
[[nodiscard]] const std::string& case_id() const noexcept override {
|
||||
return case_id_;
|
||||
}
|
||||
const std::string& case_id() const noexcept override { return case_id_; }
|
||||
|
||||
[[nodiscard]] nlohmann::json controls() const override {
|
||||
const auto observation = visual_->observation();
|
||||
nlohmann::json controls() const override {
|
||||
return {{"resources", nlohmann::json::array()},
|
||||
{"observers",
|
||||
nlohmann::json::array({
|
||||
{{"descriptor", adminive::to_descriptor_json<
|
||||
nlohmann::json,
|
||||
Renderable_Observation>()},
|
||||
{"data", adminive::to_frontend_json<nlohmann::json>(
|
||||
observation)},
|
||||
{"target", "point-visual"},
|
||||
{"title", "3D Visual"}}})},
|
||||
{"render_plan",
|
||||
gallery_render_plan_json(scene_.render_scene())},
|
||||
{"performance_capture",
|
||||
gallery_performance_capture_json(scene_.render_scene())}};
|
||||
{"observers", nlohmann::json::array()},
|
||||
{"render_plan", gallery_render_plan_json(*scene_)},
|
||||
{"performance_capture", gallery_performance_capture_json(*scene_)}};
|
||||
}
|
||||
|
||||
[[nodiscard]] Gallery_Frame_Mode frame_mode() const noexcept override {
|
||||
return frame_mode_;
|
||||
Gallery_Frame_Mode frame_mode() const noexcept override { return mode_; }
|
||||
void reset_monitoring() override { render_count_ = 0; }
|
||||
bool can_render_automatically() const noexcept override {
|
||||
return automatic_ && mode_ == Gallery_Frame_Mode::Low_Latency;
|
||||
}
|
||||
|
||||
void reset_monitoring() override {
|
||||
render_attempt_count_ = 0;
|
||||
successful_render_count_ = 0;
|
||||
pixel_frame_count_ = 0;
|
||||
last_render_ms_ = 0.0;
|
||||
last_pixel_encode_ms_ = 0.0;
|
||||
render_history_.clear();
|
||||
pixel_history_.clear();
|
||||
last_event_ = "monitoring_reset";
|
||||
std::uint64_t kernel_refresh_interval_ns() const override {
|
||||
return scene_->observer().next_refresh_interval_ns;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool can_render_automatically() const noexcept override {
|
||||
return active_ && automatic_low_latency_ &&
|
||||
frame_mode_ == Gallery_Frame_Mode::Low_Latency;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint64_t kernel_refresh_interval_ns() const override {
|
||||
return scene_.frame_status().next_refresh_interval_ns;
|
||||
}
|
||||
|
||||
void set_client_metrics(Gallery_Client_Performance metrics) noexcept override {
|
||||
client_performance_ = metrics;
|
||||
client_ = metrics;
|
||||
}
|
||||
|
||||
[[nodiscard]] adminive::Update_Result apply_patch(
|
||||
adminive::Update_Result apply_patch(
|
||||
std::string_view, const nlohmann::json&) override {
|
||||
adminive::Update_Result result;
|
||||
result.success = false;
|
||||
result.message = "Render3D gallery has no patchable controls";
|
||||
return result;
|
||||
return {false, "no editable resource matches the requested target"};
|
||||
}
|
||||
|
||||
void resize(int width, int height) override {
|
||||
if (width <= 0 || height <= 0)
|
||||
const Extent requested{static_cast<std::uint32_t>(std::max(1, width)),
|
||||
static_cast<std::uint32_t>(std::max(1, height))};
|
||||
if (requested.width == extent_.width && requested.height == extent_.height)
|
||||
return;
|
||||
const auto error = scene_.resize({static_cast<std::uint32_t>(width),
|
||||
static_cast<std::uint32_t>(height)});
|
||||
if (error != ::renderive::render_3d::Scene_Control_Error::none) {
|
||||
last_event_ = "resize_rejected";
|
||||
return;
|
||||
}
|
||||
last_event_ = "resized";
|
||||
}
|
||||
|
||||
void dispatch(const Gallery_Input_Event& event) override {
|
||||
if (const auto* view = std::get_if<Gallery_View_Input>(&event)) {
|
||||
if (view->type == Gallery_View_Input_Type::Show)
|
||||
active_ = true;
|
||||
else if (view->type == Gallery_View_Input_Type::Hide)
|
||||
active_ = false;
|
||||
return;
|
||||
}
|
||||
if (const auto* pointer = std::get_if<Gallery_Pointer_Input>(&event)) {
|
||||
::renderive::Basic_Pointer_Event<Gallery_Point> input(
|
||||
event_type(pointer->type));
|
||||
input.position = {pointer->x, pointer->y};
|
||||
input.global_position = {pointer->global_x, pointer->global_y};
|
||||
input.button = mouse_button(pointer->button);
|
||||
input.buttons = pointer->buttons;
|
||||
input.modifiers = static_cast<::renderive::Keyboard_Modifier>(
|
||||
pointer->modifiers);
|
||||
if (scene_.dispatch(input) !=
|
||||
::renderive::render_3d::Scene_Control_Error::none)
|
||||
last_event_ = "pointer_rejected";
|
||||
return;
|
||||
}
|
||||
if (const auto* wheel = std::get_if<Gallery_Wheel_Input>(&event)) {
|
||||
::renderive::Basic_Wheel_Event<Gallery_Point> input;
|
||||
input.position = {wheel->x, wheel->y};
|
||||
input.global_position = {wheel->global_x, wheel->global_y};
|
||||
input.pixel_delta_x = wheel->pixel_delta_x;
|
||||
input.pixel_delta_y = wheel->pixel_delta_y;
|
||||
input.angle_delta_x = wheel->angle_delta_x;
|
||||
input.angle_delta_y = wheel->angle_delta_y;
|
||||
input.modifiers = static_cast<::renderive::Keyboard_Modifier>(
|
||||
wheel->modifiers);
|
||||
if (scene_.dispatch(input) !=
|
||||
::renderive::render_3d::Scene_Control_Error::none)
|
||||
last_event_ = "wheel_rejected";
|
||||
return;
|
||||
}
|
||||
const auto& keyboard = std::get<Gallery_Key_Input>(event);
|
||||
::renderive::Key_Event input(
|
||||
keyboard.type == Gallery_Key_Input_Type::Release
|
||||
? ::renderive::Event_Type::Key_Release
|
||||
: ::renderive::Event_Type::Key_Press);
|
||||
input.key = key(keyboard.key);
|
||||
input.native_key = keyboard.native_key;
|
||||
input.modifiers = static_cast<::renderive::Keyboard_Modifier>(
|
||||
keyboard.modifiers);
|
||||
input.auto_repeat = keyboard.auto_repeat;
|
||||
if (scene_.dispatch(input) !=
|
||||
::renderive::render_3d::Scene_Control_Error::none)
|
||||
last_event_ = "key_rejected";
|
||||
extent_ = requested;
|
||||
rebuild();
|
||||
}
|
||||
void dispatch(const Gallery_Input_Event&) override {}
|
||||
|
||||
Gallery_Render_Result render_latest_frame() override {
|
||||
if (!can_render_automatically())
|
||||
return Gallery_Render_Result::inactive;
|
||||
advance_blue_point(0.025F);
|
||||
const auto request_error = render_request();
|
||||
return gallery_render_error(request_error);
|
||||
if (scene_->request_frame() != Frame_Request_Result::none)
|
||||
return Gallery_Render_Result::backend_failure;
|
||||
++render_count_;
|
||||
return Gallery_Render_Result::none;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<std::string> encode_latest_pixels() override {
|
||||
if (!active_)
|
||||
return std::nullopt;
|
||||
const auto frame = scene_.latest_frame();
|
||||
if (!frame || frame->rgba8.empty())
|
||||
return std::nullopt;
|
||||
return encode_rgba8_pixel_frame(
|
||||
std::optional<std::string> encode_latest_pixels() override {
|
||||
const auto frame = scene_->latest_frame();
|
||||
if (!frame || frame->rgba8.empty()) return std::nullopt;
|
||||
auto pixels = encode_rgba8_pixel_frame(
|
||||
frame->rgba8.data(), frame->extent.width, frame->extent.height,
|
||||
frame->extent.width * 4U, frame->sequence);
|
||||
return pixels.empty() ? std::nullopt
|
||||
: std::optional<std::string>(std::move(pixels));
|
||||
}
|
||||
|
||||
void record_pixel_response(
|
||||
std::chrono::steady_clock::time_point,
|
||||
std::chrono::steady_clock::time_point encode_started,
|
||||
std::chrono::steady_clock::time_point encode_finished,
|
||||
std::size_t pixel_bytes) override {
|
||||
++pixel_frame_count_;
|
||||
last_pixel_bytes_ = pixel_bytes;
|
||||
last_pixel_encode_ms_ = std::chrono::duration<double, std::milli>(
|
||||
encode_finished - encode_started)
|
||||
.count();
|
||||
record_sample(pixel_history_, encode_finished);
|
||||
}
|
||||
void record_pixel_response(std::chrono::steady_clock::time_point,
|
||||
std::chrono::steady_clock::time_point,
|
||||
std::chrono::steady_clock::time_point,
|
||||
std::size_t) override {}
|
||||
|
||||
[[nodiscard]] std::string action(const Gallery_Action_Request& request,
|
||||
bool& recognized) override {
|
||||
std::string action(const Gallery_Action_Request& request,
|
||||
bool& recognized) override {
|
||||
recognized = true;
|
||||
if (request.id == "frame_strategy_manual" ||
|
||||
request.id == "frame_strategy_low_latency" ||
|
||||
request.id == "frame_strategy_playback") {
|
||||
const auto next = request.id == "frame_strategy_manual"
|
||||
? Gallery_Frame_Mode::Manual
|
||||
: request.id == "frame_strategy_playback"
|
||||
? Gallery_Frame_Mode::Playback
|
||||
: Gallery_Frame_Mode::Low_Latency;
|
||||
const auto operation = scene_->edit_renderables(
|
||||
[strategy = make_strategy(next)](
|
||||
Scene_3D_Base::Renderable_Editor& editor) mutable {
|
||||
editor.replace_frame_control_strategy(std::move(strategy));
|
||||
});
|
||||
if (operation.wait() != Scene_Edit_Error::none)
|
||||
return "frame strategy replacement rejected";
|
||||
mode_ = next;
|
||||
return "frame strategy replaced";
|
||||
}
|
||||
if (request.id == "mode_cycle" || request.id == "mode_render") {
|
||||
static_cast<void>(render_latest_frame());
|
||||
return "frame rendered";
|
||||
}
|
||||
if (request.id == "capture_next_frame") {
|
||||
auto& scene = scene_.render_scene();
|
||||
if (scene.capture_state().enabled())
|
||||
return "A performance capture session is already active";
|
||||
const auto capture = scene.capture_next_frame();
|
||||
if (!capture) {
|
||||
if (capture.error == Capture_Error::session_active)
|
||||
return "A performance capture session is already active";
|
||||
::renderive::error::unexpected<std::logic_error>("validated capture request rejected");
|
||||
}
|
||||
last_action_result_ =
|
||||
"capture session=" + std::to_string(capture.session_id);
|
||||
return "Next 3D frame capture requested";
|
||||
const auto capture = scene_->capture_next_frame();
|
||||
if (capture) static_cast<void>(render_latest_frame());
|
||||
return capture ? "frame captured" : "capture request rejected";
|
||||
}
|
||||
if (request.id == "capture_frames") {
|
||||
const auto argument = action_number(request);
|
||||
if (!argument) {
|
||||
recognized = false;
|
||||
return {};
|
||||
}
|
||||
const std::size_t count = static_cast<std::size_t>(
|
||||
std::clamp(std::llround(*argument), 1LL, 120LL));
|
||||
auto& scene = scene_.render_scene();
|
||||
if (scene.capture_state().enabled())
|
||||
return "A performance capture session is already active";
|
||||
const auto capture = scene.capture_frames(count);
|
||||
if (!capture) {
|
||||
if (capture.error == Capture_Error::session_active)
|
||||
return "A performance capture session is already active";
|
||||
::renderive::error::unexpected<std::logic_error>("validated capture request rejected");
|
||||
}
|
||||
last_action_result_ = "capture session=" +
|
||||
std::to_string(capture.session_id) +
|
||||
" frames=" + std::to_string(count);
|
||||
return "Consecutive 3D frame capture requested";
|
||||
}
|
||||
if (request.id == "mode_prepare" || request.id == "mode_enqueue") {
|
||||
if (frame_mode_ == Gallery_Frame_Mode::Playback) {
|
||||
blue_offset_ = std::min(0.25F, blue_offset_ + 0.0125F);
|
||||
publish_points();
|
||||
}
|
||||
const bool prepared = scene_.prepare_frame() == Frame_Request_Result::none;
|
||||
last_event_ = prepared ? "frame_prepared" : "prepare_rejected";
|
||||
return prepared ? "3D frame prepared" : "3D frame prepare rejected";
|
||||
}
|
||||
if (request.id == "mode_refresh") {
|
||||
const bool refreshed = scene_.refresh_manual_frame() == Frame_Request_Result::none;
|
||||
last_event_ = refreshed ? "manual_refreshed" : "manual_refresh_failed";
|
||||
return refreshed ? "Manual 3D frame refreshed" : "No manual frame to refresh";
|
||||
}
|
||||
if (request.id == "mode_render" || request.id == "mode_dequeue") {
|
||||
const bool rendered = render_prepared() == Frame_Request_Result::none;
|
||||
last_event_ = rendered ? "frame_rendered" : "render_rejected";
|
||||
return rendered ? "3D frame rendered" : "No 3D frame to render";
|
||||
}
|
||||
if (request.id == "move_mesh") {
|
||||
advance_blue_point(0.08F);
|
||||
last_event_ = "mesh_moved";
|
||||
return "blue point moved";
|
||||
}
|
||||
if (request.id == "add_mesh") {
|
||||
extra_attached_ = true;
|
||||
publish_points();
|
||||
last_event_ = "mesh_added";
|
||||
return "yellow point added";
|
||||
}
|
||||
if (request.id == "remove_mesh") {
|
||||
extra_attached_ = false;
|
||||
publish_points();
|
||||
last_event_ = "mesh_removed";
|
||||
return "yellow point removed";
|
||||
}
|
||||
if (request.id == "mesh_churn") {
|
||||
last_event_ = "mesh_churned";
|
||||
return "point attach/detach lifecycle completed";
|
||||
}
|
||||
if (request.id == "reset" || request.id == "reset_camera") {
|
||||
blue_offset_ = 0.0F;
|
||||
projection_scale_ = 1.0F;
|
||||
visual_->set<&Point_State::transform>(transform());
|
||||
publish_points();
|
||||
last_event_ = "camera_reset";
|
||||
return "3D view reset";
|
||||
}
|
||||
if (request.id == "toggle_projection") {
|
||||
projection_scale_ = projection_scale_ == 1.0F ? 0.72F : 1.0F;
|
||||
visual_->set<&Point_State::transform>(transform());
|
||||
last_event_ = "projection_toggled";
|
||||
return "3D projection toggled";
|
||||
}
|
||||
if (request.id == "toggle_texture") {
|
||||
texture_enabled_ = !texture_enabled_;
|
||||
publish_points();
|
||||
last_event_ = "texture_toggled";
|
||||
return texture_enabled_ ? "point pattern enabled" : "point pattern disabled";
|
||||
}
|
||||
if (request.id == "change_text") {
|
||||
alternate_text_ = !alternate_text_;
|
||||
publish_points();
|
||||
last_event_ = "text_changed";
|
||||
return "point annotation representation changed";
|
||||
std::size_t count = 20;
|
||||
if (request.argument && std::holds_alternative<double>(*request.argument))
|
||||
count = static_cast<std::size_t>(std::clamp(
|
||||
std::get<double>(*request.argument), 1.0, 1000.0));
|
||||
const auto capture = scene_->capture_frames(count);
|
||||
if (capture) static_cast<void>(render_latest_frame());
|
||||
return capture ? "frame capture started" : "capture request rejected";
|
||||
}
|
||||
recognized = false;
|
||||
return {};
|
||||
}
|
||||
|
||||
void record_action_notice_if_empty(std::string_view notice) override {
|
||||
if (last_action_result_.empty())
|
||||
last_action_result_ = std::string(notice);
|
||||
}
|
||||
void record_action_notice_if_empty(std::string_view) override {}
|
||||
|
||||
[[nodiscard]] std::string telemetry_json() const override {
|
||||
const auto status = scene_.frame_status();
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto scene_execution = scene_.render_scene().execution_statistics();
|
||||
const auto runtime = scene_.runtime_statistics();
|
||||
std::string telemetry_json() const override {
|
||||
const auto observer = scene_->observer();
|
||||
auto kernel_observer = observer_json(observer);
|
||||
kernel_observer["mode"] = mode_id(mode_);
|
||||
kernel_observer["event"] = "render_completed";
|
||||
kernel_observer["limit"] = "none";
|
||||
return nlohmann::json{
|
||||
{"kernel_observer",
|
||||
{{"mode", mode_id(frame_mode_)},
|
||||
{"last_event", last_event_},
|
||||
{"pending_frame_count", status.pending_frame_count},
|
||||
{"dropped_frame_count", status.dropped_frame_count}}},
|
||||
{"performance",
|
||||
{{"render_attempt_count", render_attempt_count_},
|
||||
{"successful_render_count", successful_render_count_},
|
||||
{"failed_render_count",
|
||||
render_attempt_count_ - successful_render_count_},
|
||||
{"measured_fps", recent_rate(render_history_, now)},
|
||||
{"last_render_ms", last_render_ms_},
|
||||
{"pixel_response_fps", recent_rate(pixel_history_, now)},
|
||||
{"last_pixel_encode_ms", last_pixel_encode_ms_},
|
||||
{"last_pixel_bytes", last_pixel_bytes_},
|
||||
{"automatic_low_latency_scheduler", can_render_automatically()}}},
|
||||
{"client_performance",
|
||||
{{"transport_fps", client_performance_.transport_fps},
|
||||
{"presentation_fps", client_performance_.presentation_fps},
|
||||
{"websocket_buffered_bytes",
|
||||
client_performance_.websocket_buffered_bytes}}},
|
||||
{"scheduler", adminive::to_frontend_json<nlohmann::json>(
|
||||
renderive::scheduling::scheduler_statistics())},
|
||||
{"queue_pressure",
|
||||
{{"scene_execution",
|
||||
{{"capacity", scene_execution.capacity},
|
||||
{"queued", scene_execution.queued},
|
||||
{"peak_queued", scene_execution.peak_queued},
|
||||
{"backpressure_count", scene_execution.backpressure_count},
|
||||
{"backpressure_wait_ns", scene_execution.backpressure_wait_ns}}},
|
||||
{"render_domain",
|
||||
{{"capacity", runtime.render_domain.capacity},
|
||||
{"active", runtime.render_domain.active},
|
||||
{"peak_active", runtime.render_domain.peak_active},
|
||||
{"queued", runtime.render_domain.queued},
|
||||
{"peak_queued", runtime.render_domain.peak_queued},
|
||||
{"backpressure_count", runtime.render_domain.backpressure_count},
|
||||
{"backpressure_wait_ns", runtime.render_domain.backpressure_wait_ns}}},
|
||||
{"gpu_completion",
|
||||
{{"capacity", runtime.gpu_completion.capacity},
|
||||
{"active", runtime.gpu_completion.active},
|
||||
{"peak_active", runtime.gpu_completion.peak_active},
|
||||
{"queued", runtime.gpu_completion.queued},
|
||||
{"peak_queued", runtime.gpu_completion.peak_queued},
|
||||
{"backpressure_count", runtime.gpu_completion.backpressure_count},
|
||||
{"backpressure_wait_ns", runtime.gpu_completion.backpressure_wait_ns},
|
||||
{"fault_count", runtime.gpu_completion_fault_count},
|
||||
{"abandoned_count", runtime.gpu_completion_abandoned_count}}}}},
|
||||
{"session_id", session_id_},
|
||||
{"performance_capture",
|
||||
gallery_performance_capture_json(scene_.render_scene())},
|
||||
{"last_action", last_action_result_}}
|
||||
{"kernel_observer", std::move(kernel_observer)},
|
||||
{"performance", {{"successful_render_count", render_count_}}},
|
||||
{"client_performance", {{"transport_fps", client_.transport_fps}}},
|
||||
{"performance_capture", gallery_performance_capture_json(*scene_)}}
|
||||
.dump();
|
||||
}
|
||||
|
||||
private:
|
||||
using Performance_Clock = std::chrono::steady_clock;
|
||||
|
||||
static void record_sample(std::deque<Performance_Clock::time_point>& samples,
|
||||
Performance_Clock::time_point now) {
|
||||
samples.push_back(now);
|
||||
const auto cutoff = now - std::chrono::seconds(2);
|
||||
while (!samples.empty() && samples.front() < cutoff)
|
||||
samples.pop_front();
|
||||
void rebuild() {
|
||||
std::vector<Point> points{
|
||||
{{-0.55F, -0.25F, 0.0F}, {255, 80, 80, 255}, 24.0F},
|
||||
{{0.0F, 0.45F, 0.0F}, {80, 255, 120, 255}, 28.0F},
|
||||
{{0.55F, -0.1F, 0.0F}, {80, 140, 255, 255}, 26.0F}};
|
||||
visual_ = Point_Visual::Builder{}.build(std::move(points));
|
||||
visual_->set_object_name(case_id_ + " Points");
|
||||
Render_Scene_3D::State state;
|
||||
state.viewport = extent_;
|
||||
state.visual_family = visual_family(case_id_);
|
||||
scene_ = Render_Scene_3D::Builder{state}.build(
|
||||
visual_, make_strategy(mode_));
|
||||
static_cast<void>(scene_->request_frame());
|
||||
}
|
||||
|
||||
static double recent_rate(
|
||||
const std::deque<Performance_Clock::time_point>& samples,
|
||||
Performance_Clock::time_point now) {
|
||||
const auto cutoff = now - std::chrono::seconds(1);
|
||||
const auto first = std::find_if(samples.begin(), samples.end(),
|
||||
[cutoff](auto sample) {
|
||||
return sample >= cutoff;
|
||||
});
|
||||
const auto count = std::distance(first, samples.end());
|
||||
if (count < 2)
|
||||
return 0.0;
|
||||
const double seconds = std::chrono::duration<double>(samples.back() - *first).count();
|
||||
return seconds > 0.0 ? static_cast<double>(count - 1) / seconds : 0.0;
|
||||
}
|
||||
|
||||
static std::vector<Point> initial_points() {
|
||||
return {{{-0.62F, -0.10F, 0.0F}, {255, 32, 24, 255}, 74.0F},
|
||||
{{0.0F, -0.10F, 0.0F}, {24, 255, 36, 255}, 74.0F},
|
||||
{{0.62F, -0.10F, 0.0F}, {28, 52, 255, 255}, 74.0F}};
|
||||
}
|
||||
|
||||
Matrix4 transform() const {
|
||||
Matrix4 value;
|
||||
value.values[0] = projection_scale_;
|
||||
value.values[5] = projection_scale_;
|
||||
return value;
|
||||
}
|
||||
|
||||
void publish_points() {
|
||||
auto points = initial_points();
|
||||
points[2].position.x += blue_offset_;
|
||||
if (!texture_enabled_)
|
||||
points[0].color = {255, 128, 128, 255};
|
||||
if (alternate_text_) {
|
||||
points[1].diameter_px = 46.0F;
|
||||
points[1].color = {24, 220, 255, 255};
|
||||
}
|
||||
if (extra_attached_)
|
||||
points.push_back({{0.0F, 0.62F, 0.0F}, {255, 230, 20, 255}, 58.0F});
|
||||
if (visual_->update_points(std::move(points)) !=
|
||||
Visual_Data_Error::none)
|
||||
::renderive::error::unexpected<std::logic_error>("generated gallery point data is invalid");
|
||||
}
|
||||
|
||||
void advance_blue_point(float step) {
|
||||
blue_offset_ += step;
|
||||
if (blue_offset_ > 0.32F)
|
||||
blue_offset_ = -0.32F;
|
||||
publish_points();
|
||||
}
|
||||
|
||||
void render_initial_frame() {
|
||||
Frame_Request_Result error{Frame_Request_Result::none};
|
||||
if (frame_mode_ == Gallery_Frame_Mode::Manual) {
|
||||
if (scene_.prepare_frame() == Frame_Request_Result::none &&
|
||||
scene_.refresh_manual_frame() == Frame_Request_Result::none)
|
||||
error = render_prepared();
|
||||
else
|
||||
error = Frame_Request_Result::no_pending_frame;
|
||||
} else {
|
||||
error = render_request();
|
||||
}
|
||||
if (error != Frame_Request_Result::none)
|
||||
last_action_result_ = "initial frame is not available";
|
||||
}
|
||||
|
||||
Frame_Request_Result render_request() {
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
++render_attempt_count_;
|
||||
auto error = scene_.request_frame();
|
||||
if (error == Frame_Request_Result::none)
|
||||
error = frame_request_error(scene_.render_scene().wait_for_render());
|
||||
const bool rendered = error == Frame_Request_Result::none;
|
||||
last_render_ms_ = std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - started)
|
||||
.count();
|
||||
successful_render_count_ += rendered ? 1U : 0U;
|
||||
if (rendered)
|
||||
record_sample(render_history_, std::chrono::steady_clock::now());
|
||||
return error;
|
||||
}
|
||||
|
||||
Frame_Request_Result render_prepared() {
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
++render_attempt_count_;
|
||||
auto error = scene_.render_prepared_frame();
|
||||
if (error == Frame_Request_Result::none)
|
||||
error = frame_request_error(scene_.render_scene().wait_for_render());
|
||||
const bool rendered = error == Frame_Request_Result::none;
|
||||
last_render_ms_ = std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - started)
|
||||
.count();
|
||||
successful_render_count_ += rendered ? 1U : 0U;
|
||||
if (rendered)
|
||||
record_sample(render_history_, std::chrono::steady_clock::now());
|
||||
return error;
|
||||
}
|
||||
|
||||
std::uint64_t session_id_{};
|
||||
std::string case_id_;
|
||||
Gallery_Frame_Mode frame_mode_{Gallery_Frame_Mode::Low_Latency};
|
||||
bool automatic_low_latency_{};
|
||||
bool active_{true};
|
||||
Gallery_Frame_Mode mode_;
|
||||
bool automatic_{};
|
||||
Extent extent_{560, 320};
|
||||
std::shared_ptr<Point_Visual> visual_;
|
||||
Render_Scene_3D scene_;
|
||||
Gallery_Client_Performance client_performance_;
|
||||
float blue_offset_{};
|
||||
float projection_scale_{1.0F};
|
||||
bool extra_attached_{};
|
||||
bool texture_enabled_{true};
|
||||
bool alternate_text_{};
|
||||
std::uint64_t render_attempt_count_{};
|
||||
std::uint64_t successful_render_count_{};
|
||||
std::uint64_t pixel_frame_count_{};
|
||||
double last_render_ms_{};
|
||||
double last_pixel_encode_ms_{};
|
||||
std::size_t last_pixel_bytes_{};
|
||||
std::deque<Performance_Clock::time_point> render_history_;
|
||||
std::deque<Performance_Clock::time_point> pixel_history_;
|
||||
std::string last_event_{"initialized"};
|
||||
std::string last_action_result_;
|
||||
std::unique_ptr<Render_Scene_3D> scene_;
|
||||
Gallery_Client_Performance client_;
|
||||
std::uint64_t render_count_{};
|
||||
};
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<Gallery_Scene_Interface> make_gallery_scene_3d(
|
||||
std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode,
|
||||
std::uint64_t, std::string case_id, Gallery_Frame_Mode mode,
|
||||
bool automatic_low_latency) {
|
||||
return std::make_unique<Gallery_Scene3D>(
|
||||
session_id, std::move(case_id), mode, automatic_low_latency);
|
||||
std::move(case_id), mode, automatic_low_latency);
|
||||
}
|
||||
|
||||
} // namespace renderive::web
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
#include "../app/Gallery_Plot_Session.h"
|
||||
#include "../app/Gallery_Protocol.h"
|
||||
#include "../app/common/Pixel_Frame.h"
|
||||
#include "../app/render_3D/Gallery_Scene3D.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace renderive::web {
|
||||
namespace {
|
||||
|
||||
std::uint32_t read_u32_le(const std::string& value, std::size_t offset) {
|
||||
return static_cast<std::uint8_t>(value[offset]) |
|
||||
(static_cast<std::uint32_t>(static_cast<std::uint8_t>(value[offset + 1])) << 8U) |
|
||||
@@ -28,130 +20,6 @@ std::uint32_t read_u32_le(const std::string& value, std::size_t offset) {
|
||||
(static_cast<std::uint32_t>(static_cast<std::uint8_t>(value[offset + 3])) << 24U);
|
||||
}
|
||||
|
||||
Gallery_Request gallery_request(Gallery_Request_Kind kind, std::string message) {
|
||||
return {kind, std::move(message)};
|
||||
}
|
||||
|
||||
std::string open_message(std::string_view mode) {
|
||||
return std::string(
|
||||
R"({"category":"event","type":"gallery_open","case":"datoviz_3d","frame_mode":")") +
|
||||
std::string(mode) + R"("})";
|
||||
}
|
||||
|
||||
nlohmann::json response_json(std::optional<Web_Response> response) {
|
||||
if (!response) {
|
||||
ADD_FAILURE() << "expected a Web response";
|
||||
return nlohmann::json::object();
|
||||
}
|
||||
EXPECT_EQ(response->type, Web_Response_Type::Json);
|
||||
return nlohmann::json::parse(response->payload);
|
||||
}
|
||||
|
||||
nlohmann::json open_scene(Gallery_Plot_Session& session, std::string_view mode) {
|
||||
return response_json(session.handle(gallery_request(
|
||||
Gallery_Request_Kind::Open, open_message(mode))));
|
||||
}
|
||||
|
||||
nlohmann::json invoke_action(Gallery_Plot_Session& session,
|
||||
std::string_view action_id) {
|
||||
const nlohmann::json request{{"category", "event"},
|
||||
{"type", "gallery_action"},
|
||||
{"action", action_id}};
|
||||
return response_json(session.handle(gallery_request(
|
||||
Gallery_Request_Kind::Action, request.dump())));
|
||||
}
|
||||
|
||||
std::string request_pixels(Gallery_Plot_Session& session) {
|
||||
const auto response = session.handle(Frame_Request{});
|
||||
if (!response) {
|
||||
ADD_FAILURE() << "expected a pixel response";
|
||||
return {};
|
||||
}
|
||||
EXPECT_EQ(response->type, Web_Response_Type::Pixels);
|
||||
return response->payload;
|
||||
}
|
||||
|
||||
void expect_actual_datoviz_pixels(const std::string& frame,
|
||||
std::uint32_t expected_width,
|
||||
std::uint32_t expected_height) {
|
||||
ASSERT_GE(frame.size(), pixel_frame_header_size);
|
||||
ASSERT_EQ(frame.substr(0, 4), "RVP2");
|
||||
ASSERT_EQ(read_u32_le(frame, 4), expected_width);
|
||||
ASSERT_EQ(read_u32_le(frame, 8), expected_height);
|
||||
ASSERT_EQ(read_u32_le(frame, 12), expected_width * 4U);
|
||||
ASSERT_EQ(frame.size(), pixel_frame_header_size +
|
||||
static_cast<std::size_t>(expected_width) *
|
||||
expected_height * 4U);
|
||||
|
||||
const auto* pixels = reinterpret_cast<const std::uint8_t*>(
|
||||
frame.data() + pixel_frame_header_size);
|
||||
const std::array<std::uint8_t, 4> background{
|
||||
pixels[0], pixels[1], pixels[2], pixels[3]};
|
||||
std::size_t non_background{};
|
||||
std::size_t red_dominant{};
|
||||
std::size_t green_dominant{};
|
||||
std::size_t blue_dominant{};
|
||||
const std::size_t pixel_count =
|
||||
static_cast<std::size_t>(expected_width) * expected_height;
|
||||
for (std::size_t index = 0; index < pixel_count; ++index) {
|
||||
const auto* pixel = pixels + index * 4U;
|
||||
non_background += pixel[0] != background[0] ||
|
||||
pixel[1] != background[1] ||
|
||||
pixel[2] != background[2] ||
|
||||
pixel[3] != background[3];
|
||||
red_dominant += pixel[0] > static_cast<unsigned>(pixel[1]) + 20U &&
|
||||
pixel[0] > static_cast<unsigned>(pixel[2]) + 20U;
|
||||
green_dominant += pixel[1] > static_cast<unsigned>(pixel[0]) + 20U &&
|
||||
pixel[1] > static_cast<unsigned>(pixel[2]) + 20U;
|
||||
blue_dominant += pixel[2] > static_cast<unsigned>(pixel[0]) + 20U &&
|
||||
pixel[2] > static_cast<unsigned>(pixel[1]) + 20U;
|
||||
}
|
||||
EXPECT_GT(non_background, 0U);
|
||||
EXPECT_GT(red_dominant, 32U);
|
||||
EXPECT_GT(green_dominant, 32U);
|
||||
EXPECT_GT(blue_dominant, 32U);
|
||||
}
|
||||
|
||||
std::optional<double> blue_centroid_x(const std::string& frame) {
|
||||
if (frame.size() < pixel_frame_header_size)
|
||||
return std::nullopt;
|
||||
const std::uint32_t width = read_u32_le(frame, 4);
|
||||
const std::uint32_t height = read_u32_le(frame, 8);
|
||||
if (frame.size() != pixel_frame_header_size +
|
||||
static_cast<std::size_t>(width) * height * 4U)
|
||||
return std::nullopt;
|
||||
const auto* pixels = reinterpret_cast<const std::uint8_t*>(
|
||||
frame.data() + pixel_frame_header_size);
|
||||
double x_sum{};
|
||||
std::size_t count{};
|
||||
for (std::uint32_t y = 0; y < height; ++y) {
|
||||
for (std::uint32_t x = 0; x < width; ++x) {
|
||||
const auto* pixel = pixels +
|
||||
(static_cast<std::size_t>(y) * width + x) * 4U;
|
||||
if (pixel[2] > static_cast<unsigned>(pixel[0]) + 20U &&
|
||||
pixel[2] > static_cast<unsigned>(pixel[1]) + 20U) {
|
||||
x_sum += x;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count == 0 ? std::nullopt
|
||||
: std::optional<double>(x_sum / static_cast<double>(count));
|
||||
}
|
||||
|
||||
void manual_render_cycle(Gallery_Plot_Session& session) {
|
||||
ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state");
|
||||
ASSERT_EQ(invoke_action(session, "mode_refresh").at("type"), "case_state");
|
||||
ASSERT_EQ(invoke_action(session, "mode_render").at("type"), "case_state");
|
||||
}
|
||||
|
||||
std::set<std::string> action_ids(const nlohmann::json& state) {
|
||||
std::set<std::string> result;
|
||||
for (const auto& action : state.at("actions").at("data"))
|
||||
result.insert(action.at("id").get<std::string>());
|
||||
return result;
|
||||
}
|
||||
|
||||
TEST(RenderiveWebDatovizGallery, RgbaEncoderPreservesRowsAndRemovesPadding) {
|
||||
std::array<std::uint8_t, 24> source{};
|
||||
const std::array<std::uint8_t, 16> expected{
|
||||
@@ -159,7 +27,6 @@ TEST(RenderiveWebDatovizGallery, RgbaEncoderPreservesRowsAndRemovesPadding) {
|
||||
5, 6, 253, 255, 252, 7, 8, 255};
|
||||
std::copy_n(expected.begin(), 8, source.begin());
|
||||
std::copy_n(expected.begin() + 8, 8, source.begin() + 12);
|
||||
source[8] = source[9] = source[10] = source[11] = 99;
|
||||
|
||||
const std::string frame = encode_rgba8_pixel_frame(
|
||||
reinterpret_cast<const std::byte*>(source.data()), 2, 2, 12, 42);
|
||||
@@ -168,233 +35,33 @@ TEST(RenderiveWebDatovizGallery, RgbaEncoderPreservesRowsAndRemovesPadding) {
|
||||
EXPECT_EQ(read_u32_le(frame, 4), 2U);
|
||||
EXPECT_EQ(read_u32_le(frame, 8), 2U);
|
||||
EXPECT_EQ(read_u32_le(frame, 12), 8U);
|
||||
EXPECT_TRUE(std::equal(
|
||||
expected.begin(), expected.end(),
|
||||
reinterpret_cast<const std::uint8_t*>(
|
||||
frame.data() + pixel_frame_header_size)));
|
||||
EXPECT_TRUE(std::equal(expected.begin(), expected.end(),
|
||||
reinterpret_cast<const std::uint8_t*>(
|
||||
frame.data() + pixel_frame_header_size)));
|
||||
EXPECT_TRUE(encode_rgba8_pixel_frame(
|
||||
reinterpret_cast<const std::byte*>(source.data()), 2, 2, 7, 42)
|
||||
.empty());
|
||||
}
|
||||
|
||||
TEST(RenderiveWebDatovizGallery, CatalogUsesOnlyThreeDimensionalActions) {
|
||||
const auto catalog = nlohmann::json::parse(Gallery_Protocol::catalog_json());
|
||||
const std::set<std::string> visual_cases{
|
||||
"datoviz_3d", "datoviz_splat", "datoviz_pixel", "datoviz_marker",
|
||||
"datoviz_sphere", "datoviz_segment", "datoviz_vector",
|
||||
"datoviz_primitive", "datoviz_mesh", "datoviz_path", "datoviz_image",
|
||||
"datoviz_labels", "datoviz_glyph", "datoviz_text", "datoviz_volume"};
|
||||
for (const auto& id : visual_cases) {
|
||||
EXPECT_NE(std::find_if(
|
||||
catalog.at("cases").begin(), catalog.at("cases").end(),
|
||||
[&id](const nlohmann::json& value) {
|
||||
return value.at("id") == id;
|
||||
}),
|
||||
catalog.at("cases").end())
|
||||
<< id;
|
||||
TEST(RenderiveWebDatovizGallery, CurrentThreeDimensionalSceneRendersRgba) {
|
||||
try {
|
||||
auto scene = make_gallery_scene_3d(
|
||||
1, "datoviz_3d", Gallery_Frame_Mode::Low_Latency, false);
|
||||
scene->resize(320, 200);
|
||||
ASSERT_EQ(scene->render_latest_frame(), Gallery_Render_Result::none);
|
||||
const auto frame = scene->encode_latest_pixels();
|
||||
ASSERT_TRUE(frame);
|
||||
ASSERT_GE(frame->size(), pixel_frame_header_size);
|
||||
EXPECT_EQ(frame->substr(0, 4), "RVP2");
|
||||
EXPECT_EQ(read_u32_le(*frame, 4), 320U);
|
||||
EXPECT_EQ(read_u32_le(*frame, 8), 200U);
|
||||
|
||||
const auto telemetry = nlohmann::json::parse(scene->telemetry_json());
|
||||
EXPECT_EQ(telemetry.at("kernel_observer").at("point_count"), 3U);
|
||||
EXPECT_EQ(telemetry.at("performance").at("successful_render_count"), 1U);
|
||||
} catch (const std::exception& error) {
|
||||
GTEST_SKIP() << "Datoviz backend unavailable: " << error.what();
|
||||
}
|
||||
const auto item = std::find_if(
|
||||
catalog.at("cases").begin(), catalog.at("cases").end(),
|
||||
[](const nlohmann::json& value) { return value.at("id") == "datoviz_3d"; });
|
||||
ASSERT_NE(item, catalog.at("cases").end());
|
||||
EXPECT_EQ(item->at("category"), "Datoviz Visuals");
|
||||
EXPECT_EQ(item->at("control_count"), 0);
|
||||
EXPECT_EQ(item->at("control_count_by_mode").at("manual"), 0);
|
||||
EXPECT_EQ(item->at("control_count_by_mode").at("low_latency"), 0);
|
||||
EXPECT_EQ(item->at("control_count_by_mode").at("playback"), 0);
|
||||
|
||||
const std::string empty_controls =
|
||||
R"({"resources":[],"observers":[],"render_plan":null,"performance_capture":null})";
|
||||
const auto state_for = [&](Gallery_Frame_Mode mode) {
|
||||
return nlohmann::json::parse(Gallery_Protocol::case_json_from_controls(
|
||||
"datoviz_3d", empty_controls, "{}", {}, mode, false));
|
||||
};
|
||||
const std::set<std::string> common{
|
||||
"add_mesh", "change_text", "move_mesh", "remove_mesh", "reset",
|
||||
"reset_camera", "mesh_churn", "toggle_projection", "toggle_texture"};
|
||||
auto expected = common;
|
||||
expected.insert({"mode_prepare", "mode_refresh", "mode_render"});
|
||||
EXPECT_EQ(action_ids(state_for(Gallery_Frame_Mode::Manual)), expected);
|
||||
expected = common;
|
||||
expected.insert({"mode_prepare", "mode_render"});
|
||||
EXPECT_EQ(action_ids(state_for(Gallery_Frame_Mode::Low_Latency)), expected);
|
||||
expected = common;
|
||||
expected.insert({"mode_enqueue", "mode_dequeue"});
|
||||
EXPECT_EQ(action_ids(state_for(Gallery_Frame_Mode::Playback)), expected);
|
||||
EXPECT_FALSE(action_ids(state_for(Gallery_Frame_Mode::Manual)).contains("mode_discard"));
|
||||
EXPECT_FALSE(action_ids(state_for(Gallery_Frame_Mode::Low_Latency)).contains("mode_discard"));
|
||||
}
|
||||
|
||||
TEST(RenderiveWebDatovizGallery, PublishesRealRgbaAndRendersAfterResize) {
|
||||
Gallery_Plot_Session session;
|
||||
ASSERT_EQ(open_scene(session, "low_latency").at("type"), "case_state");
|
||||
const std::string initial = request_pixels(session);
|
||||
expect_actual_datoviz_pixels(initial, 560, 320);
|
||||
|
||||
EXPECT_FALSE(session.handle(Viewport_Resize{{900, 500}}).has_value());
|
||||
ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state");
|
||||
ASSERT_EQ(invoke_action(session, "mode_render").at("type"), "case_state");
|
||||
const std::string resized = request_pixels(session);
|
||||
expect_actual_datoviz_pixels(resized, 900, 500);
|
||||
|
||||
const auto observed = response_json(session.handle(gallery_request(
|
||||
Gallery_Request_Kind::Observe,
|
||||
R"({"category":"event","type":"gallery_observe","client_metrics":{}})")));
|
||||
EXPECT_TRUE(observed.at("telemetry").contains("scheduler"));
|
||||
const auto& performance = observed.at("telemetry").at("performance");
|
||||
EXPECT_GT(performance.at("measured_fps").get<double>(), 0.0);
|
||||
EXPECT_GT(performance.at("pixel_response_fps").get<double>(), 0.0);
|
||||
EXPECT_GT(performance.at("last_render_ms").get<double>(), 0.0);
|
||||
EXPECT_GT(performance.at("last_pixel_encode_ms").get<double>(), 0.0);
|
||||
}
|
||||
|
||||
TEST(RenderiveWebDatovizGallery, AutomaticLowLatencyFramesCarryAnimatedState) {
|
||||
Gallery_Plot_Session session(true);
|
||||
ASSERT_EQ(open_scene(session, "low_latency").at("type"), "case_state");
|
||||
const std::string first = request_pixels(session);
|
||||
expect_actual_datoviz_pixels(first, 560, 320);
|
||||
|
||||
std::string later;
|
||||
const auto deadline = std::chrono::steady_clock::now() +
|
||||
std::chrono::seconds(5);
|
||||
do {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(40));
|
||||
later = request_pixels(session);
|
||||
} while (later == first && std::chrono::steady_clock::now() < deadline);
|
||||
ASSERT_NE(later, first);
|
||||
expect_actual_datoviz_pixels(later, 560, 320);
|
||||
}
|
||||
|
||||
TEST(RenderiveWebDatovizGallery, ManualRenderUsesThePreparedLogicalState) {
|
||||
Gallery_Plot_Session session;
|
||||
ASSERT_EQ(open_scene(session, "manual").at("type"), "case_state");
|
||||
ASSERT_EQ(invoke_action(session, "move_mesh").at("type"), "case_state");
|
||||
ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state");
|
||||
ASSERT_EQ(invoke_action(session, "move_mesh").at("type"), "case_state");
|
||||
ASSERT_EQ(invoke_action(session, "mode_refresh").at("type"), "case_state");
|
||||
ASSERT_EQ(invoke_action(session, "mode_render").at("type"), "case_state");
|
||||
const std::string prepared = request_pixels(session);
|
||||
expect_actual_datoviz_pixels(prepared, 560, 320);
|
||||
|
||||
manual_render_cycle(session);
|
||||
const std::string current = request_pixels(session);
|
||||
expect_actual_datoviz_pixels(current, 560, 320);
|
||||
EXPECT_NE(current, prepared);
|
||||
}
|
||||
|
||||
TEST(RenderiveWebDatovizGallery, PlaybackDequeuesHistoricalStatesInOrder) {
|
||||
Gallery_Plot_Session session;
|
||||
ASSERT_EQ(open_scene(session, "playback").at("type"), "case_state");
|
||||
for (int index = 0; index < 20; ++index)
|
||||
ASSERT_EQ(invoke_action(session, "mode_enqueue").at("type"), "case_state");
|
||||
|
||||
ASSERT_EQ(invoke_action(session, "mode_dequeue").at("type"), "case_state");
|
||||
const std::string first = request_pixels(session);
|
||||
const auto first_x = blue_centroid_x(first);
|
||||
ASSERT_TRUE(first_x.has_value());
|
||||
|
||||
ASSERT_EQ(invoke_action(session, "mode_dequeue").at("type"), "case_state");
|
||||
const std::string second = request_pixels(session);
|
||||
const auto second_x = blue_centroid_x(second);
|
||||
ASSERT_TRUE(second_x.has_value());
|
||||
EXPECT_GT(*second_x, *first_x);
|
||||
|
||||
std::string last = second;
|
||||
for (int index = 2; index < 20; ++index) {
|
||||
ASSERT_EQ(invoke_action(session, "mode_dequeue").at("type"), "case_state");
|
||||
last = request_pixels(session);
|
||||
}
|
||||
const auto last_x = blue_centroid_x(last);
|
||||
ASSERT_TRUE(last_x.has_value());
|
||||
EXPECT_GT(*last_x, *second_x);
|
||||
EXPECT_NE(last, second);
|
||||
}
|
||||
|
||||
TEST(RenderiveWebDatovizGallery, AddedAndChurnedVisualsAreActuallyRemoved) {
|
||||
Gallery_Plot_Session session;
|
||||
ASSERT_EQ(open_scene(session, "manual").at("type"), "case_state");
|
||||
const std::string baseline = request_pixels(session);
|
||||
|
||||
ASSERT_EQ(invoke_action(session, "add_mesh").at("type"), "case_state");
|
||||
manual_render_cycle(session);
|
||||
const std::string added = request_pixels(session);
|
||||
EXPECT_NE(added, baseline);
|
||||
const auto centroid_y = [&](const auto& matches) -> std::optional<double> {
|
||||
const auto* pixels = reinterpret_cast<const std::uint8_t*>(
|
||||
added.data() + pixel_frame_header_size);
|
||||
double y_sum{};
|
||||
std::size_t count{};
|
||||
for (std::uint32_t y = 0; y < 320; ++y) {
|
||||
for (std::uint32_t x = 0; x < 560; ++x) {
|
||||
const auto* pixel = pixels +
|
||||
(static_cast<std::size_t>(y) * 560U + x) * 4U;
|
||||
if (matches(pixel[0], pixel[1], pixel[2])) {
|
||||
y_sum += y;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count == 0
|
||||
? std::nullopt
|
||||
: std::optional<double>(y_sum / static_cast<double>(count));
|
||||
};
|
||||
const auto green_y = centroid_y([](unsigned red, unsigned green, unsigned blue) {
|
||||
return green > red + 20U && green > blue + 20U;
|
||||
});
|
||||
const auto yellow_y = centroid_y([](unsigned red, unsigned green, unsigned blue) {
|
||||
return red > 100U && green > 100U && blue < 100U;
|
||||
});
|
||||
ASSERT_TRUE(green_y.has_value());
|
||||
ASSERT_TRUE(yellow_y.has_value());
|
||||
EXPECT_LT(*yellow_y, *green_y)
|
||||
<< "Frame_3D_View must expose top-left-origin RGBA rows";
|
||||
|
||||
ASSERT_EQ(invoke_action(session, "remove_mesh").at("type"), "case_state");
|
||||
manual_render_cycle(session);
|
||||
const std::string removed = request_pixels(session);
|
||||
ASSERT_GE(removed.size(), pixel_frame_header_size);
|
||||
ASSERT_GE(baseline.size(), pixel_frame_header_size);
|
||||
EXPECT_EQ(removed.substr(pixel_frame_header_size),
|
||||
baseline.substr(pixel_frame_header_size));
|
||||
|
||||
const auto churned = invoke_action(session, "mesh_churn");
|
||||
ASSERT_EQ(churned.at("type"), "case_state");
|
||||
EXPECT_EQ(churned.at("telemetry").at("kernel_observer").at("last_event"),
|
||||
"mesh_churned");
|
||||
const std::string after_churn = request_pixels(session);
|
||||
expect_actual_datoviz_pixels(after_churn, 560, 320);
|
||||
EXPECT_EQ(after_churn.substr(pixel_frame_header_size),
|
||||
baseline.substr(pixel_frame_header_size));
|
||||
}
|
||||
|
||||
TEST(RenderiveWebDatovizGallery, TextureAndTextActionsReachTheThreeDimensionalBackend) {
|
||||
Gallery_Plot_Session session;
|
||||
ASSERT_EQ(open_scene(session, "manual").at("type"), "case_state");
|
||||
const std::string baseline = request_pixels(session);
|
||||
expect_actual_datoviz_pixels(baseline, 560, 320);
|
||||
|
||||
ASSERT_EQ(invoke_action(session, "toggle_texture").at("type"), "case_state");
|
||||
manual_render_cycle(session);
|
||||
const std::string without_texture = request_pixels(session);
|
||||
ASSERT_GE(without_texture.size(), pixel_frame_header_size);
|
||||
ASSERT_GE(baseline.size(), pixel_frame_header_size);
|
||||
EXPECT_NE(without_texture.substr(pixel_frame_header_size),
|
||||
baseline.substr(pixel_frame_header_size));
|
||||
|
||||
ASSERT_EQ(invoke_action(session, "toggle_texture").at("type"), "case_state");
|
||||
manual_render_cycle(session);
|
||||
const std::string restored_texture = request_pixels(session);
|
||||
ASSERT_GE(restored_texture.size(), pixel_frame_header_size);
|
||||
EXPECT_EQ(restored_texture.substr(pixel_frame_header_size),
|
||||
baseline.substr(pixel_frame_header_size));
|
||||
|
||||
ASSERT_EQ(invoke_action(session, "change_text").at("type"), "case_state");
|
||||
manual_render_cycle(session);
|
||||
const std::string changed_text = request_pixels(session);
|
||||
ASSERT_GE(changed_text.size(), pixel_frame_header_size);
|
||||
EXPECT_NE(changed_text.substr(pixel_frame_header_size),
|
||||
restored_texture.substr(pixel_frame_header_size));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace renderive::web
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ import {useTheme} from "@mui/material/styles";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {Group, Panel, Separator} from "react-resizable-panels";
|
||||
import {Category_Filter} from "./gallery/category_filter";
|
||||
import {Frame_Mode_Tabs} from "./gallery/frame_mode_tabs";
|
||||
import {Gallery_Page} from "./gallery/gallery_page";
|
||||
import {Gallery_Summary} from "./gallery/gallery_summary";
|
||||
import {Gallery_Toolbar} from "./gallery/gallery_toolbar";
|
||||
@@ -13,7 +12,6 @@ import {Inspector_Panel} from "./inspector/inspector_panel";
|
||||
|
||||
export function App() {
|
||||
const {catalog, state: socket_state, message: socket_message} = use_gallery_catalog();
|
||||
const [active_mode, set_active_mode] = useState("");
|
||||
const [active_category, set_active_category] = useState("");
|
||||
const [streams_paused, set_streams_paused] = useState(false);
|
||||
const [selected_plot, set_selected_plot] = useState<Selected_Plot | null>(null);
|
||||
@@ -31,11 +29,6 @@ export function App() {
|
||||
useEffect(() => {
|
||||
if (!catalog)
|
||||
return;
|
||||
set_active_mode(
|
||||
catalog.frame_modes.some(mode => mode.id === catalog.navigation.default_mode)
|
||||
? catalog.navigation.default_mode
|
||||
: catalog.frame_modes[0]?.id ?? "",
|
||||
);
|
||||
set_active_category(catalog.navigation.all_categories_label);
|
||||
}, [catalog]);
|
||||
|
||||
@@ -47,7 +40,9 @@ export function App() {
|
||||
() => [...(catalog?.cases ?? [])].sort((left, right) => left.order - right.order),
|
||||
[catalog],
|
||||
);
|
||||
const frame_mode = modes.find(mode => mode.id === active_mode);
|
||||
const frame_mode = modes.find(mode => mode.id === catalog?.navigation.default_mode)
|
||||
?? modes.find(mode => mode.id === "low_latency")
|
||||
?? modes[0];
|
||||
const categories = catalog
|
||||
? [catalog.navigation.all_categories_label, ...new Set(cases.map(item => item.category))]
|
||||
: [];
|
||||
@@ -65,24 +60,16 @@ export function App() {
|
||||
{!catalog && socket_state === "error" && <Alert severity="error">{socket_message}</Alert>}
|
||||
{catalog && <>
|
||||
<Gallery_Summary catalog={catalog}/>
|
||||
<Frame_Mode_Tabs
|
||||
frame_modes={modes}
|
||||
active_mode={active_mode}
|
||||
on_change={id => {
|
||||
set_active_mode(id);
|
||||
set_selected_plot(null);
|
||||
}}
|
||||
/>
|
||||
<Category_Filter
|
||||
categories={categories}
|
||||
active_category={active_category}
|
||||
on_change={set_active_category}
|
||||
/>
|
||||
{frame_mode && <>
|
||||
<Typography color="text.secondary">{frame_mode.description}</Typography>
|
||||
<Gallery_Page
|
||||
cases={visible_cases}
|
||||
frame_mode={frame_mode}
|
||||
frame_modes={modes}
|
||||
dashboard={catalog.dashboard}
|
||||
streams_paused={streams_paused}
|
||||
on_open_inspector={set_selected_plot}
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
import {Box} from "@mui/material";import {useCallback,useEffect,useRef} from "react";import type {Gallery_Case,Gallery_Dashboard,Gallery_Frame_Mode} from "../protocol/gallery_types";import type {Gallery_Plot_Session} from "../session/gallery_plot_session";import {Plot_Card,type Selected_Plot} from "./plot_card";
|
||||
export function Gallery_Page({cases,frame_mode,dashboard,streams_paused,on_open_inspector}:{cases:Gallery_Case[];frame_mode:Gallery_Frame_Mode;dashboard:Gallery_Dashboard;streams_paused:boolean;on_open_inspector:(plot:Selected_Plot)=>void}) {const sessions=useRef(new Set<Gallery_Plot_Session>());const register=useCallback((session:Gallery_Plot_Session,mount:boolean)=>{mount?sessions.current.add(session):sessions.current.delete(session);},[]);useEffect(()=>{let animation_frame=0;const loop=(now:number)=>{for(const session of sessions.current)session.tick(now);animation_frame=requestAnimationFrame(loop);};animation_frame=requestAnimationFrame(loop);return()=>cancelAnimationFrame(animation_frame);},[]);return <Box sx={{display:"grid",gridTemplateColumns:"repeat(auto-fit,minmax(min(100%,520px),1fr))",gap:2.5}}>{cases.map(gallery_case=><Plot_Card key={`${gallery_case.id}:${frame_mode.id}`} gallery_case={gallery_case} frame_mode={frame_mode} dashboard={dashboard} active={!document.hidden} streams_paused={streams_paused} on_register={register} on_open_inspector={on_open_inspector}/>)}</Box>;}
|
||||
import {Box} from "@mui/material";
|
||||
import {useCallback,useEffect,useRef} from "react";
|
||||
import type {Gallery_Case,Gallery_Dashboard,Gallery_Frame_Mode} from "../protocol/gallery_types";
|
||||
import type {Gallery_Plot_Session} from "../session/gallery_plot_session";
|
||||
import {Plot_Card,type Selected_Plot} from "./plot_card";
|
||||
|
||||
export function Gallery_Page({cases,frame_mode,frame_modes,dashboard,streams_paused,on_open_inspector}:{cases:Gallery_Case[];frame_mode:Gallery_Frame_Mode;frame_modes:Gallery_Frame_Mode[];dashboard:Gallery_Dashboard;streams_paused:boolean;on_open_inspector:(plot:Selected_Plot)=>void}) {
|
||||
const sessions=useRef(new Set<Gallery_Plot_Session>());
|
||||
const register=useCallback((session:Gallery_Plot_Session,mount:boolean)=>{mount?sessions.current.add(session):sessions.current.delete(session);},[]);
|
||||
useEffect(()=>{let animation_frame=0;const loop=(now:number)=>{for(const session of sessions.current)session.tick(now);animation_frame=requestAnimationFrame(loop);};animation_frame=requestAnimationFrame(loop);return()=>cancelAnimationFrame(animation_frame);},[]);
|
||||
return <Box sx={{display:"grid",gridTemplateColumns:"repeat(auto-fit,minmax(min(100%,520px),1fr))",gap:2.5}}>
|
||||
{cases.map(gallery_case=><Plot_Card key={gallery_case.id} gallery_case={gallery_case} frame_mode={frame_mode} frame_modes={frame_modes} dashboard={dashboard} active={!document.hidden} streams_paused={streams_paused} on_register={register} on_open_inspector={on_open_inspector}/>)}
|
||||
</Box>;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,20 @@
|
||||
import {Box,Paper,Typography} from "@mui/material";import type {Gallery_Catalog} from "../protocol/gallery_types";import {Metric_Grid} from "../common/metric_grid";
|
||||
export function Gallery_Summary({catalog}:{catalog:Gallery_Catalog}) {const coverage=catalog.coverage;return <Paper sx={{p:{xs:2,md:3},display:"grid",gridTemplateColumns:{xs:"1fr",md:"1.5fr 1fr"},gap:3,background:"linear-gradient(135deg,rgba(89,214,197,.12),rgba(255,189,105,.05))"}}><Box><Typography variant="overline" color="primary.main">{catalog.navigation.hero_eyebrow}</Typography><Typography variant="h4">{catalog.navigation.hero_title}</Typography><Typography color="text.secondary" mt={1}>所有属性、动作、观察者和性能数据均由后端通过 WebSocket 返回。</Typography></Box><Metric_Grid values={[["帧策略页",coverage.page_count],["每页控件",coverage.case_count],["独立场景",coverage.canvas_count],["手测入口",coverage.manual_control_count+coverage.manual_action_count]]}/></Paper>;}
|
||||
import {Box,Paper,Typography} from "@mui/material";
|
||||
import type {Gallery_Catalog} from "../protocol/gallery_types";
|
||||
import {Metric_Grid} from "../common/metric_grid";
|
||||
|
||||
export function Gallery_Summary({catalog}:{catalog:Gallery_Catalog}) {
|
||||
const coverage=catalog.coverage;
|
||||
return <Paper sx={{p:{xs:2,md:3},display:"grid",gridTemplateColumns:{xs:"1fr",md:"1.5fr 1fr"},gap:3,background:"linear-gradient(135deg,rgba(89,214,197,.12),rgba(255,189,105,.05))"}}>
|
||||
<Box>
|
||||
<Typography variant="overline" color="primary.main">{catalog.navigation.hero_eyebrow}</Typography>
|
||||
<Typography variant="h4">{catalog.navigation.hero_title}</Typography>
|
||||
<Typography color="text.secondary" mt={1}>所有属性、动作、观察者和性能数据均由后端通过 WebSocket 返回。</Typography>
|
||||
</Box>
|
||||
<Metric_Grid values={[
|
||||
["默认策略","Low latency"],
|
||||
["场景类型",coverage.case_count],
|
||||
["独立场景",coverage.canvas_count],
|
||||
["手测入口",coverage.manual_control_count+coverage.manual_action_count],
|
||||
]}/>
|
||||
</Paper>;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import {Performance_Strip} from "../plot/performance_strip";
|
||||
import {Kernel_Observer_Summary} from "../plot/kernel_observer_summary";
|
||||
|
||||
export interface Selected_Plot {session:Gallery_Plot_Session;}
|
||||
export function Plot_Card({gallery_case,frame_mode,dashboard,active,streams_paused,on_register,on_open_inspector}:{gallery_case:Gallery_Case;frame_mode:Gallery_Frame_Mode;dashboard:Gallery_Dashboard;active:boolean;streams_paused:boolean;on_register:(session:Gallery_Plot_Session,mount:boolean)=>void;on_open_inspector:(plot:Selected_Plot)=>void}) {
|
||||
const [session,snapshot]=use_plot_session(gallery_case,frame_mode);
|
||||
export function Plot_Card({gallery_case,frame_mode,frame_modes,dashboard,active,streams_paused,on_register,on_open_inspector}:{gallery_case:Gallery_Case;frame_mode:Gallery_Frame_Mode;frame_modes:Gallery_Frame_Mode[];dashboard:Gallery_Dashboard;active:boolean;streams_paused:boolean;on_register:(session:Gallery_Plot_Session,mount:boolean)=>void;on_open_inspector:(plot:Selected_Plot)=>void}) {
|
||||
const [session,snapshot]=use_plot_session(gallery_case,frame_mode,frame_modes);
|
||||
const card_ref=useRef<HTMLDivElement|null>(null);
|
||||
const [in_viewport,set_in_viewport]=useState(false);
|
||||
useEffect(()=>{on_register(session,true);return()=>on_register(session,false);},[session,on_register]);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {useEffect, useMemo, useSyncExternalStore} from "react";
|
||||
import type {Gallery_Case, Gallery_Frame_Mode} from "../protocol/gallery_types";
|
||||
import {Gallery_Plot_Session} from "../session/gallery_plot_session";
|
||||
export function use_plot_session(gallery_case: Gallery_Case, frame_mode: Gallery_Frame_Mode): [Gallery_Plot_Session, ReturnType<Gallery_Plot_Session["get_snapshot"]>] {
|
||||
const session=useMemo(()=>new Gallery_Plot_Session(gallery_case,frame_mode),[gallery_case,frame_mode]);
|
||||
export function use_plot_session(gallery_case: Gallery_Case, frame_mode: Gallery_Frame_Mode, frame_modes: Gallery_Frame_Mode[]): [Gallery_Plot_Session, ReturnType<Gallery_Plot_Session["get_snapshot"]>] {
|
||||
const session=useMemo(()=>new Gallery_Plot_Session(gallery_case,frame_mode,frame_modes),[gallery_case,frame_mode,frame_modes]);
|
||||
const snapshot=useSyncExternalStore(session.subscribe,session.get_snapshot,session.get_snapshot);
|
||||
useEffect(()=>()=>session.dispose(),[session]);
|
||||
return [session,snapshot];
|
||||
|
||||
@@ -14,11 +14,15 @@ export class Gallery_Plot_Session {
|
||||
private readonly presenter = new Pixel_Presenter();
|
||||
private readonly client_performance = new Client_Performance();
|
||||
private readonly frame_controller: Frame_Request_Controller;
|
||||
private current_frame_mode: Gallery_Frame_Mode;
|
||||
private disposed = false; private visible = false; private streams_paused = false; private last_observe_at = 0; private sent_width = 0; private sent_height = 0; private received_frames = 0;
|
||||
constructor(readonly gallery_case: Gallery_Case, readonly frame_mode: Gallery_Frame_Mode) {
|
||||
constructor(readonly gallery_case: Gallery_Case, frame_mode: Gallery_Frame_Mode,
|
||||
private readonly frame_modes: Gallery_Frame_Mode[]) {
|
||||
this.current_frame_mode=frame_mode;
|
||||
this.socket = new Gallery_Socket(gallery_socket_url(), {on_json: raw=>this.receive_json(raw),on_binary: buffer=>this.receive_pixels(buffer),on_state: state=>this.receive_socket_state(state)});
|
||||
this.frame_controller = new Frame_Request_Controller(()=>this.socket.send("frame"),()=>{this.client_performance.record_timeout();if(this.stream_active())this.request_frame(performance.now());},(now,value)=>this.client_performance.record_round_trip(now,value));
|
||||
}
|
||||
get frame_mode(): Gallery_Frame_Mode {return this.current_frame_mode;}
|
||||
subscribe = (listener: () => void): (() => void) => {this.listeners.add(listener);return()=>this.listeners.delete(listener);};
|
||||
get_snapshot = (): Gallery_Plot_Snapshot => this.snapshot;
|
||||
private update(patch: Partial<Gallery_Plot_Snapshot>): void {this.snapshot={...this.snapshot,...patch};this.listeners.forEach(listener=>listener());}
|
||||
@@ -29,7 +33,7 @@ export class Gallery_Plot_Session {
|
||||
set_activity(visible: boolean, streams_paused: boolean): void {const was_active=this.stream_active();this.visible=visible;this.streams_paused=streams_paused;if(visible&&!this.socket.is_open())this.connect();const active=this.stream_active();if(this.snapshot.ready&&active!==was_active)this.socket.send(active?"show":"hide");if(active)this.request_frame(performance.now());}
|
||||
private stream_active(): boolean {return this.visible&&!this.streams_paused;}
|
||||
private receive_socket_state(state: "connecting"|"ready"|"error"|"closed"): void {if(this.disposed)return;if(state==="ready"){this.update({connection_state:"ready",protocol_error:""});this.socket.send("gallery_open",{case:this.gallery_case.id,frame_mode:this.frame_mode.id});return;}this.update({connection_state:state,ready:state==="error"?this.snapshot.ready:false});if(state==="closed"&&!this.visible)this.socket.close();}
|
||||
private receive_json(raw: string): void {try{const response=parse_gallery_response(raw);if(!response)return;if(response.type==="error"){this.update({notice:Object.values(response.field_errors??{})[0]??response.message});return;}if(response.type==="observer_state"){this.update({telemetry:response.telemetry,performance_capture:response.telemetry.performance_capture??this.snapshot.performance_capture,frame_count:this.received_frames});return;}if(response.type==="case_state"||response.type==="refresh_state"){const controls=response.controls;this.update({ready:true,controls:build_controls(controls?.resources),actions:response.actions?.data??[],telemetry:response.telemetry??{},render_plan:controls?.render_plan??null,performance_capture:controls?.performance_capture??response.telemetry?.performance_capture??null,notice:response.notice??"",frame_count:this.received_frames,protocol_error:""});if(this.stream_active()){this.socket.send("show");this.request_frame(performance.now());}}}catch(error){this.update({connection_state:"error",protocol_error:error instanceof Error?error.message:"协议解析失败"});this.socket.close();}}
|
||||
private receive_json(raw: string): void {try{const response=parse_gallery_response(raw);if(!response)return;if(response.type==="error"){this.update({notice:Object.values(response.field_errors??{})[0]??response.message});return;}if(response.type==="observer_state"){this.update({telemetry:response.telemetry,performance_capture:response.telemetry.performance_capture??this.snapshot.performance_capture,frame_count:this.received_frames});return;}if(response.type==="case_state"||response.type==="refresh_state"){const controls=response.controls;const mode_id=typeof response.frame_mode==="string"?response.frame_mode:undefined;const mode=this.frame_modes.find(value=>value.id===mode_id);if(mode)this.current_frame_mode=mode;this.update({ready:true,controls:build_controls(controls?.resources),actions:response.actions?.data??[],telemetry:response.telemetry??{},render_plan:controls?.render_plan??null,performance_capture:controls?.performance_capture??response.telemetry?.performance_capture??null,notice:response.notice??"",frame_count:this.received_frames,protocol_error:""});if(this.stream_active()){this.socket.send("show");this.request_frame(performance.now());}}}catch(error){this.update({connection_state:"error",protocol_error:error instanceof Error?error.message:"协议解析失败"});this.socket.close();}}
|
||||
private receive_pixels(buffer: ArrayBuffer): void {try{const now=performance.now();this.frame_controller.receive_frame(now);const accepted=this.presenter.accept(buffer);this.received_frames++;this.client_performance.record_pixel(now,accepted.sequence,accepted.overwritten);}catch(error){this.update({connection_state:"error",protocol_error:error instanceof Error?error.message:"像素协议错误"});this.socket.close();}}
|
||||
tick(now: number): void {if(!this.stream_active())return;const presented=this.presenter.present();if(presented)this.client_performance.record_presentation(now);if(this.frame_mode.request_on_animation_frame||(this.frame_mode.request_after_response&&presented))this.request_frame(now);if(this.snapshot.ready&&now-this.last_observe_at>=650){this.last_observe_at=now;this.socket.send("gallery_observe",{client_metrics:this.client_performance.snapshot(now,this.socket.buffered_bytes()) as unknown as Json_Value});}}
|
||||
request_frame(now=performance.now(), explicit=false): boolean {if(!this.snapshot.ready||!this.socket.is_open()||(!explicit&&!this.stream_active())||(explicit&&!this.visible)||(!explicit&&!this.frame_mode.automatic))return false;return this.frame_controller.request_frame(now);}
|
||||
|
||||
Reference in New Issue
Block a user