474 lines
22 KiB
C++
474 lines
22 KiB
C++
#include "Gallery_Scene2D.h"
|
|
|
|
#include "../Gallery_Capture_Json.h"
|
|
#include "../common/Observer_Json.h"
|
|
#include "../Pixel_Frame.h"
|
|
#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 "render_2D/scene/Render_Scene_2D.h"
|
|
|
|
#include <renderive/error/Error_Policy.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <numbers>
|
|
#include <span>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace renderive::web {
|
|
template <>
|
|
struct Observer_Pfr_Adapter<Render_Scene_2D::State> {
|
|
using Type = adminive::Boost_Pfr_Base_Reflection_Adapter<
|
|
Render_Scene_2D::State, Render_Scene_2D::State_Fields>;
|
|
};
|
|
template <>
|
|
struct Observer_Pfr_Adapter<Render_Scene_2D::Observer> {
|
|
using Type = adminive::Boost_Pfr_Base_Reflection_Adapter<
|
|
Render_Scene_2D::Observer, Render_Scene_2D::Observer_Fields>;
|
|
};
|
|
namespace {
|
|
constexpr std::size_t sweep_bins_per_block = 64;
|
|
constexpr std::size_t sweep_block_count = 8;
|
|
std::shared_ptr<Frame_Control_Strategy_Base> make_strategy(
|
|
Gallery_Frame_Mode mode) {
|
|
switch (mode) {
|
|
case Gallery_Frame_Mode::Manual:
|
|
return std::make_shared<Manual_Render_Scene_2D_Strategy>();
|
|
case Gallery_Frame_Mode::Low_Latency:
|
|
return std::make_shared<Low_Latency_Render_Scene_2D_Strategy>();
|
|
case Gallery_Frame_Mode::Playback:
|
|
return std::make_shared<Playback_Render_Scene_2D_Strategy>();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
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";
|
|
}
|
|
return "low_latency";
|
|
}
|
|
|
|
Gallery_Frame_Mode strategy_mode(const Frame_Control_Strategy_Base& strategy) {
|
|
if (dynamic_cast<const Manual_Render_Scene_2D_Strategy*>(&strategy))
|
|
return Gallery_Frame_Mode::Manual;
|
|
if (dynamic_cast<const Playback_Render_Scene_2D_Strategy*>(&strategy))
|
|
return Gallery_Frame_Mode::Playback;
|
|
return Gallery_Frame_Mode::Low_Latency;
|
|
}
|
|
|
|
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};
|
|
}
|
|
|
|
class Gallery_Scene2D final : public Gallery_Scene_Interface {
|
|
public:
|
|
Gallery_Scene2D(std::string case_id, Gallery_Frame_Mode mode)
|
|
: case_id_(std::move(case_id)) {
|
|
rebuild(mode);
|
|
}
|
|
|
|
~Gallery_Scene2D() override {
|
|
if (scene_) scene_->deactivate_view();
|
|
}
|
|
|
|
const std::string& case_id() const noexcept override { return case_id_; }
|
|
|
|
nlohmann::json controls() const override {
|
|
return {{"resources", nlohmann::json::array()},
|
|
{"observers", nlohmann::json::array()},
|
|
{"render_plan", gallery_render_plan_json(*scene_)},
|
|
{"performance_capture",
|
|
gallery_performance_capture_json(*scene_)}};
|
|
}
|
|
|
|
Gallery_Frame_Mode frame_mode() const noexcept override {
|
|
return strategy_mode(*scene_->frame_control_strategy());
|
|
}
|
|
void reset_monitoring() override { render_count_ = 0; }
|
|
|
|
void set_client_metrics(Gallery_Client_Performance metrics) noexcept override {
|
|
client_ = metrics;
|
|
constexpr double maximum_interval_ms =
|
|
static_cast<double>(std::numeric_limits<std::uint64_t>::max()) /
|
|
1'000'000.0;
|
|
if (metrics.display_interval_latest_ms <= 0.0 ||
|
|
metrics.display_interval_latest_ms > maximum_interval_ms) return;
|
|
const auto observed_interval_ns = static_cast<std::uint64_t>(
|
|
metrics.display_interval_latest_ms * 1'000'000.0);
|
|
if (observed_interval_ns == 0) return;
|
|
const auto strategy = scene_->frame_control_strategy();
|
|
if (auto* low_latency =
|
|
dynamic_cast<Low_Latency_Render_Scene_2D_Strategy*>(strategy.get())) {
|
|
static_cast<void>(low_latency->set_consumer_feedback(
|
|
Frame_Consumer_Feedback{observed_interval_ns}));
|
|
}
|
|
}
|
|
|
|
adminive::Update_Result apply_patch(
|
|
std::string_view, const nlohmann::json&) override {
|
|
return {false, "no editable resource matches the requested target"};
|
|
}
|
|
|
|
void resize(int width, int height) override {
|
|
const Size requested{std::max(1, width), std::max(1, height)};
|
|
if (requested == viewport_) return;
|
|
const auto mode = frame_mode();
|
|
viewport_ = requested;
|
|
rebuild(mode);
|
|
}
|
|
|
|
void dispatch(const Gallery_Input_Event& event) override {
|
|
std::visit([this](const auto& value) {
|
|
using T = std::decay_t<decltype(value)>;
|
|
if constexpr (std::same_as<T, Gallery_View_Input>) {
|
|
if (value.type == Gallery_View_Input_Type::Show)
|
|
scene_->activate_view();
|
|
else if (value.type == Gallery_View_Input_Type::Hide)
|
|
scene_->deactivate_view();
|
|
}
|
|
}, event);
|
|
}
|
|
|
|
bool request_frame() override {
|
|
update_samples();
|
|
const auto result = scene_->render_frame();
|
|
if (!result || *result != Plot_Render_Status::rendered)
|
|
return false;
|
|
if (scene_->wait_for_render() != Scene_Render_Result::none)
|
|
return false;
|
|
++render_count_;
|
|
return true;
|
|
}
|
|
|
|
std::optional<std::string> encode_latest_pixels() override {
|
|
std::string result;
|
|
scene_->with_frame([&](Image_View image) {
|
|
result = encode_pixel_frame(image, render_count_);
|
|
});
|
|
if (result.empty()) return std::nullopt;
|
|
return result;
|
|
}
|
|
|
|
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 {}
|
|
|
|
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_2D_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";
|
|
return "frame strategy replaced";
|
|
}
|
|
if (request.id == "capture_next_frame") {
|
|
const auto capture = scene_->capture_next_frame();
|
|
return capture ? "capture request accepted" : "capture request rejected";
|
|
}
|
|
if (request.id == "capture_frames") {
|
|
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);
|
|
return capture ? "frame capture started" : "capture request rejected";
|
|
}
|
|
recognized = false;
|
|
return {};
|
|
}
|
|
|
|
void record_action_notice_if_empty(std::string_view) override {}
|
|
|
|
std::string telemetry_json() const override {
|
|
const auto observer = scene_->observer();
|
|
auto kernel_observer = observer_json(observer);
|
|
kernel_observer["mode"] = mode_id(frame_mode());
|
|
kernel_observer["event"] = "render_completed";
|
|
kernel_observer["limit"] = "none";
|
|
return nlohmann::json{
|
|
{"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:
|
|
void rebuild(Gallery_Frame_Mode mode) {
|
|
auto scene_builder = Render_Scene_2D::Builder(make_strategy(mode));
|
|
scene_builder
|
|
.set<&Render_Scene_2D::Properties::viewport>(viewport_)
|
|
.set<&Render_Scene_2D::Properties::background>(Color{3, 8, 14, 255});
|
|
const auto root = scene_builder.root_renderable();
|
|
const bool numeric_domain =
|
|
case_id_ == "sweep_spectrum" || case_id_ == "constellation";
|
|
const bool time_domain = case_id_ == "frequency_trace";
|
|
const int left = 62;
|
|
const int top = 18;
|
|
const auto width = static_cast<std::size_t>(
|
|
std::max(1, viewport_.width - 86));
|
|
const auto height = static_cast<std::size_t>(
|
|
std::max(1, viewport_.height - 64));
|
|
if (time_domain || case_id_ == "waterfall" || case_id_ == "axis_lab") {
|
|
const auto orientation = time_domain || case_id_ == "axis_lab"
|
|
? Orientation::Horizontal
|
|
: Orientation::Vertical;
|
|
time_axis_ = Time_Axis::Builder{}
|
|
.set<&Time_Axis::Properties::orientation>(orientation)
|
|
.set<&Time_Axis::Properties::visible_count>(80)
|
|
.set<&Time_Axis::Properties::x>(left)
|
|
.set<&Time_Axis::Properties::y>(
|
|
orientation == Orientation::Horizontal
|
|
? top + static_cast<int>(height)
|
|
: top)
|
|
.set<&Time_Axis::Properties::pixel_length>(
|
|
orientation == Orientation::Horizontal ? width : height)
|
|
.set<&Time_Axis::Properties::color>(Color{69, 221, 190, 255})
|
|
.set_paint_rely({root})
|
|
.set_render_rely({root})
|
|
.set_object_name("Time Axis")
|
|
.build();
|
|
scene_builder.add_renderable(time_axis_);
|
|
}
|
|
if (!time_domain && !numeric_domain) {
|
|
frequency_axis_ = Frequency_Axis::Builder{}
|
|
.set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal)
|
|
.set<&Frequency_Axis::Properties::coordinates>(Range{88'000'000.0, 108'000'000.0})
|
|
.set<&Frequency_Axis::Properties::x>(left)
|
|
.set<&Frequency_Axis::Properties::y>(top + static_cast<int>(height))
|
|
.set<&Frequency_Axis::Properties::pixel_length>(width)
|
|
.set<&Frequency_Axis::Properties::color>(Color{118, 145, 184, 255})
|
|
.set<&Frequency_Axis::Properties::wheel>(true)
|
|
.set<&Frequency_Axis::Properties::drag>(true)
|
|
.set_paint_rely({root})
|
|
.set_render_rely({root})
|
|
.set_object_name("Frequency Axis")
|
|
.build();
|
|
scene_builder.add_renderable(frequency_axis_);
|
|
} else if (numeric_domain) {
|
|
domain_axis_ = Axis::Builder{}
|
|
.set<&Axis::Properties::orientation>(Orientation::Horizontal)
|
|
.set<&Axis::Properties::coordinates>(
|
|
case_id_ == "constellation" ? Range{-1.25, 1.25} : Range{0.0, 300.0})
|
|
.set<&Axis::Properties::x>(left)
|
|
.set<&Axis::Properties::y>(top + static_cast<int>(height))
|
|
.set<&Axis::Properties::pixel_length>(width)
|
|
.set<&Axis::Properties::color>(Color{118, 145, 184, 255})
|
|
.set<&Axis::Properties::wheel>(true)
|
|
.set<&Axis::Properties::drag>(true)
|
|
.set_paint_rely({root})
|
|
.set_render_rely({root})
|
|
.set_object_name("Domain Axis")
|
|
.build();
|
|
scene_builder.add_renderable(domain_axis_);
|
|
}
|
|
if (case_id_ != "waterfall") {
|
|
value_axis_ = Axis::Builder{}
|
|
.set<&Axis::Properties::orientation>(Orientation::Vertical)
|
|
.set<&Axis::Properties::coordinates>(
|
|
case_id_ == "constellation"
|
|
? Range{-1.25, 1.25}
|
|
: time_domain ? Range{-1.0, 1.0} : Range{-120.0, -20.0})
|
|
.set<&Axis::Properties::x>(left)
|
|
.set<&Axis::Properties::y>(top)
|
|
.set<&Axis::Properties::pixel_length>(height)
|
|
.set<&Axis::Properties::color>(Color{118, 145, 184, 255})
|
|
.set_paint_rely({root})
|
|
.set_render_rely({root})
|
|
.set_object_name("Value Axis")
|
|
.build();
|
|
scene_builder.add_renderable(value_axis_);
|
|
}
|
|
if (case_id_ == "spectrum" || case_id_ == "selection_overlay") {
|
|
spectrum_ = Spectrum::Builder(frequency_axis_, value_axis_)
|
|
.set<&Spectrum::Properties::frequency_range>(Range{88'000'000.0, 108'000'000.0})
|
|
.set<&Spectrum::Properties::frequency_point_size>(512)
|
|
.set<&Spectrum::Properties::center_frequency>(98'000'000.0)
|
|
.set<&Spectrum::Properties::max_hold_visible>(true)
|
|
.set<&Spectrum::Properties::current_pen>(Pen{Color{53, 230, 178, 255}, 2.0})
|
|
.set<&Spectrum::Properties::max_pen>(Pen{Color{255, 209, 102, 255}, 1.2})
|
|
.set<&Spectrum::Properties::min_pen>(Pen{Color{122, 162, 255, 255}, 1.2})
|
|
.set_paint_rely({root})
|
|
.set_render_rely({root})
|
|
.set_object_name("Spectrum")
|
|
.build();
|
|
scene_builder.add_renderable(spectrum_);
|
|
if (case_id_ == "selection_overlay") {
|
|
selection_ = Selection_Rectangle_Overlay::Builder(frequency_axis_, value_axis_)
|
|
.set<&Selection_Rectangle_Overlay::Properties::selection_brush>(
|
|
Brush{Color{23, 59, 102, 90}, Brush_Style::Solid})
|
|
.set_paint_rely({root})
|
|
.set_render_rely({root})
|
|
.set_object_name("Selection Overlay")
|
|
.build();
|
|
scene_builder.add_renderable(selection_);
|
|
}
|
|
} else if (case_id_ == "waterfall") {
|
|
waterfall_ = Waterfall::Builder(frequency_axis_, time_axis_)
|
|
.set<&Waterfall::Properties::frequency_range>(Range{88'000'000.0, 108'000'000.0})
|
|
.set<&Waterfall::Properties::power_range>(Range{-120.0, -20.0})
|
|
.set<&Waterfall::Properties::frequency_bin_count>(256)
|
|
.set_paint_rely({root})
|
|
.set_render_rely({root})
|
|
.set_object_name("Waterfall")
|
|
.build();
|
|
scene_builder.add_renderable(waterfall_);
|
|
} else if (case_id_ == "afterglow") {
|
|
afterglow_ = Afterglow::Builder(frequency_axis_, value_axis_)
|
|
.set<&Afterglow::Properties::frequency_range>(Range{88'000'000.0, 108'000'000.0})
|
|
.set<&Afterglow::Properties::power_range>(Range{-120.0, -20.0})
|
|
.set<&Afterglow::Properties::frequency_point_size>(192)
|
|
.set<&Afterglow::Properties::power_point_size>(96)
|
|
.set<&Afterglow::Properties::attenuation_rate>(0.18)
|
|
.set_paint_rely({root})
|
|
.set_render_rely({root})
|
|
.set_object_name("Afterglow")
|
|
.build();
|
|
scene_builder.add_renderable(afterglow_);
|
|
} else if (case_id_ == "sweep_spectrum") {
|
|
sweep_ = Sweep_Spectrum::Builder(domain_axis_, value_axis_)
|
|
.set<&Sweep_Spectrum::Properties::frequency_range>(Range{0.0, 300.0})
|
|
.set<&Sweep_Spectrum::Properties::bins_per_block>(static_cast<int>(sweep_bins_per_block))
|
|
.set<&Sweep_Spectrum::Properties::block_count>(static_cast<int>(sweep_block_count))
|
|
.set_paint_rely({root})
|
|
.set_render_rely({root})
|
|
.set_object_name("Sweep Spectrum")
|
|
.build();
|
|
scene_builder.add_renderable(sweep_);
|
|
} else if (case_id_ == "frequency_trace") {
|
|
trace_ = Frequency_Trace::Builder(time_axis_, value_axis_)
|
|
.set<&Frequency_Trace::Properties::pen>(Pen{Color{53, 230, 178, 255}, 2.0})
|
|
.set_paint_rely({root})
|
|
.set_render_rely({root})
|
|
.set_object_name("Frequency Trace")
|
|
.build();
|
|
scene_builder.add_renderable(trace_);
|
|
} else if (case_id_ == "constellation") {
|
|
constellation_ = Constellation_Diagram::Builder(domain_axis_, value_axis_)
|
|
.set<&Constellation_Diagram::Properties::i_range>(Range{-1.25, 1.25})
|
|
.set<&Constellation_Diagram::Properties::q_range>(Range{-1.25, 1.25})
|
|
.set<&Constellation_Diagram::Properties::point_color>(Color{73, 230, 195, 255})
|
|
.set<&Constellation_Diagram::Properties::anchor_color>(Color{255, 209, 102, 255})
|
|
.set_paint_rely({root})
|
|
.set_render_rely({root})
|
|
.set_object_name("Constellation Diagram")
|
|
.build();
|
|
scene_builder.add_renderable(constellation_);
|
|
}
|
|
auto built_scene = scene_builder.build();
|
|
if (!built_scene)
|
|
::renderive::error::unexpected<std::logic_error>(
|
|
"gallery 2D scene topology validation failed");
|
|
scene_ = std::move(*built_scene);
|
|
scene_->activate_view();
|
|
}
|
|
void update_samples() {
|
|
const std::size_t count =
|
|
waterfall_ ? 256U :
|
|
afterglow_ ? 192U :
|
|
sweep_ ? sweep_bins_per_block * sweep_block_count : 512U;
|
|
std::vector<double> values(count);
|
|
const std::uint64_t sample_sequence = sample_sequence_++;
|
|
const std::uint64_t time_sequence = sweep_
|
|
? sample_sequence / sweep_block_count * sweep_block_count
|
|
: sample_sequence;
|
|
const double time = static_cast<double>(time_sequence) * 0.075;
|
|
for (std::size_t index = 0; index < count; ++index) {
|
|
const double x = static_cast<double>(index) / (count - 1);
|
|
const auto gaussian = [](double value, double center, double width) {
|
|
const double normalized = (value - center) / width;
|
|
return std::exp(-0.5 * normalized * normalized);
|
|
};
|
|
values[index] = std::clamp(
|
|
-112.0 + std::sin(index * 0.31 + time) * 3.5 +
|
|
62.0 * gaussian(x, 0.31 + std::sin(time) * 0.025, 0.025) +
|
|
43.0 * gaussian(x, 0.58, 0.06) +
|
|
30.0 * gaussian(x, 0.79, 0.015),
|
|
-120.0, -20.0);
|
|
}
|
|
if (spectrum_) spectrum_->update_samples(values);
|
|
if (waterfall_) waterfall_->append_row(current_time_of_day(), values);
|
|
if (afterglow_) afterglow_->append_spectrum(values);
|
|
if (sweep_) {
|
|
const std::size_t block_index = static_cast<std::size_t>(
|
|
sample_sequence % sweep_block_count);
|
|
const std::size_t first = block_index * sweep_bins_per_block;
|
|
sweep_->append_block(std::span<const double>(
|
|
values.data() + first, sweep_bins_per_block));
|
|
}
|
|
if (trace_) trace_->append_sample(current_time_of_day(), std::sin(time));
|
|
if (constellation_) {
|
|
constexpr std::size_t symbol_count =
|
|
static_cast<std::size_t>(Constellation_Diagram_Type::Psk8);
|
|
const std::size_t symbol_index = sample_sequence % symbol_count;
|
|
const double phase = 2.0 * std::numbers::pi *
|
|
static_cast<double>(symbol_index) /
|
|
static_cast<double>(symbol_count);
|
|
const double i_noise =
|
|
std::sin(time * 17.0 + static_cast<double>(symbol_index) * 0.73) * 0.04;
|
|
const double q_noise =
|
|
std::cos(time * 19.0 + static_cast<double>(symbol_index) * 1.11) * 0.04;
|
|
constellation_->append_point(
|
|
{std::cos(phase) + i_noise, std::sin(phase) + q_noise});
|
|
}
|
|
}
|
|
|
|
std::string case_id_;
|
|
Size viewport_{560, 320};
|
|
std::unique_ptr<Render_Scene_2D> scene_;
|
|
renderive_Owner<Frequency_Axis> frequency_axis_;
|
|
renderive_Owner<Axis> domain_axis_;
|
|
renderive_Owner<Axis> value_axis_;
|
|
renderive_Owner<Time_Axis> time_axis_;
|
|
renderive_Owner<Spectrum> spectrum_;
|
|
renderive_Owner<Waterfall> waterfall_;
|
|
renderive_Owner<Afterglow> afterglow_;
|
|
renderive_Owner<Sweep_Spectrum> sweep_;
|
|
renderive_Owner<Frequency_Trace> trace_;
|
|
renderive_Owner<Selection_Rectangle_Overlay> selection_;
|
|
renderive_Owner<Constellation_Diagram> constellation_;
|
|
Gallery_Client_Performance client_;
|
|
std::uint64_t render_count_{};
|
|
std::uint64_t sample_sequence_{};
|
|
};
|
|
} // namespace
|
|
|
|
std::unique_ptr<Gallery_Scene_Interface> make_gallery_scene_2d(
|
|
std::uint64_t, std::string case_id, Gallery_Frame_Mode mode) {
|
|
return std::make_unique<Gallery_Scene2D>(std::move(case_id), mode);
|
|
}
|
|
} // namespace renderive::web
|