四个方向 分块渲染
This commit is contained in:
@@ -81,7 +81,8 @@ private:
|
||||
coordinates,
|
||||
state.orientation == Orientation::Horizontal ? static_cast<double>(state.x)
|
||||
: static_cast<double>(state.y),
|
||||
static_cast<double>(state.pixel_length)
|
||||
static_cast<double>(state.pixel_length),
|
||||
state.orientation
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ struct Axis_Transform {
|
||||
Range coordinate_range;
|
||||
double pixel_origin{};
|
||||
double pixel_length{};
|
||||
Orientation orientation = Orientation::Horizontal;
|
||||
|
||||
[[nodiscard]] double coord_to_pixel(double coordinate) const noexcept {
|
||||
const double span = coordinate_range.length();
|
||||
@@ -23,6 +24,10 @@ struct Axis_Transform {
|
||||
return coordinate_range.origin;
|
||||
return coordinate_range.origin + (pixel - pixel_origin) / pixel_length * coordinate_range.length();
|
||||
}
|
||||
|
||||
[[nodiscard]] double point_to_coord(PointF point) const noexcept {
|
||||
return pixel_to_coord(orientation == Orientation::Horizontal ? point.x : point.y);
|
||||
}
|
||||
};
|
||||
|
||||
struct Axis_Base_Properties {
|
||||
|
||||
@@ -2,12 +2,26 @@
|
||||
#include "Heatmap_Utils.h"
|
||||
#include "Plottable_Real_Time_Data.h"
|
||||
#include "../render/Blend2D_Cache.h"
|
||||
#include "../renderable/Render_Partition.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <deque>
|
||||
namespace renderive {
|
||||
namespace detail {
|
||||
namespace {
|
||||
using Afterglow_History = Plottable_History_Real_Time_Data<std::vector<double>, std::deque<std::vector<double>>>;
|
||||
struct Afterglow_Render_Frame {
|
||||
Adaptive_Render_Partitioner partitioner;
|
||||
Axis_Raster_Layout layout;
|
||||
std::vector<double> intensity;
|
||||
std::vector<Pixel> pixels;
|
||||
int source_width{};
|
||||
int source_height{};
|
||||
int active_partitions{1};
|
||||
double maximum{1.0};
|
||||
std::size_t work_size{};
|
||||
bool valid{};
|
||||
};
|
||||
}
|
||||
struct Afterglow_Control::Impl {
|
||||
Impl(Afterglow_Control& owner, std::shared_ptr<Frequency_Axis> frequency, std::shared_ptr<Axis> power)
|
||||
@@ -15,6 +29,7 @@ struct Afterglow_Control::Impl {
|
||||
std::shared_ptr<Frequency_Axis> frequency_axis;
|
||||
std::shared_ptr<Axis> power_axis;
|
||||
Afterglow_History history;
|
||||
Afterglow_Render_Frame render_frame;
|
||||
};
|
||||
Afterglow_Control::Afterglow_Control(Plot_Core& plot, const Afterglow_Properties& properties, std::shared_ptr<Frequency_Axis> frequency_axis, std::shared_ptr<Axis> power_axis)
|
||||
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(*this, std::move(frequency_axis), std::move(power_axis))) {}
|
||||
@@ -49,9 +64,47 @@ void Afterglow_Control::append_spectrum(std::pmr::vector<double>&& values) {
|
||||
void Afterglow_Control::publish() {
|
||||
publish_properties();
|
||||
}
|
||||
void Afterglow_Control::paint(Painter& painter, const Render_State_View& view) {
|
||||
|
||||
void Afterglow_Control::build_paint_task_graph(Renderable_Task_Graph& graph) {
|
||||
const auto prepare = graph.emplace([this](const Scene_Render_Context&) {
|
||||
prepare_render_frame(render_state_view());
|
||||
}, "prepare afterglow");
|
||||
std::array<Renderable_Task_Graph::Task, maximum_render_partitions> accumulation;
|
||||
std::array<Renderable_Task_Graph::Task, maximum_render_partitions> coloring;
|
||||
for (int index = 0; index < maximum_render_partitions; ++index) {
|
||||
accumulation[static_cast<std::size_t>(index)] = graph.emplace(
|
||||
[this, index](const Scene_Render_Context&) {
|
||||
accumulate_partition(render_state_view(), index);
|
||||
},
|
||||
"accumulate afterglow partition");
|
||||
graph.precede(prepare, accumulation[static_cast<std::size_t>(index)]);
|
||||
}
|
||||
const auto normalize = graph.emplace([this](const Scene_Render_Context&) {
|
||||
normalize_render_frame();
|
||||
}, "normalize afterglow");
|
||||
for (const auto task : accumulation)
|
||||
graph.precede(task, normalize);
|
||||
for (int index = 0; index < maximum_render_partitions; ++index) {
|
||||
coloring[static_cast<std::size_t>(index)] = graph.emplace(
|
||||
[this, index](const Scene_Render_Context&) {
|
||||
color_partition(render_state_view(), index);
|
||||
},
|
||||
"color afterglow partition");
|
||||
graph.precede(normalize, coloring[static_cast<std::size_t>(index)]);
|
||||
}
|
||||
const auto compose = add_paint_task(
|
||||
graph, "compose afterglow",
|
||||
[this](Painter& painter, const Render_State_View& view) { paint(painter, view); });
|
||||
for (const auto task : coloring)
|
||||
graph.precede(task, compose);
|
||||
}
|
||||
|
||||
void Afterglow_Control::prepare_render_frame(const Render_State_View& view) {
|
||||
const auto& state = render_properties(view);
|
||||
const auto& history = view.get(impl_->history);
|
||||
auto& output = impl_->render_frame;
|
||||
output.valid = false;
|
||||
output.work_size = 0;
|
||||
if (history.empty())
|
||||
return;
|
||||
const int width = std::min(state.frequency_point_size.get(), static_cast<int>(history.back().size()));
|
||||
@@ -60,27 +113,83 @@ void Afterglow_Control::paint(Painter& painter, const Render_State_View& view) {
|
||||
: std::max(1, static_cast<int>(impl_->power_axis->transform(view).pixel_length));
|
||||
if (width <= 0 || height <= 0)
|
||||
return;
|
||||
std::vector<double> intensity(static_cast<std::size_t>(width) * height);
|
||||
const auto layout = axis_raster_layout(impl_->frequency_axis->transform(view),
|
||||
impl_->power_axis->transform(view),
|
||||
state.frequency_range, state.power_range,
|
||||
width, height);
|
||||
if (!layout.valid())
|
||||
return;
|
||||
output.layout = layout;
|
||||
output.source_width = width;
|
||||
output.source_height = height;
|
||||
output.work_size = static_cast<std::size_t>(width) * height;
|
||||
output.intensity.resize(output.work_size);
|
||||
output.pixels.resize(output.work_size);
|
||||
output.active_partitions =
|
||||
output.partitioner.begin(state.partition_count.get(), output.work_size);
|
||||
output.valid = true;
|
||||
}
|
||||
|
||||
void Afterglow_Control::accumulate_partition(const Render_State_View& view,
|
||||
int partition_index) {
|
||||
auto& output = impl_->render_frame;
|
||||
if (!output.valid || partition_index >= output.active_partitions)
|
||||
return;
|
||||
const auto& state = render_properties(view);
|
||||
const auto& history = view.get(impl_->history);
|
||||
const auto columns = render_partition_range(static_cast<std::size_t>(output.source_width),
|
||||
partition_index, output.active_partitions);
|
||||
for (std::size_t x = columns.first; x < columns.last; ++x)
|
||||
for (int y = 0; y < output.source_height; ++y)
|
||||
output.intensity[static_cast<std::size_t>(y) * output.source_width + x] = 0.0;
|
||||
double weight = 1.0;
|
||||
const double decay = 1.0 - state.attenuation_rate.get();
|
||||
for (auto iterator = history.rbegin(); iterator != history.rend(); ++iterator) {
|
||||
const int count = std::min(width, static_cast<int>(iterator->size()));
|
||||
for (int x = 0; x < count; ++x) {
|
||||
const double normalized = normalized_value((*iterator)[static_cast<std::size_t>(x)], state.power_range);
|
||||
const int y = std::clamp(height - 1 - static_cast<int>(normalized * (height - 1)), 0, height - 1);
|
||||
intensity[static_cast<std::size_t>(y) * width + x] += weight;
|
||||
if (state.interpolate && y + 1 < height)
|
||||
intensity[static_cast<std::size_t>(y + 1) * width + x] += weight * 0.35;
|
||||
const std::size_t count = std::min<std::size_t>(output.source_width, iterator->size());
|
||||
for (std::size_t x = columns.first; x < std::min(columns.last, count); ++x) {
|
||||
const double normalized = normalized_value((*iterator)[x], state.power_range);
|
||||
const int y = std::clamp(static_cast<int>(normalized * (output.source_height - 1)),
|
||||
0, output.source_height - 1);
|
||||
output.intensity[static_cast<std::size_t>(y) * output.source_width + x] += weight;
|
||||
if (state.interpolate && y + 1 < output.source_height)
|
||||
output.intensity[static_cast<std::size_t>(y + 1) * output.source_width + x] += weight * 0.35;
|
||||
}
|
||||
weight *= decay;
|
||||
if (weight < 0.01)
|
||||
break;
|
||||
}
|
||||
const double maximum = std::max(1.0, *std::max_element(intensity.begin(), intensity.end()));
|
||||
std::vector<Pixel> pixels(intensity.size());
|
||||
for (std::size_t index = 0; index < pixels.size(); ++index)
|
||||
pixels[index] = state.color_map.at_normalized(intensity[index] / maximum);
|
||||
painter.heatmap(mapped_rect(impl_->frequency_axis->transform(view), impl_->power_axis->transform(view), state.frequency_range, state.power_range), width, height, pixels, Image_Interpolation_Mode::Bilinear);
|
||||
}
|
||||
|
||||
void Afterglow_Control::normalize_render_frame() {
|
||||
auto& output = impl_->render_frame;
|
||||
if (output.valid)
|
||||
output.maximum = std::max(1.0, *std::max_element(output.intensity.begin(),
|
||||
output.intensity.end()));
|
||||
}
|
||||
|
||||
void Afterglow_Control::color_partition(const Render_State_View& view,
|
||||
int partition_index) {
|
||||
auto& output = impl_->render_frame;
|
||||
if (!output.valid || partition_index >= output.active_partitions)
|
||||
return;
|
||||
const auto& state = render_properties(view);
|
||||
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));
|
||||
output.pixels[output.layout.index(x, y, output.source_width, output.source_height)] =
|
||||
state.color_map.at_normalized(output.intensity[cell] / output.maximum);
|
||||
}
|
||||
}
|
||||
|
||||
void Afterglow_Control::paint(Painter& painter, const Render_State_View& view) {
|
||||
auto& output = impl_->render_frame;
|
||||
if (!output.valid)
|
||||
return;
|
||||
painter.heatmap(output.layout.target, output.layout.width, output.layout.height,
|
||||
output.pixels, Image_Interpolation_Mode::Bilinear);
|
||||
output.partitioner.finish(output.work_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ struct Afterglow_Properties {
|
||||
Range power_range{0.0, 10.0};
|
||||
Nonnegative_Count frequency_point_size;
|
||||
Nonnegative_Count power_point_size;
|
||||
Nonnegative_Count partition_count;
|
||||
bool interpolate = true;
|
||||
Unit_Interval attenuation_rate{0.2};
|
||||
Color_Map color_map;
|
||||
@@ -29,9 +30,14 @@ public:
|
||||
}
|
||||
protected:
|
||||
void paint(Painter& painter, const Render_State_View& state) override;
|
||||
void build_paint_task_graph(Renderable_Task_Graph& graph) override;
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
void prepare_render_frame(const Render_State_View& state);
|
||||
void accumulate_partition(const Render_State_View& state, int partition_index);
|
||||
void normalize_render_frame();
|
||||
void color_partition(const Render_State_View& state, int partition_index);
|
||||
void publish() override;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "Constellation_Diagram.h"
|
||||
#include "Plottable_Real_Time_Data.h"
|
||||
#include "Heatmap_Utils.h"
|
||||
#include "../render/Blend2D_Cache.h"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -58,13 +59,17 @@ void Constellation_Diagram_Control::paint(Painter& painter, const Render_State_V
|
||||
for(int index = 0; index < count; ++index) {
|
||||
const double angle = state.phase_offset_radians + 2.0 * std::numbers::pi * index / count;
|
||||
const PointF point{state.i_range.center() + std::cos(angle) * radius, state.q_range.center() + std::sin(angle) * radius};
|
||||
painter.circle({x.coord_to_pixel(point.x), y.coord_to_pixel(point.y)}, 3.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::Solid});
|
||||
painter.circle(mapped_point(x, point.x, y, point.y), 3.0,
|
||||
Pen{state.anchor_color},
|
||||
Brush{state.anchor_color, Brush_Style::Solid});
|
||||
}
|
||||
const auto cutoff = std::chrono::steady_clock::now() - std::chrono::milliseconds(state.point_lifetime_ms.get());
|
||||
for(const auto& value : points) {
|
||||
if(value.time < cutoff)
|
||||
continue;
|
||||
painter.circle({x.coord_to_pixel(value.point.x), y.coord_to_pixel(value.point.y)}, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::Solid});
|
||||
painter.circle(mapped_point(x, value.point.x, y, value.point.y), 2.0,
|
||||
Pen{state.point_color},
|
||||
Brush{state.point_color, Brush_Style::Solid});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../axis/Axis.h"
|
||||
#include "Heatmap_Utils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -141,8 +142,7 @@ inline std::vector<PointF> curve_points(std::span<const double> values,
|
||||
points.reserve(samples.size());
|
||||
for (const auto& sample : samples) {
|
||||
if (std::isfinite(sample.coordinate) && std::isfinite(sample.value))
|
||||
points.push_back({x_axis.coord_to_pixel(sample.coordinate),
|
||||
y_axis.coord_to_pixel(sample.value)});
|
||||
points.push_back(mapped_point(x_axis, sample.coordinate, y_axis, sample.value));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "Frequency_Trace.h"
|
||||
#include "Plottable_Real_Time_Data.h"
|
||||
#include "Heatmap_Utils.h"
|
||||
#include "../render/Blend2D_Cache.h"
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
@@ -47,7 +48,7 @@ void Frequency_Trace_Control::paint(Painter& painter, const Render_State_View& v
|
||||
std::vector<PointF> points;
|
||||
points.reserve(samples.size());
|
||||
for(const auto& [tick, value] : samples)
|
||||
points.push_back({x.coord_to_pixel(tick), y.coord_to_pixel(value)});
|
||||
points.push_back(mapped_point(x, tick, y, value));
|
||||
painter.polyline(points, state.pen);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,77 @@
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
namespace renderive::detail {
|
||||
inline RectF mapped_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical, Range horizontal_range, Range vertical_range) {
|
||||
const double x1 = horizontal.coord_to_pixel(horizontal_range.origin);
|
||||
const double x2 = horizontal.coord_to_pixel(horizontal_range.target);
|
||||
const double y1 = vertical.coord_to_pixel(vertical_range.origin);
|
||||
const double y2 = vertical.coord_to_pixel(vertical_range.target);
|
||||
return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)};
|
||||
inline bool axes_are_orthogonal(const Axis_Transform& first,
|
||||
const Axis_Transform& second) noexcept {
|
||||
return first.orientation != second.orientation;
|
||||
}
|
||||
|
||||
inline PointF mapped_point(const Axis_Transform& first,
|
||||
double first_coordinate,
|
||||
const Axis_Transform& second,
|
||||
double second_coordinate) noexcept {
|
||||
if (!axes_are_orthogonal(first, second))
|
||||
return {};
|
||||
const double first_pixel = first.coord_to_pixel(first_coordinate);
|
||||
const double second_pixel = second.coord_to_pixel(second_coordinate);
|
||||
return first.orientation == Orientation::Horizontal
|
||||
? PointF{first_pixel, second_pixel}
|
||||
: PointF{second_pixel, first_pixel};
|
||||
}
|
||||
|
||||
inline RectF mapped_rect(const Axis_Transform& first,
|
||||
const Axis_Transform& second,
|
||||
Range first_range,
|
||||
Range second_range) noexcept {
|
||||
if (!axes_are_orthogonal(first, second))
|
||||
return {};
|
||||
const PointF origin = mapped_point(first, first_range.origin, second, second_range.origin);
|
||||
const PointF target = mapped_point(first, first_range.target, second, second_range.target);
|
||||
return {std::min(origin.x, target.x), std::min(origin.y, target.y),
|
||||
std::abs(target.x - origin.x), std::abs(target.y - origin.y)};
|
||||
}
|
||||
|
||||
struct Axis_Raster_Layout {
|
||||
int width{};
|
||||
int height{};
|
||||
RectF target;
|
||||
bool first_reversed{};
|
||||
bool second_reversed{};
|
||||
bool first_is_horizontal{};
|
||||
|
||||
[[nodiscard]] bool valid() const noexcept {
|
||||
return width > 0 && height > 0 && !target.empty();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t index(int first, int second,
|
||||
int first_count, int second_count) const noexcept {
|
||||
if (first_reversed)
|
||||
first = first_count - 1 - first;
|
||||
if (second_reversed)
|
||||
second = second_count - 1 - second;
|
||||
const int x = first_is_horizontal ? first : second;
|
||||
const int y = first_is_horizontal ? second : first;
|
||||
return static_cast<std::size_t>(y) * width + x;
|
||||
}
|
||||
};
|
||||
|
||||
inline Axis_Raster_Layout axis_raster_layout(const Axis_Transform& first,
|
||||
const Axis_Transform& second,
|
||||
Range first_range,
|
||||
Range second_range,
|
||||
int first_count,
|
||||
int second_count) noexcept {
|
||||
if (!axes_are_orthogonal(first, second) || first_count <= 0 || second_count <= 0)
|
||||
return {};
|
||||
const bool first_horizontal = first.orientation == Orientation::Horizontal;
|
||||
return {
|
||||
first_horizontal ? first_count : second_count,
|
||||
first_horizontal ? second_count : first_count,
|
||||
mapped_rect(first, second, first_range, second_range),
|
||||
first.coord_to_pixel(first_range.origin) > first.coord_to_pixel(first_range.target),
|
||||
second.coord_to_pixel(second_range.origin) > second.coord_to_pixel(second_range.target),
|
||||
first_horizontal
|
||||
};
|
||||
}
|
||||
inline double normalized_value(double value, Range range) {
|
||||
if(range.length() == 0.0)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "Selection_Rectangle_Overlay.h"
|
||||
#include "Plottable_Real_Time_Data.h"
|
||||
#include "Heatmap_Utils.h"
|
||||
#include "../render/Blend2D_Cache.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -14,12 +15,8 @@ struct Selection_Interaction {
|
||||
PointF selection_current{};
|
||||
};
|
||||
using Selection_Interaction_State = Double_State_Strategy<Selection_Interaction_Base, Selection_Interaction>;
|
||||
RectF axis_content_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) {
|
||||
const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin);
|
||||
const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target);
|
||||
const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin);
|
||||
const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target);
|
||||
return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)};
|
||||
RectF axis_content_rect(const Axis_Transform& first, const Axis_Transform& second) {
|
||||
return mapped_rect(first, second, first.coordinate_range, second.coordinate_range);
|
||||
}
|
||||
}
|
||||
struct Selection_Rectangle_Overlay_Control::Impl {
|
||||
@@ -88,9 +85,11 @@ void Selection_Rectangle_Overlay_Control::handle_event(const Event& event) {
|
||||
return;
|
||||
const Axis_Transform horizontal = impl_->horizontal_axis->transform();
|
||||
const Axis_Transform vertical = impl_->vertical_axis->transform();
|
||||
const RectF region{horizontal.pixel_to_coord(start.x), vertical.pixel_to_coord(start.y),
|
||||
horizontal.pixel_to_coord(pointer.position.x) - horizontal.pixel_to_coord(start.x),
|
||||
vertical.pixel_to_coord(pointer.position.y) - vertical.pixel_to_coord(start.y)};
|
||||
const double first_start = horizontal.point_to_coord(start);
|
||||
const double second_start = vertical.point_to_coord(start);
|
||||
const RectF region{first_start, second_start,
|
||||
horizontal.point_to_coord(pointer.position) - first_start,
|
||||
vertical.point_to_coord(pointer.position) - second_start};
|
||||
if(std::abs(region.width) > 1e-9 && std::abs(region.height) > 1e-9)
|
||||
impl_->regions.update(region.normalized());
|
||||
event.accept();
|
||||
@@ -106,7 +105,9 @@ void Selection_Rectangle_Overlay_Control::paint(Painter& painter, const Render_S
|
||||
const auto horizontal = impl_->horizontal_axis->transform(view);
|
||||
const auto vertical = impl_->vertical_axis->transform(view);
|
||||
for(const RectF& region : view.get(impl_->regions)) {
|
||||
RectF pixels{horizontal.coord_to_pixel(region.x), vertical.coord_to_pixel(region.y), horizontal.coord_to_pixel(region.right()) - horizontal.coord_to_pixel(region.x), vertical.coord_to_pixel(region.bottom()) - vertical.coord_to_pixel(region.y)};
|
||||
const RectF pixels = mapped_rect(horizontal, vertical,
|
||||
{region.x, region.right()},
|
||||
{region.y, region.bottom()});
|
||||
painter.rect(pixels, state.selection_border_pen, state.selection_brush);
|
||||
std::ostringstream text;
|
||||
text << region.width << " x " << region.height;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#include "Spectrum.h"
|
||||
#include "Curve_Sampling.h"
|
||||
#include "Plottable_Real_Time_Data.h"
|
||||
#include "../render/Blend2D_Cache.h"
|
||||
#include "../renderable/Render_Partition.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
@@ -20,23 +23,56 @@ struct Spectrum_Interaction {
|
||||
Hover_Tooltip_Runtime tooltip;
|
||||
};
|
||||
using Spectrum_Interaction_State = Double_State_Strategy<Spectrum_Interaction_Base, Spectrum_Interaction>;
|
||||
RectF axes_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) {
|
||||
const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin);
|
||||
const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target);
|
||||
const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin);
|
||||
const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target);
|
||||
return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)};
|
||||
struct Spectrum_Render_Frame {
|
||||
Adaptive_Render_Partitioner partitioner;
|
||||
std::array<Blend2D_Color_Cache, maximum_render_partitions> layers;
|
||||
int active_partitions{1};
|
||||
std::size_t work_size{};
|
||||
bool valid{};
|
||||
};
|
||||
|
||||
struct Curve_Partition {
|
||||
std::span<const double> values;
|
||||
Range domain;
|
||||
Range clip_domain;
|
||||
};
|
||||
|
||||
Curve_Partition curve_partition(std::span<const double> values, Range domain,
|
||||
int partition_index, int partition_count) {
|
||||
if (values.size() < 2 || partition_count <= 0)
|
||||
return {};
|
||||
const std::size_t segment_count = values.size() - 1;
|
||||
const auto core = render_partition_range(segment_count, partition_index, partition_count);
|
||||
if (core.first == core.last)
|
||||
return {};
|
||||
const std::size_t first = core.first == 0 ? 0 : core.first - 1;
|
||||
const std::size_t last = std::min(segment_count, core.last + 1);
|
||||
const auto coordinate = [domain, segment_count](std::size_t index) {
|
||||
return domain.origin + domain.length() * static_cast<double>(index) /
|
||||
static_cast<double>(segment_count);
|
||||
};
|
||||
return {
|
||||
values.subspan(first, last - first + 1),
|
||||
{coordinate(first), coordinate(last)},
|
||||
{coordinate(core.first), coordinate(core.last)}
|
||||
};
|
||||
}
|
||||
void draw_curve(Painter& painter, std::span<const double> values, Range domain, const Axis_Transform& x_axis, const Axis_Transform& y_axis, bool visible_only, Line_Interpolation_Mode interpolation, const Pen& pen, const Brush& brush) {
|
||||
auto points = curve_points(values, domain, x_axis, y_axis, visible_only, interpolation);
|
||||
void draw_curve(Painter& painter, std::span<const double> values, Range domain,
|
||||
const Axis_Transform& frequency_axis, const Axis_Transform& power_axis,
|
||||
bool visible_only, Line_Interpolation_Mode interpolation,
|
||||
const Pen& pen, const Brush& brush) {
|
||||
auto points = curve_points(values, domain, frequency_axis, power_axis,
|
||||
visible_only, interpolation);
|
||||
if(points.size() < 2)
|
||||
return;
|
||||
if(brush.enabled()) {
|
||||
std::vector<PointF> polygon;
|
||||
polygon.reserve(points.size() + 2);
|
||||
polygon.push_back({points.front().x, y_axis.coord_to_pixel(y_axis.coordinate_range.target)});
|
||||
polygon.push_back(mapped_point(frequency_axis, domain.origin, power_axis,
|
||||
power_axis.coordinate_range.target));
|
||||
polygon.insert(polygon.end(), points.begin(), points.end());
|
||||
polygon.push_back({points.back().x, y_axis.coord_to_pixel(y_axis.coordinate_range.target)});
|
||||
polygon.push_back(mapped_point(frequency_axis, domain.target, power_axis,
|
||||
power_axis.coordinate_range.target));
|
||||
painter.polygon(polygon, Pen{.style = Line_Style::None}, brush);
|
||||
}
|
||||
painter.polyline(points, pen);
|
||||
@@ -62,6 +98,7 @@ struct Spectrum_Control::Impl {
|
||||
Plottable_Latest_Real_Time_Data<Spectrum_Frame> frame;
|
||||
Spectrum_Interaction_State interaction;
|
||||
std::mutex frame_update_mutex;
|
||||
Spectrum_Render_Frame render_frame;
|
||||
};
|
||||
Spectrum_Control::Spectrum_Control(Plot_Core& plot, const Spectrum_Properties& properties, std::shared_ptr<Frequency_Axis> frequency_axis, std::shared_ptr<Axis> power_axis)
|
||||
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(*this, std::move(frequency_axis), std::move(power_axis))) {}
|
||||
@@ -205,32 +242,116 @@ void Spectrum_Control::publish() {
|
||||
publish_properties();
|
||||
impl_->interaction.publish();
|
||||
}
|
||||
|
||||
void Spectrum_Control::build_paint_task_graph(Renderable_Task_Graph& graph) {
|
||||
const auto prepare = graph.emplace([this](const Scene_Render_Context&) {
|
||||
prepare_render_frame(render_state_view());
|
||||
}, "prepare spectrum");
|
||||
std::array<Renderable_Task_Graph::Task, maximum_render_partitions> partitions;
|
||||
for (int index = 0; index < maximum_render_partitions; ++index) {
|
||||
partitions[static_cast<std::size_t>(index)] = graph.emplace(
|
||||
[this, index](const Scene_Render_Context&) {
|
||||
render_partition(render_state_view(), index);
|
||||
},
|
||||
"paint spectrum partition");
|
||||
graph.precede(prepare, partitions[static_cast<std::size_t>(index)]);
|
||||
}
|
||||
const auto compose = add_paint_task(
|
||||
graph, "compose spectrum",
|
||||
[this](Painter& painter, const Render_State_View& view) { paint(painter, view); });
|
||||
for (const auto task : partitions)
|
||||
graph.precede(task, compose);
|
||||
}
|
||||
|
||||
void Spectrum_Control::prepare_render_frame(const Render_State_View& view) {
|
||||
const auto& state = render_properties(view);
|
||||
const auto& published_frame = view.get(impl_->frame);
|
||||
auto& output = impl_->render_frame;
|
||||
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
|
||||
const Axis_Transform power_axis = impl_->power_axis->transform(view);
|
||||
output.valid = axes_are_orthogonal(frequency_axis, power_axis);
|
||||
output.work_size = published_frame ? published_frame->samples.size() : 0;
|
||||
output.active_partitions =
|
||||
output.partitioner.begin(state.partition_count.get(), output.work_size);
|
||||
for (int index = 0; index < output.active_partitions; ++index)
|
||||
output.layers[static_cast<std::size_t>(index)].clear();
|
||||
}
|
||||
|
||||
void Spectrum_Control::render_partition(const Render_State_View& view,
|
||||
int partition_index) {
|
||||
auto& output = impl_->render_frame;
|
||||
if (!output.valid || partition_index >= output.active_partitions)
|
||||
return;
|
||||
const auto& state = render_properties(view);
|
||||
const auto& published_frame = view.get(impl_->frame);
|
||||
if (!published_frame)
|
||||
return;
|
||||
const Spectrum_Frame& frame = *published_frame;
|
||||
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
|
||||
const Axis_Transform power_axis = impl_->power_axis->transform(view);
|
||||
const auto current = curve_partition(frame.samples, state.frequency_range,
|
||||
partition_index, output.active_partitions);
|
||||
if (current.values.empty())
|
||||
return;
|
||||
|
||||
Painter painter(output.layers[static_cast<std::size_t>(partition_index)], viewport_size());
|
||||
if (!painter)
|
||||
return;
|
||||
painter.clip(mapped_rect(frequency_axis, power_axis, current.clip_domain,
|
||||
power_axis.coordinate_range));
|
||||
if (state.max_hold_visible) {
|
||||
const auto maximum = curve_partition(frame.maxima, state.frequency_range,
|
||||
partition_index, output.active_partitions);
|
||||
draw_curve(painter, maximum.values, maximum.domain, frequency_axis, power_axis,
|
||||
state.visible_range_only, state.interpolation_mode,
|
||||
state.max_pen, state.max_brush);
|
||||
}
|
||||
if (state.min_hold_visible) {
|
||||
const auto minimum = curve_partition(frame.minima, state.frequency_range,
|
||||
partition_index, output.active_partitions);
|
||||
draw_curve(painter, minimum.values, minimum.domain, frequency_axis, power_axis,
|
||||
state.visible_range_only, state.interpolation_mode,
|
||||
state.min_pen, state.min_brush);
|
||||
}
|
||||
draw_curve(painter, current.values, current.domain, frequency_axis, power_axis,
|
||||
state.visible_range_only, state.interpolation_mode,
|
||||
state.current_pen, state.current_brush);
|
||||
}
|
||||
|
||||
void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) {
|
||||
const auto& state = render_properties(view);
|
||||
const auto& published_frame = view.get(impl_->frame);
|
||||
const Spectrum_Frame empty_frame;
|
||||
const Spectrum_Frame& frame = published_frame ? *published_frame : empty_frame;
|
||||
const auto& interaction = view.get(impl_->interaction);
|
||||
const Axis_Transform horizontal = impl_->frequency_axis->transform(view);
|
||||
const Axis_Transform vertical = impl_->power_axis->transform(view);
|
||||
const RectF content = axes_rect(horizontal, vertical);
|
||||
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
|
||||
const Axis_Transform power_axis = impl_->power_axis->transform(view);
|
||||
const RectF content = mapped_rect(frequency_axis, power_axis,
|
||||
frequency_axis.coordinate_range,
|
||||
power_axis.coordinate_range);
|
||||
if (content.empty())
|
||||
return;
|
||||
auto& output = impl_->render_frame;
|
||||
if(state.sweep_region_visible) {
|
||||
const double first = horizontal.coord_to_pixel(state.sweep_frequency_range.origin);
|
||||
const double last = horizontal.coord_to_pixel(state.sweep_frequency_range.target);
|
||||
painter.rect({std::min(first, last), content.y, std::abs(last - first), content.height}, Pen{.style = Line_Style::None}, state.sweep_region_brush);
|
||||
painter.rect(mapped_rect(frequency_axis, power_axis, state.sweep_frequency_range,
|
||||
power_axis.coordinate_range),
|
||||
Pen{.style = Line_Style::None}, state.sweep_region_brush);
|
||||
}
|
||||
if(state.max_hold_visible)
|
||||
draw_curve(painter, frame.maxima, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.max_pen, state.max_brush);
|
||||
if(state.min_hold_visible)
|
||||
draw_curve(painter, frame.minima, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.min_pen, state.min_brush);
|
||||
draw_curve(painter, frame.samples, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.current_pen, state.current_brush);
|
||||
for (int index = 0; output.valid && index < output.active_partitions; ++index)
|
||||
painter.composite(output.layers[static_cast<std::size_t>(index)]);
|
||||
if(state.middle_frequency_pen.enabled()) {
|
||||
const double x = horizontal.coord_to_pixel(state.center_frequency);
|
||||
painter.line({x, content.y}, {x, content.bottom()}, state.middle_frequency_pen);
|
||||
painter.line(mapped_point(frequency_axis, state.center_frequency, power_axis,
|
||||
power_axis.coordinate_range.origin),
|
||||
mapped_point(frequency_axis, state.center_frequency, power_axis,
|
||||
power_axis.coordinate_range.target),
|
||||
state.middle_frequency_pen);
|
||||
}
|
||||
for(std::size_t index = 0; index < interaction.markers.size(); ++index) {
|
||||
const double x = horizontal.coord_to_pixel(interaction.markers[index]);
|
||||
painter.line({x, content.y}, {x, content.bottom()}, static_cast<int>(index) == interaction.selected_marker ? state.selected_marker_pen : state.marker_pen);
|
||||
painter.line(mapped_point(frequency_axis, interaction.markers[index], power_axis,
|
||||
power_axis.coordinate_range.origin),
|
||||
mapped_point(frequency_axis, interaction.markers[index], power_axis,
|
||||
power_axis.coordinate_range.target),
|
||||
static_cast<int>(index) == interaction.selected_marker ? state.selected_marker_pen : state.marker_pen);
|
||||
}
|
||||
if(!frame.samples.empty() && (state.max_marker_visible || state.use_min_marker)) {
|
||||
const auto draw_extreme = [&](bool maximum) {
|
||||
@@ -238,7 +359,7 @@ void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) {
|
||||
const std::size_t index = static_cast<std::size_t>(std::distance(frame.samples.begin(), iterator));
|
||||
const double denominator = frame.samples.size() > 1 ? frame.samples.size() - 1.0 : 1.0;
|
||||
const double frequency = state.frequency_range.origin + state.frequency_range.length() * index / denominator;
|
||||
const PointF point{horizontal.coord_to_pixel(frequency), vertical.coord_to_pixel(*iterator)};
|
||||
const PointF point = mapped_point(frequency_axis, frequency, power_axis, *iterator);
|
||||
const Pen& pen = maximum ? state.max_pen : state.min_pen;
|
||||
painter.circle(point, 3.0, pen, Brush{pen.color, Brush_Style::Solid});
|
||||
};
|
||||
@@ -248,7 +369,7 @@ void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) {
|
||||
draw_extreme(false);
|
||||
}
|
||||
if(state.tooltip_enabled && interaction.tooltip.active && content.contains(interaction.tooltip.position)) {
|
||||
const double frequency = horizontal.pixel_to_coord(interaction.tooltip.position.x);
|
||||
const double frequency = frequency_axis.point_to_coord(interaction.tooltip.position);
|
||||
bool ok{};
|
||||
const double power = spectrum_power_at(state, frame, frequency, ok);
|
||||
if(ok) {
|
||||
@@ -259,6 +380,7 @@ void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) {
|
||||
painter.text({box.x + 4.0, box.y + 3.0}, text.str(), state.tooltip_font, state.tooltip_text_pen);
|
||||
}
|
||||
}
|
||||
output.partitioner.finish(output.work_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
namespace renderive {
|
||||
struct Spectrum_Properties : Hover_Tooltip_Properties {
|
||||
Nonnegative_Count frequency_point_size;
|
||||
Nonnegative_Count partition_count;
|
||||
Range frequency_range{};
|
||||
double center_frequency = 50.0;
|
||||
Range sweep_frequency_range{40.0, 60.0};
|
||||
@@ -60,9 +61,12 @@ public:
|
||||
void handle_event(const Event& event) override;
|
||||
protected:
|
||||
void paint(Painter& painter, const Render_State_View& state) override;
|
||||
void build_paint_task_graph(Renderable_Task_Graph& graph) override;
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
void prepare_render_frame(const Render_State_View& state);
|
||||
void render_partition(const Render_State_View& state, int partition_index);
|
||||
void publish() override;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,13 +9,6 @@ namespace renderive {
|
||||
namespace detail {
|
||||
namespace {
|
||||
using Sweep_Spectrum_History = Plottable_History_Real_Time_Data<std::vector<double>, std::deque<std::vector<double>>>;
|
||||
RectF axis_content_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) {
|
||||
const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin);
|
||||
const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target);
|
||||
const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin);
|
||||
const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target);
|
||||
return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)};
|
||||
}
|
||||
std::vector<double> flatten(const std::deque<std::vector<double>>& blocks) {
|
||||
std::vector<double> values;
|
||||
for(const auto& block : blocks)
|
||||
@@ -74,9 +67,9 @@ void Sweep_Spectrum_Control::paint(Painter& painter, const Render_State_View& vi
|
||||
painter.polyline(curve_points(values, state.frequency_range, x, y, state.visible_range_only, state.interpolation_mode), state.pen);
|
||||
const double completed = std::min(1.0, static_cast<double>(blocks.size()) / state.block_count.get());
|
||||
const double frequency = state.frequency_range.origin + state.frequency_range.length() * completed;
|
||||
const RectF content = axis_content_rect(x, y);
|
||||
const double marker_x = x.coord_to_pixel(frequency);
|
||||
painter.line({marker_x, content.y}, {marker_x, content.bottom()}, state.current_frequency_pen);
|
||||
painter.line(mapped_point(x, frequency, y, y.coordinate_range.origin),
|
||||
mapped_point(x, frequency, y, y.coordinate_range.target),
|
||||
state.current_frequency_pen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "Waterfall.h"
|
||||
#include "Heatmap_Utils.h"
|
||||
#include "Plottable_Real_Time_Data.h"
|
||||
#include "../renderable/Render_Partition.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <deque>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
@@ -18,6 +20,17 @@ struct Waterfall_Interaction {
|
||||
};
|
||||
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_Render_Frame {
|
||||
Adaptive_Render_Partitioner partitioner;
|
||||
Axis_Raster_Layout layout;
|
||||
Frequency_Columns columns;
|
||||
std::vector<Pixel> pixels;
|
||||
int source_width{};
|
||||
int source_height{};
|
||||
int active_partitions{1};
|
||||
std::size_t work_size{};
|
||||
bool valid{};
|
||||
};
|
||||
}
|
||||
struct Waterfall_Control::Impl {
|
||||
Impl(Waterfall_Control& owner, std::shared_ptr<Frequency_Axis> frequency, std::shared_ptr<Time_Axis> time)
|
||||
@@ -26,6 +39,7 @@ struct Waterfall_Control::Impl {
|
||||
std::shared_ptr<Time_Axis> time_axis;
|
||||
Waterfall_History rows;
|
||||
Waterfall_Interaction_State interaction;
|
||||
Waterfall_Render_Frame render_frame;
|
||||
};
|
||||
Waterfall_Control::Waterfall_Control(Plot_Core& plot, const Waterfall_Properties& properties, std::shared_ptr<Frequency_Axis> frequency_axis, std::shared_ptr<Time_Axis> time_axis)
|
||||
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(*this, std::move(frequency_axis), std::move(time_axis))) {}
|
||||
@@ -82,35 +96,95 @@ void Waterfall_Control::publish() {
|
||||
publish_properties();
|
||||
impl_->interaction.publish();
|
||||
}
|
||||
void Waterfall_Control::paint(Painter& painter, const Render_State_View& view) {
|
||||
|
||||
void Waterfall_Control::build_paint_task_graph(Renderable_Task_Graph& graph) {
|
||||
const auto prepare = graph.emplace([this](const Scene_Render_Context&) {
|
||||
prepare_render_frame(render_state_view());
|
||||
}, "prepare waterfall");
|
||||
std::array<Renderable_Task_Graph::Task, maximum_render_partitions> partitions;
|
||||
for (int index = 0; index < maximum_render_partitions; ++index) {
|
||||
partitions[static_cast<std::size_t>(index)] = graph.emplace(
|
||||
[this, index](const Scene_Render_Context&) {
|
||||
render_partition(render_state_view(), index);
|
||||
},
|
||||
"raster waterfall partition");
|
||||
graph.precede(prepare, partitions[static_cast<std::size_t>(index)]);
|
||||
}
|
||||
const auto compose = add_paint_task(
|
||||
graph, "compose waterfall",
|
||||
[this](Painter& painter, const Render_State_View& view) { paint(painter, view); });
|
||||
for (const auto partition : partitions)
|
||||
graph.precede(partition, compose);
|
||||
}
|
||||
|
||||
void Waterfall_Control::prepare_render_frame(const Render_State_View& view) {
|
||||
const auto& state = render_properties(view);
|
||||
const auto& rows = view.get(impl_->rows);
|
||||
const auto& interaction = view.get(impl_->interaction);
|
||||
if(rows.empty())
|
||||
auto& output = impl_->render_frame;
|
||||
output.valid = false;
|
||||
output.work_size = 0;
|
||||
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)
|
||||
if (source_width <= 0 || height <= 0)
|
||||
return;
|
||||
const Axis_Transform horizontal = impl_->frequency_axis->transform(view);
|
||||
const auto columns = frequency_columns(state.frequency_range, horizontal.coordinate_range, source_width, state.visible_range_only);
|
||||
if(!columns)
|
||||
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;
|
||||
std::vector<Pixel> pixels(static_cast<std::size_t>(width) * height);
|
||||
for(int y = 0; y < height; ++y) {
|
||||
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.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 =
|
||||
output.partitioner.begin(state.partition_count.get(), output.work_size);
|
||||
output.valid = true;
|
||||
}
|
||||
|
||||
void Waterfall_Control::render_partition(const Render_State_View& view, int partition_index) {
|
||||
auto& output = impl_->render_frame;
|
||||
if (!output.valid || partition_index >= output.active_partitions)
|
||||
return;
|
||||
const auto& state = render_properties(view);
|
||||
const auto& rows = view.get(impl_->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;
|
||||
for(int x = 0; x < width; ++x)
|
||||
pixels[static_cast<std::size_t>(y) * width + x] = state.color_map.at_normalized(normalized_value(row[static_cast<std::size_t>(columns->first + x)], state.power_range));
|
||||
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 + x)], state.power_range));
|
||||
}
|
||||
const Axis_Transform vertical = impl_->time_axis->transform(view);
|
||||
const Range time_range{static_cast<double>(rows.front().tick), static_cast<double>(rows.back().tick)};
|
||||
RectF target = mapped_rect(horizontal, vertical, columns->range, time_range);
|
||||
if(target.height < 1.0)
|
||||
target.height = std::max(1.0, impl_->time_axis->transform(view).pixel_length);
|
||||
painter.heatmap(target, width, height, pixels, state.interpolation_mode);
|
||||
if(state.tooltip_enabled && interaction.tooltip.active && target.contains(interaction.tooltip.position)) {
|
||||
const double frequency = horizontal.pixel_to_coord(interaction.tooltip.position.x);
|
||||
}
|
||||
|
||||
void Waterfall_Control::paint(Painter& painter, const Render_State_View& view) {
|
||||
const auto& state = render_properties(view);
|
||||
const auto& interaction = view.get(impl_->interaction);
|
||||
auto& output = impl_->render_frame;
|
||||
if (!output.valid)
|
||||
return;
|
||||
painter.heatmap(output.layout.target, output.layout.width, output.layout.height,
|
||||
output.pixels, state.interpolation_mode);
|
||||
output.partitioner.finish(output.work_size);
|
||||
if(state.tooltip_enabled && interaction.tooltip.active && output.layout.target.contains(interaction.tooltip.position)) {
|
||||
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
|
||||
const double frequency = frequency_axis.point_to_coord(interaction.tooltip.position);
|
||||
std::ostringstream text;
|
||||
text << std::fixed << std::setprecision(2) << frequency << " Hz";
|
||||
const RectF box{interaction.tooltip.position.x + 8.0, interaction.tooltip.position.y + 8.0, 110.0, 24.0};
|
||||
|
||||
@@ -9,6 +9,7 @@ struct Waterfall_Properties : Hover_Tooltip_Properties {
|
||||
Range frequency_range{0.0, 10.0};
|
||||
Range power_range{0.0, 10.0};
|
||||
Nonnegative_Count frequency_bin_count;
|
||||
Nonnegative_Count partition_count;
|
||||
bool visible_range_only{true};
|
||||
Image_Interpolation_Mode interpolation_mode = Image_Interpolation_Mode::Nearest;
|
||||
Color_Map color_map;
|
||||
@@ -36,9 +37,12 @@ public:
|
||||
void handle_event(const Event& event) override;
|
||||
protected:
|
||||
void paint(Painter& painter, const Render_State_View& state) override;
|
||||
void build_paint_task_graph(Renderable_Task_Graph& graph) override;
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
void prepare_render_frame(const Render_State_View& state);
|
||||
void render_partition(const Render_State_View& state, int partition_index);
|
||||
void publish() override;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,6 +222,24 @@ Painter::~Painter() {
|
||||
context_.end();
|
||||
}
|
||||
|
||||
void Painter::clip(RectF value) {
|
||||
value = value.normalized();
|
||||
if (!active_ || value.empty())
|
||||
return;
|
||||
context_.clip_to_rect(BLRect(value.x, value.y, value.width, value.height));
|
||||
}
|
||||
|
||||
void Painter::composite(const Blend2D_Color_Cache& source) {
|
||||
if (!active_ || source.image_.is_empty())
|
||||
return;
|
||||
const Size source_size = source.size();
|
||||
context_.blit_image(BLRect(0.0, 0.0,
|
||||
static_cast<double>(source_size.width),
|
||||
static_cast<double>(source_size.height)),
|
||||
source.image_,
|
||||
BLRectI(0, 0, source_size.width, source_size.height));
|
||||
}
|
||||
|
||||
BLRgba Painter::rgba(Color color) noexcept {
|
||||
constexpr double scale = 1.0 / 255.0;
|
||||
return BLRgba(color.r * scale, color.g * scale, color.b * scale, color.a * scale);
|
||||
|
||||
@@ -34,6 +34,8 @@ public:
|
||||
Painter& operator=(const Painter&) = delete;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const noexcept { return active_; }
|
||||
void clip(RectF rect);
|
||||
void composite(const Blend2D_Color_Cache& source);
|
||||
void line(PointF first, PointF second, const Pen& pen);
|
||||
void polyline(std::span<const PointF> points, const Pen& pen);
|
||||
void polygon(std::span<const PointF> points, const Pen& pen, const Brush& brush);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <thread>
|
||||
|
||||
namespace renderive::detail {
|
||||
|
||||
inline constexpr int maximum_render_partitions = 16;
|
||||
|
||||
struct Render_Partition_Range {
|
||||
std::size_t first{};
|
||||
std::size_t last{};
|
||||
};
|
||||
|
||||
inline Render_Partition_Range render_partition_range(std::size_t size,
|
||||
int index,
|
||||
int count) noexcept {
|
||||
if (size == 0 || count <= 0 || index < 0 || index >= count)
|
||||
return {};
|
||||
return {size * static_cast<std::size_t>(index) / static_cast<std::size_t>(count),
|
||||
size * static_cast<std::size_t>(index + 1) / static_cast<std::size_t>(count)};
|
||||
}
|
||||
|
||||
class Adaptive_Render_Partitioner {
|
||||
public:
|
||||
Adaptive_Render_Partitioner() noexcept
|
||||
: automatic_count_(std::clamp(
|
||||
static_cast<int>(std::thread::hardware_concurrency()), 1,
|
||||
maximum_render_partitions)) {}
|
||||
|
||||
int begin(int configured_count, std::size_t work_size) noexcept {
|
||||
automatic_ = configured_count == 0;
|
||||
const int requested = automatic_ ? automatic_count_ : configured_count;
|
||||
const int useful = static_cast<int>(
|
||||
std::min<std::size_t>(maximum_render_partitions,
|
||||
std::max<std::size_t>(1, work_size)));
|
||||
active_count_ = std::clamp(requested, 1,
|
||||
std::min(maximum_render_partitions, useful));
|
||||
started_at_ = Clock::now();
|
||||
return active_count_;
|
||||
}
|
||||
|
||||
void finish(std::size_t work_size) noexcept {
|
||||
if (!automatic_)
|
||||
return;
|
||||
const auto elapsed =
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(Clock::now() - started_at_);
|
||||
constexpr auto target = std::chrono::microseconds(1500);
|
||||
if (elapsed > target * 2 && active_count_ < maximum_render_partitions &&
|
||||
work_size / static_cast<std::size_t>(active_count_) >= 512) {
|
||||
automatic_count_ = std::min(maximum_render_partitions, active_count_ + 1);
|
||||
} else if (elapsed < target / 2 && active_count_ > 1) {
|
||||
automatic_count_ = active_count_ - 1;
|
||||
} else {
|
||||
automatic_count_ = active_count_;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
Clock::time_point started_at_{};
|
||||
int automatic_count_{1};
|
||||
int active_count_{1};
|
||||
bool automatic_{true};
|
||||
};
|
||||
|
||||
} // namespace renderive::detail
|
||||
@@ -54,6 +54,35 @@ void Renderable::render(const ::Scene_Render_Context& context) {
|
||||
}
|
||||
}
|
||||
|
||||
void Renderable::build_task_graph(Renderable_Task_Graph& graph) {
|
||||
build_paint_task_graph(graph);
|
||||
}
|
||||
|
||||
void Renderable::build_paint_task_graph(Renderable_Task_Graph& graph) {
|
||||
add_paint_task(graph, "paint", [this](detail::Painter& painter,
|
||||
const Render_State_View& state) {
|
||||
paint(painter, state);
|
||||
});
|
||||
}
|
||||
|
||||
Renderable_Task_Graph::Task Renderable::add_paint_task(
|
||||
Renderable_Task_Graph& graph,
|
||||
std::string name,
|
||||
Paint_Task_Function function) {
|
||||
return graph.emplace(
|
||||
[this, function = std::move(function)](const Scene_Render_Context& context) {
|
||||
if (!is_visible() || context.color_cache == nullptr)
|
||||
return;
|
||||
auto* cache = dynamic_cast<detail::Blend2D_Color_Cache*>(context.color_cache);
|
||||
if (!cache)
|
||||
return;
|
||||
detail::Painter painter(*cache, viewport_size());
|
||||
if (painter)
|
||||
function(painter, render_state_view());
|
||||
},
|
||||
std::move(name));
|
||||
}
|
||||
|
||||
void Renderable::changed() noexcept {
|
||||
invalidate_cache();
|
||||
plot_.notify_model_dirty();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <renderive/renderable/base/Renderable_Base.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
@@ -40,11 +41,19 @@ public:
|
||||
void render(const ::Scene_Render_Context& context) final;
|
||||
|
||||
protected:
|
||||
using Paint_Task_Function =
|
||||
std::function<void(detail::Painter&, const Render_State_View&)>;
|
||||
|
||||
virtual void paint(detail::Painter& painter, const Render_State_View& state) = 0;
|
||||
virtual void build_paint_task_graph(Renderable_Task_Graph& graph);
|
||||
Renderable_Task_Graph::Task add_paint_task(Renderable_Task_Graph& graph,
|
||||
std::string name,
|
||||
Paint_Task_Function function);
|
||||
void changed() noexcept;
|
||||
[[nodiscard]] Size viewport_size() const noexcept;
|
||||
|
||||
private:
|
||||
void build_task_graph(Renderable_Task_Graph& graph) final;
|
||||
friend class detail::Renderable_State_Observer;
|
||||
Plot_Core& plot_;
|
||||
mutable std::mutex metadata_mutex_;
|
||||
|
||||
@@ -74,7 +74,7 @@ TEST(Renderive_Core2, EveryCurveInterpolationModeHasDistinctSamplingSemantics) {
|
||||
EXPECT_NEAR(cubic[2].value, 5.625, 1e-9);
|
||||
const std::array<double, 11> visible_values{};
|
||||
const Axis_Transform visible_x{{4.0, 6.0}, 0.0, 100.0};
|
||||
const Axis_Transform visible_y{{0.0, 1.0}, 0.0, 100.0};
|
||||
const Axis_Transform visible_y{{0.0, 1.0}, 0.0, 100.0, Orientation::Vertical};
|
||||
const auto clipped = detail::curve_points(
|
||||
visible_values, {0.0, 10.0}, visible_x, visible_y, true,
|
||||
Line_Interpolation_Mode::Linear_Value);
|
||||
@@ -608,6 +608,82 @@ TEST(Renderive_Core2, PlottableDataUpdatesReachKernelRealTimeDataStrategy) {
|
||||
EXPECT_EQ(history.last_event, "real_time_data_updated");
|
||||
EXPECT_GT(history.observation_count, latest.observation_count);
|
||||
}
|
||||
TEST(Renderive_Core2, AxisRasterLayoutCoversAxisSwapAndEveryReversal) {
|
||||
constexpr Range first_source{0.0, 2.0};
|
||||
constexpr Range second_source{0.0, 3.0};
|
||||
for (const bool swapped : {false, true}) {
|
||||
for (const bool first_reversed : {false, true}) {
|
||||
for (const bool second_reversed : {false, true}) {
|
||||
const Axis_Transform first{
|
||||
first_reversed ? Range{2.0, 0.0} : Range{0.0, 2.0},
|
||||
swapped ? 20.0 : 10.0, swapped ? 30.0 : 20.0,
|
||||
swapped ? Orientation::Vertical : Orientation::Horizontal};
|
||||
const Axis_Transform second{
|
||||
second_reversed ? Range{3.0, 0.0} : Range{0.0, 3.0},
|
||||
swapped ? 10.0 : 20.0, swapped ? 20.0 : 30.0,
|
||||
swapped ? Orientation::Horizontal : Orientation::Vertical};
|
||||
const auto layout = detail::axis_raster_layout(
|
||||
first, second, first_source, second_source, 3, 4);
|
||||
ASSERT_TRUE(layout.valid());
|
||||
EXPECT_EQ(layout.width, swapped ? 4 : 3);
|
||||
EXPECT_EQ(layout.height, swapped ? 3 : 4);
|
||||
const std::size_t origin = layout.index(0, 0, 3, 4);
|
||||
const int x = static_cast<int>(origin % layout.width);
|
||||
const int y = static_cast<int>(origin / layout.width);
|
||||
EXPECT_EQ(x, swapped ? (second_reversed ? 3 : 0)
|
||||
: (first_reversed ? 2 : 0));
|
||||
EXPECT_EQ(y, swapped ? (first_reversed ? 2 : 0)
|
||||
: (second_reversed ? 3 : 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::array<double, 2> values{1.0, 3.0};
|
||||
const Axis_Transform vertical_frequency{{0.0, 1.0}, 10.0, 20.0,
|
||||
Orientation::Vertical};
|
||||
const Axis_Transform horizontal_power{{0.0, 4.0}, 100.0, 40.0,
|
||||
Orientation::Horizontal};
|
||||
const auto points = detail::curve_points(
|
||||
values, {0.0, 1.0}, vertical_frequency, horizontal_power, false,
|
||||
Line_Interpolation_Mode::Linear_Value);
|
||||
ASSERT_EQ(points.size(), 2u);
|
||||
EXPECT_DOUBLE_EQ(points.front().x, 110.0);
|
||||
EXPECT_DOUBLE_EQ(points.front().y, 10.0);
|
||||
EXPECT_DOUBLE_EQ(points.back().x, 130.0);
|
||||
EXPECT_DOUBLE_EQ(points.back().y, 30.0);
|
||||
}
|
||||
TEST(Renderive_Core2, PartitionedPlotsBuildExplicitInternalTaskGraphs) {
|
||||
Plot_Core plot;
|
||||
plot.init();
|
||||
const auto root = plot.root_renderable();
|
||||
const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal)
|
||||
.set_pixel_length(320)
|
||||
.set_coord_range({0.0, 10.0})
|
||||
.build();
|
||||
const auto power = Axis::Builder(root, Orientation::Vertical)
|
||||
.set_pixel_length(180)
|
||||
.set_coord_range({-120.0, 0.0})
|
||||
.build();
|
||||
const auto time = Time_Axis::Builder(root, Orientation::Vertical)
|
||||
.set_pixel_length(180)
|
||||
.build();
|
||||
const auto spectrum = Spectrum::Builder{}
|
||||
.set<&Spectrum::Properties::partition_count>(4)
|
||||
.build(root, frequency, power);
|
||||
const auto waterfall = Waterfall::Builder{}
|
||||
.set<&Waterfall::Properties::partition_count>(4)
|
||||
.build(root, frequency, time);
|
||||
const auto afterglow = Afterglow::Builder{}
|
||||
.set<&Afterglow::Properties::partition_count>(4)
|
||||
.build(root, frequency, power);
|
||||
|
||||
EXPECT_EQ(spectrum->get<&Spectrum::Properties::partition_count>(), 4);
|
||||
EXPECT_EQ(waterfall->get<&Waterfall::Properties::partition_count>(), 4);
|
||||
EXPECT_EQ(afterglow->get<&Afterglow::Properties::partition_count>(), 4);
|
||||
EXPECT_EQ(spectrum->task_graph()->nodes().size(), 18u);
|
||||
EXPECT_EQ(waterfall->task_graph()->nodes().size(), 18u);
|
||||
EXPECT_EQ(afterglow->task_graph()->nodes().size(), 35u);
|
||||
}
|
||||
TEST(Renderive_Core2, PlottableAxesAreDataDependenciesAndPaintOverlays) {
|
||||
Plot_Core plot;
|
||||
plot.init();
|
||||
|
||||
Reference in New Issue
Block a user