Files
Renderive/render_2D/plottable/Waterfall.cpp
T
2026-08-13 00:35:00 +08:00

305 lines
14 KiB
C++

#include "Waterfall.h"
#include "Heatmap_Utils.h"
#include "Plottable_Real_Time_Data.h"
#include "../renderable/Render_Partition.h"
#include <algorithm>
#include <deque>
#include <iomanip>
#include <sstream>
namespace renderive::detail {
namespace {
struct Waterfall_Row {
int tick{};
std::vector<double> values;
};
struct Waterfall_Interaction_Base {};
struct Waterfall_Interaction {
Hover_Tooltip_Runtime tooltip;
};
using Waterfall_History = Plottable_History_Real_Time_Data<Waterfall_Row, std::deque<Waterfall_Row>>;
using Waterfall_Interaction_State = Double_State_Strategy<Waterfall_Interaction_Base, Waterfall_Interaction>;
struct Waterfall_Prepare_Buffer {
Waterfall_Properties properties;
std::deque<Waterfall_Row> rows;
Waterfall_Interaction interaction;
Axis_Raster_Layout layout;
Frequency_Columns columns;
Axis_Transform frequency_axis;
std::vector<Pixel> 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<Waterfall_Row>& 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<int>(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<std::size_t>(columns->last - columns->first + 1) * rows.size()
: 0;
}
}
struct Waterfall_Control::Impl {
Impl(Waterfall_Control& owner, std::shared_ptr<Frequency_Axis> frequency, std::shared_ptr<Time_Axis> time)
: frequency_axis(std::move(frequency)), time_axis(std::move(time)), rows(owner) {}
std::shared_ptr<Frequency_Axis> frequency_axis;
std::shared_ptr<Time_Axis> time_axis;
Waterfall_History rows;
Waterfall_Interaction_State interaction;
Adaptive_Render_Partitioner partitioner;
Waterfall_Prepare_Buffer prepare_buffer;
};
Waterfall_Control::Waterfall_Control(::Scene_Base& scene, const Waterfall_Properties& properties, std::shared_ptr<Frequency_Axis> frequency_axis, std::shared_ptr<Time_Axis> time_axis)
: Plottable_State(scene, properties), impl_(std::make_unique<Impl>(*this, std::move(frequency_axis), std::move(time_axis))) {}
Waterfall_Control::~Waterfall_Control() = default;
void Waterfall_Control::append_row(int tick, std::span<const double> values) {
const std::size_t limit = static_cast<std::size_t>(
std::max(2, impl_->time_axis->get<&Time_Axis_Properties::visible_count>()));
if(get<&Waterfall_Properties::frequency_bin_count>() <= 0)
set<&Waterfall_Properties::frequency_bin_count>(static_cast<int>(values.size()));
impl_->rows.update({tick, {values.begin(), values.end()}}, limit);
render_graph_changed();
}
void Waterfall_Control::append_row(int tick, std::pmr::vector<double>&& values) {
append_row(tick, std::span<const double>(values.data(), values.size()));
}
void Waterfall_Control::append_row(Time_Of_Day time, std::span<const double> values) {
append_row(impl_->time_axis->append_time(time), values);
}
void Waterfall_Control::append_row(Time_Of_Day time, std::pmr::vector<double>&& values) {
append_row(time, std::span<const double>(values.data(), values.size()));
}
std::size_t Waterfall_Control::row_count() const {
return impl_->rows.size();
}
std::size_t Waterfall_Control::stored_point_count() const {
const auto rows = impl_->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 = impl_->rows.snapshot();
if(rows.empty())
return 0;
const int source_width = std::min(state.frequency_bin_count.get(), static_cast<int>(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,
impl_->frequency_axis->get<&Axis_Properties::coordinates>(),
source_width,
state.visible_range_only);
return columns ? static_cast<std::size_t>(columns->last - columns->first + 1) * rows.size() : 0;
}
void Waterfall_Control::handle_event(const Event& event) {
bool updated{};
impl_->interaction.update([&](Waterfall_Interaction& interaction) { updated = update_hover_tooltip(interaction.tooltip, event); });
if(updated)
changed();
}
void Waterfall_Control::publish() {
publish_properties();
impl_->interaction.publish();
}
void Waterfall_Control::build_prepare_graph(Renderable_Graph_Builder& builder) {
const auto view = render_state_view();
const auto state = properties();
const auto& rows = view.get(impl_->rows);
const std::size_t work_size = waterfall_work_size(
state, rows, impl_->frequency_axis->transform(view));
const int partition_count = impl_->partitioner.graph_partition_count(
state.partition_mode, state.partition_count.get(),
static_cast<int>(Scene_Base::task_executor_worker_count()), work_size, 4096);
const auto prepare = add_prepare_task(
builder, "prepare", "Prepare Waterfall",
[this, partition_count](const Render_State_View& frame_view,
const Scene_Render_Context& context) {
prepare_render_frame(frame_view, partition_count);
if (context.metrics) {
context.metrics->set(Node_Metric_Kind::input_count,
impl_->prepare_buffer.rows.size());
context.metrics->set(Node_Metric_Kind::chunk_size,
impl_->prepare_buffer.work_size /
std::max(1, impl_->prepare_buffer.active_partitions));
}
});
std::vector<Renderable_Graph_Builder::Task> partitions;
partitions.reserve(static_cast<std::size_t>(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",
Render_Node_Kind::prepare,
[this, index](const Scene_Render_Context& context) {
render_partition(index);
if (context.metrics) {
const auto range = render_partition_range(
impl_->prepare_buffer.work_size, index,
impl_->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 Scene_Render_Context& context) {
paint_render_frame(painter);
if (context.metrics)
context.metrics->set(Node_Metric_Kind::pixel_count,
impl_->prepare_buffer.work_size);
});
const auto view = render_state_view();
const auto state = properties();
const auto& rows = view.get(impl_->rows);
const std::size_t work_size = waterfall_work_size(
state, rows, impl_->frequency_axis->transform(view));
const int count = impl_->partitioner.graph_partition_count(
state.partition_mode, state.partition_count.get(),
static_cast<int>(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(impl_->rows);
auto& output = impl_->prepare_buffer;
output = {};
output.properties = state;
output.rows.assign(rows.begin(), rows.end());
output.interaction = view.get(impl_->interaction);
if (rows.empty())
return;
const int source_width = std::min(state.frequency_bin_count.get(), static_cast<int>(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<int>(rows.size());
if (source_width <= 0 || height <= 0)
return;
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
const Axis_Transform time_axis = impl_->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<double>(rows.front().tick),
static_cast<double>(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<std::size_t>(width) * height;
output.pixels.resize(output.work_size);
output.active_partitions = impl_->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 = impl_->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<int>(cell / static_cast<std::size_t>(output.source_width));
const int x = static_cast<int>(cell % static_cast<std::size_t>(output.source_width));
const auto& row = rows[static_cast<std::size_t>(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<std::size_t>(output.columns.first) + static_cast<std::size_t>(x)],
state.power_range));
}
}
void Waterfall_Control::paint_render_frame(Painter& painter) {
const auto& output = impl_->prepare_buffer;
if (!output.valid)
return;
const auto view = render_state_view();
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) noexcept {
const auto& output = impl_->prepare_buffer;
if (!output.valid || !is_visible())
return;
const auto& state = output.properties;
if (impl_->partitioner.finish(
state.partition_mode, output.active_partitions, target_interval_ns,
static_cast<int>(Scene_Base::task_executor_worker_count()),
output.work_size, 4096))
render_graph_changed();
}
void Waterfall_Control::prepare_frame(const Render_State_View& view,
const Scene_Render_Context&) {
prepare_render_frame(view, 1);
render_partition(0);
}
void Waterfall_Control::paint(Painter& painter, const Scene_Render_Context&) {
paint_render_frame(painter);
}
}