trigger: a phosphor persistence, driven by the plotter's density image
The trigger pane gains the display the user asked for and the library now provides, and the
persistence it already had is renamed to what it is.
**Persistence is one image; the last-N-traces display is "multiple events".** Both live in a new
`Advanced` expander in the trigger panel, together with the settings that are set up once and left
alone. `persistence` (a switch) turns on the plotter's `Plot::set_density()` for every row:
every triggered acquisition is accumulated into a screen-sized intensity grid drawn behind the
traces, brightness being how many acquisitions covered a cell -- no depth to choose, no memory
that grows with the acquisitions (2.2 MB over four 573x412 rows in the probe's run). `fades by`
picks the fade the user asked for: **acquisitions** (each keeps a factor of what was before it,
0.5 by default -- the phosphor afterglow) or **time** (a time constant in stream seconds, so the
display fades while you watch whatever the trigger rate).
- **Trigger mode only, as asked.** `apply_density()` (the tick) states the display on for `Held`
and `Capturing` and off for `Live`, `Waiting`, `Paused` and a file, and turning it off drops the
grid at once rather than at an acquisition a quiet trigger never sends. It is unpickable and
reserves no layout space, exactly as the invalid-span bands are: a density can never be in the
way of a cursor or a drop, and it cannot move the framing.
- **One deposit per acquisition, one frame after the frame that frames it.** `Plot::accumulate_density()`
deposits what the plot holds through the transforms of its *last* layout -- the frame's own is
computed inside the paint that follows the tick -- so `density_pending` is set beside
`record_pending` and consumed by the next tick. In the same tick it was right on a Held-to-Held
transition and wrong on the first latch, whose axis was still the live ring's. Switching the
display on under an acquisition already on screen deposits it too, so the switch does not appear
to do nothing until the next latch (`single` mode may never send one).
- **The session carries it**: `trigger_density`, `trigger_density_decay` ("acquisitions"/"time",
the words the panel shows), `trigger_density_tau` and `trigger_density_factor`, written whether
or not it is on -- and `trigger_persistence` still means the multiple-event depth, so an older
file is unchanged.
- **The library changed twice under this**: the deposit now counts a cell **once per trace per
sweep** (counting the hits put one dense sweep at the ceiling of the brightness scale and hid the
sweeps behind it -- the probe's `on` leg read `peak=18` for a single sweep and reads `peak=1`
now), and a frame's upload is every row's rather than the last row's. Both are in the plotter's
own commit.
`UDPSCOPE_PROBE_DENSITY_MS` reads it all back off the plots -- the plotter's own `ink()`/`peak()` --
with the panel's own widgets driving the switch and the fade, in four legs: **off** (`rows=0
ink=0.0 peak=0.0000`), **on** (`rows=4`, `peak=1.0000`), **step-acquisitions** (`peak=1.0000 ->
1.5000`, the factor then the new sweep) and **step-time** (`ratio=0.367879`, `1/e` for `dt = tau`).
It also reads the frame cost as an A/B **inside one run** (the accumulators at the switch against
the frames after it): `tick 0.74 -> 9.99 ms`, `draw 2.09 -> 20.91 ms` at four rows and 2.2 M cells
on llvmpipe.
Acceptance: clean build 0 errors and exactly the three documented warnings (12352-byte GResource
blob, two `MathNode::source`), `test-ui` 13/13, `test-help` green, a plain run with **0
Gtk-CRITICAL / Gtk-WARNING** and `gtk warnings: 0`, and the panel's own frame reading
`boxes=16 controls_inside=15/15 advanced_widgets=5/5 advanced_inside=5/5` (the block's controls
counted from the expander's child, which is not in the widget-parent chain -- and the search
block's walk now names *its* expander instead of "the first one", which the second expander would
have stolen).
Docs: the trigger/pinning invariants carry the rule and its five consequences, `probe-readings.md`
the four legs and the dump line, `verification.md` the probe's sequence and assertions, and the
manual's trigger page explains both displays to a user.
This commit is contained in:
1 parent
e14e1ee069
commit
974d6aa2d9
15 files changed
+993
-41
No files matched your search
+129
-5
@@ -248,6 +248,20 @@ void write_trigger_widgets(App& app) {
|
||||
static_cast<double>(
|
||||
std::clamp(tc.persist_depth, 1, kMaxPersistDepth)));
|
||||
}
|
||||
if (app.trigger_density_switch != nullptr) {
|
||||
gtk_switch_set_active(GTK_SWITCH(app.trigger_density_switch), tc.density);
|
||||
}
|
||||
if (app.trigger_density_decay_drop != nullptr) {
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_density_decay_drop),
|
||||
tc.density_decay == PersistDecay::Time ? 1u : 0u);
|
||||
}
|
||||
if (app.trigger_density_tau_spin != nullptr) {
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_density_tau_spin), tc.density_tau_s);
|
||||
}
|
||||
if (app.trigger_density_factor_spin != nullptr) {
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_density_factor_spin),
|
||||
tc.density_factor);
|
||||
}
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_type_drop),
|
||||
dropdown_index_of(app.trigger_type_drop,
|
||||
trigger_type_text(tc.condition.type)));
|
||||
@@ -393,6 +407,23 @@ void publish_trigger_fields(App* app) {
|
||||
const double hf = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_hf_spin));
|
||||
const int persist =
|
||||
static_cast<int>(std::lround(gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_persist_spin))));
|
||||
// The density's four controls. Guarded on null, unlike the ones above: they are built by this
|
||||
// panel and a `.ui` that lost one leaves the field at its default rather than crashing the
|
||||
// publish -- and the switch's own state is what a session file and the tick then agree on.
|
||||
const bool density = app->trigger_density_switch != nullptr &&
|
||||
gtk_switch_get_active(GTK_SWITCH(app->trigger_density_switch)) != FALSE;
|
||||
const guint density_decay =
|
||||
app->trigger_density_decay_drop != nullptr
|
||||
? gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_density_decay_drop))
|
||||
: 0;
|
||||
const double density_tau =
|
||||
app->trigger_density_tau_spin != nullptr
|
||||
? gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_density_tau_spin))
|
||||
: 1.0;
|
||||
const double density_factor =
|
||||
app->trigger_density_factor_spin != nullptr
|
||||
? gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_density_factor_spin))
|
||||
: 0.5;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app->model->mutex());
|
||||
TriggerConfig& tc = app->model->trigger();
|
||||
@@ -411,6 +442,10 @@ void publish_trigger_fields(App* app) {
|
||||
tc.condition.timeout_s = timeout;
|
||||
tc.condition.noise_band = band;
|
||||
tc.condition.hf_fc = hf;
|
||||
tc.density = density;
|
||||
tc.density_decay = density_decay == 1u ? PersistDecay::Time : PersistDecay::Acquisitions;
|
||||
tc.density_tau_s = density_tau;
|
||||
tc.density_factor = density_factor;
|
||||
}
|
||||
// The persistence depth is applied through the one setter rather than written into the config
|
||||
// here: it is clamped to the cap *and* it prunes the retained acquisitions at once, so lowering
|
||||
@@ -460,6 +495,37 @@ void sync_trigger_rows(App& app) {
|
||||
gtk_spin_button_set_value(l2, other < level ? level - step : level + step);
|
||||
}
|
||||
}
|
||||
// The density's own rows follow the *display*, not the condition: the two fades take different
|
||||
// numbers, so only the one in use is on screen -- a greyed spin for the other is a field the user
|
||||
// cannot tell from a broken one -- and the whole choice is hidden while the display is off, since
|
||||
// a decay kind for an image that is not being drawn has no subject. The switch itself stays: it is
|
||||
// the one control that turns the display on.
|
||||
const bool density_on = app.trigger_density_switch != nullptr &&
|
||||
gtk_switch_get_active(GTK_SWITCH(app.trigger_density_switch)) != FALSE;
|
||||
const bool density_by_time =
|
||||
app.trigger_density_decay_drop != nullptr &&
|
||||
gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_density_decay_drop)) == 1u;
|
||||
show(app.trigger_density_decay_row, density_on);
|
||||
show(app.trigger_density_tau_row, density_on && density_by_time);
|
||||
show(app.trigger_density_factor_row, density_on && !density_by_time);
|
||||
}
|
||||
|
||||
/// The **density switch**. It publishes the field and nothing else: whether the image is on is
|
||||
/// read every frame from the model, so there is no widget state to keep in step, and the tick
|
||||
/// notices the transition itself -- which is what deposits the acquisition already on screen
|
||||
/// instead of waiting for the next latch (in `single` mode there may never be one).
|
||||
void on_density_toggled(GObject* o, GParamSpec*, gpointer user) {
|
||||
(void)o;
|
||||
publish_trigger_fields(static_cast<App*>(user));
|
||||
}
|
||||
|
||||
/// The **fade** dropdown. Publishes, then re-states the block: which of the two parameter rows is
|
||||
/// on screen is this dropdown's business and `sync_trigger_rows()` is the one place that reads it.
|
||||
void on_density_decay_changed(GObject* o, GParamSpec*, gpointer user) {
|
||||
(void)o;
|
||||
App* app = static_cast<App*>(user);
|
||||
publish_trigger_fields(app);
|
||||
sync_trigger_rows(*app);
|
||||
}
|
||||
|
||||
void on_trigger_field_changed(GtkWidget*, gpointer user) {
|
||||
@@ -684,12 +750,65 @@ GtkWidget* build_trigger_panel(App& app) {
|
||||
static_cast<double>(persist_seed));
|
||||
gtk_widget_set_tooltip_text(
|
||||
app.trigger_persist_spin,
|
||||
"how many of the most recent triggered acquisitions stay on screen, each older one at half "
|
||||
"the previous one's brightness; 1 is the event alone. A layer needs a free trace slot in the "
|
||||
"row that plots its signal, and the retained acquisitions are also bounded by the buffer's "
|
||||
"own size");
|
||||
"multiple events: how many of the most recent triggered acquisitions stay on screen as "
|
||||
"separate traces, each older one at half the previous one's brightness; 1 is the event "
|
||||
"alone. A layer needs a free trace slot in the row that plots its signal, and the retained "
|
||||
"acquisitions are also bounded by the buffer's own size");
|
||||
app.trigger_persist_row = row_for(ui, "persistence_row", app.trigger_persist_spin);
|
||||
|
||||
// The phosphor *density*: one screen-sized image every acquisition is accumulated into, drawn
|
||||
// behind the traces and fading by a time constant or by a per-acquisition factor -- the other
|
||||
// display of the same acquisitions, and nothing like the multiple-event one above (N traces
|
||||
// against one image, and no count to choose). It applies in trigger mode only, which the body
|
||||
// already is: the whole block goes insensitive with the trigger off, the panel's own rule.
|
||||
app.trigger_density_switch = gtk_switch_new();
|
||||
gtk_widget_set_valign(app.trigger_density_switch, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_halign(app.trigger_density_switch, GTK_ALIGN_END);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
gtk_switch_set_active(GTK_SWITCH(app.trigger_density_switch),
|
||||
app.model->trigger().density);
|
||||
}
|
||||
gtk_widget_set_tooltip_text(
|
||||
app.trigger_density_switch,
|
||||
"accumulate every triggered acquisition into one screen-sized image, drawn behind the "
|
||||
"traces: brightness is how often a trace passed, so an image of a thousand acquisitions "
|
||||
"costs what one costs. Trigger mode only, and cleared whenever the display leaves it");
|
||||
g_signal_connect(app.trigger_density_switch, "notify::active",
|
||||
G_CALLBACK(on_density_toggled), &app);
|
||||
// Not `row_for()`: it expands its child, and an expanded switch is a wide pill that reads as a
|
||||
// slider rather than as a switch -- the record row's rule, and the label carries the `hexpand`.
|
||||
if (GtkWidget* density_row = GTK_WIDGET(gtk_builder_get_object(ui, "density_row"))) {
|
||||
gtk_box_append(GTK_BOX(density_row), app.trigger_density_switch);
|
||||
app.trigger_density_row = density_row;
|
||||
}
|
||||
|
||||
const char* density_decays[] = {"acquisitions", "time", nullptr};
|
||||
app.trigger_density_decay_drop = gtk_drop_down_new_from_strings(density_decays);
|
||||
gtk_widget_set_tooltip_text(
|
||||
app.trigger_density_decay_drop,
|
||||
"'acquisitions': every acquisition multiplies the image by the factor below, so the N-th "
|
||||
"acquisition back is the factor to the N -- it scales with the trigger rate. 'time': the "
|
||||
"image fades by exp(-dt / the time constant) as the display runs, whatever the rate");
|
||||
app.trigger_density_decay_row =
|
||||
row_for(ui, "density_decay_row", app.trigger_density_decay_drop);
|
||||
g_signal_connect(app.trigger_density_decay_drop, "notify::selected",
|
||||
G_CALLBACK(on_density_decay_changed), &app);
|
||||
|
||||
app.trigger_density_tau_spin = spin(0.01, 3600.0, 0.1, 3, 1.0);
|
||||
gtk_widget_set_tooltip_text(app.trigger_density_tau_spin,
|
||||
"seconds for the image to fall to 1/e of its ink: the trace is still "
|
||||
"visible a second afterwards, faintly, which is what a phosphor does");
|
||||
app.trigger_density_tau_row = row_for(ui, "density_tau_row", app.trigger_density_tau_spin);
|
||||
|
||||
app.trigger_density_factor_spin = spin(0.0, 1.0, 0.05, 2, 0.5);
|
||||
gtk_widget_set_tooltip_text(app.trigger_density_factor_spin,
|
||||
"what each acquisition keeps of the ones before it: 0.5 is the "
|
||||
"schedule a phosphor's afterglow has -- 1, 0.5, 0.25, ... -- and 0 "
|
||||
"shows only the newest acquisition");
|
||||
app.trigger_density_factor_row =
|
||||
row_for(ui, "density_factor_row", app.trigger_density_factor_spin);
|
||||
|
||||
app.arm_button = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_arm"));
|
||||
if (app.arm_button != nullptr) {
|
||||
g_signal_connect(app.arm_button, "clicked", G_CALLBACK(on_arm_clicked), &app);
|
||||
@@ -708,9 +827,14 @@ GtkWidget* build_trigger_panel(App& app) {
|
||||
for (GtkWidget* w : {app.trigger_level_spin, app.trigger_pre_spin, app.trigger_holdoff_spin,
|
||||
app.trigger_width_spin, app.trigger_width2_spin,
|
||||
app.trigger_level2_spin, app.trigger_timeout_spin, app.trigger_band_spin,
|
||||
app.trigger_hf_spin, app.trigger_persist_spin}) {
|
||||
app.trigger_hf_spin, app.trigger_persist_spin,
|
||||
app.trigger_density_tau_spin, app.trigger_density_factor_spin}) {
|
||||
g_signal_connect(w, "value-changed", G_CALLBACK(on_trigger_field_changed), &app);
|
||||
}
|
||||
// The advanced block itself, so the probe can read it back and the dump can walk into it: an
|
||||
// expander's child is *not* in the widget-parent chain (see docs/claude/invariants-gtk-and-chrome.md),
|
||||
// so "is this control in the panel" has to start from the child the expander returns.
|
||||
app.trigger_advanced = GTK_WIDGET(gtk_builder_get_object(ui, "advanced_exp"));
|
||||
g_signal_connect(app.trigger_edge_drop, "notify::selected",
|
||||
G_CALLBACK(on_trigger_drop_changed), &app);
|
||||
g_signal_connect(app.trigger_mode_drop, "notify::selected",
|
||||
|
||||
Reference in new issue
Block a user