#include "Waterfall.h" #include "Heatmap_Utils.h" #include "Plottable_Real_Time_Data.h" #include "../renderable/Render_Partition.h" #include "../renderable/Renderable_p.h" #include #include #include #include #include namespace renderive::detail { namespace { struct Waterfall_Row { int tick{}; std::vector values; }; struct Waterfall_Interaction_Base {}; struct Waterfall_Interaction { Hover_Tooltip_Runtime tooltip; }; using Waterfall_History = Plottable_History_Real_Time_Data>; using Waterfall_Interaction_State = Double_State_Strategy; struct Waterfall_Prepare_Buffer { Waterfall_Properties properties; std::deque rows; Waterfall_Interaction interaction; Axis_Raster_Layout layout; Frequency_Columns columns; Axis_Transform frequency_axis; std::vector pixels; RectF tooltip_box; std::string tooltip_text; int source_width{}; int source_height{}; int active_partitions{1}; std::size_t work_size{}; bool valid{}; }; std::size_t waterfall_work_size(const Waterfall_Properties& state, const std::deque& rows, const Axis_Transform& frequency_axis) { if (rows.empty()) return 0; const auto shortest = std::min_element( rows.begin(), rows.end(), [](const auto& left, const auto& right) { return left.values.size() < right.values.size(); }); const int source_width = std::max( 0, std::min(state.frequency_bin_count.get(), static_cast(shortest->values.size()))); const auto columns = frequency_columns(state.frequency_range, frequency_axis.coordinate_range, source_width, state.visible_range_only); return columns ? static_cast(columns->last - columns->first + 1) * rows.size() : 0; } } struct Waterfall_Control::Impl : Renderable::Impl { Impl(renderive_Owner frequency, renderive_Owner time) : frequency_axis(std::move(frequency)), time_axis(std::move(time)) {} renderive_Owner frequency_axis; renderive_Owner time_axis; std::optional rows; Waterfall_Interaction_State interaction; Adaptive_Render_Partitioner partitioner; Waterfall_Prepare_Buffer prepare_buffer; }; Waterfall_Control::Waterfall_Control(const Waterfall_Properties& properties, renderive_Owner frequency_axis, renderive_Owner time_axis) : Plottable_State(properties, std::make_unique( std::move(frequency_axis), std::move(time_axis))) { d_func().rows.emplace(*this); } Waterfall_Control::~Waterfall_Control() = default; void Waterfall_Control::append_row(int tick, std::span values) { const std::size_t limit = static_cast( std::max(2, d_func().time_axis->get<&Time_Axis_Properties::visible_count>())); if(get<&Waterfall_Properties::frequency_bin_count>() <= 0) set<&Waterfall_Properties::frequency_bin_count>(static_cast(values.size())); d_func().rows->update({tick, {values.begin(), values.end()}}, limit); render_graph_changed(); } void Waterfall_Control::append_row(int tick, std::pmr::vector&& values) { append_row(tick, std::span(values.data(), values.size())); } void Waterfall_Control::append_row(Time_Of_Day time, std::span values) { append_row(d_func().time_axis->append_time(time), values); } void Waterfall_Control::append_row(Time_Of_Day time, std::pmr::vector&& values) { append_row(time, std::span(values.data(), values.size())); } std::size_t Waterfall_Control::row_count() const { return d_func().rows->size(); } std::size_t Waterfall_Control::stored_point_count() const { const auto rows = d_func().rows->snapshot(); std::size_t count{}; for(const auto& row : rows) count += row.values.size(); return count; } std::size_t Waterfall_Control::rendered_cell_count() const { const auto state = properties(); const auto rows = d_func().rows->snapshot(); if(rows.empty()) return 0; const int source_width = std::min(state.frequency_bin_count.get(), static_cast(std::min_element(rows.begin(), rows.end(), [](const auto& left, const auto& right) { return left.values.size() < right.values.size(); })->values.size())); if(source_width <= 0) return 0; const auto columns = frequency_columns( state.frequency_range, d_func().frequency_axis->get<&Axis_Properties::coordinates>(), source_width, state.visible_range_only); return columns ? static_cast(columns->last - columns->first + 1) * rows.size() : 0; } void Waterfall_Control::handle_event(const Event& event) { bool updated{}; d_func().interaction.update([&](Waterfall_Interaction& interaction) { updated = update_hover_tooltip(interaction.tooltip, event); }); if(updated) changed(); } void Waterfall_Control::publish() { publish_properties(); d_func().interaction.publish(); } void Waterfall_Control::build_prepare_graph(Renderable_Graph_Builder& builder) { const auto view = d_func().render_state_view(); const auto state = properties(); const auto& rows = view.get(*d_func().rows); const std::size_t work_size = waterfall_work_size( state, rows, d_func().frequency_axis->transform(view)); const int partition_count = d_func().partitioner.graph_partition_count( state.partition_mode, state.partition_count.get(), static_cast(Scene_Base::task_executor_worker_count()), work_size, 4096); const auto prepare = add_prepare_task( builder, "prepare", "Prepare Waterfall", [this, partition_count](const Prepare_Render_Context& context) { prepare_render_frame(context.frame.render_state, partition_count); if (context.metrics) { context.metrics->set(Node_Metric_Kind::input_count, d_func().prepare_buffer.rows.size()); context.metrics->set(Node_Metric_Kind::chunk_size, d_func().prepare_buffer.work_size / std::max(1, d_func().prepare_buffer.active_partitions)); } }); std::vector partitions; partitions.reserve(static_cast(partition_count)); for (int index = 0; index < partition_count; ++index) { const auto partition = builder.emplace( "chunk_prepare:" + std::to_string(index), "Waterfall Chunk " + std::to_string(index + 1) + " Prepare", [this, index](const Prepare_Render_Context& context) { render_partition(index); if (context.metrics) { const auto range = render_partition_range( d_func().prepare_buffer.work_size, index, d_func().prepare_buffer.active_partitions); context.metrics->set(Node_Metric_Kind::prepared_cells, range.last - range.first); } }); builder.precede(prepare, partition); partitions.push_back(partition); } } void Waterfall_Control::build_paint_graph(Renderable_Graph_Builder& builder) { const auto paint_image = add_paint_task( builder, "paint", "Paint Waterfall", [this](Painter& painter, const Paint_Render_Context& context) { paint_render_frame(painter, context.frame.render_state); if (context.metrics) context.metrics->set(Node_Metric_Kind::pixel_count, d_func().prepare_buffer.work_size); }); const auto view = d_func().render_state_view(); const auto state = properties(); const auto& rows = view.get(*d_func().rows); const std::size_t work_size = waterfall_work_size( state, rows, d_func().frequency_axis->transform(view)); const int count = d_func().partitioner.graph_partition_count( state.partition_mode, state.partition_count.get(), static_cast(Scene_Base::task_executor_worker_count()), work_size, 4096); if (count == 0) { builder.precede(builder.find("prepare"), paint_image); } else { for (int index = 0; index < count; ++index) builder.precede(builder.find("chunk_prepare:" + std::to_string(index)), paint_image); } } void Waterfall_Control::prepare_render_frame(const Render_State_View& view, int graph_partition_count) { const auto& state = render_properties(view); const auto& rows = view.get(*d_func().rows); auto& output = d_func().prepare_buffer; output = {}; output.properties = state; output.rows.assign(rows.begin(), rows.end()); output.interaction = view.get(d_func().interaction); if (rows.empty()) return; const int source_width = std::min(state.frequency_bin_count.get(), static_cast(std::min_element(rows.begin(), rows.end(), [](const auto& left, const auto& right) { return left.values.size() < right.values.size(); })->values.size())); const int height = static_cast(rows.size()); if (source_width <= 0 || height <= 0) return; const Axis_Transform frequency_axis = d_func().frequency_axis->transform(view); const Axis_Transform time_axis = d_func().time_axis->transform(view); const auto columns = frequency_columns(state.frequency_range, frequency_axis.coordinate_range, source_width, state.visible_range_only); if (!columns) return; const int width = columns->last - columns->first + 1; const Range time_range = rows.size() == 1 ? time_axis.coordinate_range : Range{static_cast(rows.front().tick), static_cast(rows.back().tick)}; const auto layout = axis_raster_layout(frequency_axis, time_axis, columns->range, time_range, width, height); if (!layout.valid()) return; output.layout = layout; output.columns = *columns; output.frequency_axis = frequency_axis; output.source_width = width; output.source_height = height; output.work_size = static_cast(width) * height; output.pixels.resize(output.work_size); output.active_partitions = d_func().partitioner.begin(graph_partition_count, output.work_size); if (state.tooltip_enabled && output.interaction.tooltip.active && output.layout.target.contains(output.interaction.tooltip.position)) { const double frequency = frequency_axis.point_to_coord( output.interaction.tooltip.position); std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency << " Hz"; output.tooltip_text = text.str(); output.tooltip_box = {output.interaction.tooltip.position.x + 8.0, output.interaction.tooltip.position.y + 8.0, 110.0, 24.0}; } output.valid = true; } void Waterfall_Control::render_partition(int partition_index) { auto& output = d_func().prepare_buffer; if (!output.valid || partition_index >= output.active_partitions) return; const auto& state = output.properties; const auto& rows = output.rows; const auto range = render_partition_range(output.work_size, partition_index, output.active_partitions); for (std::size_t cell = range.first; cell < range.last; ++cell) { const int y = static_cast(cell / static_cast(output.source_width)); const int x = static_cast(cell % static_cast(output.source_width)); const auto& row = rows[static_cast(y)].values; output.pixels[output.layout.index(x, y, output.source_width, output.source_height)] = state.color_map.at_normalized(normalized_value( row[static_cast(output.columns.first) + static_cast(x)], state.power_range)); } } void Waterfall_Control::paint_render_frame(Painter& painter, const Render_State_View& view) { const auto& output = d_func().prepare_buffer; if (!output.valid) return; const auto& paint_state = render_properties(view); painter.heatmap(output.layout.target, output.layout.width, output.layout.height, output.pixels, paint_state.interpolation_mode); if (!output.tooltip_text.empty()) { painter.rect(output.tooltip_box, Pen{paint_state.tooltip_text_pen.color}, paint_state.tooltip_background_brush); painter.text({output.tooltip_box.x + 4.0, output.tooltip_box.y + 3.0}, output.tooltip_text, paint_state.tooltip_font, paint_state.tooltip_text_pen); } } void Waterfall_Control::render_frame_completed( std::uint64_t target_interval_ns) { const auto& output = d_func().prepare_buffer; if (!output.valid || !is_visible()) return; const auto& state = output.properties; if (d_func().partitioner.finish( state.partition_mode, output.active_partitions, target_interval_ns, static_cast(Scene_Base::task_executor_worker_count()), output.work_size, 4096)) render_graph_changed(); } void Waterfall_Control::prepare_frame(const Prepare_Render_Context& context) { prepare_render_frame(context.frame.render_state, 1); render_partition(0); } void Waterfall_Control::paint(Painter& painter, const Paint_Render_Context& context) { paint_render_frame(painter, context.frame.render_state); } }