Feature/source plugins #1

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

No files matched your search

+22 -16
View File
@@ -1271,8 +1271,10 @@ snapshots there and then, because a trigger that has stopped firing never reache
**Persistence: one image per signal, every acquisition.** The trigger panel's `Advanced` block
carries the *other* display of the same acquisitions, and it is not a count at all: `persistence`
turns on a **phosphor density** — the plotter's screen-sized intensity image (`Plot::set_density()`,
one `f32` cell per device pixel of the axes rect **per trace, in that trace's own signal colour**)
turns on a **phosphor density** — the plotter's intensity image (`Plot::set_density()`, the axes rect
in device pixels **per trace, in that trace's own signal colour**, bounded by the plotter's
`kDensityCellCap`: past **262 144 cells** — a mebibyte of counts — it scales the rect down uniformly
and magnifies the image back over it with a linear filter)
into which every triggered acquisition is deposited, the newest included, and which is drawn *in
place of* the traces. The current acquisition is therefore a sweep *inside* the image and fades with
the rest; no solid polyline is drawn over it, which is what a report of "the persistent image plus
@@ -1280,20 +1282,24 @@ the last acquisition, and the last one never fades in time mode" turned out to b
brightness is how many acquisitions passed through it — one sweep's ink is the trace's own colour,
and where several agree it clamps there rather than going white — so the display brightens where the
signal is steady and dims where it only passed, **with no depth to choose and no memory that grows
with the acquisitions**: the cost is the screen's times the traces on it (2.2 MB over the probe's
four one-signal rows; the default four signals in one 800x480 row are 6 MB) and a thousand
acquisitions cost the same. Each signal is a separate image, so two signals agreeing in a cell read
as two colours and not one blended grey. `fades by` picks how it decays: `acquisitions` (each
acquisition keeps the factor of what was before it, 0.5 by default — the phosphor afterglow
schedule) or `time` (a time constant in stream seconds, so the display fades while you watch
whatever the trigger rate). It applies in **trigger mode only** — while a captured acquisition is
on screen — and it goes away with the trigger, on a pause and on an opened file, because the image
is an image *of* a stream of acquisitions. Like the invalid-span bands it reserves no layout space
and nothing on it can be picked. The `[density-probe]` run that measured it is in
`docs/claude/probe-readings.md`; what it costs is in the same place (`[density-probe] frame cost:`:
tick 0.74 → 9.99 ms and draw 2.09 → 20.91 ms over the same run, at four rows and 2.2 M cells a
frame, on llvmpipe — measured before the per-trace split, when the one image was still re-filled
per row).
with the acquisitions**: the cost is bounded, and the bound is the grid's — at most a mebibyte of
counts a signal, so the app's four signals hold 4 MiB between them however wide the window, and the
probe's four one-signal rows read `cells=1044714` (4 178 856 bytes, 1 044 714 of them uploaded on a
moved frame) — and a thousand acquisitions cost the same. Each signal is a separate image, so two
signals agreeing in a cell read as two colours and not one blended grey. `fades by` picks how it
decays: `acquisitions` (each acquisition keeps the factor of what was before it, 0.5 by default —
the phosphor afterglow schedule) or `time` (a time constant in stream seconds, so the display fades
while you watch whatever the trigger rate). It applies in **trigger mode only** — while a captured
acquisition is on screen — and it goes away with the trigger, on a pause and on an opened file,
because the image is an image *of* a stream of acquisitions. Like the invalid-span bands it reserves
no layout space and nothing on it can be picked. The `[density-probe]` run that measured it is in
`docs/claude/probe-readings.md`; what it costs is there and in the plotter's
`docs/data-and-performance.md`, where the density's own pass is priced headless at a fixed geometry —
the app's in-run A/B of it is a mean against a mean and the "before" side includes the run's own
startup frames. A 3040x1700 framebuffer with four rows of one trace and a 2 s window (the frozen
configuration: at 2x device scale that is a 1520x850 window) read **4.5 M cells a trace, 12.9 ms of
tick and 12.8 ms of draw a frame and 4.9 MB uploaded** on llvmpipe, and reads **1.0 M cells, 3.9 /
5.6 ms and 1.0 MB** at the cap.
**And it says what it costs.** A retained acquisition is a whole window copied out of the ring, so
the retention is bounded by two things: the depth, and **`--buffer-mb`** — persistence may hold at
+1 -1
@@ -379,13 +379,16 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
- **The phosphor density is the *trace*, and that is a different display from the layers above, not a
mode of them.** The trigger panel's `Advanced` block offers both, and the words matter: *multiple
events* is `persist_depth` (N retained acquisitions, N traces, dimmer with age) while *persistence*
is `TriggerConfig::density` -- the plotter's `Plot::set_density()`, a screen-sized intensity grid
is `TriggerConfig::density` -- the plotter's `Plot::set_density()`, an intensity grid
**per trace, drawn in that trace's own colour**, every acquisition deposited into it, which fades
either by a time constant or by a per-acquisition factor. There is **no depth** on the density and
no per-acquisition memory: the image is `traces * columns * rows * 4` bytes, one cell per device
pixel of the row's axes for every trace the row draws (the app's default four signals in one row
are four grids -- 6 MB at 800x480; the probe's one-signal-per-row run is 2.2 MB over four rows), so
a thousand acquisitions cost what one costs. A cell's value is a number of *sweeps*: the library
no per-acquisition memory: the image is `traces * columns * rows * 4` bytes, and both factors are
bounded -- the grid is the row's axes rect in device pixels **up to the plotter's
`kDensityCellCap`**, and there is one grid a trace the row draws. Four traces in a row
therefore hold at most 4 MiB between them, whoever dragged the window where (the probe's
one-signal-per-row run reads `cells=1044714`, 4 178 856 bytes), so a thousand acquisitions cost
what one costs and *neither* a wide window nor a long run can make the image grow. A cell's value
is a number of *sweeps*: the library
deposits a cell once per trace per sweep however many of that trace's own samples crossed it
(`[density-probe] on` reads `peak=1.0000` for one sweep; a per-hit deposit read 18) -- dwell is
`Style::intensity_grading`'s question, not this one's. The colour is the trace's own
+38 -25
View File
@@ -1981,27 +1981,29 @@ ring's bindings, and a reading taken there says nothing about either.
```
[density-probe] arming on 'square', window 0.5 s, display off
[density-probe] off enabled=0 fade=acquisitions tau=1.000 factor=0.500 rows=0 traces=0 cells=0 ink=0.0 peak=0.0000 ratio=1.000000
[density-probe] on enabled=1 fade=acquisitions tau=1.000 factor=0.500 rows=4 traces=4 cells=2226150 ink=226786.0 peak=1.0000 ratio=1.000000
[density-probe] step-acquisitions enabled=1 fade=acquisitions tau=1.000 factor=0.500 rows=4 traces=4 cells=2226150 ink=313971.0 peak=1.5000 ratio=1.384437
[density-probe] time-before enabled=1 fade=time tau=1.000 factor=0.500 rows=4 traces=4 cells=2226150 ink=458610.1 peak=2.1359 ratio=1.000000
[density-probe] step-time enabled=1 fade=time tau=1.000 factor=0.500 rows=4 traces=4 cells=2226150 ink=168713.2 peak=0.7858 ratio=0.367879
[density-probe] frame cost: tick before=0.62 ms (184 frames) after=6.96 ms (23 frames) draw before=2.02 ms (177) after=6.16 ms (18)
[udpscope] density: frames=748 rows=4 traces=4 cells=2226150 grid_bytes=8904600 ink=489400.2 peak=2.2 upload_bytes=2226150
[density-probe] on enabled=1 fade=acquisitions tau=1.000 factor=0.500 rows=4 traces=4 cells=1044714 ink=99403.0 peak=1.0000 ratio=1.000000
[density-probe] step-acquisitions enabled=1 fade=acquisitions tau=1.000 factor=0.500 rows=4 traces=4 cells=1044714 ink=149104.5 peak=1.5000 ratio=1.500000
[density-probe] time-before enabled=1 fade=time tau=1.000 factor=0.500 rows=4 traces=4 cells=1044714 ink=121738.0 peak=1.2247 ratio=1.000000
[density-probe] step-time enabled=1 fade=time tau=1.000 factor=0.500 rows=4 traces=4 cells=1044714 ink=44784.9 peak=0.4505 ratio=0.367879
[density-probe] frame cost: tick before=3.75 ms (145 frames) after=3.59 ms (24 frames) draw before=3.93 ms (125) after=3.23 ms (6)
[udpscope] density: frames=918 rows=4 traces=4 cells=1044714 grid_bytes=4178856 ink=279073.5 peak=2.2 upload_bytes=1044714
```
The **phosphor density** (`Plot::set_density()`, the trigger pane's `persistence` switch): one
screen-sized intensity image **per trace**, in that trace's own signal colour, every acquisition
accumulated into it (the newest included), drawn *in place of* the traces and fading by a count or by
the clock. The numbers are the plotter's own -- `DensityGrid::ink()`/`peak()` summed and maxed over the
traces the display is on, which is the same source the library's tests read -- and the
probe drives the panel's **own widgets** for the switch and the fade, so what it measures is this app's
wiring.
The **phosphor density** (`Plot::set_density()`, the trigger pane's `persistence` switch): the axes
rect in device pixels **up to `kDensityCellCap`** (262 144 cells, 1 MiB of counts) as an intensity
image **per trace**, in that trace's own signal colour, every acquisition accumulated into it (the
newest included), drawn *in place of* the traces and fading by a count or by the clock. Past the cap
the plotter scales the rect down uniformly and magnifies the image back over it with a linear
filter, so the number this probe reports is bounded whatever the window is. The numbers are the
plotter's own -- `DensityGrid::ink()`/`peak()` summed and maxed over the traces the display is on,
which is the same source the library's tests read -- and the probe drives the panel's **own
widgets** for the switch and the fade, so what it measures is this app's wiring.
- **`off` is the display without it**: `rows=0 traces=0`, so there is no grid at all and both numbers
are zero. That is also the run the frame cost is read against, and it is a plain run -- the density
is off by default and nothing draws it.
- **`on` is one sweep**: `traces=4` -- one grid per signal, four rows of one signal each -- and
`peak=1.0000` exactly, with `ink=226786` the cells that sweep covered. A cell takes **1.0 once per
`peak=1.0000` exactly, with `ink=99403` the cells that sweep covered. A cell takes **1.0 once per
trace per sweep** however many of the trace's own samples crossed it, which is what makes a sweep's
peak *one* rather than the eighteen a per-hit deposit read on the same run -- and what makes the
sweeps behind it visible at all.
@@ -2013,23 +2015,34 @@ wiring.
twice.
- **`step-time` is `1/e` by construction**: one step of `dt = tau` through the plot's own `decay_density()`,
so `ratio=0.367879` is `exp(-1)`. The leg before it (`time-before`) reads the same image under the time
fade (`peak=2.1359`), and the pair is the reading that says the *panel's* constant reaches the grid.
fade (a peak between 1 and 2, `1.2247` here), and the pair is the reading that says the *panel's* constant
reaches the grid.
- **`frame cost:` is the A/B inside one run**: the accumulators as of the tick the display was switched on
against the frames after it, so both means are the same stream, the same layout and the same mock. At four
rows of 573x412 cells it reads **tick 0.62 -> 6.96 ms** and **draw 2.02 -> 6.16 ms**: the per-frame
conversion, upload and deposit. It is llvmpipe, and the arithmetic is the grid's -- 2.2 M cells, one byte
each, every frame the time fade moves it. The *draw* mean is now below the run before the per-trace
split (2.02 -> 6.16 against 2.09 -> 20.91): the solid traces are no longer drawn over the image, so the
frame draws four quads where it used to draw four quads *and* every trace.
- **`cells`/`grid_bytes` are the per-signal memory figure**: 556 537 cells and 2.2 MB *per signal*, times
the four signals here (2 226 150 cells, 8 904 600 bytes). It is the axes rect's device pixels, so it is
the screen and the trace count and nothing the trigger has done -- and the app's default layout, four
signals in one row, is four grids for that one row.
against the frames after it, so both means are the same stream, the same layout and the same mock. The
"before" side is a mean that **includes the run's startup frames** (the first paints are the expensive
ones), so it is a high-water mark and not a steady-state mean: at four rows of ~261 000 cells each (the
cap) the pair above reads `tick 3.75 -> 3.59 ms`, i.e. the switch is *inside* the startup noise, and a
quiet machine reading the same line before the cap read `tick 0.62 -> 9.53 ms` at 2.2 M cells. **The
reading that prices a display at a given window is the library's**, taken headless and against a fixed
geometry, in the plotter's `docs/data-and-performance.md`: a 3040x1700 framebuffer with four rows of one
trace and a 2 s window, where the grid pass alone was **12.9 ms of tick and 12.8 ms of draw a frame and
4.9 MB uploaded** before the cap and **3.9 / 5.6 ms and 1.0 MB** after it, over the same frames.
- **`cells`/`grid_bytes` are the per-signal memory figure**: at most **262 144 cells and 1 048 576 bytes
*per signal***, and the four signals here read 1 044 714 cells / 4 178 856 bytes because a 1349x424 device
axes rect is already past the cap. It is the cap and the trace count, and neither the trigger nor the
window beyond the cap has anything to say about it -- the app's default layout, four signals in one row,
is four grids for that one row and 4 MiB whoever dragged the window where.
- **The dump's `density:` line** is captured on the last tick the display was **on**, not read at exit: the
exit probe switches the trigger off before it dumps, which takes the display with it, so a dump that asked
the plots would say `rows=0` about a run whose tail had the image up. `upload_bytes` is the **most** any
one frame uploaded -- one byte a cell **per trace**, over the grids that moved -- and not the last frame's,
for the same reason.
- **A frame the display is drawing is not a `[notrace]` frame.** `Renderer::draw_traces()` returns while a
density is on, so every frame of a persistence display stages no trace; the paint probe skips its
traceless accounting for those (they are the display working, not "a frame asked to draw something it had
nothing for") and the `[notrace]` line -- one per frame, and so behind `UDPSCOPE_PROBE_NOTRACE` -- is not
printed for them. A density run's `traceless paints:` therefore no longer counts them: it read `653 (637
after the first bind)` for a run whose density was up six hundred frames.
**Read `peak` as the display's ceiling**: with the default `full_scale` of 1.0 a cell at 1.0 is full ink
(the trace's own colour), so a steady display under the acquisition fade sits between 1 and 2, and a `peak`
+12 -1
View File
@@ -115,7 +115,9 @@ precedent, not an exception.
schedule in use, oldest first — halves, and `1/2^age` at that. Both lines and the two exit-dump
lines are in [probe-readings.md](probe-readings.md).
- `UDPSCOPE_PROBE_DENSITY_MS` (+ `UDPSCOPE_PROBE_DENSITY_TAU`, default 1.0 s) — the **phosphor
density**: one screen-sized intensity image **per trace**, in that trace's own colour, every
density**: an intensity image **per trace**, in that trace's own colour, the axes rect in device
pixels up to the plotter's `kDensityCellCap` (past which it scales the rect down uniformly and
magnifies the image back over it with a linear filter), every
triggered acquisition accumulated into it (the newest included), drawn *in place of* the traces,
fading by a per-acquisition factor or by a time constant. It states the display
**off** through the panel's own switch first, so the `off` leg is the run without it, then arms the
@@ -144,6 +146,15 @@ precedent, not an exception.
one run), and the exit dump's `density:` line says what the last frame it was on held. Both lines
and the dump line are in [probe-readings.md](probe-readings.md).
- `UDPSCOPE_PROBE_NOTRACE` (no value; **not** a scheduled probe, and off unless it is set) — the
per-paint detail behind the exit dump's `traceless paints:` counter: one `[notrace]` line for each
of the first 40 paints after the first bind that staged no trace, naming the mode, the axis the
frame was asked for and every slot's bounds. It is behind a variable because a state that draws
nothing lasts as long as it lasts, so an ungated line is one per frame for the whole time -- and it
is *not* printed for a frame a phosphor density is drawing, which stages no trace by construction
(the frame's ink is the image) and would otherwise be counted as well. The counter itself is in
every build; see [probe-readings.md](probe-readings.md).
- `UDPSCOPE_PROBE_TRIGRENUM_MS` — the trigger's capture must survive a **renumbering of its own
channel**. It sets the reported geometry through the real widgets (1 s window, 10 % pre share,
hold-off from `--holdoff`) on the mock's *square* alone, polls for a capture to open, and then
+4 -3
View File
@@ -133,9 +133,10 @@
<p>Two things make it different from <gui>multiple events</gui> beside it, and the panel offers
both because they answer different questions. <em>It never runs out of room</em>: each signal's
image is the size of the plot on screen, so a display of a thousand acquisitions costs what a
display of one costs — one image per signal, whatever the plot's trace slots — and there is no
count to choose. <em>And it fades</em>, in one of two ways — <gui>fades by</gui> picks them:</p>
image is sized to the plot on screen and capped, so a display of a thousand acquisitions costs
what a display of one costs — one image per signal, whatever the plot's trace slots, and a very
wide window does not make it grow — and there is no count to choose. <em>And it fades</em>, in
one of two ways — <gui>fades by</gui> picks them:</p>
<list>
<item><p><gui>acquisitions</gui>: every acquisition keeps
<gui>per acquisition</gui> of what was there before it (0.5, the default, is the schedule a
+2 -2
View File
@@ -102,8 +102,8 @@ struct LayoutState {
/// for: it is what the display does with the acquisitions, and there is no second switch.
///
/// This is the **multiple event** display -- N retained acquisitions, drawn as N traces. The
/// phosphor density beside it is a different display with a different cost (one screen-sized
/// image against N window copies), which is why it has its own four fields below and why this
/// phosphor density beside it is a different display with a different cost (one bounded image a
/// trace against N window copies), which is why it has its own four fields below and why this
/// one keeps the key it has always had: an older file's `trigger_persistence` is still the
/// multiple-event depth.
int persistence = 1;
+2 -1
View File
@@ -55,7 +55,8 @@ struct TriggerConfig {
/// The **phosphor density** (the plotter's `Plot::set_density()`): off by default, and trigger
/// mode only. Where `persist_depth` keeps N whole acquisitions as N traces, this accumulates
/// every acquisition into one screen-sized intensity image -- brightness is how often a trace
/// every acquisition into one intensity image **per trace** (the axes rect in device pixels,
/// bounded by the plotter's `kDensityCellCap`) -- brightness is how often a trace
/// passed a cell, so the cost of a display of a thousand acquisitions is the cost of one, and
/// there is no depth to get wrong. The image is drawn behind the traces, it is unpickable and it
/// reserves no layout space, exactly as the invalid-span bands do.
+1 -1
View File
@@ -217,7 +217,7 @@
Both displays live here and they are two different things: *multiple events* keeps the
last N acquisitions as N traces, dimmer with age, while *persistence* accumulates every
acquisition into an image (one screen-sized grid per trace, in that trace's own colour,
acquisition into an image (one grid per trace, in that trace's own colour,
the plotter's `Plot::set_density()`), drawn in place of the traces. The first is a
count and the second is a decay, which is why neither is a mode of the other. -->
<object class="GtkExpander" id="advanced_exp">
+39 -19
View File
@@ -40,26 +40,46 @@ int on_render_probe(GtkGLArea*, GdkGLContext*, gpointer user) {
if (s.graded_traces > 0) ++app->dbg_graded_frames;
if (s.raw_traces > 0 || s.lod_traces > 0) check_frame_ink(*app);
if (s.raw_traces == 0 && s.lod_traces == 0) {
++app->dbg_notrace_frames;
app->dbg_notrace_last = app->dbg_paint_n;
if (app->dbg_ever_bound) {
++app->dbg_notrace_after_bound;
// What the frame was asked to draw, beside what there was to draw it from: an axis
// that left the data and a trace that had nothing bound are the same picture and
// different bugs, so both are named here rather than inferred from the count.
if (app->dbg_notrace_after_bound <= 40) {
const RowSet::Row& r = app->rows->row(0);
const plot::Range ax = r.plot != nullptr ? r.plot->x_range(r.axis_x) : plot::Range{};
std::fprintf(stderr, "[notrace] paint %d mode=%s axis=[%.6f..%.6f] segments=%u",
app->dbg_paint_n, display_mode_name(app->dbg_display), ax.min, ax.max,
s.staged_segments);
for (int sl = 0; sl < RowSet::kSlotsPerRow; ++sl) {
if (r.channel[static_cast<std::size_t>(sl)] < 0) continue;
const plot::Range b = r.slots[static_cast<std::size_t>(sl)].x_bounds();
std::fprintf(stderr, " slot%d n=%zu x=[%.6f..%.6f]", sl,
r.slots[static_cast<std::size_t>(sl)].size(), b.min, b.max);
// **A frame the density is drawing is not a traceless one.** `Renderer::draw_traces()`
// returns while a plot's phosphor density is on -- the trace ink *is* the image -- so every
// frame of a persistence display stages no trace and would be counted here, which inflates
// the dump's reading and (with `UDPSCOPE_PROBE_NOTRACE` set below) prints a line per frame.
// That is the display working, not "what the frame was asked to draw" with nothing to draw
// it from. Read off the plots, because the frame's own stats can only say no trace was
// drawn and not why.
bool density_on = false;
for (int i = 0; i < app->rows->row_count() && !density_on; ++i) {
const plot::Plot* p = app->rows->row(i).plot;
density_on = p != nullptr && p->density().enabled;
}
if (!density_on) {
++app->dbg_notrace_frames;
app->dbg_notrace_last = app->dbg_paint_n;
if (app->dbg_ever_bound) {
++app->dbg_notrace_after_bound;
// What the frame was asked to draw, beside what there was to draw it from: an axis
// that left the data and a trace that had nothing bound are the same picture and
// different bugs, so both are named here rather than inferred from the count.
//
// **A per-paint line, so it is behind a variable** (`UDPSCOPE_PROBE_NOTRACE`), unlike
// the counters beside it: a state that draws nothing lasts as long as it lasts -- a
// `Waiting` tail, a bound row with no data -- and the line would be one per frame the
// whole time. The count in the dump is the reading; this is the detail behind it.
static const bool notrace_print = g_getenv("UDPSCOPE_PROBE_NOTRACE") != nullptr;
if (notrace_print && app->dbg_notrace_after_bound <= 40) {
const RowSet::Row& r = app->rows->row(0);
const plot::Range ax = r.plot != nullptr ? r.plot->x_range(r.axis_x) : plot::Range{};
std::fprintf(stderr, "[notrace] paint %d mode=%s axis=[%.6f..%.6f] segments=%u",
app->dbg_paint_n, display_mode_name(app->dbg_display), ax.min, ax.max,
s.staged_segments);
for (int sl = 0; sl < RowSet::kSlotsPerRow; ++sl) {
if (r.channel[static_cast<std::size_t>(sl)] < 0) continue;
const plot::Range b = r.slots[static_cast<std::size_t>(sl)].x_bounds();
std::fprintf(stderr, " slot%d n=%zu x=[%.6f..%.6f]", sl,
r.slots[static_cast<std::size_t>(sl)].size(), b.min, b.max);
}
std::fprintf(stderr, "\n");
}
std::fprintf(stderr, "\n");
}
}
}
+2 -2
View File
@@ -756,7 +756,7 @@ GtkWidget* build_trigger_panel(App& app) {
"acquisitions are also bounded by the buffer's own size");
app.trigger_persist_row = row_for(ui, "persistence_row", app.trigger_persist_spin);
// The phosphor *density*: one screen-sized image per trace, in that trace's own colour, every
// The phosphor *density*: one bounded image per trace, in that trace's own colour, every
// acquisition accumulated into it, drawn *in place of* the traces and fading by a time constant
// or by a per-acquisition factor -- the other display of the same acquisitions, and nothing like
// the multiple-event one above (an image per signal against N retained traces, and no count to
@@ -772,7 +772,7 @@ GtkWidget* build_trigger_panel(App& app) {
}
gtk_widget_set_tooltip_text(
app.trigger_density_switch,
"accumulate every triggered acquisition into a screen-sized image per signal, in that "
"accumulate every triggered acquisition into an image per signal, in that "
"signal's own colour, drawn in place of the traces: brightness is how often a trace passed, "
"so an image of a thousand acquisitions costs what one costs. Trigger mode only, and cleared "
"whenever the display leaves it");