#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 #include #include #include #include #include #include #include #include #include #include #include namespace renderive::web { template <> struct Observer_Pfr_Adapter { using Type = adminive::Boost_Pfr_Base_Reflection_Adapter< Render_Scene_2D::State, Render_Scene_2D::State_Fields>; }; template <> struct Observer_Pfr_Adapter { 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 make_strategy( Gallery_Frame_Mode mode) { switch (mode) { case Gallery_Frame_Mode::Manual: return std::make_shared(); case Gallery_Frame_Mode::Low_Latency: return std::make_shared(); case Gallery_Frame_Mode::Playback: return std::make_shared(); } 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(&strategy)) return Gallery_Frame_Mode::Manual; if (dynamic_cast(&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::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(std::numeric_limits::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( 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(strategy.get())) { static_cast(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; if constexpr (std::same_as) { 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; } bool async_request_frame(Scene_Render_Completion completion) override { update_samples(); const auto result = scene_->render_frame( [this, completion = std::move(completion)](Scene_Render_Result render_result, std::exception_ptr exception) mutable { if (!exception && render_result == Scene_Render_Result::none) ++render_count_; completion(render_result, std::move(exception)); }); return result && *result == Plot_Render_Status::rendered; } std::optional 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(*request.argument)) count = static_cast(std::clamp( std::get(*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::max(1, viewport_.width - 86)); const auto height = static_cast( 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(height) : top) .set<&Time_Axis::Properties::pixel_length>( orientation == Orientation::Horizontal ? width : height) .set<&Time_Axis::Properties::color>(Color{69, 221, 190, 255}) .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(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_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(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_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_object_name("Value Axis") .build(); scene_builder.add_renderable(value_axis_); } if (case_id_ == "spectrum" || case_id_ == "selection_overlay") { spectrum_ = Spectrum::Builder{} .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_object_name("Spectrum") .build(frequency_axis_, value_axis_); scene_builder.add_renderable(spectrum_); if (case_id_ == "selection_overlay") { selection_ = Selection_Rectangle_Overlay::Builder{} .set<&Selection_Rectangle_Overlay::Properties::selection_brush>( Brush{Color{23, 59, 102, 90}, Brush_Style::Solid}) .set_object_name("Selection Overlay") .build(frequency_axis_, value_axis_); scene_builder.add_renderable(selection_); } } else if (case_id_ == "waterfall") { waterfall_ = Waterfall::Builder{} .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_object_name("Waterfall") .build(frequency_axis_, time_axis_); scene_builder.add_renderable(waterfall_); } else if (case_id_ == "afterglow") { afterglow_ = Afterglow::Builder{} .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_object_name("Afterglow") .build(frequency_axis_, value_axis_); scene_builder.add_renderable(afterglow_); } else if (case_id_ == "sweep_spectrum") { sweep_ = Sweep_Spectrum::Builder{} .set<&Sweep_Spectrum::Properties::frequency_range>(Range{0.0, 300.0}) .set<&Sweep_Spectrum::Properties::bins_per_block>(static_cast(sweep_bins_per_block)) .set<&Sweep_Spectrum::Properties::block_count>(static_cast(sweep_block_count)) .set_object_name("Sweep Spectrum") .build(domain_axis_, value_axis_); scene_builder.add_renderable(sweep_); } else if (case_id_ == "frequency_trace") { trace_ = Frequency_Trace::Builder{} .set<&Frequency_Trace::Properties::pen>(Pen{Color{53, 230, 178, 255}, 2.0}) .set_object_name("Frequency Trace") .build(time_axis_, value_axis_); scene_builder.add_renderable(trace_); } else if (case_id_ == "constellation") { constellation_ = Constellation_Diagram::Builder{} .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_object_name("Constellation Diagram") .build(domain_axis_, value_axis_); scene_builder.add_renderable(constellation_); } auto built_scene = scene_builder.build(); if (!built_scene) ::renderive::error::unexpected( "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 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(time_sequence) * 0.075; for (std::size_t index = 0; index < count; ++index) { const double x = static_cast(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( sample_sequence % sweep_block_count); const std::size_t first = block_index * sweep_bins_per_block; sweep_->append_block(std::span( 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(Constellation_Diagram_Type::Psk8); const std::size_t symbol_index = sample_sequence % symbol_count; const double phase = 2.0 * std::numbers::pi * static_cast(symbol_index) / static_cast(symbol_count); const double i_noise = std::sin(time * 17.0 + static_cast(symbol_index) * 0.73) * 0.04; const double q_noise = std::cos(time * 19.0 + static_cast(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 scene_; renderive_Owner frequency_axis_; renderive_Owner domain_axis_; renderive_Owner value_axis_; renderive_Owner time_axis_; renderive_Owner spectrum_; renderive_Owner waterfall_; renderive_Owner afterglow_; renderive_Owner sweep_; renderive_Owner trace_; renderive_Owner selection_; renderive_Owner constellation_; Gallery_Client_Performance client_; std::uint64_t render_count_{}; std::uint64_t sample_sequence_{}; }; } // namespace std::unique_ptr make_gallery_scene_2d( std::uint64_t, std::string case_id, Gallery_Frame_Mode mode) { return std::make_unique(std::move(case_id), mode); } } // namespace renderive::web