Feature/source plugins #1

Merged
martino merged 130 commits from feature/source-plugins into main 2026-11-04 07:35:21 +01:00
9 changed files with 148 additions and 4 deletions
Showing only changes of commit fcf268d249 - Show all commits

No files matched your search

+27 -1
View File
@@ -27,7 +27,10 @@ Options: `--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb`
window the toolbar offers, default 600), `--holdoff` (default trigger hold-off),
`--recv-buffer`, `--plots` (the number of stacked plots to start with, 1..8; rows are
added and removed from the **Layout editor** afterwards), `--relative-time` (see below),
`--capture-packets N` (see "Packet capture" below).
`--capture-packets N` (see "Packet capture" below), `--renderer NAME` (GTK's renderer:
`cairo`, `gl` or `vulkan`; `cairo` is the software preset that exports both
`GSK_RENDERER` and `LIBGL_ALWAYS_SOFTWARE` before GTK initialises — see the SSH
limitation below).
**What a large `--buffer-mb` costs at startup, and what the window says while it happens.** The
buffer's rings are value-initialised, so allocating one touches every byte of `--buffer-mb` --- about
@@ -2922,6 +2925,29 @@ silence timeout and then resumes, which is the EMI case end to end.
inner popover takes the grab, the menu closes under the click, and the bubble is
left parented to a row in a hidden popover. Export is described in the
Documentation instead.
- **Over SSH with X11 forwarding the window may never paint or allocate, and the cure is
`--renderer cairo`.** There are two walls, in order.
GTK4 defaults to Vulkan and a forwarded display has no usable Vulkan device, so the
log shows `vkCreateSwapchainKHR(): Initialization of an object could not be completed
... (VK_ERROR_INITIALIZATION_FAILED)` and nothing is drawn. `GSK_RENDERER` alone gets
past that, but the plot is a `GtkGLArea` and asks GDK for a GL context of its own
whatever GSK draws with; over forwarding there is no hardware one, and the window then
stays unallocated — `header: window_button_w=0` with a zero `bar_w`, and no
`[width-probe]` line (it prints only once the plot has a viewport wider than 100 px) —
while the stream side is fine. Absence of `buffer allocated:` is *not* part of that
reading: signals arrive disabled, so a start with nothing enabled plans no buffer and
prints no line even on a healthy display, and the plot stays empty until a signal's
switch is turned on.
`--renderer cairo` is the preset for exactly this display: it exports
`GSK_RENDERER=cairo` **and** `LIBGL_ALWAYS_SOFTWARE=1` (llvmpipe answers the GL area),
the same pair `tools/make-help-shots.sh` uses. `--renderer gl` is the faster
alternative when indirect GL works. The variables are exported in `main()` **before**
`gtk_init()`/`adw_init()`, which is the whole trap: they are read when the renderer and
the GL context are created, so setting them later is a silent no-op. Without the option
nothing is set and GTK's own choice stands. No `GDK_BACKEND` companion exists on
purpose: an X11-forwarded session carries `DISPLAY` and no `WAYLAND_DISPLAY`, so GDK
already selects X11; forcing it would override a working local Wayland session for no
gain (if both are set and Wayland is broken, export `GDK_BACKEND=x11` yourself).
- **The tour card can cover its own target, and in the documented headless run it
does.** The card is placed under, over or beside the hole whenever one of those has
room; when the target covers the whole content -- which is what the plot does at the
+6
View File
@@ -103,6 +103,12 @@ count to *start* with, 1..`RowSet::kMaxRows`; the layout editor owns it afterwar
the value is required, and the popover button uses 1000 when the flag was absent).
The flag is **not** persisted in the session file: persistence of the enable state
belongs with `todo.md` item 3, alongside the relative-time toggle.
`--renderer cairo|gl|vulkan` exports `GSK_RENDERER` **at the top of `main()`** —
before `choose_source()`'s `adw_init()` and `run_ui()`'s, because GSK reads the variable when
it creates its renderer and setting it later is a silent no-op. `cairo` is the software preset
and also exports `LIBGL_ALWAYS_SOFTWARE=1`: the plot is a `GtkGLArea`, so a display with no
usable GPU fails twice, and this is the pair `tools/make-help-shots.sh` already sets. Nothing
is exported when the option is absent, so GTK's own choice stands.
`tools/mock_udpstreamer.py` has waveform-injection options used by the probes:
+16 -2
View File
@@ -450,11 +450,25 @@ Two lines of the self-check output are assertions in disguise:
floor are the two refusals, and because the probe emits `::clicked` straight at the
widget it reaches the handler even when the button is insensitive, so `add above 0` there
must leave `rows=` and `traces=` untouched rather than growing a ninth row.
- `header: window_button_w=W bar_w=B` (stderr, once a second) — **`W` must be
- `[udpscope] header: window_button_w=W bar_w=B` (stderr, once a second, **only when
`UDPSCOPE_PROBE_HEADER` is set**) — **`W` must be
a small constant and must not track `B`**: the window control is a header-bar child,
and an expanding one is measured into the title's centre box, so it stretched from the
label to the title (516 px at a 1270 px bar before the fix, 121 px after). A `W` that
grows with the window width is that bug back.
grows with the window width is that bug back. The widths are still sampled every second
in every run, because the exit dump's `header:` line reads them; only the line is behind
the variable, since a reading printed on every run is a log flood (the `[notrace]` rule).
An unallocated window reads `0` for both, which is the reading for "the toplevel never
got a size", not for a layout drift.
- `[renderer] option=NAME GSK_RENDERER=V LIBGL_ALWAYS_SOFTWARE=V` (stderr, at startup,
before GTK initialises; only when `UDPSCOPE_PROBE_RENDERER` is set) — what `--renderer`
exported, printed from `main()` so the reading is of the environment a pre-init GTK will
see. `option=(unset)` with both variables `(unset)` is the default and the assertion that
the flag changes nothing unless asked.
- `[renderer] realized=TYPE` (stderr, once, from the first tick that finds a window renderer;
same probe) — the class GTK actually built (`GskCairoRenderer` for `--renderer cairo`),
which is the half of the claim the export alone cannot make. It is null until the window is
realized, so a tick that finds none tries again rather than printing a false reading.
- `toolbar icons: trigger=NAME(ok) events=... pause=... cursors=... sidebar=... menu=...` (in the
exit dump, beside `panel controls:`) — the icon name every toolbar button
carries, with `gtk_icon_theme_has_icon()`'s verdict. **Every entry must read `(ok)`.** Three
+11
View File
@@ -155,6 +155,17 @@ precedent, not an exception.
(the frame's ink is the image) and would otherwise be counted as well. The counter itself is in
every build; see [probe-readings.md](probe-readings.md).
- `UDPSCOPE_PROBE_HEADER` (no value; **not** a scheduled probe) — the once-a-second
`[udpscope] header: window_button_w=… bar_w=…` line. The widths are sampled either way (the
exit dump reads them); the line is behind this variable because it is detail behind that
reading, not a line every run should print. An unallocated window reads `0` for both.
- `UDPSCOPE_PROBE_RENDERER` (no value; **not** a scheduled probe) — the renderer pair
`--renderer` should have exported, printed from `main()` before GTK initialises
(`[renderer] option=… GSK_RENDERER=… LIBGL_ALWAYS_SOFTWARE=…`), then the renderer GTK
built (`[renderer] realized=…`) once the window has one. Run it with `--renderer cairo` to
check the option reaches the environment before `gtk_init()`/`adw_init()`; run it without
the option to check the default is untouched.
- `UDPSCOPE_PROBE_TRIGRENUM_MS` — the trigger's capture must survive a **renumbering of its own
channel**. It sets the reported geometry through the real widgets (1 s window, 10 % pre share,
hold-off from `--holdoff`) on the mock's *square* alone, polls for a capture to open, and then
+14 -1
View File
@@ -55,7 +55,20 @@
<p>The signals arrive <em>disabled</em>. Nothing is acquired until you turn one on in the
signal list, and enabling is what starts acquisition — so a freshly opened scope with an
idle side panel is working correctly, not failing to connect.</p>
idle side panel is working correctly, not failing to connect. For the same reason a start
that has enabled nothing prints no <code>buffer allocated:</code> line on stderr.</p>
<note style="warning">
<p>Over SSH with X11 forwarding the window may never appear, and the cure is
<cmd>udpscope --renderer cairo</cmd>. Two symptoms tell the two halves apart: a
<code>vkCreateSwapchainKHR() ... VK_ERROR_INITIALIZATION_FAILED</code> warning is
GTK's default Vulkan renderer failing to make a swapchain and is what the option's
<code>GSK_RENDERER=cairo</code> answers; a window whose allocation stays zero
(<code>window_button_w=0</code>) means the plot's GL area has no context either, and
that is what its <code>LIBGL_ALWAYS_SOFTWARE=1</code> answers. Both variables are
exported before GTK initialises, because they are read when the renderer and the GL
context are created. <cmd>--renderer gl</cmd> is faster when indirect GL works.</p>
</note>
<note style="tip">
<p>See <link xref="source">Choosing a source</link> for the chooser and the multicast
+15
View File
@@ -62,6 +62,12 @@ void print_usage(std::FILE* out, const char* argv0) {
" time is then kept for export only\n"
" --open FILE open an exported waveform (.npy) instead of a live source:\n"
" signals from its columns, no socket, no acquisition\n"
" --renderer NAME render with cairo (software), gl or vulkan. cairo also\n"
" exports LIBGL_ALWAYS_SOFTWARE=1, because the plot is a\n"
" GtkGLArea and still needs a GL context. Over SSH/X11\n"
" forwarding use cairo: Vulkan cannot make a swapchain there\n"
" and the GL area has no hardware context, so the window\n"
" never paints or allocates\n"
" -h, --help this text\n",
argv0);
}
@@ -113,6 +119,15 @@ Config parse_args(int argc, char** argv) {
c.capture_packets = static_cast<std::uint32_t>(n);
} else if (a == "--open" || a.rfind("--open=", 0) == 0) {
c.open_waveform = value_of(argc, argv, i, "--open");
} else if (a == "--renderer" || a.rfind("--renderer=", 0) == 0) {
const std::string v = value_of(argc, argv, i, "--renderer");
// Only the three a source or a forwarded display actually chooses between. Anything
// GTK does not know would fall back with a warning and leave the user reading a
// renderer failure that is really a typo, so it is refused here instead.
if (v != "cairo" && v != "gl" && v != "vulkan") {
fail("--renderer must be cairo, gl or vulkan: " + v);
}
c.renderer = v;
} else if (a == "--relative-time") {
c.relative_time = true;
c.relative_time_specified = true;
+9
View File
@@ -60,6 +60,15 @@ struct Config {
/// and the chooser's Waveforms page sets this rather than the connection fields.
std::string open_waveform;
/// `--renderer NAME`: the GSK renderer to force, by exporting `GSK_RENDERER` **before any GTK
/// call**. Empty leaves GTK's own choice alone. `cairo` is the software preset over SSH/X11
/// forwarding: it also exports `LIBGL_ALWAYS_SOFTWARE=1`, because the plot's `GtkGLArea` asks
/// for a GL context whatever GSK draws with. `gl` is the faster alternative when indirect GL
/// works. The variables are read when the renderer/context is created, so setting them anywhere
/// after `gtk_init()`/`adw_init()` is a no-op -- which is why this is a `Config` field applied
/// at the top of `main()` and not a preference.
std::string renderer;
/// The source chooser's "Guided tour" entry. The tour is drawn on the application's own window,
/// which does not exist while the chooser runs, so the request rides on the one value `main()`
/// owns and hands to both the chooser and the front end (`open_waveform` and the preferences'
+27
View File
@@ -10,6 +10,8 @@
#include "source_dialog.hpp"
#include "ui.hpp"
#include <glib.h>
#include <cstdio>
#include <exception>
@@ -26,6 +28,31 @@ int main(int argc, char** argv) {
return 2;
}
// `--renderer` is exported here and nowhere else, because GSK reads `GSK_RENDERER` when it
// *creates* its renderer: inside the first `adw_init()`/`gtk_init()`, which `choose_source()`
// below may call. Setting it after that point is a silent no-op, so the one place it can be
// honoured is before anything touches GTK. Overwrite: a flag on the command line is a
// statement about this run, and the environment is only a default it may override.
//
// `cairo` is the whole software preset and not just GSK's half: the plot is a `GtkGLArea`,
// which asks GDK for a GL context of its own whatever GSK draws with. Over X11 forwarding
// that request is what leaves the window unallocated (no `buffer allocated:` line, zero
// width) once the Vulkan swapchain failure is out of the way, so `LIBGL_ALWAYS_SOFTWARE`
// makes Mesa answer it with llvmpipe. This is the pair `tools/make-help-shots.sh` sets.
if (!cfg.renderer.empty()) g_setenv("GSK_RENDERER", cfg.renderer.c_str(), TRUE);
if (cfg.renderer == "cairo") g_setenv("LIBGL_ALWAYS_SOFTWARE", "1", TRUE);
// The startup half of the renderer probe, printed before GTK exists: the request and the
// variables this process actually exported. The window's `[renderer] realized=` line is the
// other half and says which renderer GTK built.
if (g_getenv("UDPSCOPE_PROBE_RENDERER") != nullptr) {
const char* gsk = g_getenv("GSK_RENDERER");
const char* gl = g_getenv("LIBGL_ALWAYS_SOFTWARE");
std::fprintf(stderr, "[renderer] option=%s GSK_RENDERER=%s LIBGL_ALWAYS_SOFTWARE=%s\n",
cfg.renderer.empty() ? "(unset)" : cfg.renderer.c_str(),
gsk != nullptr ? gsk : "(unset)", gl != nullptr ? gl : "(unset)");
std::fflush(stderr);
}
// No `--server` means the user has not said which source to open, so ask. Before the model:
// declining must cost no buffer and no socket, and nothing below has to cope with "no source".
// `--open` is the other answer to that question, and the chooser knows it: its Waveforms page
+23
View File
@@ -3465,6 +3465,22 @@ gboolean tick(gpointer user) {
ScopeModel& model = *app->model;
const gint64 tick_start_us = g_get_monotonic_time();
// The renderer GTK actually built, printed once and only under the probe: `--renderer` exports
// `GSK_RENDERER` before any GTK call, and this is the other half of that claim -- the class GTK
// chose, not the name it was asked for. The renderer is null until the window is realized, so a
// tick that finds none simply tries again.
{
static const bool renderer_probe = g_getenv("UDPSCOPE_PROBE_RENDERER") != nullptr;
static bool renderer_printed = false;
if (renderer_probe && !renderer_printed) {
if (GskRenderer* r = gtk_native_get_renderer(GTK_NATIVE(app->window))) {
std::fprintf(stderr, "[renderer] realized=%s\n", G_OBJECT_TYPE_NAME(r));
std::fflush(stderr);
renderer_printed = true;
}
}
}
// The paint probe's clock, claimed on the first tick that finds one: a widget has no
// frame clock until it is realized, and the area is realized on its first draw.
if (app->dbg_clock == nullptr && app->area_widget != nullptr) {
@@ -3500,16 +3516,23 @@ gboolean tick(gpointer user) {
// the title's centre box, i.e. window_button_w tracked the window width).
if (app->window_button != nullptr) {
static gint64 next_header_log = 0;
// The widths stay sampled on this one-second cadence whatever the environment, because the
// exit dump's `header:` line reads them; only the *line* is behind a variable. A reading
// printed on every run is a log flood and not a reading -- the same rule the `[notrace]`
// per-paint line follows.
static const bool header_print = g_getenv("UDPSCOPE_PROBE_HEADER") != nullptr;
const gint64 now = g_get_monotonic_time();
if (now >= next_header_log) {
next_header_log = now + G_TIME_SPAN_SECOND;
app->dbg_win_btn_w = gtk_widget_get_width(app->window_button);
app->dbg_bar_w = gtk_widget_get_width(app->window);
if (header_print) {
std::fprintf(stderr, "[udpscope] header: window_button_w=%d bar_w=%d\n",
app->dbg_win_btn_w, app->dbg_bar_w);
std::fflush(stderr);
}
}
}
// The help layer's own readings and the bounds the help's annotated figures crop, on the same
// once-a-second cadence and for the same reason as the line above: the dump runs after the window
// is destroyed and `verification.md` forbids a `gtk_widget_*` call there.