Initial commit
This commit is contained in:
commit
8059b90de7
29 files changed
+7893
No files matched your search
@@ -0,0 +1,141 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
#pragma once
|
||||
|
||||
#include <plot/data/series.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace udpscope {
|
||||
|
||||
using plot::f32;
|
||||
using plot::f64;
|
||||
using plot::u32;
|
||||
using plot::u64;
|
||||
|
||||
/// One captured trigger event, immutable once published: per-channel values over
|
||||
/// a shared, edge-relative time stamp array (the edge sits at exactly x = 0, the
|
||||
/// window spans [-pre, +post]).
|
||||
///
|
||||
/// The plot binds to a snapshot while the trigger holds an event. Because the
|
||||
/// snapshot can never change after publication, nothing can drag, re-base or
|
||||
/// refresh the view while it is held -- the failures the previous design (two
|
||||
/// live ring buffers switched by a pending flag, plus a display-epoch shift)
|
||||
/// could not stop.
|
||||
struct EventSnapshot {
|
||||
std::vector<f32> stamps; ///< edge-relative seconds, monotonic
|
||||
std::vector<std::vector<f32>> values; ///< one array per channel, same length
|
||||
double pre_s = 0.0; ///< seconds of pre-trigger data kept
|
||||
double post_s = 0.0; ///< seconds of post-trigger data kept
|
||||
std::vector<plot::Range> y; ///< per-channel value bounds, precomputed
|
||||
};
|
||||
|
||||
/// A plot::data::Series view of one channel of an EventSnapshot. The stamps are
|
||||
/// near-uniform (the arrival clock), so dx is the mean spacing and x_at returns
|
||||
/// the exact stamp; gather decimates by 2^level like the ring series does.
|
||||
class EventSeries final : public plot::data::Series {
|
||||
public:
|
||||
explicit EventSeries(std::shared_ptr<const EventSnapshot> snap, std::size_t channel)
|
||||
: snap_(std::move(snap)), channel_(channel) {}
|
||||
|
||||
/// Identity for rebind: same snapshot object and channel means the existing
|
||||
/// EventSeries already shows this event.
|
||||
bool same(const std::shared_ptr<const EventSnapshot>& snap, std::size_t channel) const {
|
||||
return snap_ == snap && channel_ == channel;
|
||||
}
|
||||
|
||||
/// The shorter of the two arrays this series reads. `gather()` indexes `stamps`
|
||||
/// and `values[channel]` at the same i, so reporting the stamp count alone would
|
||||
/// be an out-of-bounds read the moment they differ.
|
||||
u64 size() const override {
|
||||
if (channel_ >= snap_->values.size()) return 0;
|
||||
return static_cast<u64>(
|
||||
std::min(snap_->stamps.size(), snap_->values[channel_].size()));
|
||||
}
|
||||
f64 x0() const override { return snap_->stamps.empty() ? 0.0 : snap_->stamps.front(); }
|
||||
f64 dx() const override {
|
||||
const auto& s = snap_->stamps;
|
||||
return s.size() > 1 ? (s.back() - s.front()) / static_cast<f64>(s.size() - 1) : 1.0;
|
||||
}
|
||||
f64 x_at(u64 i) const override {
|
||||
const auto& s = snap_->stamps;
|
||||
return i < s.size() ? s[static_cast<std::size_t>(i)] : 0.0;
|
||||
}
|
||||
plot::Range x_bounds() const override {
|
||||
const auto& s = snap_->stamps;
|
||||
return s.empty() ? plot::Range{0.0, 0.0} : plot::Range{s.front(), s.back()};
|
||||
}
|
||||
plot::Range y_bounds() const override {
|
||||
return channel_ < snap_->y.size() ? snap_->y[channel_] : plot::Range{0.0, 0.0};
|
||||
}
|
||||
const plot::data::MinMaxPyramid& pyramid() const override { return empty_pyramid_; }
|
||||
bool streaming() const noexcept override { return false; }
|
||||
|
||||
void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
||||
plot::data::GatherMode /*mode*/ =
|
||||
plot::data::GatherMode::Envelope) const override {
|
||||
if (channel_ >= snap_->values.size()) return;
|
||||
const auto& vals = snap_->values[channel_];
|
||||
const auto& stamps = snap_->stamps;
|
||||
const u64 n = size();
|
||||
const u64 stride = u64{1} << level;
|
||||
if (stride == 0 || begin >= n) return;
|
||||
if (end > n) end = n;
|
||||
// x **relative to x0()**, which is the library's gather contract: the renderer
|
||||
// adds `x0()` back before mapping to the screen. Emitting the absolute stamp made
|
||||
// it add x0 twice, so the whole capture was drawn one `x0()` -- a full
|
||||
// pre-trigger window -- to the left of the axis it had just been measured
|
||||
// against. The event's own stamps are small (edge-relative, so [-pre, +post]),
|
||||
// which is why that shift looked like a plausible picture: with a 50/50 split the
|
||||
// right half of the plot was simply empty, zooming out showed the trace ending at
|
||||
// the x = 0 marker, and zooming in past the shift showed nothing at all while the
|
||||
// crosshair -- which reads `x_at()`, never `gather()` -- went on reporting the
|
||||
// right values.
|
||||
const f64 base = x0();
|
||||
out_xy.reserve(out_xy.size() + static_cast<std::size_t>((end - begin) / stride * 2 + 4));
|
||||
for (u64 i = (begin / stride) * stride; i < end; i += stride) {
|
||||
out_xy.push_back(static_cast<f32>(stamps[static_cast<std::size_t>(i)] - base));
|
||||
out_xy.push_back(vals[static_cast<std::size_t>(i)]);
|
||||
}
|
||||
}
|
||||
|
||||
bool neighbours(f64 x, u64& i0, u64& i1) const override {
|
||||
const u64 n = size();
|
||||
if (n < 2) return false;
|
||||
const f64 step = dx();
|
||||
if (!(step > 0.0)) return false;
|
||||
f64 idx = (x - x0()) / step;
|
||||
if (idx < 0.0) idx = 0.0;
|
||||
if (idx > static_cast<f64>(n - 1)) idx = static_cast<f64>(n - 1);
|
||||
i0 = static_cast<u64>(idx);
|
||||
i1 = i0 + 1 < n ? i0 + 1 : i0;
|
||||
return true;
|
||||
}
|
||||
u64 snap_index(f64 x) const override {
|
||||
u64 i0 = 0, i1 = 0;
|
||||
if (!neighbours(x, i0, i1)) return 0;
|
||||
return i1;
|
||||
}
|
||||
|
||||
/// SlotSeries::copy_raw forwards here (public: the base's hook is protected,
|
||||
/// and SlotSeries is a different class).
|
||||
void raw_values(u64 begin, u64 end, f32* out) const {
|
||||
const auto& vals = snap_->values[channel_];
|
||||
const u64 n = static_cast<u64>(vals.size());
|
||||
if (end > n) end = n;
|
||||
for (u64 i = begin; i < end; ++i)
|
||||
out[static_cast<std::size_t>(i - begin)] = vals[static_cast<std::size_t>(i)];
|
||||
}
|
||||
|
||||
protected:
|
||||
void copy_raw(u64 begin, u64 end, f32* out) const override { raw_values(begin, end, out); }
|
||||
|
||||
private:
|
||||
std::shared_ptr<const EventSnapshot> snap_;
|
||||
std::size_t channel_;
|
||||
plot::data::MinMaxPyramid empty_pyramid_{};
|
||||
};
|
||||
|
||||
} // namespace udpscope
|
||||
Reference in new issue
Block a user