--buffer-samples (k/M/G) replaces --buffer-mb: the request is rounded down to a power of two for every signal and the shared time ring. A separate memory limit (--memory-limit-mb, a Preferences row, default 8 GiB) halves the plan until the real ring bytes fit and says so on stderr and in the status line. The per-source key buffer_mb becomes buffer_samples; an old buffer_mb is ignored. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
6.8 KiB
C++
128 lines
6.8 KiB
C++
// 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 <glib.h>
|
|
|
|
#include <cstdio>
|
|
#include <exception>
|
|
|
|
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<std::uint64_t>(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<int>(std::max<int>(cfg.plots, static_cast<int>(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<unsigned long long>(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;
|
|
}
|