Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
cmake_minimum_required(VERSION 3.16)

project(vnm_plot
VERSION 0.1.0
VERSION 1.0.4
DESCRIPTION "High-performance GPU-accelerated plotting library"
LANGUAGES CXX C
)
Expand Down Expand Up @@ -331,10 +331,11 @@ set(VNM_PLOT_SOURCES

set(VNM_PLOT_HEADERS
# Public headers
include/vnm_plot/qt/plot_renderer.h
include/vnm_plot/qt/plot_widget.h
include/vnm_plot/qt/plot_interaction_item.h
include/vnm_plot/qt/plot_time_axis.h
# Private internal header (lives next to its sources, not installed)
src/qt/plot_renderer.h
)

# -----------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ import VnmPlot 1.0
PlotTimeAxis { id: sharedAxis }

Column {
PlotView { timeAxis: sharedAxis }
PlotView { timeAxis: sharedAxis }
PlotView { time_axis: sharedAxis }
PlotView { time_axis: sharedAxis }
}
```

Expand Down
2 changes: 1 addition & 1 deletion benchmark/include/benchmark_data_source.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class Benchmark_data_source : public vnm::plot::Data_source {

/// Update value range from current snapshot data
void update_value_range(const vnm::plot::data_snapshot_t& snapshot) {
if (!snapshot || snapshot.count == 0 || snapshot.stride == 0) {
if (!snapshot.is_valid()) {
m_value_min = 0.0f;
m_value_max = 0.0f;
m_range_valid = false;
Expand Down
10 changes: 7 additions & 3 deletions include/vnm_plot/core.h
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
#pragma once
// VNM Plot Library - Core Public Header
//
// Pulls in the user-facing core API. Renderer-internal headers
// (default_shaders.h, vertex_layout.h, gl_program.h, primitive_renderer.h,
// chrome_renderer.h, series_renderer.h, layout_calculator.h, range_cache.h,
// font_renderer.h, text_renderer.h) are reachable individually but are not
// surfaced through this umbrella because applications should not need them.

#include <vnm_plot/core/access_policy.h>
#include <vnm_plot/core/access_policy.h> // pulls vertex_layout.h transitively
#include <vnm_plot/core/algo.h>
#include <vnm_plot/core/asset_loader.h>
#include <vnm_plot/core/color_palette.h>
#include <vnm_plot/core/constants.h>
#include <vnm_plot/core/default_shaders.h>
#include <vnm_plot/core/function_sample.h>
#include <vnm_plot/core/plot_config.h>
#include <vnm_plot/core/plot_core.h>
#include <vnm_plot/core/series_builder.h>
#include <vnm_plot/core/types.h>
#include <vnm_plot/core/vertex_layout.h>
2 changes: 1 addition & 1 deletion include/vnm_plot/core/plot_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ struct Plot_config

// Maintenance aid: bump when adding a field so that comparators (e.g.
// plot_config_equivalent) fail to compile until they are updated.
static constexpr int field_count = 20;
static constexpr int field_count = 22;
};

// -----------------------------------------------------------------------------
Expand Down
11 changes: 3 additions & 8 deletions include/vnm_plot/core/plot_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@
namespace vnm::plot {

class Asset_loader;
class Primitive_renderer;
class Series_renderer;
class Chrome_renderer;
class Text_renderer;
struct Plot_config;

class Plot_core
Expand Down Expand Up @@ -57,11 +53,10 @@ class Plot_core
bool initialize(const init_params_t& params);
void cleanup_gl_resources();

// Loader for application-supplied assets (custom shaders, fonts, etc.).
// Users register overrides through this; the sub-renderers themselves are
// intentionally not exposed.
Asset_loader& asset_loader();
Primitive_renderer& primitives();
Series_renderer& series_renderer();
Chrome_renderer& chrome_renderer();
Text_renderer* text_renderer();

void render(
const render_params_t& params,
Expand Down
5 changes: 4 additions & 1 deletion include/vnm_plot/core/series_renderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ class Series_renderer
Series_renderer(const Series_renderer&) = delete;
Series_renderer& operator=(const Series_renderer&) = delete;

// Initialize with asset loader for shader loading
// Remember the asset loader used for on-demand shader lookup. Shaders are
// compiled lazily from render() rather than up front, so this call performs
// no GL work and cannot fail. The referenced loader must outlive the
// Series_renderer.
void initialize(Asset_loader& asset_loader);

// Clean up GL resources
Expand Down
39 changes: 32 additions & 7 deletions include/vnm_plot/core/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,18 @@ using Byte_view = std::string_view;
// If data2/count2 is set, the logical snapshot is split into two contiguous
// segments (e.g., ring buffer wrap). The total count is `count`.
// `sequence` is a monotonic counter that increments on data changes.
// NOTE: The Data_source implementation owns the data contract. If it returns
// copied data, it must keep that buffer alive. If it returns a direct view,
// it must ensure the view stays valid (e.g., by holding a lock in `hold`).
//
// Lifetime contract:
// - The Data_source implementation decides whether the pointers refer to a
// copy it owns, or a direct view into its live storage.
// - Whatever guarantees the view's validity (an internal buffer, a lock, a
// reference count) must be kept alive via `hold`; the snapshot is safe to
// read for exactly as long as the caller keeps `hold` alive (or, if `hold`
// is empty, as long as the Data_source promises the view remains stable —
// usually until the next call into the Data_source from the same thread).
// - Consumers must not copy `data`/`data2` into long-lived storage without
// also retaining `hold`. Treat the pointers as only valid while a
// `data_snapshot_t` value is live.
struct data_snapshot_t
{
const void* data = nullptr; ///< Pointer to first sample
Expand All @@ -71,6 +80,15 @@ struct data_snapshot_t

explicit operator bool() const { return data != nullptr && count > 0; }

/// True iff the snapshot is non-empty AND has a usable stride.
/// Prefer this over the bool conversion when downstream code is going to
/// dereference samples; the bool conversion only checks `data` and
/// `count` and does not catch a zero-stride source.
bool is_valid() const noexcept
{
return data != nullptr && count > 0 && stride > 0;
}

const void* at(size_t index) const
{
if (index >= count || stride == 0) {
Expand Down Expand Up @@ -262,14 +280,21 @@ class Vector_data_source : public Data_source
// -----------------------------------------------------------------------------
// Defines how the renderer extracts meaningful values from opaque sample data.
// This enables rendering of arbitrary sample types without template explosion.
//
// Precision note: get_value and get_range return float because the renderer
// ultimately uploads values as float attributes. If your source data has
// narrow dynamic range offset by a large bias (e.g. physical quantities with
// an absolute reference), subtract that bias inside the accessor before
// narrowing to float so the remaining dynamic range survives the conversion.
// Timestamps and aux metrics use double and are not affected.
struct Data_access_policy
{
// --- Sample value extraction ---
std::function<double(const void* sample)> get_timestamp; ///< Extract timestamp
std::function<float(const void* sample)> get_value; ///< Extract primary value
std::function<std::pair<float, float>(const void* sample)> get_range; ///< Extract min/max range
std::function<double(const void* sample)> get_timestamp; ///< Extract timestamp (double precision)
std::function<float(const void* sample)> get_value; ///< Extract primary value (narrow with care; see above)
std::function<std::pair<float, float>(const void* sample)> get_range; ///< Extract min/max range (narrow with care; see above)

std::function<double(const void* sample)> get_aux_metric; ///< Optional auxiliary metric
std::function<double(const void* sample)> get_aux_metric; ///< Optional auxiliary metric (double precision)
std::function<float(const void* sample)> get_signal; ///< Optional [0,1] signal for COLORMAP_LINE

// Optional sample cloning with timestamp rewrite, used for render-only hold-forward paths.
Expand Down
1 change: 0 additions & 1 deletion include/vnm_plot/qt.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@
// VNM Plot Library - Qt Public Header

#include <vnm_plot/qt/plot_interaction_item.h>
#include <vnm_plot/qt/plot_renderer.h>
#include <vnm_plot/qt/plot_time_axis.h>
#include <vnm_plot/qt/plot_widget.h>
40 changes: 36 additions & 4 deletions include/vnm_plot/qt/plot_widget.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ struct Plot_view
// -----------------------------------------------------------------------------
// Qt Quick widget for rendering GPU-accelerated plots.
// This is the main public interface for the vnm_plot library.
//
// Threading model:
// - All public setters (e.g. set_t_range, set_v_range, set_config, add_series,
// adjust_* helpers) must be called from the Qt GUI thread (the one that
// owns the QQuickWindow). Internally they take short-lived locks on
// m_config_mutex, m_data_cfg_mutex, or m_series_mutex so the render thread
// can read a consistent snapshot.
// - The render thread never calls public setters directly. Plot_renderer
// posts updates through static forwarders that reach the private
// set_*_from_renderer hooks via QMetaObject::invokeMethod, so the actual
// state mutation always runs on the GUI thread once the queued event is
// dispatched. set_rendered_v_range / set_rendered_t_range (also private)
// are the exception: they run on the render thread and publish through
// atomics for UI-side readers (rendered_v_range / rendered_t_range).
// - Non-finite values (NaN, +/-inf) are silently ignored by the range
// setters; they never produce a partial update. Invalid ranges (max <= min)
// are also ignored.
class Plot_widget : public QQuickFramebufferObject
{
Q_OBJECT
Expand All @@ -67,7 +84,7 @@ class Plot_widget : public QQuickFramebufferObject
Q_PROPERTY(double line_width_px READ line_width_px WRITE set_line_width_px NOTIFY line_width_px_changed)
Q_PROPERTY(double vbar_width_px READ vbar_width_pixels NOTIFY vbar_width_changed)
Q_PROPERTY(double vbar_width_qml READ vbar_width_qml NOTIFY vbar_width_changed)
Q_PROPERTY(Plot_time_axis* timeAxis READ time_axis WRITE set_time_axis NOTIFY time_axis_changed)
Q_PROPERTY(Plot_time_axis* time_axis READ time_axis WRITE set_time_axis NOTIFY time_axis_changed)

public:
Plot_widget();
Expand Down Expand Up @@ -148,9 +165,6 @@ class Plot_widget : public QQuickFramebufferObject
double vbar_width_pixels() const;
double vbar_width_qml() const;
Q_INVOKABLE void set_vbar_width(double vbar_width);
Q_INVOKABLE void set_vbar_width_from_renderer(double px);
// Render-thread updates to keep v_min/v_max in sync with auto range.
Q_INVOKABLE void set_auto_v_range_from_renderer(float v_min, float v_max);

// --- Interaction ---

Expand Down Expand Up @@ -204,6 +218,14 @@ class Plot_widget : public QQuickFramebufferObject
private:
friend class Plot_renderer;

// Render-thread callbacks. Plot_renderer is a friend of this class and
// exposes static forwarders (Plot_renderer::post_vbar_width_from_renderer
// and post_auto_v_range_from_renderer) so its nested pimpl can invoke
// these through QMetaObject::invokeMethod. They are intentionally not part
// of the public or QML-visible API; application code must not call them.
void set_vbar_width_from_renderer(double px);
void set_auto_v_range_from_renderer(float v_min, float v_max);

// Lock order (if ever needed concurrently): config -> data_cfg -> series.
// Prefer holding only one lock at a time.

Expand Down Expand Up @@ -256,6 +278,16 @@ class Plot_widget : public QQuickFramebufferObject
std::pair<float, float> current_v_range() const;
data_config_t data_cfg_snapshot() const;

// Shared body of the QML property setters: locks m_config_mutex, no-ops
// when the value is unchanged, otherwise bumps m_config_revision, fires
// the supplied signal, and requests a repaint.
template <typename Field, typename Value, typename Signal>
void update_config_field(Field& field, Value new_value, Signal signal);

// Apply an available-range clamp to m_data_cfg in place. Caller must hold
// m_data_cfg_mutex. Used by both set_available_t_range and set_view.
void clamp_t_range_to_available(double t_avail_min, double t_avail_max);

bool consume_view_state_reset_request();
void set_rendered_v_range(float v_min, float v_max) const;
void set_rendered_t_range(double t_min, double t_max) const;
Expand Down
11 changes: 6 additions & 5 deletions include/vnm_plot/vnm_plot.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@

namespace vnm::plot {

// Library version
constexpr int k_version_major = 0;
constexpr int k_version_minor = 1;
constexpr int k_version_patch = 0;
// Library version. Kept in sync with the CMake project() VERSION; bump both
// together when releasing.
constexpr int k_version_major = 1;
constexpr int k_version_minor = 0;
constexpr int k_version_patch = 4;

constexpr const char* k_version_string = "0.1.0";
constexpr const char* k_version_string = "1.0.4";

} // namespace vnm::plot
2 changes: 1 addition & 1 deletion qml/VnmPlot/PlotIndicator.qml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Item {
property string x_value_label: "x"
property string y_value_label: "y"

readonly property var time_axis: plot_widget ? plot_widget.timeAxis : null
readonly property var time_axis: plot_widget ? plot_widget.time_axis : null

readonly property real usable_width: width - plot_widget.vbar_width_qml
readonly property real usable_height: height - plot_widget.reserved_height
Expand Down
2 changes: 1 addition & 1 deletion qml/VnmPlot/PlotView.qml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Item {
PlotWidget {
id: plot
anchors.fill: parent
timeAxis: root.time_axis
time_axis: root.time_axis
}

PlotIndicator {
Expand Down
12 changes: 8 additions & 4 deletions src/core/plot_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,6 @@ void Plot_core::cleanup_gl_resources()
}

Asset_loader& Plot_core::asset_loader() { return m_impl->asset_loader; }
Primitive_renderer& Plot_core::primitives() { return m_impl->primitives; }
Series_renderer& Plot_core::series_renderer() { return m_impl->series; }
Chrome_renderer& Plot_core::chrome_renderer() { return m_impl->chrome; }
Text_renderer* Plot_core::text_renderer() { return m_impl->text.get(); }

void Plot_core::render(
const render_params_t& params,
Expand All @@ -169,6 +165,14 @@ void Plot_core::render(
vnm::plot::Profiler* profiler = config ? config->profiler.get() : nullptr;
m_impl->primitives.set_profiler(profiler);

// Route failures from asset loading, primitive rendering, and (where
// applicable) font rendering through the caller-provided log callback so
// standalone core users see the same diagnostics the Qt wrapper does.
if (config && config->log_error) {
m_impl->asset_loader.set_log_callback(config->log_error);
m_impl->primitives.set_log_callback(config->log_error);
}

const float preview_v_min = params.preview_v_min.value_or(params.v_min);
const float preview_v_max = params.preview_v_max.value_or(params.v_max);
const double t_available_min = params.t_available_min.value_or(params.t_min);
Expand Down
24 changes: 17 additions & 7 deletions src/core/series_renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ bool compute_aux_metric_range(
{
out_used_data_source_range = false;

if (!access.get_aux_metric || !snapshot || snapshot.count == 0 || snapshot.stride == 0) {
if (!access.get_aux_metric || !snapshot.is_valid()) {
return false;
}

Expand Down Expand Up @@ -315,7 +315,18 @@ std::shared_ptr<GL_program> Series_renderer::get_or_load_shader(
const shader_set_t& shader_set,
const Plot_config* config)
{
if (shader_set.vert.empty() || !m_asset_loader) {
const auto log_error = [&](const std::string& message) {
if (config && config->log_error) {
config->log_error(message);
}
};

if (!m_asset_loader) {
log_error("Series_renderer: asset loader not initialized; cannot compile shaders");
return nullptr;
}
if (shader_set.vert.empty()) {
log_error("Series_renderer: shader set has no vertex stage; cannot compile program");
return nullptr;
}

Expand All @@ -332,9 +343,7 @@ std::shared_ptr<GL_program> Series_renderer::get_or_load_shader(
}

if (!vert_src || !frag_src) {
if (config && config->log_error) {
config->log_error("Failed to load shader sources: " + normalized.vert);
}
log_error("Failed to load shader sources: " + normalized.vert);
return nullptr;
}

Expand All @@ -345,10 +354,11 @@ std::shared_ptr<GL_program> Series_renderer::get_or_load_shader(
geom_str.assign(geom_src->begin(), geom_src->end());
}

auto log_error = config ? config->log_error : std::function<void(const std::string&)>();
auto sp = create_gl_program(vert_str, geom_str, frag_str, log_error);
auto log_error_fn = config ? config->log_error : std::function<void(const std::string&)>();
auto sp = create_gl_program(vert_str, geom_str, frag_str, log_error_fn);

if (!sp) {
log_error("Shader program creation failed for: " + normalized.vert);
return nullptr;
}
auto shared_sp = std::shared_ptr<GL_program>(std::move(sp));
Expand Down
Loading
Loading