// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #include "acquisition.hpp" #include "config.hpp" #include "settings.hpp" #include "waveform.hpp" #include "model.hpp" #include "plot_host.hpp" #include "source/registry.hpp" #include "source_dialog.hpp" #include "ui.hpp" #include #include #include int main(int argc, char** argv) { udpscope::Config cfg; try { udpscope::register_builtin_sources(); cfg = udpscope::parse_args(argc, argv); // The preferences fill in what the command line did not decide. `--relative-time` is an explicit // instruction for this run, so a preference can only turn the mode on, never off -- which is also // why this is read here, before the buffers are planned: the mode changes how they are filled. const udpscope::Settings prefs = udpscope::load_settings(); if (prefs.relative_time) cfg.relative_time = true; // The memory limit is the same kind of statement, the other way round: the flag is the explicit // instruction for this run and the preference only fills in when it was absent. if (!cfg.memory_limit_specified) cfg.memory_limit_mb = static_cast(prefs.memory_limit_mb); } catch (const std::exception& e) { std::fprintf(stderr, "udpscope: %s\n", e.what()); 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 // fills `open_waveform` in instead of the connection fields. const bool asked = !cfg.has_source() && cfg.open_waveform.empty(); if (asked && !udpscope::choose_source(cfg)) return 0; // A source named on the command line skips the chooser, but its *stored* configuration still // applies: the buffer size, the clock and the capture ring are decided here, before the model -- // which is the only moment they can be -- and a saved host whose buffer was ignored because the // address came from the command line is an edit that looks like it never took effect. if (cfg.has_source()) udpscope::apply_stored_settings(cfg); // Everything from here to `gtk_window_present()` inside `run_ui()` runs with no main loop: the // buffer is allocated, a recording is read, then the whole UI is built. The chooser's window is // already gone, so without this the screen is empty for all of it -- "the dialog disappeared and // nothing happened", which at a few gigabytes of buffer is ten seconds long. Only on the path // that *asked*: a source named with `--server` had no window to lose, and the status line after // the present is the right place to say what such a run is waiting for. if (asked) udpscope::loading_show(cfg); udpscope::ScopeModel model(cfg); udpscope::RowSet rows; rows.set_row_count(cfg.plots); udpscope::Acquisition acquisition(model, cfg); if (cfg.open_waveform.empty()) { try { acquisition.start(); } catch (const std::exception& e) { std::fprintf(stderr, "udpscope: %s\n", e.what()); return 1; } } else { // No socket and no thread: the file is the source, and there is nothing to arrive afterwards. // The acquisition object is passed to the UI all the same -- stopped, which is what every // "paused"/"capturing" question answers as false. udpscope::WaveformLoadReport report; std::string error; if (!udpscope::load_waveform_into_model(model, cfg.open_waveform, &report, &error)) { std::fprintf(stderr, "udpscope: %s\n", error.c_str()); return 1; } // One row per column, so every trace opens where a user can see and rearrange it -- and sized // *here*, before the UI exists, exactly as `--plots` does: resizing the rows from inside the // activation leaves the panels built for the old count. rows.set_row_count(std::min(std::max(cfg.plots, static_cast(report.signals)), udpscope::RowSet::kMaxRows)); udpscope::remember_waveform(cfg.open_waveform); // The whole file framed from the start: the window is the recording's own span, and nothing // afterwards restates it -- a zoom is the user's. if (report.span_s > 0.0) rows.set_time_window(report.span_s); // `rows`/`decim` say whether what is on screen is the file sample for sample or a filtered // version of it: `samples` alone below `rows` is how a recording that did not fit its buffer at // its own rate used to read as a truncated one. std::fprintf(stderr, "[udpscope] waveform loaded: %s signals=%zu samples=%zu rows=%zu decim=%llu " "span=%.6fs rate=%.6g time=[%.6f..%.6f]\n", cfg.open_waveform.c_str(), report.signals, report.samples, report.rows, static_cast(report.decimation), report.span_s, report.rate_hz, report.first_time, report.last_time); std::fflush(stderr); } const int result = udpscope::run_ui(argc, argv, cfg, model, acquisition, rows); acquisition.stop(); return result; }