fixed event/live transition

This commit is contained in:
Martino Ferrari committed 2026-10-13 23:37:49 +02:00
1 parent bb99be5889
commit 881b52748e
9 files changed
+314 -2

No files matched your search

+93 -1
View File
@@ -141,11 +141,94 @@ bool record_event(App& app) {
return true;
}
/// Remembers the live view's settings before a file view is opened over them.
///
/// A no-op unless the view being left *is* the live one: replaying a second event from inside the
/// first, or a group opened from a replay, must not overwrite the snapshot with a file's settings --
/// those are the very values *Go live* exists to undo.
void snapshot_live_state(App& app) {
if (app.replaying || app.model == nullptr || app.rows == nullptr) return;
App::LiveState& s = app.live_state;
s.valid = true;
s.window_s = app.rows->time_window();
s.trigger_on = app.trigger_toggle != nullptr &&
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.trigger_toggle)) != FALSE;
s.level_touched = app.trigger_level_touched;
std::lock_guard<std::mutex> lock(app.model->mutex());
s.trigger = app.model->trigger();
s.trigger_channel = s.trigger.channel >= 0 &&
s.trigger.channel < static_cast<int>(app.model->channels().size())
? app.model->channels()[static_cast<std::size_t>(s.trigger.channel)].name
: std::string();
}
/// Puts those settings back on the way out of a file view.
///
/// Deferred by `go_live_again()` to the first tick that sees the stream's *own* channel list, which
/// is what makes this the last writer over the two things that would otherwise overwrite it: the
/// deferred layout re-apply (which ends in `apply_time_window()` with the *saved* window, not the
/// live one) and `rebuild_trigger_signals()`, which, finding a `channel` of `-1` after a rebuild
/// renumbered or renamed the channels, silently settles on the first signal and re-defaults the level
/// with it.
///
/// Inside, the order is the whole of it:
///
/// - The configuration goes in under the lock, with the channel resolved from the stored *name*.
/// `enabled` and `armed` are deliberately *not* written: they belong to the toolbar toggle, which
/// owns an atomic mirror of `enabled` that a plain field write would leave stale.
/// - The two controls a file view actually moved are restated from it -- the signal dropdown and the
/// level spin. The rest of the panel was never touched, and writing to a control fires its handler,
/// so the ones that have nothing to put back are left alone.
/// - `apply_time_window()`, the only path allowed to restate the time scale; it also drops the event
/// pin the file view left on the axis and re-arms the live follow.
/// - The toggle last, because its handler reads the configuration it is turning on.
void restore_live_state(App& app) {
App::LiveState& s = app.live_state;
if (!s.valid) return;
s.valid = false; // consumed: the next way in takes its own snapshot
int channel = -1;
{
std::lock_guard<std::mutex> lock(app.model->mutex());
const std::vector<ChannelInfo>& channels = app.model->channels();
for (std::size_t i = 0; i < channels.size(); ++i) {
if (channels[i].name == s.trigger_channel) {
channel = static_cast<int>(i);
break;
}
}
TriggerConfig& tc = app.model->trigger();
const bool enabled = tc.enabled;
const bool armed = tc.armed;
tc = s.trigger;
tc.enabled = enabled;
tc.armed = armed;
tc.channel = channel;
}
app.trigger_level_touched = s.level_touched;
app.updating_panels = true;
if (channel >= 0 && app.trigger_signal_drop != nullptr) {
gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_signal_drop),
static_cast<guint>(channel));
}
if (app.trigger_level_spin != nullptr) {
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_level_spin), s.trigger.condition.level);
}
app.updating_panels = false;
apply_time_window(app, s.window_s);
if (app.trigger_toggle != nullptr &&
(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.trigger_toggle)) != FALSE) !=
s.trigger_on) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), s.trigger_on ? TRUE : FALSE);
}
}
/// Replays a cached event: the same loader `--open` uses, on the event's file. The acquisition is
/// stopped first, because a live producer overwrites the loaded samples within a frame -- so the
/// scope becomes a file viewer until *Go live* starts it again.
bool replay_event(App& app, const EventRecord& record) {
if (app.acq == nullptr || app.model == nullptr) return false;
// Before the stop, while the live view is still the view: everything below overwrites it.
snapshot_live_state(app);
app.acq->stop();
WaveformLoadReport report;
std::string error;
@@ -199,6 +282,13 @@ bool go_live_again(App& app) {
app.layout_loaded = false; // the session's layout applies to the source again
app.event_held = false;
app.model->set_display_epoch(0.0);
// What the live view had, put back from the tick rather than here: the stream's own channel list is
// what the stored trigger channel has to be resolved against, and it does not exist until the first
// CONFIG of the restarted client lands on the producer thread. The generation read here is what the
// tick compares against to know that it has (see `restore_live_state()` for what else that ordering
// buys).
app.live_restore_generation = app.model->generation();
app.live_restore_pending = app.live_state.valid;
try {
app.acq->start();
} catch (const std::exception& e) {
@@ -307,7 +397,9 @@ bool build_event_group(App& app, const std::vector<EventRecord>& records, EventG
// The acquisition first: a live producer keeps pushing into the model it was started with -- and it
// pushes *four* stream signals into the group's eight channels, so the group's own samples are the
// ones that go missing.
// ones that go missing. The live view's settings are taken ahead of it, for `go_live_again()`:
// after the checks above, because a group that is refused leaves the live view alone.
snapshot_live_state(app);
app.acq->stop();
std::vector<EventSamples> events(records.size());