improved measuraments ,cursors, zooming and user interactions
This commit is contained in:
1 parent
0eceaf537b
commit
02620ca946
12 files changed
+1488
-171
No files matched your search
+653
-68
@@ -82,6 +82,11 @@ struct App {
|
||||
int dbg_cursors_step = 0;
|
||||
/// Which step of the fit probe runs next (`UDPSCOPE_PROBE_FIT_MS`).
|
||||
int dbg_fit_step = 0;
|
||||
/// Which step of the x-label-form probe runs next (`UDPSCOPE_PROBE_TIMELABELS_MS`).
|
||||
int dbg_timelabels_step = 0;
|
||||
/// The cursors probe's own bookkeeping: the stored time before a gestureless move, so the
|
||||
/// report can assert the move was ignored.
|
||||
double dbg_gestureless_before = 0.0;
|
||||
/// What `PlotArea::create()` is handed. The area is in figure mode from the start and
|
||||
/// never draws this, but it keeps a reference to the plot it was created with (its
|
||||
/// single-plot controller holds one) -- and every plot of the figure is destroyed by a
|
||||
@@ -171,7 +176,29 @@ struct App {
|
||||
bool measure_was_held = false;
|
||||
/// The visible samples, per channel, for the *pass*: several measurements on one signal
|
||||
/// must not copy the ring several times.
|
||||
std::unordered_map<int, std::pair<std::vector<double>, std::vector<double>>> measure_samples;
|
||||
/// The visible window of one channel, with the extremes several measurements on it share:
|
||||
/// each of them needs `lo`/`hi` (a threshold is a share of the range, and two of the types
|
||||
/// *are* the range), and the scan is over every visible sample.
|
||||
struct MeasureWindow {
|
||||
std::vector<double> time;
|
||||
std::vector<double> value;
|
||||
MeasureStats stats;
|
||||
};
|
||||
std::unordered_map<int, MeasureWindow> measure_samples;
|
||||
/// When the pane was last recomputed, for the refresh cadence above.
|
||||
gint64 measure_stamp_us = 0;
|
||||
bool measure_stamp_valid = false;
|
||||
/// What the pane cost: the recomputes it ran and the wall time they took. A readout that is
|
||||
/// recomputed per frame over a live window is exactly the kind of thing that is invisible in a
|
||||
/// frame mean, so it is measured where it happens and reported in the dump.
|
||||
gint64 measure_cost_us = 0;
|
||||
int measure_passes = 0;
|
||||
/// The last pass' own duration, which the cadence adapts to.
|
||||
gint64 measure_last_pass_us = 0;
|
||||
/// The last *valid* result per measurement, which the marks fall back to: a level is a property
|
||||
/// of the signal and survives the window it was measured in, so the rulers do not blink off when
|
||||
/// a pass lands on a window with no samples (a rebuild, a capture boundary, a mode change).
|
||||
std::vector<MeasureResult> measure_last_valid;
|
||||
/// The marks the last computation produced: re-placed every frame, which is what makes the
|
||||
/// measurement cursors read-only.
|
||||
std::vector<RowSet::MeasureMark> measure_marks;
|
||||
@@ -259,6 +286,12 @@ struct App {
|
||||
/// Frames drawn with every bound trace slot empty although its channel has
|
||||
/// samples: the view was swapped and not yet published.
|
||||
int dbg_blank_frames = 0;
|
||||
/// The *renderer's* blank: a frame that staged no trace geometry while every bound trace held
|
||||
/// samples. `dbg_blank_frames` cannot see this one -- it asks the rings, and a ring that holds
|
||||
/// data while the renderer draws nothing is exactly the "the data disappears for a frame" glitch.
|
||||
int dbg_render_blank_frames = 0;
|
||||
/// The last frame's staged segment count, so the tick can judge the frame the renderer drew.
|
||||
unsigned long long dbg_seg_last = 0;
|
||||
/// Frames the one area staged: a figure is one `::render` for every row.
|
||||
int dbg_frames_painted = 0;
|
||||
/// Where a frame's time goes, split at the two boundaries that can be observed from
|
||||
@@ -309,6 +342,20 @@ struct App {
|
||||
int dbg_axes_w_max = -(1 << 20);
|
||||
int dbg_axes_w_prev = -1;
|
||||
int dbg_axes_w_changes = 0;
|
||||
/// What the pan probe asked for, so the frame after it can be compared against it: a capture
|
||||
/// during the probe is not a value either side can recompute, and a capturing lambda cannot be
|
||||
/// a `GSourceFunc`.
|
||||
double dbg_pan_asked_lo = 0.0;
|
||||
double dbg_pan_asked_hi = 0.0;
|
||||
bool dbg_pan_asked = false;
|
||||
/// Which gesture the probe is on: 1 = the pan was asked and its verdict is due, 2 = the
|
||||
/// zoom-out was asked and its verdict is due.
|
||||
int dbg_gesture_stage = 0;
|
||||
/// A verdict the *tick* owes, one frame after a gesture was asked: 1 pan, 2 zoom-out, 3
|
||||
/// zoom-in. Reported from the frame loop rather than a `g_timeout`, because a 300 ms timer
|
||||
/// slips past the state it was meant to observe as soon as the frames are heavy -- which is
|
||||
/// exactly the load this probe runs under.
|
||||
int dbg_gesture_verdict = 0;
|
||||
int dbg_axes_w_change_frame = -1;
|
||||
int dbg_axes_w_change_side = -1;
|
||||
float dbg_scale_min = 1e9f;
|
||||
@@ -773,6 +820,7 @@ int on_render_probe(GtkGLArea*, GdkGLContext*, gpointer user) {
|
||||
app->dbg_seg_sum += s.staged_segments;
|
||||
app->dbg_seg_max = std::max(app->dbg_seg_max, s.staged_segments);
|
||||
app->dbg_visible_max = std::max(app->dbg_visible_max, s.visible_samples);
|
||||
app->dbg_seg_last = s.staged_segments;
|
||||
if (s.raw_traces > 0) ++app->dbg_raw_frames;
|
||||
if (s.lod_traces > 0) ++app->dbg_lod_frames;
|
||||
}
|
||||
@@ -1492,6 +1540,30 @@ void on_pause_toggled(GtkToggleButton* button, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
const bool paused = gtk_toggle_button_get_active(button);
|
||||
app->acq->set_paused(paused);
|
||||
app->model->set_paused(paused);
|
||||
if (paused) {
|
||||
// Freeze the *view*: snapshot exactly the window on screen, and pin the axes there. The
|
||||
// acquisition thread keeps writing the live ring behind it, so resuming shows the data that
|
||||
// arrived while paused instead of a hole.
|
||||
if (RowSet::Row& r0 = app->rows->row(0); r0.plot != nullptr) {
|
||||
const plot::Range view = r0.plot->x_range(r0.axis_x);
|
||||
const double epoch = app->model->display_epoch();
|
||||
app->model->publish_pause_view(view.min - epoch, view.max - epoch);
|
||||
// Pin on what the snapshot *got*: the cut is clamped to the ring, so it can be narrower than
|
||||
// the view, and pinning the request left the shortfall as an empty left margin.
|
||||
plot::Range pin = view;
|
||||
if (std::shared_ptr<const EventSnapshot> snap = app->model->pause_view()) {
|
||||
if (!snap->stamps.empty()) {
|
||||
pin = plot::Range{static_cast<double>(snap->stamps.front()),
|
||||
static_cast<double>(snap->stamps.back())};
|
||||
}
|
||||
}
|
||||
app->rows->pin_pause_window(pin.min, pin.max);
|
||||
}
|
||||
} else {
|
||||
app->model->clear_pause_view();
|
||||
app->rows->unpin_pause_window();
|
||||
}
|
||||
gtk_button_set_icon_name(GTK_BUTTON(button), paused ? "media-playback-start-symbolic"
|
||||
: "media-playback-pause-symbolic");
|
||||
gtk_widget_set_tooltip_text(GTK_WIDGET(button), paused ? "resume acquisition"
|
||||
@@ -1553,19 +1625,23 @@ bool visible_samples(App& app, std::size_t channel, std::vector<double>& time,
|
||||
if (r0.plot == nullptr) return false;
|
||||
const plot::Range view = r0.plot->x_range(r0.axis_x);
|
||||
|
||||
if (app.model->display_mode() == DisplayMode::Held) {
|
||||
std::shared_ptr<const EventSnapshot> snap = app.model->event();
|
||||
if (std::shared_ptr<const EventSnapshot> snap = app.model->display_snapshot()) {
|
||||
if (snap == nullptr || channel >= snap->values.size()) return false;
|
||||
const std::vector<f32>& st = snap->stamps;
|
||||
const std::vector<f32>& vals = snap->values[channel];
|
||||
if (vals.size() != st.size()) return false;
|
||||
const auto lo = std::lower_bound(st.begin(), st.end(), static_cast<f32>(view.min));
|
||||
const auto hi = std::upper_bound(st.begin(), st.end(), static_cast<f32>(view.max));
|
||||
for (auto it = lo; it != hi; ++it) {
|
||||
time.push_back(static_cast<double>(*it));
|
||||
value.push_back(static_cast<double>(vals[static_cast<std::size_t>(it - st.begin())]));
|
||||
const std::size_t count = static_cast<std::size_t>(hi - lo);
|
||||
if (count == 0) return false;
|
||||
const std::size_t off = static_cast<std::size_t>(lo - st.begin());
|
||||
time.resize(count);
|
||||
value.resize(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
time[i] = static_cast<double>(st[off + i]);
|
||||
value[i] = static_cast<double>(vals[off + i]);
|
||||
}
|
||||
return !time.empty();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
@@ -1577,17 +1653,26 @@ bool visible_samples(App& app, std::size_t channel, std::vector<double>& time,
|
||||
app.model->live_window_locked(view.min - epoch, view.max - epoch, first, last);
|
||||
const u64 n = last - first;
|
||||
if (n == 0) return false;
|
||||
std::vector<f32> stamps(static_cast<std::size_t>(n));
|
||||
std::vector<f32> vals(static_cast<std::size_t>(n));
|
||||
store.time().copy_from(first, n, stamps.data());
|
||||
store.values(channel).copy_from(first, n, vals.data());
|
||||
time.reserve(stamps.size());
|
||||
value.reserve(vals.size());
|
||||
for (std::size_t i = 0; i < stamps.size(); ++i) {
|
||||
time.push_back(epoch + static_cast<double>(stamps[i]));
|
||||
value.push_back(static_cast<double>(vals[i]));
|
||||
time.resize(static_cast<std::size_t>(n));
|
||||
value.resize(static_cast<std::size_t>(n));
|
||||
// In bounded chunks rather than through two whole-window temporaries: the rings hold f32 and
|
||||
// the engine reads f64, so the widening is unavoidable, but the *copies* are not -- a full
|
||||
// window is millions of samples, and the temporaries cost a full write and read of it on top
|
||||
// of the result. `kChunk` keeps the scratch in cache.
|
||||
constexpr u64 kChunk = 8192;
|
||||
std::vector<f32> scratch(kChunk);
|
||||
for (u64 at = 0; at < n; at += kChunk) {
|
||||
const u64 take = std::min<u64>(kChunk, n - at);
|
||||
store.time().copy_from(first + at, take, scratch.data());
|
||||
for (u64 i = 0; i < take; ++i) {
|
||||
time[static_cast<std::size_t>(at + i)] = epoch + static_cast<double>(scratch[i]);
|
||||
}
|
||||
store.values(channel).copy_from(first + at, take, scratch.data());
|
||||
for (u64 i = 0; i < take; ++i) {
|
||||
value[static_cast<std::size_t>(at + i)] = static_cast<double>(scratch[i]);
|
||||
}
|
||||
}
|
||||
return !time.empty();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The channel a measurement's signal resolved to, or -1 while the stream does not carry it.
|
||||
@@ -1626,6 +1711,16 @@ std::string measure_unit(App& app, const std::string& signal) {
|
||||
return "";
|
||||
}
|
||||
|
||||
/// How often the measurements pane is recomputed while the window is still moving: a readout,
|
||||
/// where the honest currency is the window's own settling, not the frame rate. Ten a second is
|
||||
/// past the point a number can be read, and it makes the cost independent of the framerate.
|
||||
constexpr gint64 kMeasureRefreshUs = 100 * 1000;
|
||||
/// A pass that costs more than this is a *hitch*, not a refresh: the pane waits four times as
|
||||
/// long next time. Bounding the interval alone does not bound what a frame pays -- a window of
|
||||
/// thirteen million samples costs 13 ms, which is most of a frame -- so the cadence follows the
|
||||
/// *work*, and what stays bounded is the duty cycle at any window size.
|
||||
constexpr gint64 kMeasureHeavyUs = 5 * 1000;
|
||||
|
||||
void rebuild_measures_pane(App& app);
|
||||
void update_measures(App& app, bool force);
|
||||
void open_measure_dialog(App& app, int edit_index);
|
||||
@@ -1997,6 +2092,13 @@ void measure_dialog_sync(MeasureDialog* d) {
|
||||
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show),
|
||||
"show on plot (ruler at the value, marker where it is)");
|
||||
break;
|
||||
case MeasureType::PeakToPeak:
|
||||
case MeasureType::Amplitude:
|
||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), "");
|
||||
gtk_label_set_text(GTK_LABEL(d->side_label), "");
|
||||
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show),
|
||||
"show on plot (rulers at both extremes)");
|
||||
break;
|
||||
case MeasureType::Top:
|
||||
case MeasureType::Bottom:
|
||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), "split % of the range");
|
||||
@@ -2212,7 +2314,32 @@ void update_measures(App& app, bool force) {
|
||||
std::fabs(view.min - app.measure_window.min) +
|
||||
std::fabs(view.max - app.measure_window.max) >
|
||||
0.02 * std::max(1.0e-12, view.span());
|
||||
if (!force && app.measure_window_valid && !moved && held == app.measure_was_held) {
|
||||
// A measurement is a statistic of the visible window, and *a live view moves that window every
|
||||
// frame* -- so "recompute when the window moves" is "recompute every frame", at O(visible
|
||||
// samples) per measurement. That is what stalled the frame tick (0.4 ms -> 13.7 ms, a third of
|
||||
// the run's frames lost) once the window held a few million samples. The pane is a *readout*,
|
||||
// so it is refreshed on the terms the status line's own fields already use: at most every
|
||||
// `kMeasureRefreshUs`, plus immediately whenever the window stops moving or the mode changes,
|
||||
// which is when the number has to be exact. Between those a tenth of a second of lag is
|
||||
// invisible on a statistic that is itself a window average, and the marks below are re-placed
|
||||
// from the last result every frame either way.
|
||||
const gint64 now_us = g_get_monotonic_time();
|
||||
const gint64 interval =
|
||||
app.measure_last_pass_us > kMeasureHeavyUs ? 4 * kMeasureRefreshUs : kMeasureRefreshUs;
|
||||
const bool due = !app.measure_stamp_valid || now_us - app.measure_stamp_us >= interval;
|
||||
// Two reasons to skip, and they are different questions. `measure_window` is the window the
|
||||
// *last recompute described*, so no change at all means the pane already says the right thing:
|
||||
// that is the settled case, and it must recompute once when it settles and then never again --
|
||||
// getting this backwards is what had a *pinned* window recomputing every single frame (457
|
||||
// passes in one run) instead of once. The other is a window still scrolling: at most one pass
|
||||
// per `kMeasureRefreshUs`, because the number is a window average and a tenth of a second of
|
||||
// lag on it is not readable.
|
||||
const bool changed = !app.measure_window_valid || held != app.measure_was_held || moved;
|
||||
if (!force && !changed) {
|
||||
app.rows->set_measure_marks(app.measure_marks);
|
||||
return;
|
||||
}
|
||||
if (!force && moved && held == app.measure_was_held && !due) {
|
||||
// Nothing to recompute -- but the marks are still re-placed below, which is what makes
|
||||
// them read-only: a gesture moves one on the plot, and this is what puts it back.
|
||||
app.rows->set_measure_marks(app.measure_marks);
|
||||
@@ -2221,45 +2348,62 @@ void update_measures(App& app, bool force) {
|
||||
app.measure_window = view;
|
||||
app.measure_window_valid = true;
|
||||
app.measure_was_held = held;
|
||||
app.measure_stamp_us = now_us;
|
||||
app.measure_stamp_valid = true;
|
||||
const gint64 pass_start_us = g_get_monotonic_time();
|
||||
app.measure_samples.clear();
|
||||
|
||||
app.measure_results.assign(app.measures.size(), MeasureResult{});
|
||||
app.measure_last_valid.resize(app.measures.size());
|
||||
app.measure_text.assign(app.measures.size(), std::string("--"));
|
||||
app.measure_marks.clear();
|
||||
std::vector<RowSet::MeasureMark> marks;
|
||||
struct PassTimer {
|
||||
App& app;
|
||||
gint64 start;
|
||||
~PassTimer() {
|
||||
const gint64 took = g_get_monotonic_time() - start;
|
||||
app.measure_cost_us += took;
|
||||
app.measure_last_pass_us = took;
|
||||
++app.measure_passes;
|
||||
}
|
||||
} pass_timer{app, pass_start_us};
|
||||
for (std::size_t i = 0; i < app.measures.size(); ++i) {
|
||||
const Measure& m = app.measures[i];
|
||||
GtkWidget* label = app.measure_labels[i];
|
||||
const int channel = measure_channel(app, m.signal);
|
||||
std::vector<double> t;
|
||||
std::vector<double> v;
|
||||
if (channel < 0) {
|
||||
// Its signal is momentarily unresolvable -- a rebuild renumbers the channels and empties
|
||||
// the list -- so there is nothing to measure *this* pass. The marks still fall back (below);
|
||||
// `continue`ing here was the other half of the rulers blinking off.
|
||||
app.measure_text[i] = "--";
|
||||
if (label != nullptr) gtk_label_set_text(GTK_LABEL(label), "--");
|
||||
continue;
|
||||
}
|
||||
{
|
||||
// One copy of the visible window per channel per pass, shared by every measurement on
|
||||
// that signal (the common case is several on one trace).
|
||||
auto it = app.measure_samples.find(channel);
|
||||
if (it == app.measure_samples.end()) {
|
||||
std::pair<std::vector<double>, std::vector<double>> fresh;
|
||||
if (visible_samples(app, static_cast<std::size_t>(channel), fresh.first, fresh.second)) {
|
||||
it = app.measure_samples.emplace(channel, std::move(fresh)).first;
|
||||
}
|
||||
} else if (it->second.first.empty()) {
|
||||
it = app.measure_samples.end();
|
||||
// One *window* per channel per pass, shared by every measurement on that signal (the common
|
||||
// case is several on one trace). It is held by reference from here on: this loop used to copy
|
||||
// both vectors out of the cache for every measurement, which at four measurements over a
|
||||
// 4.7M-sample window is 300 MB of memcpy per frame -- memory-bandwidth bound, and the tick's
|
||||
// mean went from 0.4 ms to 13.7 ms with the pane holding them.
|
||||
auto it = app.measure_samples.find(channel);
|
||||
if (it == app.measure_samples.end()) {
|
||||
App::MeasureWindow fresh;
|
||||
if (visible_samples(app, static_cast<std::size_t>(channel), fresh.time, fresh.value)) {
|
||||
fresh.stats = measure_stats(fresh.time, fresh.value);
|
||||
it = app.measure_samples.emplace(channel, std::move(fresh)).first;
|
||||
}
|
||||
if (it == app.measure_samples.end()) {
|
||||
app.measure_text[i] = "--";
|
||||
if (label != nullptr) gtk_label_set_text(GTK_LABEL(label), "--");
|
||||
continue;
|
||||
}
|
||||
t = it->second.first;
|
||||
v = it->second.second;
|
||||
} else if (it->second.time.empty()) {
|
||||
it = app.measure_samples.end();
|
||||
}
|
||||
const MeasureResult r = compute_measure(t, v, m.type, m.options);
|
||||
if (it == app.measure_samples.end()) {
|
||||
app.measure_text[i] = "--";
|
||||
if (label != nullptr) gtk_label_set_text(GTK_LABEL(label), "--");
|
||||
continue;
|
||||
}
|
||||
const App::MeasureWindow& w = it->second;
|
||||
const MeasureResult r = compute_measure(w.time, w.value, m.type, m.options, &w.stats);
|
||||
app.measure_results[i] = r;
|
||||
if (r.valid) app.measure_last_valid[i] = r;
|
||||
if (label != nullptr) {
|
||||
std::string text = "--";
|
||||
if (r.valid) {
|
||||
@@ -2279,12 +2423,30 @@ void update_measures(App& app, bool force) {
|
||||
app.measure_text[i] = text;
|
||||
gtk_label_set_text(GTK_LABEL(label), text.c_str());
|
||||
}
|
||||
if (m.options.show_on_plot && r.valid) {
|
||||
const int row = measure_row(app, m.signal);
|
||||
const plot::u32 color = measure_color(i);
|
||||
for (const MeasureMarkSpec& spec : r.marks) {
|
||||
marks.push_back({row, spec.level, spec.value, color});
|
||||
}
|
||||
}
|
||||
|
||||
// The marks last, in their own pass over the measurements, so that no `continue` above can skip
|
||||
// them. That is exactly how the rulers came to blink off -- first at a mode change, then at a
|
||||
// rebuild where the signal is momentarily unresolvable -- while the plot put them back a few
|
||||
// frames later, which reads as the lines flickering.
|
||||
//
|
||||
// A pass with no answer falls back to the last valid one, and the split is by *kind*: a
|
||||
// **level** is a property of the signal (a mean, a top, an amplitude) and outlives the window it
|
||||
// was measured in, so its ruler stays; a **time** mark is a position *inside* the window that
|
||||
// result describes, so it may not outlive it and is dropped.
|
||||
for (std::size_t i = 0; i < app.measures.size() && i < app.measure_results.size(); ++i) {
|
||||
const Measure& m = app.measures[i];
|
||||
if (!m.options.show_on_plot) continue;
|
||||
const MeasureResult& r = app.measure_results[i];
|
||||
const MeasureResult* mk = &r;
|
||||
if (!r.valid && i < app.measure_last_valid.size() && app.measure_last_valid[i].valid) {
|
||||
mk = &app.measure_last_valid[i];
|
||||
}
|
||||
const int row = measure_row(app, m.signal);
|
||||
const plot::u32 color = measure_color(i);
|
||||
for (const MeasureMarkSpec& spec : mk->marks) {
|
||||
if (!spec.level && !r.valid) continue;
|
||||
marks.push_back({row, spec.level, spec.value, color});
|
||||
}
|
||||
}
|
||||
app.measure_marks = std::move(marks);
|
||||
@@ -2818,7 +2980,21 @@ gboolean tick(gpointer user) {
|
||||
if (model.generation() != app->signal_generation) rebuild_signal_panels(*app);
|
||||
// A gesture moves the *plot's* cursor; adopting it here is what makes the two
|
||||
// vertical markers one time across every row, and what the pane then reads.
|
||||
app->rows->sync_cursors();
|
||||
app->rows->update_time_labels();
|
||||
// Which row's controller holds a cursor: the only row whose plots are worth reading back
|
||||
// (`cursor(id).x` is the free pointer position while a drag runs and the snapped value
|
||||
// otherwise, so reading outside a gesture reads our own echo).
|
||||
int cursor_drag_row = -1;
|
||||
if (app->area != nullptr) {
|
||||
for (int i = 0; i < app->rows->row_count(); ++i) {
|
||||
plot::input::Controller* c = app->area->row_controller(static_cast<plot::u32>(i));
|
||||
if (c != nullptr && c->grabbed_cursor() != plot::kInvalid) {
|
||||
cursor_drag_row = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
app->rows->sync_cursors(cursor_drag_row);
|
||||
update_cursors_pane(*app);
|
||||
update_measures(*app, false);
|
||||
|
||||
@@ -2863,6 +3039,69 @@ gboolean tick(gpointer user) {
|
||||
app->rows->acquire(model);
|
||||
}
|
||||
|
||||
// The gesture probe's verdicts, one frame after the ask and after this frame's clamp: a
|
||||
// `g_timeout` was the first version and it drifted past the paused/resumed boundary, so the
|
||||
// "zoom in" verdict read the *live* window and called a working fix broken.
|
||||
if (app->dbg_gesture_verdict != 0 && app->rows->row_count() > 0) {
|
||||
const int was_verdict = app->dbg_gesture_verdict;
|
||||
{
|
||||
const RowSet::Row& r0 = app->rows->row(0);
|
||||
const plot::Range after = app->rows->view_range();
|
||||
if (app->dbg_gesture_verdict == 1) {
|
||||
const bool moved = std::abs(after.min - app->dbg_pan_asked_lo) < 1e-9 &&
|
||||
std::abs(after.max - app->dbg_pan_asked_hi) < 1e-9;
|
||||
std::fprintf(stderr, "[pan-probe] after=[%.6f..%.6f] held=%d asked=[%.6f..%.6f]\n",
|
||||
after.min, after.max, moved ? 0 : 1, app->dbg_pan_asked_lo,
|
||||
app->dbg_pan_asked_hi);
|
||||
// The next gesture: a zoom-out ten times the current span, anchored where a wheel zoom
|
||||
// about the right edge would put it.
|
||||
const double span = after.span();
|
||||
if (span > 0.0) {
|
||||
const plot::Range zoomed{after.max - 10.0 * span, after.max};
|
||||
app->dbg_pan_asked_lo = zoomed.min;
|
||||
app->dbg_pan_asked_hi = zoomed.max;
|
||||
const_cast<RowSet::Row&>(r0).plot->set_x_range(r0.axis_x, zoomed);
|
||||
// Judged on the next frame's tick: the member's range is only pushed by the layout.
|
||||
app->dbg_gesture_verdict = 2;
|
||||
}
|
||||
} else if (app->dbg_gesture_verdict == 2) {
|
||||
const double window = app->rows->time_window();
|
||||
std::fprintf(stderr,
|
||||
"[pan-probe] zoom asked=[%.6f..%.6f] after=[%.6f..%.6f] span=%.6f "
|
||||
"window=%.6f limited=%d\n",
|
||||
app->dbg_pan_asked_lo, app->dbg_pan_asked_hi, after.min, after.max,
|
||||
after.span(), window, after.span() <= window * (1.0 + 1e-9) ? 1 : 0);
|
||||
// The last gesture: a window a quarter of what is on screen, about its centre. A pinned
|
||||
// view must allow it -- "not outside the event" is not "not readable".
|
||||
const double span = after.span();
|
||||
if (span > 0.0 && r0.plot != nullptr) {
|
||||
const double centre = 0.5 * (after.min + after.max);
|
||||
const plot::Range zoomed{centre - 0.125 * span, centre + 0.125 * span};
|
||||
app->dbg_pan_asked_lo = zoomed.min;
|
||||
app->dbg_pan_asked_hi = zoomed.max;
|
||||
const_cast<RowSet::Row&>(r0).plot->set_x_range(r0.axis_x, zoomed);
|
||||
app->dbg_gesture_verdict = 3;
|
||||
}
|
||||
} else if (app->dbg_gesture_verdict == 3) {
|
||||
// The zoom-in ask was a quarter of the span: allowed means the view actually narrowed.
|
||||
const double asked = app->dbg_pan_asked_hi - app->dbg_pan_asked_lo;
|
||||
const plot::Range member = r0.plot != nullptr ? r0.plot->x_range(r0.axis_x)
|
||||
: plot::Range{0.0, 0.0};
|
||||
std::fprintf(stderr,
|
||||
"[pan-probe] zoom-in asked=[%.6f..%.6f] group=[%.6f..%.6f] member=[%.6f..%.6f]"
|
||||
" span=%.6f allowed=%d\n",
|
||||
app->dbg_pan_asked_lo, app->dbg_pan_asked_hi, after.min, after.max,
|
||||
member.min, member.max, after.span(),
|
||||
after.span() <= 2.0 * asked ? 1 : 0);
|
||||
}
|
||||
std::fflush(stderr);
|
||||
// Cleared only if this frame did not hand the probe the *next* gesture: a report that asks
|
||||
// for the next one must leave it armed for the following frame.
|
||||
app->dbg_gesture_verdict = app->dbg_gesture_verdict == was_verdict ? 0 : app->dbg_gesture_verdict;
|
||||
app->dbg_gesture_stage = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to do about the rows' x windows: they are one window, the x link group's.
|
||||
// A gesture changes the row it landed on, `Figure::layout()` adopts that change into
|
||||
// the group and pushes it to the others, so the "which row acted" search this replaced
|
||||
@@ -2873,7 +3112,24 @@ gboolean tick(gpointer user) {
|
||||
// reports the acquisition buffer's raw coordinates, dragging the view along the ring.
|
||||
// Clamping is a live-view concern only, and a capture is live data on a pinned axis,
|
||||
// which is exactly the case it must not touch.
|
||||
if (!app->event_held && !app->capturing) app->rows->clamp_to_data();
|
||||
if (app->event_held && !app->capturing) {
|
||||
// A held event is navigable *within its data*. `pin_event_window()` framed the axis from the
|
||||
// snapshot's recorded `pre_s`/`post_s` -- the *request* -- and a capture shorter than it (a
|
||||
// ring that had not reached back a full pre-trigger window, or a trimmed one) left the
|
||||
// shortfall as an empty margin, exactly as the pause did. The pin is the snapshot's own stamps
|
||||
// here; the framing's split is what the marker at x = 0 already says.
|
||||
double lo = 0.0;
|
||||
double hi = 0.0;
|
||||
if (app->rows->event_data_window(lo, hi)) app->rows->set_data_pin(lo, hi);
|
||||
app->rows->hold_pinned_window();
|
||||
} else if (display == DisplayMode::Paused || app->capturing) {
|
||||
// Pinned: the event (or the frozen pause), not a navigation surface. `clamp_to_data()` is
|
||||
// the live-mode half of the same rule -- a live view must stay on the data -- and the two
|
||||
// are complements, which is why this is one branch rather than two conditions.
|
||||
app->rows->hold_pinned_window();
|
||||
} else {
|
||||
app->rows->clamp_to_data();
|
||||
}
|
||||
|
||||
// Curator: the trigger's arm state can change under us (single mode disarms
|
||||
// itself), so the button and the status line read the published atomics.
|
||||
@@ -3001,6 +3257,8 @@ gboolean tick(gpointer user) {
|
||||
}
|
||||
}
|
||||
if (bound > 0 && empty == bound) ++app->dbg_blank_frames;
|
||||
// And the renderer's blank: the rings hold samples, yet the frame staged nothing.
|
||||
if (bound > 0 && empty == 0 && app->dbg_seg_last == 0) ++app->dbg_render_blank_frames;
|
||||
}
|
||||
|
||||
// The gutter the rows need, measured from the frame just laid out, imposed as a
|
||||
@@ -3399,7 +3657,7 @@ void note_starvation(App& app) {
|
||||
|
||||
if (!starved) {
|
||||
app.starve_reported = false;
|
||||
return;
|
||||
// (the zoom-in is judged on the next frame, after the layout pushes it)
|
||||
}
|
||||
app.notice =
|
||||
"connected, but no data is arriving: another reader may hold the source (a unicast "
|
||||
@@ -3901,15 +4159,27 @@ gboolean trig_trace_probe(gpointer user) {
|
||||
dx_real = n >= 2 ? shown->x_at(n - 1) - shown->x_at(n - 2) : 0.0;
|
||||
bound_lo = shown->x_bounds().min;
|
||||
bound_hi = shown->x_bounds().max;
|
||||
if (dx_nominal > 0.0) {
|
||||
const auto clamp_index = [n](double v) -> long long {
|
||||
if (!(v > 0.0)) return 0;
|
||||
return v >= double(n) ? (long long)n : (long long)v;
|
||||
};
|
||||
got_lo = clamp_index(std::floor((axis.min - sx0) / dx_nominal) - 1.0);
|
||||
got_hi = clamp_index(std::ceil((axis.max - sx0) / dx_nominal) + 1.0);
|
||||
}
|
||||
u64 i0 = 0, i1 = 0;
|
||||
(void)sx0;
|
||||
if (dx_nominal > 0.0) {
|
||||
// What the renderer draws, through the series' own x: it used to walk the
|
||||
// `(x - x0()) / dx()` grid, and on a ring-backed series that landed the window inside
|
||||
// the visible range and dropped the two segments that draw into the left and right
|
||||
// margins. `neighbours()`, one sample of slack either side, is the whole of it now --
|
||||
// the same call the crosshair reads through. The *guard* against the grid walk coming
|
||||
// back is the library's own pixel test
|
||||
// (`a_sparse_window_between_samples_still_inks_both_margins`), because a probe that
|
||||
// recomputes the renderer's formula cannot see one that no longer matches it; what this
|
||||
// line still says is the rate: `dx_ratio` is how far the nominal grid sits from the real
|
||||
// stamps, which is what made the walk wrong in the first place.
|
||||
// A window edge outside the series keeps the renderer's own default on that side: 0 on
|
||||
// the left (draw from the oldest sample) and n on the right (draw to the newest), which
|
||||
// is why a view reaching past the data leaves the margin beyond it genuinely empty.
|
||||
got_lo = shown->neighbours(axis.min, i0, i1) ? (i0 > 0 ? (long long)i0 - 1 : 0) : 0;
|
||||
got_hi = shown->neighbours(axis.max, i0, i1)
|
||||
? std::min<long long>((long long)i1 + 1, (long long)n)
|
||||
: (long long)n;
|
||||
}
|
||||
want_lo = shown->neighbours(axis.min, i0, i1) ? (long long)i0
|
||||
: (axis.min < shown->x_at(0) ? 0 : (long long)n);
|
||||
want_hi = shown->neighbours(axis.max, i0, i1) ? (long long)i1 + 1
|
||||
@@ -3935,6 +4205,93 @@ gboolean trig_trace_probe(gpointer user) {
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
|
||||
/// Headless self-check: adds one measurement of each statistical type, which is what a user does
|
||||
/// from the pane's `+`.
|
||||
///
|
||||
/// It exists because the frame cost with measurements on is a thing to *measure*, not to reason
|
||||
/// about: a measurement is a statistic of the visible window, and a live view moves that window
|
||||
/// every frame. The values land in the exit dump (`measurements:`), next to `frame cost:`.
|
||||
gboolean measure_trace(gpointer user);
|
||||
|
||||
gboolean measure_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
std::string name;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
// A *channel*, not `signals().front()`: the first signal of a stream is its timestamp, which
|
||||
// is not acquired and has no samples to measure. That mistake made the probe add four
|
||||
// measurements that all read `--` -- and, because a measurement with no channel bails before
|
||||
// collecting anything, it also made the first baseline look free.
|
||||
if (a->model->channels().empty()) return G_SOURCE_REMOVE;
|
||||
name = a->model->channels().front().name;
|
||||
}
|
||||
for (const MeasureType t : {MeasureType::Average, MeasureType::Top, MeasureType::PeakToPeak,
|
||||
MeasureType::Amplitude}) {
|
||||
MeasureOptions options;
|
||||
// The marks are what a wrong window shows up in, so the probe asks for them.
|
||||
options.show_on_plot = measure_can_show_on_plot(t);
|
||||
a->measures.push_back({name, t, options});
|
||||
}
|
||||
rebuild_measures_pane(*a);
|
||||
update_measures(*a, true);
|
||||
std::fprintf(stderr, "[measure-probe] %zu measurements on '%s'\n", a->measures.size(),
|
||||
name.c_str());
|
||||
std::fflush(stderr);
|
||||
g_timeout_add(250, measure_trace, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Repeating trace of the pane's numbers beside the *current* window and the marks actually
|
||||
/// placed: the only way a stale result is visible as a number rather than as a stutter.
|
||||
gboolean measure_trace(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->rows->row_count() <= 0) return G_SOURCE_REMOVE;
|
||||
RowSet::Row& r0 = a->rows->row(0);
|
||||
if (r0.plot == nullptr) return G_SOURCE_REMOVE;
|
||||
const plot::Range view = r0.plot->x_range(r0.axis_x);
|
||||
std::string line = "[measure-trace] view=[" + std::to_string(view.min) + ".." +
|
||||
std::to_string(view.max) + "]";
|
||||
for (std::size_t i = 0; i < a->measures.size() && i < a->measure_results.size(); ++i) {
|
||||
const MeasureResult& r = a->measure_results[i];
|
||||
line += " " + std::string(measure_type_name(a->measures[i].type)) + "=" +
|
||||
(r.valid ? std::to_string(r.primary) : std::string("--"));
|
||||
}
|
||||
line += " marks=" + std::to_string(a->measure_marks.size());
|
||||
for (const RowSet::MeasureMark& m : a->measure_marks) {
|
||||
line += " " + std::string(m.level ? "level" : "time") + ":" + std::to_string(m.value);
|
||||
}
|
||||
std::fprintf(stderr, "%s\n", line.c_str());
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
/// Headless self-check: a gesture must not move a pinned window.
|
||||
///
|
||||
/// It writes the far-away range the way a gesture does -- on the *member* axis, which
|
||||
/// `Figure::layout()` adopts into the group -- and then reports the axis the frame after it
|
||||
/// produced: `held=1` when the pin survived, and the panned range when it did not.
|
||||
gboolean pan_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr || a->rows->row_count() <= 0) return G_SOURCE_REMOVE;
|
||||
RowSet::Row& r0 = a->rows->row(0);
|
||||
if (r0.plot == nullptr) return G_SOURCE_REMOVE;
|
||||
const plot::Range before = r0.plot->x_range(r0.axis_x);
|
||||
const double span = before.span();
|
||||
if (!(span > 0.0)) return G_SOURCE_REMOVE;
|
||||
// Where the data is not: ten windows to the left, the same order of magnitude a drag moves.
|
||||
const plot::Range panned{before.min - 10.0 * span, before.max - 10.0 * span};
|
||||
r0.plot->set_x_range(r0.axis_x, panned);
|
||||
std::fprintf(stderr, "[pan-probe] asked=[%.6f..%.6f] was=[%.6f..%.6f]\n", panned.min,
|
||||
panned.max, before.min, before.max);
|
||||
std::fflush(stderr);
|
||||
a->dbg_pan_asked_lo = panned.min;
|
||||
a->dbg_pan_asked_hi = panned.max;
|
||||
a->dbg_gesture_verdict = 1;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Headless self-check: arms the trigger the way the toolbar's trigger switch does.
|
||||
gboolean arm_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
@@ -4289,11 +4646,53 @@ gboolean disarm_probe_step1(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// What the pause work is *for*: the acquisition keeps filling the ring while the picture is
|
||||
/// frozen on the view the pause snapshotted, and resuming shows the data that arrived meanwhile
|
||||
/// instead of a hole. Reported from the probe below.
|
||||
void pause_probe_report(App& a, const char* what) {
|
||||
double bounds_lo = 0.0;
|
||||
double bounds_hi = 0.0;
|
||||
std::size_t event_size = 0;
|
||||
std::size_t ring_produced = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a.model->mutex());
|
||||
if (!a.model->channels().empty()) ring_produced = a.model->live().values(0).produced();
|
||||
}
|
||||
if (const EventSeries* ev = a.rows->row(0).slots[0].bound_event()) {
|
||||
event_size = ev->size();
|
||||
const plot::Range b = ev->x_bounds();
|
||||
bounds_lo = b.min;
|
||||
bounds_hi = b.max;
|
||||
}
|
||||
// The axis beside the data, because those two disagreeing is the whole failure: the frozen view
|
||||
// is the *snapshot*, and a pin taken from the requested window instead of from the snapshot left
|
||||
// the difference as an empty margin. `frozen_x` must equal `axis` while paused.
|
||||
plot::Range axis{0.0, 0.0};
|
||||
if (a.rows->row_count() > 0 && a.rows->row(0).plot != nullptr) {
|
||||
const RowSet::Row& r0 = a.rows->row(0);
|
||||
axis = r0.plot->x_range(r0.axis_x);
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
"[pause-probe] %-9s paused=%d ring_produced=%zu event_size=%zu "
|
||||
"frozen_x=[%.6f..%.6f] axis=[%.6f..%.6f]\n",
|
||||
what, a.model->paused() ? 1 : 0, ring_produced, event_size, bounds_lo, bounds_hi,
|
||||
double(axis.min), double(axis.max));
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
/// Headless self-check: pause the acquisition and reset the movement sample, so
|
||||
/// what is measured is a still picture -- the state a wiggle is most visible in.
|
||||
gboolean pause_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
a->acq->set_paused(true);
|
||||
// Through the button, so the *view* snapshot and the axis pin run: pausing is a display
|
||||
// state, not only an acquisition one.
|
||||
pause_probe_report(*a, "live");
|
||||
if (a->pause_button != nullptr) {
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->pause_button), TRUE);
|
||||
} else {
|
||||
a->acq->set_paused(true);
|
||||
a->model->set_paused(true);
|
||||
}
|
||||
a->dbg_axes_x_min = 1 << 20;
|
||||
a->dbg_axes_x_max = -(1 << 20);
|
||||
a->dbg_axes_w_min = 1 << 20;
|
||||
@@ -4304,6 +4703,27 @@ gboolean pause_probe(gpointer user) {
|
||||
a->dbg_axes_w_change_side = -1;
|
||||
a->dbg_vp_w_min = 1 << 20;
|
||||
a->dbg_vp_w_max = -(1 << 20);
|
||||
// Reported again a second later (the probe is re-armed below) and once after the resume: the
|
||||
// ring count must have grown while the frozen window did not move.
|
||||
static bool reported_paused = false;
|
||||
if (!reported_paused) {
|
||||
reported_paused = true;
|
||||
g_timeout_add(1200, +[](gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
pause_probe_report(*app, "paused");
|
||||
return G_SOURCE_REMOVE;
|
||||
}, a);
|
||||
g_timeout_add(1600, +[](gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app->pause_button), FALSE);
|
||||
return G_SOURCE_REMOVE;
|
||||
}, a);
|
||||
g_timeout_add(2200, +[](gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
pause_probe_report(*app, "resumed");
|
||||
return G_SOURCE_REMOVE;
|
||||
}, a);
|
||||
}
|
||||
a->dbg_xtick_min = 1 << 20;
|
||||
a->dbg_xtick_max = -(1 << 20);
|
||||
a->dbg_gutter_min = 1 << 20;
|
||||
@@ -4435,6 +4855,79 @@ void fit_probe_report(App& a, const char* what) {
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
/// The x label form across a zoom, driven through the *app's* window path (`apply_time_window`:
|
||||
/// under the figure's x link group the group owns the window, so a row-level `set_time_window`
|
||||
/// is ignored). Absolute at 2 s and at 500 ms, relative below a second with the caption naming
|
||||
/// the anchor and the ticks written against it.
|
||||
gboolean timelabels_probe_step(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
const char* what = "at 2 s";
|
||||
switch (a->dbg_timelabels_step) {
|
||||
case 0:
|
||||
apply_time_window(*a, 2.0);
|
||||
break;
|
||||
case 1:
|
||||
what = "at 500 ms";
|
||||
apply_time_window(*a, 0.5);
|
||||
break;
|
||||
case 2:
|
||||
what = "at 50 ms";
|
||||
apply_time_window(*a, 0.05);
|
||||
break;
|
||||
default:
|
||||
// Ends *in* a sub-second window: the dump runs after the last paint, so it is the
|
||||
// race-free place to read whether the zero tick really sits on the left edge.
|
||||
what = "final 20 ms";
|
||||
apply_time_window(*a, 0.02);
|
||||
break;
|
||||
}
|
||||
++a->dbg_timelabels_step;
|
||||
if (a->dbg_timelabels_step <= 4) {
|
||||
g_timeout_add(400, timelabels_probe_step, a);
|
||||
}
|
||||
// Report the *previous* step's state: the labels are laid out on the frame after the window
|
||||
// changes, so reading them in the same call would report the old form.
|
||||
const RowSet::Row& r0 = a->rows->row(0);
|
||||
const RowSet::Row& last = a->rows->row(a->rows->row_count() - 1);
|
||||
std::string ticks;
|
||||
std::string note;
|
||||
if (last.plot != nullptr) {
|
||||
int n = 0;
|
||||
for (const plot::Tick& t : last.plot->x_axis(last.axis_x).ticks) {
|
||||
if (!t.major) continue;
|
||||
if (n++ != 0) ticks += ",";
|
||||
ticks += t.label;
|
||||
if (n >= 4) break;
|
||||
}
|
||||
if (last.time_origin_note < last.plot->annotation_count()) {
|
||||
const plot::Annotation& an = last.plot->annotation(last.time_origin_note);
|
||||
if (an.visible) note = an.text;
|
||||
}
|
||||
}
|
||||
double first_value = 0.0;
|
||||
plot::Range shown{};
|
||||
if (last.plot != nullptr) {
|
||||
for (const plot::Tick& t : last.plot->x_axis(last.axis_x).ticks) {
|
||||
if (t.major) {
|
||||
first_value = t.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
shown = last.plot->x_range(last.axis_x);
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
"[timelabels-probe] %-12s relative=%d window=%.6g origin=%.6f note=[%s] "
|
||||
"ticks=[%s] first_value=%.6f first_rel=%.6f range=[%.6f..%.6f]\n",
|
||||
what, a->rows->time_labels_relative() ? 1 : 0, a->rows->time_window(),
|
||||
a->rows->time_labels_origin(), note.c_str(), ticks.c_str(), first_value,
|
||||
first_value - (last.plot != nullptr ? last.plot->x_axis_origin(last.axis_x) : 0.0),
|
||||
shown.min, shown.max);
|
||||
std::fflush(stderr);
|
||||
(void)r0;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean fit_probe_step(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
@@ -4502,9 +4995,12 @@ gboolean cursors_probe_step(gpointer user) {
|
||||
cursors_probe_next(*a, 5, 300);
|
||||
break;
|
||||
case 5: {
|
||||
// A drag, as the adapter delivers one: the *plot's* cursor moves and the frame
|
||||
// tick is what notices. If the sync works, every row's marker follows.
|
||||
// A plot-level move with *no gesture behind it*: the sync reads a row only while its
|
||||
// controller holds a cursor, so this must be ignored. That is the invariant the
|
||||
// "drag it and it springs back" defect broke -- the tick adopted a row's own echo, and a
|
||||
// row that had not been re-written yet adopted the stale shared value straight back.
|
||||
RowSet::Row& r0 = a->rows->row(0);
|
||||
a->dbg_gestureless_before = a->rows->cursor_a_x();
|
||||
if (r0.plot != nullptr) {
|
||||
const plot::Range xr = r0.plot->x_range(r0.axis_x);
|
||||
r0.plot->set_cursor_x(r0.cursor_a, xr.min + 0.20 * xr.span(), plot::CursorPlacement::Free);
|
||||
@@ -4512,17 +5008,26 @@ gboolean cursors_probe_step(gpointer user) {
|
||||
cursors_probe_next(*a, 6, 300);
|
||||
break;
|
||||
}
|
||||
case 6:
|
||||
case 6: {
|
||||
const double after = a->rows->cursor_a_x();
|
||||
std::fprintf(stderr,
|
||||
"[cursors-probe] %-14s plot move with no gesture: stored %.6f -> %.6f "
|
||||
"ignored=%d\n",
|
||||
"gestureless", a->dbg_gestureless_before, after,
|
||||
std::fabs(after - a->dbg_gestureless_before) < 1e-12 ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
cursors_probe_report(*a, "after drag");
|
||||
// The other half of issue 1: a *ruler* is dragged through its own plot, and the
|
||||
// pane's Y cells have to follow it. Nothing shares a ruler, so this is the read-back
|
||||
// path alone -- the one that used to leave the numbers showing where the app last
|
||||
// put them.
|
||||
a->rows->set_cursor_mode(RowSet::CursorMode::Both);
|
||||
// (The adoption half -- a gesture that *does* move a cursor -- is the library's own
|
||||
// controller test plus the app's rule that the tick passes the row whose controller holds
|
||||
// a cursor; a programmatic press here cannot reach the adapter's event pump, so this probe
|
||||
// asserts the invariant that is reachable from here.)
|
||||
cursors_probe_next(*a, 7, 300);
|
||||
break;
|
||||
}
|
||||
case 7: {
|
||||
cursors_probe_report(*a, "both");
|
||||
// A ruler is dragged through its own plot, and the pane's Y cells have to follow it.
|
||||
// Nothing shares a ruler, so this is the read-back path alone.
|
||||
RowSet::Row& r0 = a->rows->row(0);
|
||||
if (r0.plot != nullptr) {
|
||||
const plot::Range yr = r0.plot->y_range(r0.axis_y);
|
||||
@@ -4545,12 +5050,12 @@ gboolean cursors_probe_step(gpointer user) {
|
||||
void measure_dialog_click(App& a, const char* label) {
|
||||
if (a.measure_dialog == nullptr) {
|
||||
std::fprintf(stderr, "[measures-probe] no dialog to click '%s' in\n", label);
|
||||
return;
|
||||
// (the zoom-in is judged on the next frame, after the layout pushes it)
|
||||
}
|
||||
GtkWidget* b = find_labelled_button(a.measure_dialog, label);
|
||||
if (b == nullptr) {
|
||||
std::fprintf(stderr, "[measures-probe] no '%s' button in the dialog\n", label);
|
||||
return;
|
||||
// (the zoom-in is judged on the next frame, after the layout pushes it)
|
||||
}
|
||||
g_signal_emit_by_name(b, "clicked");
|
||||
}
|
||||
@@ -4722,7 +5227,11 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
app.source_port = cfg.port;
|
||||
|
||||
GtkApplication* application =
|
||||
gtk_application_new("org.udpscope.app", G_APPLICATION_DEFAULT_FLAGS);
|
||||
// `NON_UNIQUE`: a scope is the kind of program a user runs twice side by side -- one
|
||||
// instance per source, or two viewers of the same one -- and without it GApplication's
|
||||
// D-Bus single-instance rule turns every later launch into an activation on the first
|
||||
// (the new process exits at once, so only the first instance ever lives).
|
||||
gtk_application_new("org.udpscope.app", G_APPLICATION_NON_UNIQUE);
|
||||
g_signal_connect(application, "activate", G_CALLBACK(on_activate), &app);
|
||||
|
||||
// Headless self-check: exercise the toolbar's toggles, then exit and print what
|
||||
@@ -4748,6 +5257,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_WINDOW_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), window_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_TIMELABELS_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), timelabels_probe_step, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_MEASURES_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), measures_probe_step, &app);
|
||||
}
|
||||
@@ -4763,6 +5275,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_TRIG_TRACE_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), trig_trace_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_MEASURE_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), measure_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_PAN_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), pan_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_PAUSE_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), pause_probe, &app);
|
||||
}
|
||||
@@ -4833,6 +5351,8 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
app.dbg_axes_w_changes, app.dbg_axes_w_change_frame,
|
||||
app.dbg_axes_w_change_side);
|
||||
std::printf("[udpscope] reset button visible: %d\n", app.dbg_reset_visible);
|
||||
std::printf("[udpscope] render blank frames: %d of %d painted\n",
|
||||
app.dbg_render_blank_frames, app.dbg_frames_painted);
|
||||
std::printf("[udpscope] blank view frames: %d of %d\n", app.dbg_blank_frames,
|
||||
app.dbg_axes_frames);
|
||||
std::printf("[udpscope] trigger events: %llu total, %llu at the disarm (no event after it "
|
||||
@@ -4855,6 +5375,30 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
app.dbg_marker_x, app.dbg_marker_max, app.dbg_trigger_time,
|
||||
app.dbg_trigger_time_valid);
|
||||
std::printf("[udpscope] mode after go-live: %s\n", app.dbg_mode_live.c_str());
|
||||
// Unconditional, unlike the pane's own lines below: this is about the instruments the app
|
||||
// *created*, which exist whether or not any measurement was added.
|
||||
std::printf("[udpscope] readout instruments locked: %d (trigger marker + measurement marks)\n",
|
||||
app.rows->readouts_locked() ? 1 : 0);
|
||||
// The measurements pane, with the numbers the engine computed: a probe adds measurements and
|
||||
// this is where their values are read, so a wrong threshold or a window that moved under a
|
||||
// stale result is visible in the run rather than only on screen.
|
||||
if (!app.measures.empty()) {
|
||||
std::printf("[udpscope] measurements: %zu", app.measures.size());
|
||||
for (std::size_t i = 0; i < app.measures.size() && i < app.measure_results.size(); ++i) {
|
||||
const MeasureResult& r = app.measure_results[i];
|
||||
std::printf(" %s=%s", measure_type_name(app.measures[i].type),
|
||||
r.valid ? std::to_string(r.primary).c_str() : "--");
|
||||
if (r.has_secondary) std::printf("(%s)", std::to_string(r.secondary).c_str());
|
||||
}
|
||||
std::printf("\n");
|
||||
std::printf("[udpscope] clamp fires: %d (a clamp that fires every frame is a re-frame loop)\n",
|
||||
app.rows->clamp_fires());
|
||||
if (app.measure_passes > 0) {
|
||||
std::printf("[udpscope] measure cost: %d recomputes, %.2f ms total, %.2f ms each\n",
|
||||
app.measure_passes, app.measure_cost_us / 1000.0,
|
||||
app.measure_cost_us / 1000.0 / app.measure_passes);
|
||||
}
|
||||
}
|
||||
std::printf("[udpscope] badge frames: lit=%d dim=%d idle=%d\n", app.dbg_badge_frames[0],
|
||||
app.dbg_badge_frames[1], app.dbg_badge_frames[2]);
|
||||
std::printf("[udpscope] trigger panel revealed before the probe toggled it: %d; "
|
||||
@@ -4946,6 +5490,47 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
}
|
||||
std::printf("\n");
|
||||
}
|
||||
{
|
||||
// The x label form: relative to the window's own start (with the caption that names
|
||||
// that start) while the span is sub-second, absolute otherwise. `x_origin_set` is read
|
||||
// back out of the plot, so this is what the axis is actually formatting with.
|
||||
const RowSet::Row& rlt = rows.row(rows.row_count() - 1);
|
||||
std::string note;
|
||||
if (rlt.plot != nullptr && rlt.time_origin_note < rlt.plot->annotation_count()) {
|
||||
const plot::Annotation& a = rlt.plot->annotation(rlt.time_origin_note);
|
||||
if (a.visible) note = a.text;
|
||||
}
|
||||
// The labels themselves, from the axis the reader looks at: the assertion is what the
|
||||
// ticks *say*, not only which mode is on.
|
||||
std::string ticks;
|
||||
if (rlt.plot != nullptr) {
|
||||
int shown = 0;
|
||||
for (const plot::Tick& t : rlt.plot->x_axis(rlt.axis_x).ticks) {
|
||||
if (!t.major) continue;
|
||||
if (shown++ != 0) ticks += ",";
|
||||
ticks += t.label;
|
||||
if (shown >= 4) break;
|
||||
}
|
||||
}
|
||||
// The labels against the anchor they were built with: the dump runs after the last
|
||||
// paint, so unlike the probe's timer there is no race here -- the first major must sit
|
||||
// on the origin (`first_rel` the fraction of a step the window has scrolled since).
|
||||
double first_value = 0.0;
|
||||
double origin_set = 0.0;
|
||||
if (rlt.plot != nullptr) {
|
||||
origin_set = rlt.plot->x_axis_origin(rlt.axis_x);
|
||||
for (const plot::Tick& t : rlt.plot->x_axis(rlt.axis_x).ticks) {
|
||||
if (t.major) {
|
||||
first_value = t.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::printf("[udpscope] time labels: relative=%d origin=%.6f note=[%s] x_origin_set=%.6f "
|
||||
"ticks=[%s] first_rel=%.9f\n",
|
||||
rows.time_labels_relative() ? 1 : 0, rows.time_labels_origin(), note.c_str(),
|
||||
origin_set, ticks.c_str(), first_value - origin_set);
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(model.mutex());
|
||||
// `results_for` is the window the numbers describe, `win` the one on screen now: the
|
||||
|
||||
Reference in new issue
Block a user