fix: a recompute that renumbered its ring, and readouts frozen under a zoom

Three defects, all of them a window describing data that is not there.

A math edit refilled the derived ring with `clear()`, which renumbers it
from 0, leaving `produced()` short by the inputs' retained base. Since
`live_window_locked()` clamps against every channel's `produced()`, that
one lagging channel answered `n = 0` for the live view, every pause view
and every trigger capture until a whole ring of fresh data had gone by --
the reported freeze, invisible until the ring first wraps, hence its
intermittence. It now restarts with `clear_to(base)`, and
`ChannelBuffer::republish_from(base)` puts the published window back:
`acquire()` is incremental, so a rewrite in place published nothing and
the pyramid went on describing the samples the edit replaced.

The trigger's channel is an index, re-resolved by name after a renumbering
-- but only in `rebuild_enabled_locked()`, so a math add or delete went
through `set_math_channels()` with no restore, the index was clamped to
-1, and `process()` returned at once while the panel still named the
channel. The restore moves into `rebuild_channels()`, which *is* the
renumbering, so no caller can forget it.

And `update_measures()` suppressed its "the window moved" test while an
event was held -- the one mode where a gesture is the only thing that can
move it. Zooming a latched event left the pane describing the whole
captured event. A pinned window needs no exemption: it lands on the range
it already measured and fails the 2 % test by itself.

The checks that missed all three: `UDPSCOPE_PROBE_MATHWRAP_MS` edits a
graph whose ring has already wrapped, which `math_probe` structurally
cannot reach; `[measure-trace]` now prints the mode, the window on screen
and the window the numbers describe; and the dump's `marks_in_window`
counts against the window on screen instead of against the one the marks
were computed over, where it was a tautology.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-15 16:30:48 +02:00
1 parent 074cb611bc
commit d91957a2fe
15 files changed
+374 -45

No files matched your search

+14 -5
View File
@@ -5809,6 +5809,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
if (const char* env = g_getenv("UDPSCOPE_PROBE_MATH_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), math_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_MATHWRAP_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), mathwrap_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_MEASURE_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), measure_probe, &app);
}
@@ -6476,17 +6479,23 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
// Both on the **stream's** clock, which is the space `measure_window` and the time marks
// are stored in: a live axis is a fixed `[-window, 0]`, so comparing display x would read
// no drift whatever the data did (see `App::measure_window`).
const double s0 = model.stream_at_x0();
const RowSet::Row& r0 = rows.row(0);
const plot::Range view = r0.plot != nullptr ? r0.plot->x_range(r0.axis_x) : plot::Range{};
const plot::Range win{view.min + s0, view.max + s0};
//
// `win` comes from the last **tick** (`App::measure_view_window`) and is not read back here:
// this runs from the destroy handler, where the exit probe has already switched the trigger
// on, and `stream_at_x0()` then answers for a mode no frame was drawn in.
const plot::Range win = app.measure_view_window;
const double span = app.measure_window.span();
int in_window = 0;
int marks = 0;
// Against `win`, the window **on screen** -- not against `measure_window`, which is the
// window the mark was computed over and so contains it by construction. Read that way the
// count was a tautology: it printed `2/2` while the pane's numbers described a window 11 s
// and a forty-fold zoom away from the one displayed, which is how a held event that never
// recomputed under a gesture stayed invisible to every run of this check.
for (const RowSet::MeasureMark& mk : app.measure_marks) {
if (mk.level) continue;
++marks;
if (mk.value >= app.measure_window.min && mk.value <= app.measure_window.max) ++in_window;
if (mk.value >= win.min && mk.value <= win.max) ++in_window;
}
// Whether the marks the app placed are *unselectable* -- the property the pointer's hit
// test respects. Read back out of the plot, so this is the app's own setting and not