Correct downsampling and better layout

This commit is contained in:
Martino Ferrari committed 2026-10-02 08:19:10 +02:00
1 parent 0d741c401b
commit 30db44eaa0
13 files changed
+1511 -252

No files matched your search

+33 -5
View File
@@ -69,7 +69,12 @@ struct ChannelInfo {
/// coincide and every slice is the same size.
struct Partition {
u64 time_capacity = 0; ///< time-base slots retained
u64 time_stride = 1; ///< base ticks per time-base slot (reserved; 1)
/// Base ticks per stored slot: the decimation factor the requested window forced. 1
/// until the window needs more samples than the budget can hold. The ingest path
/// *averages* each group of this many samples rather than keeping one of them (see
/// `Acquisition::on_data()`), so this is a sample rate reduction with an anti-alias
/// filter in front of it, not a stride.
u64 time_stride = 1;
std::vector<u64> stride; ///< time slots per channel sample (>= 1)
std::vector<u64> value_capacity; ///< samples retained per channel
double base_dt = 1.0; ///< seconds per base tick
@@ -79,12 +84,23 @@ struct Partition {
u64 total_bytes() const noexcept { return total_samples() * sizeof(f32); }
/// Seconds between two time-base slots (the sample interval of every channel).
double slot_dt() const noexcept { return base_dt * static_cast<double>(time_stride); }
/// The rate samples are actually stored at, in samples per second.
double stored_rate() const noexcept {
const double dt = slot_dt();
return dt > 0.0 ? 1.0 / dt : 0.0;
}
/// The anti-alias filter's corner, in Hz: half the stored rate, which is the new
/// Nyquist. The boxcar the ingest path averages with has its first null at the stored
/// rate itself, so everything that would fold back is attenuated.
double antialias_hz() const noexcept { return 0.5 * stored_rate(); }
};
/// Plans a partition for `channel_dt` cadences within `buffer_bytes`, targeting a
/// span of `target_span_s` seconds. Throws std::runtime_error when the budget
/// cannot hold even one sample per channel.
Partition plan_partition(const std::vector<double>& channel_dt, double buffer_bytes);
/// Plans a partition for `channel_dt` cadences within `buffer_bytes`, covering at least
/// `target_span_s` seconds of stream -- decimating (with the ingest filter) by whatever
/// power of two that takes. Throws std::runtime_error when the budget cannot hold even
/// one sample per channel.
Partition plan_partition(const std::vector<double>& channel_dt, double buffer_bytes,
double target_span_s);
/// One of the two identical sample buffers: a shared time-base ring plus one
/// value ring and one `ChannelBuffer` per channel.
@@ -287,6 +303,14 @@ class ScopeModel {
}
double event_span() const noexcept { return event_span_.load(std::memory_order_relaxed); }
/// The window the buffer has to cover, which is the display window. Re-plans the
/// partition -- and so drops the history -- only when the decimation factor it implies
/// actually changes, so walking the window inside one decade costs nothing. Takes
/// `mutex()` itself; do not call it holding the lock.
/// Returns true when the buffer was rebuilt.
bool set_target_span(double seconds);
double target_span() const noexcept { return target_span_.load(std::memory_order_relaxed); }
/// Trigger state for the UI, published by the acquisition thread. Atomics
/// rather than fields of `TriggerConfig` because the producer writes the arm
/// state when a single-shot trigger disarms itself, and the frame tick reads it
@@ -321,6 +345,10 @@ class ScopeModel {
StreamStats stats_{};
f64 epoch_ = 0.0;
std::atomic<double> event_span_{1.0};
/// The window the buffer is planned to cover. Read by `rebuild_locked()`, which a
/// CONFIG can reach from the producer thread, so it is an atomic rather than a plain
/// field.
std::atomic<double> target_span_{0.0};
std::atomic<bool> trigger_pending_{false};
std::atomic<double> capture_edge_{0.0};
std::atomic<std::shared_ptr<const EventSnapshot>> event_; ///< the event on screen