From cd9657663a619678eaf237f17e39cef90d768aa7 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Thu, 8 Oct 2026 16:56:25 +0200 Subject: [PATCH] fix: the math editor's source dropdown no longer hands a GParamSpec to a value handler notify::selected passes a GParamSpec* ahead of the user data and GtkSpinButton::value-changed does not, so on_math_param_changed read the pspec as its MathDialog* and faulted on the first field it touched -- which is to say on opening the math editor and picking a source. The probe missed it because filling the form sets the dropdown to the value it already holds, and GObject emits no notify for that: a channel sourcing channels().front() picks index 0 on a dropdown already at 0. math_probe now opens on a channel sourcing the *last* acquired signal, which does move the selection, and reads back the source the handler wrote into the node. Co-Authored-By: Claude Opus 4.6 --- src/ui.cpp | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/ui.cpp b/src/ui.cpp index 29bce0f..03bcb6c 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -4696,6 +4696,16 @@ void on_math_param_changed(GtkWidget*, gpointer user) { gtk_widget_queue_draw(d->canvas); } +/// The same read, for the source dropdown, and it needs its own entry point for the reason +/// `on_trigger_drop_changed()` gives: `notify::` passes a `GParamSpec*` ahead of the user +/// data and `GtkSpinButton::value-changed` does not, so wiring the two-parameter reader above to +/// `notify::selected` made it read the pspec as its `MathDialog*`. That is not a latent hazard -- +/// it segfaulted on the first field it touched, on the first change of the source dropdown, which +/// is to say on opening the math editor and picking a source. +void on_math_source_changed(GObject*, GParamSpec*, gpointer user) { + on_math_param_changed(nullptr, user); +} + /// The op changed: the new op may take a different number of inputs, so the ones it cannot use are /// cleared rather than left pointing at a node the graph would then evaluate from anyway. void on_math_op_changed(GObject* object, GParamSpec*, gpointer user) { @@ -5027,7 +5037,7 @@ void open_math_dialog(App& app, int edit) { gtk_box_append(GTK_BOX(form), op_row); d->source_drop = gtk_drop_down_new_from_strings(nullptr); - g_signal_connect(d->source_drop, "notify::selected", G_CALLBACK(on_math_param_changed), d); + g_signal_connect(d->source_drop, "notify::selected", G_CALLBACK(on_math_source_changed), d); gtk_box_append(GTK_BOX(form), math_row("channel", d->source_drop, &d->rows[0])); d->gain = gtk_spin_button_new_with_range(-1e6, 1e6, 0.1); @@ -8012,10 +8022,20 @@ gboolean math_probe(gpointer user) { App* a = static_cast(user); if (a->window == nullptr) return G_SOURCE_REMOVE; std::string name; + std::string late_name; { std::lock_guard lock(a->model->mutex()); if (a->model->channels().empty()) return G_SOURCE_REMOVE; name = a->model->channels().front().name; + // The *last* acquired channel, and the probe needs it rather than merely liking it. + // Filling the editor's form calls `gtk_drop_down_set_selected()` on the source + // dropdown, and GObject does not emit `notify` when a property is set to the value it + // already holds -- so a channel sourcing `channels().front()` picks index 0 on a + // freshly built dropdown that is already at 0, the signal never fires, and the whole + // `notify::selected` path goes unexercised. That is exactly how a handler of the wrong + // shape on that signal survived every run of this probe while crashing the app on the + // first editor a user opened. + late_name = a->model->channels().back().name; } std::vector channels; MathChannel gain; @@ -8030,7 +8050,7 @@ gboolean math_probe(gpointer user) { MathChannel lp; lp.name = "math lp"; lp.enabled = true; - lp.nodes = {MathNode{MathOp::Source, name}, MathNode{MathOp::LowPass}}; + lp.nodes = {MathNode{MathOp::Source, late_name}, MathNode{MathOp::LowPass}}; lp.nodes[1].in_a = 0; lp.nodes[1].fc = 20.0; lp.output = 1; @@ -8040,7 +8060,8 @@ gboolean math_probe(gpointer user) { // directly left the two disagreeing, and the next pane edit then silently dropped its channels. a->math = std::move(channels); apply_math_channels(*a); - std::fprintf(stderr, "[math-probe] 2 math channels on '%s'\n", name.c_str()); + std::fprintf(stderr, "[math-probe] 2 math channels on '%s' and '%s'\n", name.c_str(), + late_name.c_str()); // And open the editor on the first one: as much a construction crash test as a state check, // since a modal window is presented and its canvas, form and gestures are all built here. // The pane's own `+`, which is the path the user takes: it must source an *acquired* channel. @@ -8055,6 +8076,23 @@ gboolean math_probe(gpointer user) { } open_math_dialog(*a, 0); std::fprintf(stderr, "[math-probe] editor opened\n"); + // Opened on the channel sourcing the *last* acquired signal, which is the one open that moves + // the source dropdown off index 0 and so emits `notify::selected` -- see `late_name` above for + // why nothing else here does. The reading is the source the handler wrote back into the node: + // it must be `late_name` and not the dropdown's first entry, because a handler that reads the + // wrong argument writes through a `GParamSpec*` and the Gtk-CRITICAL baseline is what catches + // it when the dereference happens not to fault. + { + if (a->math_dialog != nullptr) gtk_window_close(GTK_WINDOW(a->math_dialog)); + open_math_dialog(*a, 1); + auto* ed = static_cast( + g_object_get_data(G_OBJECT(a->math_dialog), "udpscope-math-dialog")); + const char* read = "NO DIALOG"; + if (ed != nullptr && !ed->nodes.empty()) read = ed->nodes[0].source.c_str(); + std::fprintf(stderr, "[math-probe] source drop: node reads '%s' (want '%s')\n", read, + late_name.c_str()); + if (a->math_dialog != nullptr) gtk_window_close(GTK_WINDOW(a->math_dialog)); + } // The user's own flow, through the app's handlers: the pane's `+` makes a bare source graph, the // editor adds a gain, the gain is set, Apply saves. The *output* must be the node that was just // added -- it defaulted to node 0, the source, so every operation was computed and discarded.