Feature/source plugins #1

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

No files matched your search

+14 -1
View File
@@ -611,6 +611,15 @@ bool load_layout(const std::string& path, LayoutState& out) {
st.rows.back().signals = names;
} else if (key == "weight") {
st.rows.back().weight = std::strtod(value.c_str(), nullptr);
} else if (key == "row") {
try {
const int r = std::stoi(value);
if (r >= 0) st.rows.back().row = r;
} catch (...) {
}
} else if (key == "width") {
const double w = std::strtod(value.c_str(), nullptr);
if (w > 0.0) st.rows.back().width = w;
}
} else if (section == Section::Math && !st.math.empty()) {
MathChannel& m = st.math.back();
@@ -784,7 +793,11 @@ bool save_layout(const std::string& path, const LayoutState& state) {
o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n";
if (state.capture_packets > 0) o << "capture_packets = " << state.capture_packets << "\n";
for (const auto& row : state.rows) {
o << "\n[[plot]]\nweight = " << row.weight << "\nsignals = [";
o << "\n[[plot]]\n";
// A row of -1 is a state built without the split (nothing in the app does that now); leaving the
// key out keeps it readable as the one-cell-per-row file it is.
if (row.row >= 0) o << "row = " << row.row << "\nwidth = " << row.width << "\n";
o << "weight = " << row.weight << "\nsignals = [";
for (std::size_t i = 0; i < row.signals.size(); ++i) {
if (i != 0) o << ", ";
o << toml_escape(row.signals[i]);
+7
View File
@@ -24,6 +24,13 @@ struct LayoutState {
/// The row's share of the figure's height, 1 for equal rows. Part of the layout because a sizing a
/// user dragged is an arrangement, and an arrangement that is not saved is one that is lost.
double weight = 1.0;
/// The figure row this plot sits in; plots of one row are the cells of a horizontal split, in
/// order. -1 means the file had no `row` key (a session from before the split), which loads as
/// one cell per row.
int row = -1;
/// This cell's share of its row's width, 1 for equal cells. `weight` is the *row's* and is
/// written on every cell of the row; `width` is the cell's own.
double width = 1.0;
};
double window_s = 0.0; ///< 0 keeps the default window
+3
View File
@@ -223,6 +223,9 @@ struct App {
int dbg_layout_step = 0;
/// Which step of the split probe runs next (`UDPSCOPE_PROBE_SPLIT_MS`).
int dbg_split_step = 0;
/// The split probe's round trip: the arrangement saved, and the legacy copy of its file.
std::string dbg_split_expect;
std::string dbg_split_legacy_path;
/// Which step of the cursors probe runs next (`UDPSCOPE_PROBE_CURSORS_MS`).
int dbg_cursors_step = 0;
/// Which step of the fit probe runs next (`UDPSCOPE_PROBE_FIT_MS`).
+62 -4
View File
@@ -30,8 +30,16 @@ LayoutState collect_layout(App& app) {
row.signals.push_back(r.layer[si] > 0 ? std::string()
: r.signal_name[si]);
}
// The figure row's own weight and this cell's share of the row's width.
const int fr = app.rows->figure_row_of(i);
const std::vector<float>& weights = app.rows->row_weights();
if (static_cast<std::size_t>(i) < weights.size()) row.weight = weights[static_cast<std::size_t>(i)];
row.row = fr;
if (fr >= 0 && static_cast<std::size_t>(fr) < weights.size()) {
row.weight = weights[static_cast<std::size_t>(fr)];
}
const std::vector<float>& cw = app.rows->cell_weights(fr);
const int cell = app.rows->cell_of(i);
if (cell >= 0 && static_cast<std::size_t>(cell) < cw.size()) row.width = cw[static_cast<std::size_t>(cell)];
st.rows.push_back(std::move(row));
}
for (std::size_t si = 0; si < model.signals().size(); ++si) {
@@ -444,17 +452,67 @@ void apply_layout(App& app, const LayoutState& st) {
int rows = static_cast<int>(st.rows.size());
if (rows > RowSet::kMaxRows) rows = RowSet::kMaxRows;
if (rows > 0) app.rows->set_row_count(rows);
// The arrangement. A file that names a figure row for every plot (`row = ...`) carries a horizontal
// split; one that names none -- every session written before the split -- is one cell per row, which
// is what it was. A `row` sequence that cannot be a layout (hand-edited, truncated, from a build
// with other limits) is not a reason to refuse the file: it loads as the old shape, with the signals
// where they were.
std::vector<int> cells_per_row;
{
bool all_rows = !st.rows.empty() && st.rows.size() <= static_cast<std::size_t>(RowSet::kMaxRows);
int prev = -1;
for (const LayoutState::Row& r : st.rows) {
if (!all_rows) break;
if (r.row < 0 || r.row < prev || r.row > prev + 1 || (prev < 0 && r.row != 0)) {
all_rows = false;
} else if (r.row != prev) {
cells_per_row.push_back(1);
} else if (++cells_per_row.back() > RowSet::kMaxCellsPerRow) {
all_rows = false;
}
prev = r.row;
}
if (!all_rows) cells_per_row.clear();
}
const bool split_file = !cells_per_row.empty();
if (split_file) {
app.rows->set_cells_per_row(cells_per_row);
} else if (rows > 0) {
app.rows->set_row_count(rows);
}
// The rows' shares of the height come from the same file as their signals: a sizing a user dragged is
// an arrangement, and the layout is where an arrangement lives. Absent from an older file, the weights
// are all 1 -- equal rows -- which is what such a file was saved with.
// are all 1 -- equal rows -- which is what such a file was saved with. The weight is the *figure row's*
// and is written on every cell of it; the row's first cell is the one read.
{
std::vector<float> weights;
weights.reserve(st.rows.size());
for (const LayoutState::Row& wr : st.rows) weights.push_back(static_cast<float>(wr.weight));
if (split_file) {
std::size_t at = 0;
for (const int n : cells_per_row) {
weights.push_back(static_cast<float>(st.rows[at].weight));
at += static_cast<std::size_t>(n);
}
} else {
for (const LayoutState::Row& wr : st.rows) weights.push_back(static_cast<float>(wr.weight));
}
app.rows->set_row_weights(std::move(weights));
}
// The cells' shares of a split row's width, after the arrangement (which resets them).
if (split_file) {
std::size_t at = 0;
for (std::size_t fr = 0; fr < cells_per_row.size(); ++fr) {
const std::size_t n = static_cast<std::size_t>(cells_per_row[fr]);
if (n >= 2) {
std::vector<float> widths;
for (std::size_t c = 0; c < n; ++c) widths.push_back(static_cast<float>(st.rows[at + c].width));
app.rows->set_cell_weights(static_cast<int>(fr), std::move(widths));
}
at += n;
}
}
for (int i = 0; i < rows; ++i) {
int slot = 0;
for (const std::string& name : st.rows[static_cast<std::size_t>(i)].signals) {
+117 -1
View File
@@ -18,6 +18,7 @@
#include <cmath>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <tuple>
namespace udpscope {
@@ -3900,6 +3901,34 @@ void split_probe_report(App& a, const char* tag) {
}
namespace {
/// The arrangement and what is in it, as one comparable string: per figure row its weight, per cell
/// its width share and its traces. Weights are read back as stored, so a load is compared with a save
/// to the precision the file keeps (`%.4f`).
std::string split_probe_shape(const RowSet& rs) {
std::string out;
char buf[48];
for (int fr = 0; fr < rs.figure_row_count(); ++fr) {
const std::vector<float>& rw = rs.row_weights();
const std::vector<float>& cw = rs.cell_weights(fr);
std::snprintf(buf, sizeof(buf), "%sr%d(h=%.4f)", fr == 0 ? "" : " ", fr,
static_cast<std::size_t>(fr) < rw.size() ? static_cast<double>(rw[static_cast<std::size_t>(fr)]) : 1.0);
out += buf;
const int first = rs.first_plot_of_row(fr);
for (int k = 0; k < rs.cells_in_row(fr); ++k) {
std::snprintf(buf, sizeof(buf), "[w=%.4f:", static_cast<std::size_t>(k) < cw.size() ? static_cast<double>(cw[static_cast<std::size_t>(k)]) : 1.0);
out += buf;
bool any = false;
for (const std::string& n : rs.row(first + k).signal_name) {
if (n.empty()) continue;
out += (any ? "," : "") + n;
any = true;
}
out += any ? "]" : "-]";
}
}
return out;
}
/// After any structural change: the new figure is the area's, and the frame tick re-lays it out.
void split_probe_restructured(App& a) {
if (a.area != nullptr) a.area->set_figure(&a.rows->figure());
@@ -4002,8 +4031,95 @@ gboolean split_probe_step(gpointer user) {
split_probe_restructured(*a);
split_probe_next(*a);
break;
default:
case 6: {
split_probe_report(*a, "remove");
// The session round trip, through the real entry points. Plot 0 alone on row 0, plots 1 and 2 side
// by side on row 1, each cell holding a different trace, row 0 at twice row 1's height and the
// split row at 3:1.
rs.set_cells_per_row({1, 2});
split_probe_clear_all(*a);
for (int p = 0; p < 3; ++p) split_probe_assign(*a, p, static_cast<std::size_t>(p));
rs.set_row_weights({2.0f, 1.0f});
rs.set_cell_weights(1, {3.0f, 1.0f});
a->dbg_split_expect = split_probe_shape(rs);
const LayoutState saved = collect_layout(*a);
const std::string path = std::string(g_get_tmp_dir()) + "/udpscope-split-probe.toml";
const bool wrote = save_layout(path, saved);
// Back to the plain arrangement, so a load that did nothing cannot pass.
rs.set_cells_per_row({1});
split_probe_clear_all(*a);
LayoutState loaded;
const bool read = load_layout(path, loaded);
// The same file with its `row`/`width` lines stripped: a session from before the split.
std::string legacy_path = path + ".legacy";
{
std::ifstream in(path);
std::ofstream out(legacy_path);
std::string line;
while (std::getline(in, line)) {
if (line.rfind("row = ", 0) == 0 || line.rfind("width = ", 0) == 0) continue;
out << line << "\n";
}
}
a->dbg_split_legacy_path = legacy_path;
std::remove(path.c_str());
if (read) apply_layout(*a, loaded);
std::fprintf(stderr, "[split-probe] roundtrip-io wrote=%d read=%d plots_in_file=%zu\n",
wrote ? 1 : 0, read ? 1 : 0, loaded.rows.size());
split_probe_restructured(*a);
split_probe_next(*a);
break;
}
case 7: {
split_probe_report(*a, "roundtrip");
const std::string now = split_probe_shape(rs);
std::fprintf(stderr, "[split-probe] roundtrip rt=%d expect=%s got=%s\n",
now == a->dbg_split_expect ? 1 : 0, a->dbg_split_expect.c_str(), now.c_str());
rs.set_cells_per_row({1});
split_probe_clear_all(*a);
LayoutState loaded;
const bool read = load_layout(a->dbg_split_legacy_path, loaded);
std::remove(a->dbg_split_legacy_path.c_str());
int rows_unset = 1;
for (const LayoutState::Row& r : loaded.rows) {
if (r.row != -1) rows_unset = 0;
}
if (read) apply_layout(*a, loaded);
std::fprintf(stderr, "[split-probe] legacy-io read=%d plots_in_file=%zu row_unset=%d\n",
read ? 1 : 0, loaded.rows.size(), rows_unset);
split_probe_restructured(*a);
split_probe_next(*a);
break;
}
case 8: {
split_probe_report(*a, "legacy");
// A `row` sequence that cannot be a layout loads as one cell per row rather than refusing the
// file: five cells in one row (over the cap), a gap, a row that goes back, and one that does not
// start at 0.
const LayoutState base = collect_layout(*a);
const std::vector<std::vector<int>> bad = {{0, 0, 0, 0, 0}, {0, 2, 2}, {0, 1, 0}, {1, 1, 2}};
std::string out;
int ok = 1;
for (const std::vector<int>& seq : bad) {
LayoutState st = base;
st.rows.clear();
for (const int r : seq) {
LayoutState::Row row = base.rows[0];
row.row = r;
st.rows.push_back(row);
}
apply_layout(*a, st);
const bool one_each = rs.figure_row_count() == static_cast<int>(seq.size()) &&
rs.row_count() == static_cast<int>(seq.size());
if (!one_each) ok = 0;
out += (out.empty() ? "" : " ") + std::to_string(rs.figure_row_count()) + "/" +
std::to_string(rs.row_count());
}
std::fprintf(stderr, "[split-probe] invalid-rows ok=%d frows/plots=[%s]\n", ok, out.c_str());
split_probe_restructured(*a);
break;
}
default:
break;
}
return G_SOURCE_REMOVE;