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

+50 -2
View File
@@ -5,7 +5,7 @@
<info>
<link type="guide" xref="index#viewing"/>
<link type="seealso" xref="pause"/>
<desc>The trigger's four shapes, arming and hold-off, the event cache with its replay,
<desc>The trigger's four shapes, arming, forcing an acquisition and hold-off, the event cache with its replay,
compare and accumulate, and searching the buffer for a condition.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
@@ -35,6 +35,17 @@
offered only while trigger mode is on, because a trigger that is off has no engine to
arm.</p>
</item>
<item>
<p><gui>Force</gui> takes one acquisition <em>now</em>, with the event window above,
without waiting for the condition to hold — for when the event you are waiting for is not
coming and you want to see what the signal is doing this instant. The capture runs out its
post-trigger share and then freezes, exactly like a latch: the marker sits at the instant
you pressed. In <gui>Normal</gui> mode the trigger goes back to waiting afterwards; in
<gui>Single</gui> mode it disarms, because one acquisition is the whole of what that mode
means either way. It needs a signal to be acquired — the trigger cannot watch one that is
switched off — and pressing it while a capture is already filling leaves that capture
alone, since it is the acquisition you asked for.</p>
</item>
<item>
<p><gui>Search the buffer</gui> is at the foot of the same panel.</p>
</item>
@@ -82,6 +93,40 @@
offers a pulse width to a window trigger.</p>
</section>
<section id="persistence">
<title>Persistence: keeping the last few acquisitions</title>
<p><gui>Persistence</gui> decides how many of the most recent triggered acquisitions stay on
screen at once. It is <em>1</em> by default, which is the event alone — the display you get
when you ask for no persistence — and it can be raised to 8. Each older acquisition is drawn
at half the brightness of the one before it (1, 0.5, 0.25, 0.125, …), and they are all aligned
on their own trigger edge, so the pre-trigger part of the event lines up from acquisition to
acquisition and whatever changes between them — jitter, ringing, the occasional glitch — shows
as a faint ghost behind the current trace. That is what a phosphor display does with
successive sweeps.</p>
<p>The older acquisitions are drawn <em>behind</em> the current one and nothing else changes
because of them: the axis still frames the event on screen, the trigger marker still marks
<em>its</em> edge, and the measurements still describe the current frame. Lowering
<gui>Persistence</gui> back to 1 remembers nothing and frees the retained acquisitions
immediately.</p>
<p>Two things bound how many are drawn, and the panel tells you when one of them bites.</p>
<list>
<item><p><em>Room in the plot.</em> Each older acquisition is an extra trace in the row that
plots its signal, so it needs a free trace slot there. A row holding four signals — the
layout the app starts with — has room for one older acquisition of each of them; a row per
signal has room for all seven.</p></item>
<item><p><em>Memory.</em> A retained acquisition is a whole window copied out of the ring,
so persistence may hold at most as much again as the buffer (<gui>--buffer-mb</gui>) does.
On a long window at a high sample rate that bound is reached before the eighth layer.</p></item>
</list>
<p>If a depth cannot be shown in full, a notice says how many layers fit, and the panel's
tooltip carries the same rule. Persistence is a display of what has already been captured: it
changes neither what is acquired nor what is measured, and it does nothing while the trigger
is off.</p>
</section>
<section id="cache">
<title>The event cache, replay and compare</title>
<p><em>Every triggered acquisition is kept.</em> Latches are written to the event cache
@@ -178,7 +223,10 @@
by live data.</p></item>
<item><p>The first edge after arming is only accepted once the history reaches back a
full pre-trigger window, so a capture may legitimately be shorter than you asked for.
The axis is pinned to what the capture actually got.</p></item>
The axis is pinned to what the capture actually got. <gui>Force</gui> is the one
acquisition that skips that gate — it captures the history that exists, because "now"
is the whole of what you asked for — so on a ring that has just started filling its
left side is short even though the axis still shows only what it got.</p></item>
<item><p>Disarming <em>during</em> a capture publishes what the capture got to, so the
picture on screen at that moment stays on screen. No event fired, so the event count
does not move.</p></item>