Files
udpscope/src/main.cpp
T
Martino FerrariandClaude Opus 4.6 034c82f7d7 Budget the sample buffer in samples per signal, under a hard memory limit
--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>
2026-11-03 02:47:04 +01:00

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;
}