Correct downsampling and better layout
This commit is contained in:
1 parent
0d741c401b
commit
30db44eaa0
13 files changed
+1511
-252
No files matched your search
+88
-9
@@ -2,6 +2,7 @@
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
#pragma once
|
||||
|
||||
#include <plot/data/minmax_pyramid.hpp>
|
||||
#include <plot/data/series.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -30,6 +31,18 @@ struct EventSnapshot {
|
||||
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
|
||||
/// One LOD pyramid per channel, built once here rather than by the series.
|
||||
///
|
||||
/// It is the *renderer's* performance contract that needs it: `level_for()` descends
|
||||
/// to level 0 on a pyramid with no levels, so a snapshot without one is drawn from
|
||||
/// every raw sample it holds, every frame, for as long as the event is held -- a
|
||||
/// 2 s capture at 1 MS/s staged 6 million segments across three traces and took
|
||||
/// ~100 ms a frame, which is the "large data is laggy" report. It belongs to the
|
||||
/// snapshot and not to `EventSeries` because the snapshot is immutable and shared:
|
||||
/// built here it costs one O(n) pass per channel inside the copy loop that is
|
||||
/// already O(n), and the same channel on two rows shares it instead of building and
|
||||
/// storing it twice.
|
||||
std::vector<plot::data::MinMaxPyramid> pyramids;
|
||||
};
|
||||
|
||||
/// A plot::data::Series view of one channel of an EventSnapshot. The stamps are
|
||||
@@ -70,19 +83,35 @@ class EventSeries final : public plot::data::Series {
|
||||
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_; }
|
||||
/// The snapshot's pyramid for this channel (see `EventSnapshot::pyramids`). The
|
||||
/// empty one only stands in for a channel the snapshot does not carry, where the
|
||||
/// series has no samples either.
|
||||
const plot::data::MinMaxPyramid& pyramid() const override {
|
||||
return channel_ < snap_->pyramids.size() ? snap_->pyramids[channel_] : empty_pyramid_;
|
||||
}
|
||||
bool streaming() const noexcept override { return false; }
|
||||
|
||||
/// Envelope geometry per whole bucket, raw samples at the two partial edges --
|
||||
/// the same shape as `ChannelBuffer::gather()`, and for the same two reasons: the
|
||||
/// emitted geometry must span `[begin, end)` exactly and never reach outside it,
|
||||
/// and a *bucket's extremes* are what a scope has to draw. The decimation this
|
||||
/// replaced took every `2^level`-th sample, which hides exactly the spike a
|
||||
/// capture was triggered on -- and, since the level was always 0 before the
|
||||
/// snapshot carried a pyramid, it never even ran.
|
||||
///
|
||||
/// Indices need no rebasing: the pyramid is built from sample 0 and nothing is ever
|
||||
/// dropped from it, so `window_base()` is 0 and a window index *is* the absolute one
|
||||
/// (unlike the ring, whose window slides).
|
||||
void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
||||
plot::data::GatherMode /*mode*/ =
|
||||
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;
|
||||
const u64 b = std::min(begin, n);
|
||||
const u64 e = std::min(std::max(end, b), n);
|
||||
if (b >= e) return;
|
||||
// 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
|
||||
@@ -94,11 +123,61 @@ class EventSeries final : public plot::data::Series {
|
||||
// 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)]);
|
||||
const auto rel = [&](u64 i) {
|
||||
return static_cast<f32>(stamps[static_cast<std::size_t>(i)] - base);
|
||||
};
|
||||
const auto emit_raw = [&](u64 from, u64 to) {
|
||||
for (u64 i = from; i < to; ++i) {
|
||||
out_xy.push_back(rel(i));
|
||||
out_xy.push_back(vals[static_cast<std::size_t>(i)]);
|
||||
}
|
||||
};
|
||||
|
||||
if (level == 0) {
|
||||
emit_raw(b, e);
|
||||
return;
|
||||
}
|
||||
level = std::min(level, plot::data::kMaxPyramidLevel);
|
||||
const plot::data::MinMaxPyramid& pyr = pyramid();
|
||||
const u64 span = u64{plot::data::kBucket} << level;
|
||||
const u64 lbase = pyr.level_base(level);
|
||||
const u64 lend = lbase + pyr.level_size(level);
|
||||
const u64 first = std::max(b / span + (b % span == 0 ? u64{0} : u64{1}), lbase);
|
||||
const u64 last = std::min(e / span, lend);
|
||||
if (first >= last) {
|
||||
emit_raw(b, e);
|
||||
return;
|
||||
}
|
||||
if (b < first * span) emit_raw(b, std::min(first * span, e));
|
||||
|
||||
for (u64 bi = first; bi < last; ++bi) {
|
||||
const u32 count = pyr.bucket_count(level, bi);
|
||||
switch (mode) {
|
||||
case plot::data::GatherMode::Nearest: {
|
||||
const u64 centre = bi * span + span / 2;
|
||||
out_xy.push_back(rel(centre));
|
||||
out_xy.push_back(vals[static_cast<std::size_t>(centre)]);
|
||||
break;
|
||||
}
|
||||
case plot::data::GatherMode::Mean: {
|
||||
if (count == 0) break; // no finite sample: no mean to draw
|
||||
const u64 centre = bi * span + span / 2;
|
||||
out_xy.push_back(rel(centre));
|
||||
out_xy.push_back(static_cast<f32>(pyr.bucket_sum(level, bi) / f64(count)));
|
||||
break;
|
||||
}
|
||||
default: { // Envelope: min at the bucket's first sample, max at its last
|
||||
if (count == 0) break;
|
||||
const plot::data::MinMax m = pyr.bucket(level, bi);
|
||||
out_xy.push_back(rel(bi * span));
|
||||
out_xy.push_back(m.min);
|
||||
out_xy.push_back(rel(std::min((bi + 1) * span, e) - 1));
|
||||
out_xy.push_back(m.max);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (last * span < e) emit_raw(last * span, e);
|
||||
}
|
||||
|
||||
bool neighbours(f64 x, u64& i0, u64& i1) const override {
|
||||
|
||||
Reference in new issue
Block a user