trigger: a phosphor display keeps the last N acquisitions; and a Force button

Two changes in the trigger pane, landed together.

**Persistence.** Trigger mode keeps the last N published acquisitions and draws them fading toward
the background, N being a panel control (1..8, default 1) with N=1 meaning today's behaviour
exactly, so the knob is the switch. The layers are the accumulate path's own -- one layer per
(signal, age) in the row's free trace slots, named with the accumulate vocabulary so
`layer_base_name()` strips the suffix and `signal_color()` resolves the base signal -- so a layer is
not a signal: the signal list, the colours and the measurements see one row per signal.
`ScopeModel` retains the outgoing snapshot in seven atomic `shared_ptr` slots (no lock, no copy; the
snapshot already exists) and a layer's channel is resolved **by name inside the snapshot**, because
the channel list is renumbered by any structural change since that acquisition was cut.
`RowSet::sync_layers()` re-plans from the rows and the depth alone, so a latch costs no rebuild, and
`event_data_window()` skips layer slots so an older acquisition can never re-pin the axis. The
schedule is 0.5^age -- the acquisition on screen at full brightness and each older one half the
previous, independent of N, so raising the depth adds dimmer acquisitions rather than rescaling the
ones already on screen. Memory is bounded by the ring's own `--buffer-mb` rather than by the depth,
which is the bound that cannot be driven into an OOM, and the panel says when either bound bites.
This also wired up `set_trace_alpha()`, which the accumulate path was setting and nothing was
reading -- so the documented 0.35 translucency of an accumulation was opaque in fact.

**Force.** In the trigger pane beside Arm/Disarm, shown only while trigger mode is on: one
acquisition now, with the configured window. It is a synthetic edge rather than a second publisher
-- the engine opens one capture at the present moment and everything else is the ordinary latch, so
a forced acquisition is cut, pinned, framed, recorded and cached exactly like a latched one, and
the button owns no window and no publish. The pre-trigger gate is deliberately skipped (capture now
is a request for the history that exists, and the snapshot reports its own shorter pre_s), a
capture already filling is refused rather than restarted, and the state afterwards is the mode's:
Normal stays armed, Single disarms. The request is one atomic flag written under the model mutex
with `armed` -- the producer reads `armed` at the top of a packet and the request at the bottom, so
a request written between the two would be dropped -- and consumed at the end of a packet, after
the channel loop and before the pending publish.

**A defect fixed in passing, in the function Force was modelled on:** `on_arm_clicked()` passed
`TriggerConfig::channel` (an index into `channels()`) to `set_signal_enabled()` (an index into
`signals()`). With one signal acquired, arming switched on the stream's clock signal and left the
watched signal untouched -- and since a signal the panel was not showing is a change, it rebuilt
the buffer under the click, dropping the very pre-trigger window the arming, and now the Force, was
about to capture. Measured before: `0.0100 s of history behind it`; after: `2.5299 s`.
`trigger_signal_index()` is now the one resolution, used by both buttons.
This commit is contained in:
Martino Ferrari committed 2026-10-16 16:46:17 +02:00
1 parent fc4700d1c9
commit e14e1ee069
22 files changed
+1652 -26

No files matched your search

