Files
Renderive/Core/plottable/Waterfall_p.h
T
2026-08-02 00:06:33 +08:00

591 lines
27 KiB
C++

#pragma once
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <cmath>
#include <memory>
#include <memory_resource>
#include <optional>
#include <rigtorp/MPMCQueue.h>
#include <span>
#include <system_error>
#include <utility>
#include <vector>
#include "../Axis/Frequency_Axis_p.h"
#include "../Axis/Time_Axis_p.h"
#include "../architecture/Render_Time.h"
#include "../architecture/Renderable.h"
#include "../base/Memory.h"
#include "../base/global.h"
#include "../render/Canvas.h"
#include "../render/Image.h"
#include "Psc_Cpp_Core/Base/RingBuffer.hpp"
#include "Interpolation_p.h"
#include "Waterfall.h"
namespace renderive {
struct Waterfall_Data {
int tick{};
std::pmr::vector<double> frequency{memory_resource(Memory_Domain::Waterfall)};
std::shared_ptr<Update_State> completion_state;
};
struct Waterfall_Input_Data : Input_Data {};
struct Waterfall_Render_State : Render_State, Hover_Info_Render_State, Waterfall_Prop {};
struct Waterfall_Ring_Buffer {
Psc::StreamRingBuffer_ST buffer{memory_resource(Memory_Domain::Waterfall)};
std::pmr::vector<std::uint8_t> record_buffer{memory_resource(Memory_Domain::Waterfall)};
std::pmr::vector<std::uint8_t> snapshot_buffer{memory_resource(Memory_Domain::Waterfall)};
std::pmr::vector<double> row_buffer{memory_resource(Memory_Domain::Waterfall)};
std::size_t record_byte_size{};
std::size_t row_byte_size{};
int row_count{};
int col_count{};
int data_count{};
bool match(int col, int row) const {
return col_count == col && row_count == row && record_byte_size;
}
void resize(int col, int row) {
col_count = col;
row_count = row;
data_count = 0;
row_byte_size = sizeof(double) * col_count;
record_byte_size = row_byte_size + sizeof(int);
record_buffer.resize(record_byte_size);
snapshot_buffer.resize(record_byte_size * row_count);
row_buffer.resize(col_count);
buffer.init(record_byte_size * row_count);
}
void push_data(const double* data, int tick) {
if (data_count == row_count) {
buffer.skip(record_byte_size);
--data_count;
}
std::memcpy(record_buffer.data(), data, row_byte_size);
std::memcpy(record_buffer.data() + row_byte_size, &tick, sizeof(tick));
buffer.write(record_buffer.data(), record_byte_size);
++data_count;
}
int snapshot() {
buffer.peek_best_effort(snapshot_buffer.data(), record_byte_size * data_count);
return data_count;
}
const double* frequency_data(int row, int first_col, int last_col) {
std::size_t offset = static_cast<std::size_t>(first_col) * sizeof(double);
std::size_t size = static_cast<std::size_t>(last_col - first_col + 1) * sizeof(double);
std::memcpy(reinterpret_cast<std::uint8_t*>(row_buffer.data()) + offset, snapshot_buffer.data() + row * record_byte_size + offset, size);
return row_buffer.data();
}
const double* frequency_data(int row) {
std::memcpy(row_buffer.data(), snapshot_buffer.data() + row * record_byte_size, row_byte_size);
return row_buffer.data();
}
int tick(int row) const {
int value;
std::memcpy(&value, snapshot_buffer.data() + row * record_byte_size + row_byte_size, sizeof(value));
return value;
}
};
struct Waterfall_Image_Cache_Key {
int start_tick{};
int first_col{};
int last_col{};
Range frequency_range{};
Range power_range{};
Range visible_range{};
Color background;
Waterfall_Row_Update_Policy policy = Waterfall_Row_Update_Policy::All_Pending;
std::uint64_t color_map_hash{};
int width{};
int height{};
bool operator==(const Waterfall_Image_Cache_Key& other) const {
return start_tick == other.start_tick &&
first_col == other.first_col &&
last_col == other.last_col &&
frequency_range == other.frequency_range &&
power_range == other.power_range &&
visible_range == other.visible_range &&
background == other.background &&
policy == other.policy &&
color_map_hash == other.color_map_hash &&
width == other.width &&
height == other.height;
}
};
struct Waterfall_Image_Snapshot {
Waterfall_Image_Snapshot() : update_states(memory_resource(Memory_Domain::Update_Completion)) {}
Waterfall_Image_Cache_Key key;
Image image;
Range frequency_range{};
Range time_range{};
RectF source_rect{};
std::pmr::vector<std::shared_ptr<Update_State>> update_states;
};
struct Waterfall_Image_Request {
Waterfall_Image_Request()
: rows(memory_resource(Memory_Domain::Waterfall)),
update_states(memory_resource(Memory_Domain::Update_Completion)) {}
Waterfall_Image_Cache_Key key;
Color_Map color_map;
double value_start_coord{};
double rate{};
int lower_stream_tick{};
int upper_stream_tick{};
int visible_time_count{};
bool start_coord_to_end_coord{};
Range draw_frequency_range{};
Range draw_time_range{};
RectF draw_source_rect{};
std::pmr::vector<Waterfall_Data> rows;
std::pmr::vector<std::shared_ptr<Update_State>> update_states;
};
struct Waterfall_Image_Tile {
int y{};
Image image;
};
struct Waterfall_Image_Job {
explicit Waterfall_Image_Job(std::shared_ptr<Waterfall_Image_Request> request, int tile_count)
: request(std::move(request)), remaining(tile_count) {
tiles.resize(static_cast<std::size_t>(tile_count));
}
std::shared_ptr<Waterfall_Image_Request> request;
std::vector<Waterfall_Image_Tile> tiles;
std::atomic<int> remaining;
};
static void complete_waterfall_request_states(Waterfall_Image_Request& request, std::error_code error, Update_Outcome outcome) {
for (auto& state : request.update_states) {
if (state)
state->complete_all(error, outcome);
}
request.update_states.clear();
}
static void complete_waterfall_result_states(Waterfall_Image_Snapshot& snapshot, std::error_code error, Update_Outcome outcome) {
for (const auto& state : snapshot.update_states) {
if (state)
state->complete_all(error, outcome);
}
snapshot.update_states.clear();
}
static bool waterfall_request_coord_by_stream_tick(const Waterfall_Image_Request& request, int stream_tick, int& coord) {
if (request.visible_time_count <= 0)
return false;
if (stream_tick < request.lower_stream_tick || stream_tick > request.upper_stream_tick)
return false;
if (request.start_coord_to_end_coord)
coord = request.key.start_tick + (request.upper_stream_tick - stream_tick);
else
coord = request.key.start_tick + (request.key.height - request.visible_time_count) + (stream_tick - request.lower_stream_tick);
return coord >= request.key.start_tick && coord < request.key.start_tick + request.key.height;
}
static int waterfall_tile_count(int height) {
static constexpr int Tile_Height = 64;
return std::max(1, (height + Tile_Height - 1) / Tile_Height);
}
static void write_waterfall_tile(Waterfall_Image_Job& job, int tile_index) {
const Waterfall_Image_Request& request = *job.request;
int tile_count = static_cast<int>(job.tiles.size());
int y_begin = request.key.height * tile_index / tile_count;
int y_end = request.key.height * (tile_index + 1) / tile_count;
Waterfall_Image_Tile& tile = job.tiles[static_cast<std::size_t>(tile_index)];
tile.y = y_begin;
int tile_height = std::max(0, y_end - y_begin);
tile.image.resize(request.key.width, tile_height);
tile.image.fill(request.key.background);
Pixel bg = premultiply(request.key.background);
for (const Waterfall_Data& row : request.rows) {
int coord{};
if (!waterfall_request_coord_by_stream_tick(request, row.tick, coord))
continue;
int image_y = coord - request.key.start_tick;
if (image_y < y_begin || image_y >= y_end)
continue;
Pixel* line = tile.image.row(image_y - y_begin);
if (!line)
continue;
for (int col = 0; col < request.key.width; ++col)
line[col] = bg;
for (int col = request.key.first_col; col <= request.key.last_col; ++col) {
int color_offset = static_cast<int>((row.frequency[static_cast<std::size_t>(col)] - request.value_start_coord) * request.rate);
line[col] = request.color_map.at_offset(color_offset);
}
}
}
static Waterfall_Image_Snapshot build_waterfall_image_snapshot(const std::shared_ptr<Waterfall_Image_Request>& request_ptr) {
Waterfall_Image_Request& request = *request_ptr;
int tile_count = waterfall_tile_count(request.key.height);
Waterfall_Image_Job job(request_ptr, tile_count);
for (int tile_index = 0; tile_index < tile_count; ++tile_index)
write_waterfall_tile(job, tile_index);
Waterfall_Image_Snapshot snapshot;
snapshot.key = request.key;
snapshot.frequency_range = request.draw_frequency_range;
snapshot.time_range = request.draw_time_range;
snapshot.source_rect = request.draw_source_rect;
snapshot.image.resize(request.key.width, request.key.height);
for (const auto& tile : job.tiles) {
for (int y = 0; y < tile.image.height(); ++y) {
Pixel* target = snapshot.image.row(tile.y + y);
const Pixel* source = tile.image.row(y);
if (target && source)
std::memcpy(target, source, static_cast<std::size_t>(request.key.width) * sizeof(Pixel));
}
}
snapshot.update_states = std::move(request.update_states);
return snapshot;
}
struct Waterfall_Private : Typed_Render_Data<Waterfall, Waterfall_Render_State, Waterfall_Input_Data>, Hit_Testable, Hover_Interactive {
static constexpr std::size_t Row_Queue_Capacity = 256;
Waterfall_Ring_Buffer ring_buffer;
Image image;
std::weak_ptr<Color_Bar> color_bar;
std::optional<Waterfall_Data> pending_latest_row;
std::pmr::vector<Waterfall_Data> pending_image_rows{memory_resource(Memory_Domain::Waterfall)};
std::pmr::vector<std::shared_ptr<Update_State>> pending_image_update_states{memory_resource(Memory_Domain::Update_Completion)};
rigtorp::MPMCQueue<std::shared_ptr<Waterfall_Data>> row_queue;
std::atomic_uint64_t dropped_row_count{0};
std::uint64_t last_line_commit_ns{};
std::uint64_t image_color_map_hash{};
Waterfall_Image_Snapshot image_snapshot;
bool has_image_snapshot{};
Waterfall_Image_Cache_Key submitted_image_key;
bool has_submitted_image_key{};
std::uint64_t pending_latest_deadline_ns{};
Range m_image_frequency_range{};
Range image_power_range{};
Range image_visible_range{};
Color image_background_color{};
Waterfall_Row_Update_Policy image_update_policy = Waterfall_Row_Update_Policy::All_Pending;
int image_start_tick{};
int image_first_col{};
int image_last_col{};
Range image_draw_frequency_range{};
Range image_draw_time_range{};
RectF image_draw_source_rect{};
bool image_valid{};
bool image_draw_visible{};
Waterfall_Private()
: row_queue(Row_Queue_Capacity) {}
bool select_test(const PointF& pos) override {
return true;
}
void set_hover_state(Point pos, bool active) override {
Render_Edit_Lease<Waterfall_Private, Waterfall_Render_State> edit(this);
edit->hover_info_pos = pos;
edit->hover_info_active = active;
}
~Waterfall_Private() override {
complete_waterfall_result_states(image_snapshot, std::make_error_code(std::errc::operation_canceled), Update_Outcome::Cancelled);
std::shared_ptr<Waterfall_Data> row;
while (row_queue.try_pop(row)) {
if (row && row->completion_state)
row->completion_state->complete_all(std::make_error_code(std::errc::operation_canceled), Update_Outcome::Cancelled);
}
if (pending_latest_row && pending_latest_row->completion_state)
pending_latest_row->completion_state->complete_all(std::make_error_code(std::errc::operation_canceled), Update_Outcome::Cancelled);
for (auto& state : pending_image_update_states) {
if (state)
state->complete_all(std::make_error_code(std::errc::operation_canceled), Update_Outcome::Cancelled);
}
pending_image_update_states.clear();
}
void enqueue_row(int tick, std::span<const double> data, std::shared_ptr<Update_State> completion_state = {}) {
auto row = std::make_shared<Waterfall_Data>();
row->tick = tick;
row->frequency.assign(data.begin(), data.end());
row->completion_state = std::move(completion_state);
enqueue_row(std::move(row));
}
void enqueue_row(int tick, std::pmr::vector<double>&& data, std::shared_ptr<Update_State> completion_state = {}) {
auto row = std::make_shared<Waterfall_Data>();
row->tick = tick;
row->frequency = std::move(data);
row->completion_state = std::move(completion_state);
enqueue_row(std::move(row));
}
void enqueue_row(std::shared_ptr<Waterfall_Data> row) {
if (!row)
return;
if (!row_queue.try_push(row)) {
if (row->completion_state)
row->completion_state->complete_all({}, Update_Outcome::Superseded);
dropped_row_count.fetch_add(1, std::memory_order_relaxed);
return;
}
if (row->completion_state)
row->completion_state->complete_until(Update_Stage::Input_Released);
if (q())
q()->mark_render_dirty();
}
void consume_row_queue(std::pmr::vector<Waterfall_Data>& output) {
std::shared_ptr<Waterfall_Data> row;
while (row_queue.try_pop(row)) {
if (!row)
continue;
output.push_back(std::move(*row));
}
}
void commit_row_ticket(Waterfall_Data& row) {
if (!row.completion_state)
return;
row.completion_state->complete_until(Update_Stage::Committed);
pending_image_update_states.push_back(std::move(row.completion_state));
}
void schedule_pending_latest_deadline(std::uint64_t deadline_ns) {
if (!deadline_ns || pending_latest_deadline_ns == deadline_ns)
return;
pending_latest_deadline_ns = deadline_ns;
std::weak_ptr<Renderable> owner = q() ? std::weak_ptr<Renderable>(q()->shared_from_this()) : std::weak_ptr<Renderable>();
Global::instance()->render_scheduler().post_at(deadline_ns, [owner]() {
if (auto renderable = owner.lock())
renderable->mark_render_dirty();
});
}
bool push_row(std::span<const double> frequency_data, int tick, int col_count) {
if (frequency_data.size() != col_count) {
return false;
}
ring_buffer.push_data(frequency_data.data(), tick);
return true;
}
bool push_owned_row(std::pmr::vector<double>& frequency_data, int tick, int col_count, Waterfall_Row_Update_Policy update_policy) {
if (frequency_data.size() != col_count) {
return false;
}
ring_buffer.push_data(frequency_data.data(), tick);
if (update_policy != Waterfall_Row_Update_Policy::All_Pending) {
Waterfall_Data row;
row.tick = tick;
row.frequency.swap(frequency_data);
pending_image_rows.push_back(std::move(row));
}
return true;
}
void prepare_data(const Plot_Render_Snapshot& snapshot) override {
sync_state_pipeline();
const Waterfall_Render_State* s = render_state();
auto time_axis = s->time_axis.lock();
if (!time_axis)
return;
auto timeline = Time_Axis_Render_Access::capture_timeline(time_axis.get(), snapshot);
if (!timeline)
return;
int col_count = s->frequency_point_size, row_count = timeline->time_point_size;
if (col_count <= 0 || row_count <= 0)
return;
if (!ring_buffer.match(col_count, row_count)) {
ring_buffer.resize(col_count, row_count);
}
std::pmr::vector<Waterfall_Data> queued_rows(memory_resource(Memory_Domain::Waterfall));
consume_row_queue(queued_rows);
switch (s->row_update_policy) {
case Waterfall_Row_Update_Policy::All_Pending:
for (auto& row : queued_rows) {
if (push_row(row.frequency, row.tick, col_count))
commit_row_ticket(row);
else if (row.completion_state)
row.completion_state->complete_all(std::make_error_code(std::errc::invalid_argument), Update_Outcome::Cancelled);
}
break;
case Waterfall_Row_Update_Policy::Latest_Only:
case Waterfall_Row_Update_Policy::Rate_Limited_Latest: {
for (auto& row : queued_rows) {
if (row.frequency.size() != col_count) {
if (row.completion_state)
row.completion_state->complete_all(std::make_error_code(std::errc::invalid_argument), Update_Outcome::Cancelled);
continue;
}
if (pending_latest_row && pending_latest_row->completion_state)
pending_latest_row->completion_state->complete_all({}, Update_Outcome::Superseded);
pending_latest_row = std::move(row);
}
if (pending_latest_row && pending_latest_row->frequency.size() != col_count) {
if (pending_latest_row->completion_state)
pending_latest_row->completion_state->complete_all(std::make_error_code(std::errc::invalid_argument), Update_Outcome::Cancelled);
pending_latest_row.reset();
}
if (pending_latest_row) {
bool allow_commit = true;
std::uint64_t now_ns = steady_now_ns();
std::uint64_t deadline_ns = 0;
if (s->row_update_policy == Waterfall_Row_Update_Policy::Rate_Limited_Latest && last_line_commit_ns != 0) {
std::uint64_t interval_ns = static_cast<std::uint64_t>(std::max(1, s->latest_row_min_interval_ms)) * 1000000ull;
deadline_ns = last_line_commit_ns + interval_ns;
allow_commit = now_ns >= deadline_ns;
}
if (allow_commit) {
if (push_owned_row(pending_latest_row->frequency, pending_latest_row->tick, col_count, s->row_update_policy)) {
commit_row_ticket(*pending_latest_row);
last_line_commit_ns = now_ns;
pending_latest_deadline_ns = 0;
pending_latest_row.reset();
}
}
else {
schedule_pending_latest_deadline(deadline_ns);
}
}
break;
}
}
update_image_item(snapshot, *timeline);
}
bool write_image_row(int tick, const double* row_data, int first_col, int last_col, int start_tick, int row_count, double value_start_coord, double rate, const Color_Map& color_map, Color background_color) {
int tick_offset = tick - start_tick;
if (tick_offset < 0 || tick_offset >= row_count)
return false;
Pixel* line = image.row(tick_offset);
Pixel bg = premultiply(background_color);
for (int col = 0; col < image.width(); ++col)
line[col] = bg;
for (int col = first_col; col <= last_col; ++col) {
int color_offset = static_cast<int>((row_data[col] - value_start_coord) * rate);
line[col] = color_map.at_offset(color_offset);
}
return true;
}
bool image_cache_match(const Waterfall_Render_State* s, int start_tick, int first_col, int last_col, const Range& h_range, const Range& power_range, Color background_color, std::uint64_t color_map_hash) const {
return image_valid && image_start_tick == start_tick && image_first_col == first_col && image_last_col == last_col && m_image_frequency_range == s->frequency_range && image_power_range == power_range && image_visible_range == h_range && image_background_color == background_color && image_update_policy == s->row_update_policy && image_color_map_hash == color_map_hash;
}
void update_image_item(const Plot_Render_Snapshot& snapshot, const Timeline_Stream_Snapshot& timeline) {
Waterfall_Render_State* s = render_state();
auto frequency_axis = s->frequency_axis.lock();
auto time_axis = s->time_axis.lock();
if (!frequency_axis || !time_axis)
return;
auto color_scale = s->color_scale.snapshot();
Abs_Axis* h_axis = frequency_axis.get();
Range h_range = s->frequency_range;
Range v_range{static_cast<double>(timeline.lower), static_cast<double>(timeline.lower + timeline.time_point_size)};
if (s->visible_range_only && !intersect_range(s->frequency_range, Axis_Render_Access::coord_range(h_axis), h_range)) {
image_draw_visible = false;
return;
}
int col_count = s->frequency_point_size, row_count = timeline.time_point_size;
if (col_count <= 0 || row_count <= 0 || s->frequency_range.length() == 0.0 || color_scale->range.size() == 0.0)
return;
double source_start = static_cast<double>(col_count) * (h_range.origin - s->frequency_range.origin) / s->frequency_range.length();
double source_end = static_cast<double>(col_count) * (h_range.target - s->frequency_range.origin) / s->frequency_range.length();
double source_min = std::min(source_start, source_end);
double source_max = std::max(source_start, source_end);
int first_col = std::clamp(static_cast<int>(std::floor(source_min)), 0, col_count - 1);
int last_col = std::clamp(static_cast<int>(std::ceil(source_max)) - 1, first_col, col_count - 1);
double value_start_coord = color_scale->range.origin;
double rate = static_cast<double>(color_scale->color_map.size()) / color_scale->range.size();
int buffer_data_count = ring_buffer.snapshot();
int start_tick = timeline.lower;
std::uint64_t color_map_hash = color_scale->color_map.hash();
Waterfall_Image_Cache_Key key{
start_tick,
first_col,
last_col,
s->frequency_range,
color_scale->range,
h_range,
snapshot.background_color,
s->row_update_policy,
color_map_hash,
col_count,
row_count
};
bool should_submit = !has_submitted_image_key || !(submitted_image_key == key) || !pending_image_update_states.empty();
if (should_submit) {
submitted_image_key = key;
has_submitted_image_key = true;
auto request = std::make_shared<Waterfall_Image_Request>();
request->key = key;
request->color_map = color_scale->color_map;
request->value_start_coord = value_start_coord;
request->rate = rate;
request->lower_stream_tick = timeline.lower_stream_tick;
request->upper_stream_tick = timeline.upper_stream_tick;
request->visible_time_count = static_cast<int>(timeline.times.size());
request->start_coord_to_end_coord = timeline.start_coord_to_end_coord;
request->draw_frequency_range = h_range;
request->draw_time_range = v_range;
request->draw_source_rect = RectF(source_start, 0.0, source_end - source_start, static_cast<double>(row_count));
request->rows.reserve(static_cast<std::size_t>(buffer_data_count));
for (int row = 0; row < buffer_data_count; ++row) {
int buffer_row = buffer_data_count - row - 1;
const double* list = ring_buffer.frequency_data(buffer_row);
Waterfall_Data image_row;
image_row.tick = ring_buffer.tick(buffer_row);
image_row.frequency.assign(list, list + col_count);
request->rows.push_back(std::move(image_row));
}
request->update_states = std::move(pending_image_update_states);
pending_image_update_states = std::pmr::vector<std::shared_ptr<Update_State>>(memory_resource(Memory_Domain::Update_Completion));
image_snapshot = build_waterfall_image_snapshot(request);
has_image_snapshot = true;
image = image_snapshot.image;
image_valid = true;
image_start_tick = image_snapshot.key.start_tick;
image_first_col = image_snapshot.key.first_col;
image_last_col = image_snapshot.key.last_col;
m_image_frequency_range = image_snapshot.key.frequency_range;
image_power_range = image_snapshot.key.power_range;
image_visible_range = image_snapshot.key.visible_range;
image_background_color = image_snapshot.key.background;
image_update_policy = image_snapshot.key.policy;
image_color_map_hash = image_snapshot.key.color_map_hash;
image_draw_frequency_range = image_snapshot.frequency_range;
image_draw_time_range = image_snapshot.time_range;
image_draw_source_rect = image_snapshot.source_rect;
image_draw_visible = true;
}
pending_image_rows.clear();
}
void draw_image(Canvas& canvas, Waterfall_Render_State* s, Abs_Axis* h_axis, Time_Axis* v_axis) {
if (!image_draw_visible || image.empty())
return;
if (image_draw_frequency_range.length() == 0.0 || image_draw_time_range.length() == 0.0)
return;
if (image_draw_source_rect.width == 0.0 || image_draw_source_rect.height == 0.0)
return;
Axis_Mapping_2D mapping = Axis_Render_Access::mapping(h_axis, v_axis);
Axis_Basis_2D basis = Axis_Basis_2D::from_ranges(mapping, image_draw_frequency_range, image_draw_time_range);
canvas.save();
canvas.set_image_interpolation(s->interpolation_mode);
canvas.transform(
basis.domain_vector.x / image_draw_source_rect.width,
basis.domain_vector.y / image_draw_source_rect.width,
basis.value_vector.x / image_draw_source_rect.height,
basis.value_vector.y / image_draw_source_rect.height,
basis.origin.x,
basis.origin.y);
canvas.draw_image(RectF{0.0, 0.0, image_draw_source_rect.width, image_draw_source_rect.height}, image, image_draw_source_rect);
canvas.restore();
}
void draw(Canvas& canvas, const Plot_Render_Snapshot& snapshot) override {
Waterfall_Render_State* s = render_state();
auto frequency_axis = s->frequency_axis.lock();
auto time_axis = s->time_axis.lock();
if (!frequency_axis || !time_axis)
return;
if (snapshot.frame_update_states && has_image_snapshot) {
for (const auto& state : image_snapshot.update_states)
snapshot.frame_update_states->push_back(state);
image_snapshot.update_states.clear();
}
Abs_Axis* h_axis = frequency_axis.get();
Time_Axis* v_axis = time_axis.get();
draw_image(canvas, s, h_axis, v_axis);
if (q()->hover_ok(q())) {
q()->draw_hover(&canvas, h_axis, v_axis);
}
}
};
} // namespace renderive