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

+11 -2
View File
@@ -116,11 +116,20 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
the `help:<document>` URI to the desktop help viewer (`GtkUriLauncher`, i.e. Yelp) — a process of
its own that cannot be closed from here — because the manual is a Mallard set installed for the
freedesktop `help:` URI, not a GResource in this binary (see the help bullet below). So there is
exactly **one `GtkWindow`** among what the app opens: the shortcut overview. It closes with
exactly **one `GtkWindow`** among *that pair*: the shortcut overview. It closes with
`gtk_window_close()`, which emits `close-request` so the handler that nulls the file-scope
pointer runs. `gtk_window_destroy()` does **not** emit that signal and must not be used — the
math dialog fell into exactly this trap, and the pointer then points at a dead widget until the
next attempt to open the same window. The About dialog (an `AdwAboutDialog`) is an **`AdwDialog`
next attempt to open the same window. **A `close-request` handler's return value is load-bearing
too, and its absence is silent**: the signal is `gboolean`, `TRUE` means "handled" and suppresses
the default handler that destroys the window, and `G_CALLBACK` is a cast — so a `void` lambda in
the slot compiles without a warning and GTK reads whatever the compiler left in the return
register as the verdict. The Configurations dialog (`open_configurations()`) was written that
way, and the built binary returns non-zero from the lambda, so **both its Close button and the
window manager's close button emitted `close-request`, were told "handled", and the dialog stayed
up** — "the configurations dialog cannot be closed", which no reading of the callback's type can
see. Every `close-request` handler therefore returns `FALSE` explicitly (the chooser's
`on_chooser_close()`, the overview's `on_window_close()`, the math dialog's, and this one). The About dialog (an `AdwAboutDialog`) is an **`AdwDialog`
and not a `GtkWindow`**: it closes with `adw_dialog_close()` and clears on `::closed`, there being
no `::close-request` on an `AdwDialog`. It shares one `on_dialog_closed()` handler with the About
pointer, keyed by a **pointer to the pointer** it must null (`&g_about`), which keeps the handler