仍有bug

This commit is contained in:
2026-08-17 15:24:28 +08:00
parent e793c6a278
commit 5e976b27dd
39 changed files with 1059 additions and 6658 deletions
@@ -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;
+1 -2
View File
@@ -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> {
+1 -1
View File
@@ -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());
+5 -2
View File
@@ -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
+22
View File
@@ -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
+7 -2
View File
@@ -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
+6 -3
View File
@@ -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"
-167
View File
@@ -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
-136
View File
@@ -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
-239
View File
@@ -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
View File
@@ -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
-142
View File
@@ -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
-663
View File
@@ -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
-32
View File
@@ -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
+35 -340
View File
@@ -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>()) {}
+54
View File
@@ -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
+129 -614
View File
@@ -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
+23 -356
View File
@@ -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
+4 -17
View File
@@ -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}
+14 -2
View File
@@ -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>;
}
+20 -2
View File
@@ -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>;
}
+2 -2
View File
@@ -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]);
+2 -2
View File
@@ -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);}