Initial commit

This commit is contained in:
Martino Ferrari committed 2026-10-01 15:42:48 +02:00
commit 8059b90de7
29 files changed
+7893

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <plot/data/minmax_pyramid.hpp>
#include <plot/data/ring_buffer.hpp>
#include <plot/data/series.hpp>
#include "event_series.hpp"
#include <memory>
#include <vector>
namespace udpscope {
using plot::f32;
using plot::f64;
using plot::u32;
using plot::u64;
/// One channel's view of a (value, time) ring pair.
///
/// The sample grid the library's `Series` contract assumes -- `x = x0 + i*dx` --
/// does not hold here, because x is a *streamed timestamp*, not an index: a lost
/// datagram leaves a gap and the samples either side of it are not one grid step
/// apart. `dx()` and `x0()` therefore report a nominal grid (what the LOD level
/// and the snap use), while `gather()` and `x_bounds()` report the real
/// timestamps. `reduce()`, `sum()` and `sumsq()` are the inherited ones and read
/// only y, so they are exact either way.
///
/// The time ring is shared by every channel of one buffer (spec: the +1 slice);
/// channel sample `j` is stamped by base tick `j * stride`. A stride of 1 is the
/// undecimated case and the common one.
class ChannelBuffer final : public plot::data::Series {
public:
ChannelBuffer(std::shared_ptr<const plot::data::RingBuffer<f32>> values,
std::shared_ptr<const plot::data::RingBuffer<f32>> times, u64 stride, f64 epoch,
f64 nominal_dt);
ChannelBuffer(const ChannelBuffer&) = delete;
ChannelBuffer& operator=(const ChannelBuffer&) = delete;
/// Consumer thread: publishes everything the producer pushed since the previous
/// call into the LOD pyramid and slides the window. O(samples produced).
void acquire() noexcept;
/// Consumer thread: forgets the published window (after a rebind or a reset).
void invalidate() noexcept;
void set_nominal_dt(f64 dt) noexcept { dt_ = dt; }
/// Re-bases the x axis: `x0()`/`x_at()` become `epoch + timestamp`. Used to make
/// the axis read seconds from the trigger event instead of from the stream.
void set_epoch(f64 e) noexcept { epoch_ = e; }
u64 stride() const noexcept { return stride_; }
// --- plot::data::Series ---------------------------------------------------
u64 size() const override { return pyramid_.size(); }
f64 x0() const override;
/// The *mean* spacing of the retained window. The renderer converts a visible x
/// range into an index range with `(x - x0()) / dx()`, so this must describe the
/// samples that are actually retained: reporting the nominal `stride * dt` made
/// that index range exceed the series by the decimation factor, the gather
/// clamped to nothing, and the trace drew empty -- which is exactly what a fast
/// channel looked like next to a slow one, because only the slow one is
/// recognisable from the few samples a wrong range still happens to cover.
f64 dx() const override;
/// The authoritative x of a sample: the streamed timestamp, not a grid. `dx()`
/// is only the mean spacing (the LOD level and a coarse snap use it); a lost
/// datagram leaves a gap, so index times must come from the time ring.
f64 x_at(u64 i) const override;
/// Bracketing samples by binary search over the time ring, which is monotonic,
/// so a crosshair value is interpolated between the two samples actually either
/// side of the pointer -- a grid walk would land on the wrong pair after a gap.
bool neighbours(f64 x, u64& i0, u64& i1) const override;
u64 snap_index(f64 x) const override;
plot::Range x_bounds() const override;
plot::Range y_bounds() const override;
const plot::data::MinMaxPyramid& pyramid() const override { return pyramid_; }
/// x is emitted **relative to `x0()`** (the library's contract): staging an
/// absolute epoch time as f32 would quantise it far coarser than a pixel.
void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
plot::data::GatherMode mode = plot::data::GatherMode::Envelope) const override;
/// `copy_raw`, published so the retargetable trace series can forward the
/// protected hook of another object.
void raw(u64 begin, u64 end, f32* out) const;
protected:
void copy_raw(u64 begin, u64 end, f32* out) const override { raw(begin, end, out); }
private:
/// Absolute (channel) index of retained-window index `i`.
u64 absolute(u64 i) const noexcept { return pyramid_.window_base() + i; }
/// Timestamp of channel sample `absolute_index`, in seconds since `epoch_`.
f32 time_of(u64 absolute_index) const noexcept;
std::shared_ptr<const plot::data::RingBuffer<f32>> values_;
std::shared_ptr<const plot::data::RingBuffer<f32>> times_;
u64 stride_ = 1;
f64 epoch_ = 0.0;
f64 dt_ = 1.0;
u64 published_ = 0;
plot::data::MinMaxPyramid pyramid_;
};
/// A trace's series: one stable object whose channel can be rebound.
///
/// `Plot` has no `set_trace_series()` and no `remove_trace()`, so a trace's series
/// pointer is fixed for the plot's life. Everything that would otherwise need a
/// new pointer -- which signal a trace plots, and the switch between the live ring
/// and a frozen event -- is therefore expressed here as a rebind.
class SlotSeries final : public plot::data::Series {
public:
/// Takes shared ownership: a structural change in the model may drop its
/// `BufferStore` while a trace still points here, and the renderer reads the
/// buffer after this frame's bind has returned.
void bind(std::shared_ptr<ChannelBuffer> buffer) noexcept {
buffer_ = std::move(buffer);
event_ = nullptr;
}
/// Binds the captured event (edge-relative stamps). Binding an event clears
/// the ring binding and vice versa: the slot shows exactly one source.
void bind_event(std::shared_ptr<const EventSnapshot> snap, std::size_t channel) {
if (snap == nullptr || channel >= snap->values.size()) {
event_ = nullptr;
return;
}
if (event_ == nullptr || !event_->same(snap, channel)) {
event_ = std::make_unique<EventSeries>(std::move(snap), channel);
}
buffer_ = nullptr;
}
ChannelBuffer* bound() const noexcept { return buffer_.get(); }
const EventSeries* bound_event() const noexcept { return event_.get(); }
bool empty() const noexcept {
if (event_ != nullptr) return event_->size() == 0;
return buffer_ == nullptr || buffer_->size() == 0;
}
u64 size() const override { return event_ != nullptr ? event_->size() : buffer_ != nullptr ? buffer_->size() : 0; }
f64 x0() const override { return event_ != nullptr ? event_->x0() : buffer_ != nullptr ? buffer_->x0() : 0.0; }
f64 dx() const override { return event_ != nullptr ? event_->dx() : buffer_ != nullptr ? buffer_->dx() : 1.0; }
f64 x_at(u64 i) const override;
bool neighbours(f64 x, u64& i0, u64& i1) const override;
u64 snap_index(f64 x) const override;
plot::Range x_bounds() const override;
plot::Range y_bounds() const override;
const plot::data::MinMaxPyramid& pyramid() const override;
void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
plot::data::GatherMode mode = plot::data::GatherMode::Envelope) const override;
bool streaming() const noexcept override { return event_ == nullptr && buffer_ != nullptr; }
protected:
void copy_raw(u64 begin, u64 end, f32* out) const override;
private:
std::shared_ptr<ChannelBuffer> buffer_;
std::unique_ptr<EventSeries> event_; ///< the captured event, when held
};
} // namespace udpscope