fixed plot flicker, measurement cost and toolbar icons

The flicker ("no data" at no constant frequency, worse with the measurements
and cursors panes up) was a torn read of the window's first stamp: acquire()
pins the window to the ring's oldest slot, and reading it at paint time races
the producer lapping it, returning a stamp one ring span too new. x0() is the
origin every trace point is reconstructed from, so one torn read produced a
refused neighbours(), a filter that dropped three quarters of the points, and
a polyline shifted off its axis. It is cached by acquire() under the lock now.

EventSeries::neighbours() answered from the (x - x0)/dx grid, whose drift from
the real arrival stamps arrives as a constant offset in seconds -- a doubling
share of the screen per zoom step, which is the "zooming into a held event
clips it more from the left" report. Both series binary-search the stamps now.

The measurements pane went through the LOD pyramid rather than reading its
window raw (19.7 ms per recompute to 1.2 ms, 0.1% of a 2 M-sample window) and
recomputes on a cadence; the math engine lost a per-slot heap allocation, a
per-sample source lookup by name and a fetch_add that cost more than the
arithmetic it counted.

The cursors pane's mix mode drew a line per *named* slot, so a slot holding a
signal that can never produce a channel -- the stream's timestamp -- got a row
that could only read "--", and such names were re-saved every session. It
counts bound traces now and apply_layout() refuses them.

Four toolbar icons are drawn for this app and compiled in as a GResource: the
stock ones were a record dot for the trigger and three magnifiers for cursors,
measurements and math.

New probes and dump lines for all of it (trace ink, measure cost, math cost,
event zoom, toolbar icons); README and CLAUDE.md record what they cure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-03 23:36:07 +02:00
1 parent 714d042809
commit a92a6b5bf0
20 files changed
+1945 -243

No files matched your search

+36 -22
View File
@@ -5,11 +5,15 @@
#include <algorithm>
#include <cmath>
#include <limits>
namespace udpscope {
namespace {
constexpr double kPi = 3.14159265358979323846;
/// A value the step can return without a library call: `std::nan("")` parses its argument, and this
/// runs once per stored slot per node.
constexpr double kNaN = std::numeric_limits<double>::quiet_NaN();
/// A biquad's coefficients for one node, from the RBJ audio-EQ cookbook. `q` is 1/sqrt(2)
/// (Butterworth) for the plain low/high pass, and the node's own for the band pass -- which is
@@ -119,44 +123,54 @@ bool math_op_uses_filter(MathOp op) {
}
bool math_op_uses_clamp(MathOp op) { return op == MathOp::Clamp; }
void math_prepare(const MathChannel& channel, MathRuntime& rt, double dt) {
void math_prepare(const MathChannel& channel, MathRuntime& rt, double dt,
const std::function<int(const std::string&)>& index_of) {
const std::size_t n = channel.nodes.size();
rt.value.assign(n, 0.0);
rt.b0.assign(n, 1.0);
rt.b1.assign(n, 0.0);
rt.b2.assign(n, 0.0);
rt.a1.assign(n, 0.0);
rt.a2.assign(n, 0.0);
// A rate change *keeps* the filter state: the state words are in the signal's units, not in
// time, so a decimation change moves the cutoffs (which is why the coefficients are rebuilt)
// without making the trace jump. A node that is new gets zeros, which is a filter starting from
// rest -- the only honest starting point there is.
rt.z1.resize(n, 0.0);
rt.z2.resize(n, 0.0);
const std::vector<MathNodeRt> was = std::move(rt.node);
rt.node.assign(n, MathNodeRt{});
for (std::size_t i = 0; i < n && i < was.size(); ++i) {
rt.node[i].z1 = was[i].z1;
rt.node[i].z2 = was[i].z2;
}
rt.dt = dt;
rt.ready = true;
for (std::size_t i = 0; i < n; ++i) {
const MathNode& node = channel.nodes[i];
MathNodeRt& out = rt.node[i];
if (node.op == MathOp::Source) {
out.src = index_of ? index_of(node.source) : -1;
continue;
}
if (!math_op_uses_filter(node.op)) continue;
biquad(node.op, node.fc, node.q, dt, rt.b0[i], rt.b1[i], rt.b2[i], rt.a1[i], rt.a2[i]);
biquad(node.op, node.fc, node.q, dt, out.b0, out.b1, out.b2, out.a1, out.a2);
}
}
double math_step(const MathChannel& channel, MathRuntime& rt,
const std::function<double(const std::string&)>& source) {
double math_step(const MathChannel& channel, MathRuntime& rt, const float* values,
std::size_t count) {
const std::size_t n = channel.nodes.size();
if (n == 0 || rt.value.size() != n) return std::nan("");
if (n == 0 || rt.node.size() != n) return kNaN;
MathNodeRt* rts = rt.node.data();
const auto at = [&](int index) -> double {
if (index < 0 || static_cast<std::size_t>(index) >= n) return std::nan("");
return rt.value[static_cast<std::size_t>(index)];
if (index < 0 || static_cast<std::size_t>(index) >= n) return kNaN;
return rts[static_cast<std::size_t>(index)].value;
};
for (std::size_t i = 0; i < n; ++i) {
const MathNode& node = channel.nodes[i];
MathNodeRt& self = rts[i];
double x = 0.0;
switch (node.op) {
case MathOp::Source:
x = source(node.source);
case MathOp::Source: {
const int c = self.src;
x = c >= 0 && static_cast<std::size_t>(c) < count
? static_cast<double>(values[static_cast<std::size_t>(c)])
: kNaN;
break;
}
case MathOp::Gain:
x = at(node.in_a) * node.gain + node.offset;
break;
@@ -174,8 +188,8 @@ double math_step(const MathChannel& channel, MathRuntime& rt,
case MathOp::LowPass:
case MathOp::HighPass:
case MathOp::BandPass:
x = biquad_step(at(node.in_a), rt.b0[i], rt.b1[i], rt.b2[i], rt.a1[i], rt.a2[i], rt.z1[i],
rt.z2[i]);
x = biquad_step(at(node.in_a), self.b0, self.b1, self.b2, self.a1, self.a2, self.z1,
self.z2);
break;
case MathOp::Sum:
x = at(node.in_a) + at(node.in_b);
@@ -194,11 +208,11 @@ double math_step(const MathChannel& channel, MathRuntime& rt,
break;
}
}
rt.value[i] = x;
self.value = x;
}
const int out = channel.output;
if (out < 0 || static_cast<std::size_t>(out) >= n) return std::nan("");
return rt.value[static_cast<std::size_t>(out)];
if (out < 0 || static_cast<std::size_t>(out) >= n) return kNaN;
return rts[static_cast<std::size_t>(out)].value;
}
bool math_channel_valid(const MathChannel& channel, std::string* why) {