fix: a clock that stalled on a re-plan, and a dialog that could not close

**The clock.** Trigger mode showed a blank plot and never latched when the session defined a math
channel, because such a session gives the model a plan *before* the stream's own signals are in it:
one derived channel, declared rate 0, so `plan_partition()`'s 1 kHz fallback is the interval those
early frames are stamped at -- 1300x too fast for a 1.3 MS/s stream -- and they have nowhere to go,
since the value loop skips derived channels. Relative mode then stamped a *position* times the plan's
*current* interval, so when the CONFIG's channels landed and the plan took the real rate (769 ns) the
same positions produced times 1300x *smaller* than what was already in the ring, and the monotonic
clamp held the clock at the old value for the rest of the run: one repeated stamp, `history=0`, no
latch, and a window the renderer cannot draw. The same family as the restarted-client poisoning
fixed in 620473a, through a different door. The filter's own output was finite and correct
throughout, and disabling the math channel only appeared to cure it because that is a structural
change which rebuilds the store -- the third time that channel has looked guilty while being a
bystander.

Relative mode now accumulates *seconds* (`rel_base_s_ += n * dt`), so a re-plan is a slope change and
the clock cannot jump backwards by construction; a frame is only stamped into a model that has a
*streamed* channel; and the epoch is adopted once per connection, with "unset" a state rather than
the value zero. A one-shot warning names the family if it returns: `the clock stalled at ...: N
samples stamped [a..b] are all behind it`.

**The dialog.** The Configurations window's Close button and the window-manager close both did
nothing: `close-request` was connected to a bare `void` lambda, and that signal is `gboolean` -- TRUE
means "handled" and suppresses the default handler that destroys the window, while `G_CALLBACK` is a
cast, so GTK read whatever the compiler left in the return register. A standalone GTK program of that
shape reproduces it: `void` handler -> `window_alive=1` after `gtk_window_close()`, `gboolean FALSE`
-> 0. The same audit found `gtk_window_present(GTK_WINDOW(...))` on an `AdwDialog`, a type assertion
and a no-op; it is `adw_dialog_present()` now. The menu row stays: it is the only way to switch or
save a configuration without restarting, and what made it look useless was that it could not close.

Verified: trigger mode with that session's graph reads `ring=[0.0058..0.2446] -> [0.0058..1.9969]`,
`history 0.237 -> 2.0 s`, `edge at 1.120 s, latched at 2.120 s`, `display=event(frozen) latches=1`,
`traceless paints 266` against 1519 before the fix. Baselines hold: 0 errors, the documented three
warnings, test-ui 13 ok, test-help green, `gtk warnings: 0`.
This commit is contained in:
Martino Ferrari committed 2026-10-15 10:50:09 +02:00
1 parent 27c9b34277
commit 074cb611bc
9 files changed
+210 -34

No files matched your search

+13 -1
View File
@@ -140,8 +140,20 @@ void open_configurations(App& app) {
[](gpointer p) { delete static_cast<ConfigDialog*>(p); });
// Cleared on close: the pointer is what makes a second open a present rather than a second
// window, and a destroyed window must not leave it dangling.
g_signal_connect_swapped(window, "close-request", G_CALLBACK(+[](gpointer user) {
//
// The handler **returns FALSE**, and that return is the whole of "the dialog can be closed":
// `close-request`'s default handler is what destroys the window, and a handler that returns
// `TRUE` suppresses it. This one was a bare `void` lambda -- the value GTK read as its verdict
// was whatever the compiler happened to leave in the return register, and a build of exactly this
// shape (gtk4, -O2, one `void` lambda on `close-request`) leaves it non-zero -- so both the Close
// button and the window manager's button emitted
// `close-request`, were told "handled", and **the dialog stayed up**. It is the same shape the
// math dialog's and the chooser's handlers already carry, and it cannot be seen in a callback's
// signature: `G_CALLBACK` is a cast, so a `void(GtkWindow*, gpointer)` where GTK expects
// `gboolean(GtkWindow*, gpointer)` compiles without a warning.
g_signal_connect_swapped(window, "close-request", G_CALLBACK(+[](gpointer user) -> gboolean {
static_cast<App*>(user)->config_dialog = nullptr;
return FALSE; // let the default handler destroy the window
}),
&app);
app.config_dialog = window;