+58
View File
@@ -203,6 +203,11 @@ struct App {
/// sidebar. "signals" on a first run.
std::string side_tab = "signals";
GtkWidget* arm_button = nullptr;
/// The Force button, beside the Arm/Disarm one: one acquisition now, with the configured pre/post
/// window. It carries no state of its own -- `sync_arm_buttons()` states the one it has
/// (sensitivity, from trigger mode), so the two buttons beside each other cannot disagree about
/// whether the trigger has an engine at all.
GtkWidget* force_button = nullptr;
GtkWidget* trigger_signal_drop = nullptr;
GtkWidget* sidebar_toggle = nullptr;
GtkWidget* trigger_edge_drop = nullptr;
@@ -226,6 +231,31 @@ struct App {
GtkWidget* trigger_holdoff_spin = nullptr;
GtkWidget* trigger_band_spin = nullptr; ///< EventCondition::noise_band
GtkWidget* trigger_hf_spin = nullptr; ///< EventCondition::hf_fc, 0 is off
/// The persistence spin: how many of the most recent triggered acquisitions stay on screen, drawn
/// dimmer with age (1 = the event alone, which is the display without the feature). Its row is held
/// for a `.ui` that lost it, as the other rows are.
GtkWidget* trigger_persist_spin = nullptr;
GtkWidget* trigger_persist_row = nullptr;
/// What this frame's persistence plan drew, against what the depth asked for: `sync_layers()`
/// returns the former and fills the latter, and the two differ exactly when a row had no free
/// slot left for a layer or the retention budget refused an acquisition. Read by the exit dump and
/// by the persistence probe, and what the "only some layers fit" notice is keyed on.
int persist_want = 0;
int persist_placed = 0;
/// The last plan a notice was raised for, so a plan that stays short says so once rather than
/// every frame.
int persist_noted_depth = 0;
int persist_noted_want = -1;
int persist_noted_placed = -1;
/// The persistence probe's own state: the depth its first leg asks for (kept between steps,
/// because the steps are separate `GSourceFunc`s), how many polls it has spent waiting for the
/// retention to fill, and whether that set has stopped growing (which is how a budget that keeps
/// fewer acquisitions than the depth asks for still gets a report).
int dbg_persist_depth = 4;
int dbg_persist_polls = 0;
std::size_t dbg_persist_last_kept = 0;
int dbg_persist_stable_polls = 0;
bool dbg_persist_settled = false;
GtkWidget* signal_list = nullptr;
GtkWidget* pause_button = nullptr;
/// When the pause began, on the **monotonic wall clock**, or 0 while running. The elapsed time
@@ -942,6 +972,24 @@ struct App {
bool dbg_capturing_at_disarm = false;
int dbg_disarm_polls = 0;
int dbg_disarm_in_capture = 0;
/// The force probe's state: the button's stated state on both sides of the trigger toggle (a
/// width read a frame after the state it belongs to), and the two legs of its press. See
/// `force_probe()` in the probes' translation unit.
int dbg_force_off_sensitive = -1;
int dbg_force_off_visible = -1;
int dbg_force_off_width = -1; ///< its allocation with the trigger *off*
int dbg_force_on_sensitive = -1;
int dbg_force_on_width = -1; ///< ... and with the trigger on: the size the user can press
int dbg_force_arm_width = -1; ///< the Arm button beside it, so the pair's widths can be compared
int dbg_force_label_found = -1; ///< `find_labelled_button("Force")` *is* the panel's button
int dbg_force_leg = 0; ///< 1 while leg A is in flight, 2 for leg B
int dbg_force_polls = 0;
int dbg_force_capture_seen = 0; ///< the axis was read while the forced capture was filling
double dbg_force_capture_lo = 0.0; ///< that axis: the *requested* window, before the latch
double dbg_force_capture_hi = 0.0;
double dbg_force_want_pre = 0.0; ///< the request the engine will cut by, at the moment of the press
double dbg_force_want_post = 0.0;
std::uint64_t dbg_force_latches_before = 0;
int dbg_swaps = 0;
int dbg_axes_w_min = 1 << 20;
int dbg_axes_w_max = -(1 << 20);
@@ -1764,6 +1812,16 @@ gboolean disarm_probe_step2(gpointer user);
gboolean disarm_probe_step3(gpointer user);
gboolean disarm_probe_wait(gpointer user);
gboolean disarm_probe_step1(gpointer user);
int persist_probe_depth();
void persist_probe_report(App& a, const char* what);
void persist_probe_set_depth(App& a, int depth);
gboolean persist_probe_wait(gpointer user);
gboolean persist_probe(gpointer user);
void force_probe_report(App& a, const char* leg);
gboolean force_probe_press(gpointer user);
gboolean force_probe_poll(gpointer user);
gboolean force_probe_step2(gpointer user);
gboolean force_probe(gpointer user);
void pause_probe_report(App& a, const char* what);
gboolean pause_probe(gpointer user);
gboolean export_probe(gpointer user);