344 lines
16 KiB
C++
344 lines
16 KiB
C++
#include "Waterfall.h"
|
|
#include "Heatmap_Utils.h"
|
|
#include "Plottable_Real_Time_Data.h"
|
|
#include "../renderable/Render_Partition.h"
|
|
#include "../renderable/Renderable_p.h"
|
|
#include <renderive/renderable/Render_Frame_Completion.hpp>
|
|
#include <algorithm>
|
|
#include <deque>
|
|
#include <iomanip>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include "renderive/scheduling/Scheduler.hpp"
|
|
namespace renderive::detail {
|
|
namespace {
|
|
struct Waterfall_Row {
|
|
int tick{};
|
|
std::vector<double> values;
|
|
};
|
|
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 = Published_State_Storage<Waterfall_Interaction>;
|
|
struct Waterfall_Prepare_Buffer {
|
|
::renderive::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 ::renderive::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::Impl
|
|
: next_Impl<Impl>,
|
|
Renderable_Event_Handler,
|
|
::Render_Frame_Completion {
|
|
struct Observer : next_Observer {
|
|
|
|
static void handle(Impl& impl,
|
|
const Renderable_Event_View& observation) {
|
|
if (observation.event == Renderable_Observer_Event::Published)
|
|
impl.interaction.publish();
|
|
}
|
|
};
|
|
|
|
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;
|
|
renderive_Owner<Time_Axis> time_axis;
|
|
std::optional<Waterfall_History> rows;
|
|
Waterfall_Interaction_State interaction;
|
|
Adaptive_Render_Partitioner partitioner;
|
|
Waterfall_Prepare_Buffer prepare_buffer;
|
|
void prepare_render_frame(const Render_State_View& state, int graph_partition_count);
|
|
void paint_render_frame(Painter& painter, const Render_State_View& state);
|
|
void render_partition(int partition_index);
|
|
protected:
|
|
void prepare_frame(const Prepare_Render_Context& context) override;
|
|
void paint(Painter& painter, const Paint_Render_Context& context) override;
|
|
void build_prepare_graph(Renderable_Graph_Builder& builder) override;
|
|
void build_paint_graph(Renderable_Graph_Builder& builder) override;
|
|
private:
|
|
void handle_event(const Event& event) override;
|
|
void render_frame_completed(std::uint64_t target_interval_ns) override;
|
|
};
|
|
Waterfall::Waterfall(const State& state,
|
|
renderive_Owner<Frequency_Axis> frequency_axis,
|
|
renderive_Owner<Time_Axis> time_axis)
|
|
: Renderable(With_Attached_Impl<Impl>{}, state,
|
|
std::move(frequency_axis), std::move(time_axis)) {
|
|
d_func<Impl>().rows.emplace(*this);
|
|
}
|
|
Waterfall::~Waterfall() = default;
|
|
void Waterfall::append_row(int tick, std::span<const double> values) {
|
|
const std::size_t limit = static_cast<std::size_t>(
|
|
std::max(2, d_func<Impl>().time_axis->get<
|
|
&::renderive::Time_Axis::Properties::visible_count>()));
|
|
if (get_state<State, &State::frequency_bin_count>() <= 0)
|
|
set_state<State, &State::frequency_bin_count>(
|
|
static_cast<int>(values.size()));
|
|
d_func<Impl>().rows->update({tick, {values.begin(), values.end()}}, limit);
|
|
d_func().render_graph_changed();
|
|
}
|
|
void Waterfall::append_row(int tick, std::pmr::vector<double>&& values) {
|
|
append_row(tick, std::span<const double>(values.data(), values.size()));
|
|
}
|
|
void Waterfall::append_row(Time_Of_Day time, std::span<const double> values) {
|
|
append_row(d_func<Impl>().time_axis->append_time(time), values);
|
|
}
|
|
void Waterfall::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::row_count() const {
|
|
return d_func<Impl>().rows->size();
|
|
}
|
|
std::size_t Waterfall::stored_point_count() const {
|
|
const auto rows = d_func<Impl>().rows->snapshot();
|
|
std::size_t count{};
|
|
for (const auto& row : rows)
|
|
count += row.values.size();
|
|
return count;
|
|
}
|
|
std::size_t Waterfall::rendered_cell_count() const {
|
|
const auto state = Renderable::state<State>();
|
|
const auto rows = d_func<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,
|
|
d_func<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::Impl::handle_event(const Event& event) {
|
|
auto& control = static_cast<Waterfall&>(owner());
|
|
bool updated{};
|
|
interaction.update([&](Waterfall_Interaction& interaction) {
|
|
updated = update_hover_tooltip(interaction.tooltip, event);
|
|
});
|
|
if (updated)
|
|
changed();
|
|
}
|
|
void Waterfall::Impl::build_prepare_graph(Renderable_Graph_Builder& builder) {
|
|
auto& control = static_cast<Waterfall&>(owner());
|
|
const auto view = render_state_view();
|
|
const auto state = control.state<State>();
|
|
const auto& published_rows = view.get(*rows);
|
|
const std::size_t work_size = waterfall_work_size(
|
|
state, published_rows, frequency_axis->transform(view));
|
|
const int partition_count = partitioner.graph_partition_count(
|
|
state.partition_mode, state.partition_count.get(),
|
|
static_cast<int>(renderive::scheduling::scheduler_concurrency()), 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,
|
|
prepare_buffer.rows.size());
|
|
context.metrics->set(Node_Metric_Kind::chunk_size,
|
|
prepare_buffer.work_size /
|
|
std::max(1, 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",
|
|
[this, index](const Prepare_Render_Context& context) {
|
|
render_partition(index);
|
|
if (context.metrics) {
|
|
const auto range = render_partition_range(
|
|
prepare_buffer.work_size, index,
|
|
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::Impl::build_paint_graph(Renderable_Graph_Builder& builder) {
|
|
auto& control = static_cast<Waterfall&>(owner());
|
|
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,
|
|
prepare_buffer.work_size);
|
|
});
|
|
const auto view = render_state_view();
|
|
const auto state = control.state<State>();
|
|
const auto& published_rows = view.get(*rows);
|
|
const std::size_t work_size = waterfall_work_size(
|
|
state, published_rows, frequency_axis->transform(view));
|
|
const int count = partitioner.graph_partition_count(
|
|
state.partition_mode, state.partition_count.get(),
|
|
static_cast<int>(renderive::scheduling::scheduler_concurrency()), 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::Impl::prepare_render_frame(const Render_State_View& view,
|
|
int graph_partition_count) {
|
|
auto& control = static_cast<Waterfall&>(owner());
|
|
const auto& state = control.render_state<State>(view);
|
|
const auto& published_rows = view.get(*rows);
|
|
auto& output = prepare_buffer;
|
|
output = {};
|
|
output.properties = state;
|
|
output.rows.assign(published_rows.begin(), published_rows.end());
|
|
output.interaction = view.get(interaction);
|
|
if (published_rows.empty())
|
|
return;
|
|
const int source_width = std::min(state.frequency_bin_count.get(), static_cast<int>(std::min_element(published_rows.begin(), published_rows.end(), [](const auto& left, const auto& right) {
|
|
return left.values.size() < right.values.size();
|
|
})->values.size()));
|
|
const int height = static_cast<int>(published_rows.size());
|
|
if (source_width <= 0 || height <= 0)
|
|
return;
|
|
const Axis_Transform published_frequency_axis = frequency_axis->transform(view);
|
|
const Axis_Transform published_time_axis = time_axis->transform(view);
|
|
const auto columns = frequency_columns(state.frequency_range, published_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 = published_rows.size() == 1
|
|
? published_time_axis.coordinate_range
|
|
: Range{
|
|
static_cast<double>(published_rows.front().tick),
|
|
static_cast<double>(published_rows.back().tick)
|
|
};
|
|
const auto layout = axis_raster_layout(published_frequency_axis, published_time_axis, columns->range,
|
|
time_range, width, height);
|
|
if (!layout.valid())
|
|
return;
|
|
output.layout = layout;
|
|
output.columns = *columns;
|
|
output.frequency_axis = published_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 = 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 = published_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::Impl::render_partition(int partition_index) {
|
|
auto& output = 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::Impl::paint_render_frame(Painter& painter,
|
|
const Render_State_View& view) {
|
|
auto& control = static_cast<Waterfall&>(owner());
|
|
const auto& output = prepare_buffer;
|
|
if (!output.valid)
|
|
return;
|
|
const auto& paint_state = control.render_state<State>(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::Impl::render_frame_completed(
|
|
std::uint64_t target_interval_ns) {
|
|
auto& control = static_cast<Waterfall&>(owner());
|
|
const auto& output = prepare_buffer;
|
|
if (!output.valid || !control.is_visible())
|
|
return;
|
|
const auto& state = output.properties;
|
|
if (partitioner.finish(
|
|
state.partition_mode, output.active_partitions, target_interval_ns,
|
|
static_cast<int>(renderive::scheduling::scheduler_concurrency()),
|
|
output.work_size, 4096))
|
|
render_graph_changed();
|
|
}
|
|
void Waterfall::Impl::prepare_frame(const Prepare_Render_Context& context) {
|
|
prepare_render_frame(context.frame.render_state, 1);
|
|
render_partition(0);
|
|
}
|
|
void Waterfall::Impl::paint(Painter& painter,
|
|
const Paint_Render_Context& context) {
|
|
paint_render_frame(painter, context.frame.render_state);
|
|
}
|
|
}
|