From ee55ac84b72c6a16ed3d8fd42aba80128f118e5f Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Thu, 8 Oct 2026 20:12:38 +0200 Subject: [PATCH] feat: a reserved hole is shaded, so a sub-pixel gap is still a report --- src/plot_host.cpp | 17 +++++++++++++++++ src/plot_host.hpp | 16 ++++++++++++++++ src/ui.cpp | 3 +++ 3 files changed, 36 insertions(+) diff --git a/src/plot_host.cpp b/src/plot_host.cpp index a604234..34a9b4c 100644 --- a/src/plot_host.cpp +++ b/src/plot_host.cpp @@ -371,6 +371,23 @@ void RowSet::bind(ScopeModel& model, DisplayMode mode) { } } +void RowSet::set_invalid_spans(ScopeModel& model, DisplayMode mode) { + spans_.clear(); + if (mode == DisplayMode::Live || mode == DisplayMode::Capturing) { + // `Hole::x_begin`/`x_end` are stream seconds; the plots' x is `stream_t + epoch`. + const f64 epoch = model.display_epoch(); + for (const Hole& h : model.holes()) { + spans_.emplace_back(h.x_begin + epoch, h.x_end + epoch); + } + } + for (int i = 0; i < rows_; ++i) { + Row& r = row(i); + // Every row shares the one x window, so every row gets the same spans: a hole is a gap in + // the stream and not in one signal. + if (r.plot) r.plot->set_invalid_spans(spans_); + } +} + bool RowSet::live_edge(double& newest) const { bool any = false; for (int i = 0; i < rows_; ++i) { diff --git a/src/plot_host.hpp b/src/plot_host.hpp index b69f1ab..c5199a7 100644 --- a/src/plot_host.hpp +++ b/src/plot_host.hpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include namespace udpscope { @@ -179,6 +181,18 @@ class RowSet { /// and status line all describe the same display: the producer can open or close a /// capture between any two reads of `ScopeModel::display_mode()`. void bind(ScopeModel& model, DisplayMode mode); + /// Publishes `model.holes()` to every row as shading, in display coordinates. + /// + /// Separate from `bind()` and called *after* the display epoch has been written, because the + /// epoch the band needs is this frame's: `ui.cpp` recomputes it from the newly bound traces' + /// live edge, and the previous frame's value is a whole production interval stale -- a third + /// of the screen at 1 MS/s over a 50 ms window, which would put the band beside the gap + /// rather than over it. + /// + /// Shading only in `Live` and `Capturing`. A `Held` or `Paused` snapshot carries its own + /// stamps and its own x offset, and the live hole table does not address it; shading it would + /// accuse whichever samples happened to land under those coordinates. + void set_invalid_spans(ScopeModel& model, DisplayMode mode); /// Publishes the live buffer's new samples. void acquire(ScopeModel& model) { model.acquire_all(); } @@ -487,6 +501,8 @@ class RowSet { /// an absent entry means "the palette decides", which is what keeps a new CONFIG's signals /// coloured without anything having to seed them. std::map signal_colors_; + /// Reused across frames by `set_invalid_spans()`: see there. + std::vector> spans_; CursorMode cursor_mode_ = CursorMode::Vertical; bool cursors_on_ = false; bool relative_x_ = false; diff --git a/src/ui.cpp b/src/ui.cpp index 95ce578..ef0a8ba 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -6292,6 +6292,9 @@ gboolean tick(gpointer user) { : model.display_epoch(); } if (model.display_epoch() != want_epoch) model.set_display_epoch(want_epoch); + // The band last, after the epoch it is expressed in: it reads `model.holes()`, which the + // acquisition thread mutates, so it belongs under this lock and not after it. + app->rows->set_invalid_spans(model, display); } // The gesture probe's verdicts, one frame after the ask and after this frame's clamp: a