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 174 additions and 123 deletions
Showing only changes of commit da826f2cb5 - Show all commits

No files matched your search

+17 -9
View File
@@ -1269,14 +1269,21 @@ pinned: the axis, the framing and every measurement still describe the event on
`N = 1` retains nothing at all — no copy, no layer — and lowering the depth frees the retained
snapshots there and then, because a trigger that has stopped firing never reaches another publish.
**Persistence: one image, 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) into which every triggered acquisition is deposited and
which is drawn behind the traces. A cell's brightness is how many acquisitions passed through it,
so the sweep on screen is the brightest, the older ones add on top **with no depth to choose and no
memory that grows with them** — the image is 2.2 MB over four 573x412 rows in the probe's run, and
a thousand acquisitions cost the same. `fades by` picks how it decays: `acquisitions` (each
**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**)
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
the last acquisition, and the last one never fades in time mode" turned out to be. A cell's
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
@@ -1285,7 +1292,8 @@ is an image *of* a stream of acquisitions. Like the invalid-span bands it reserv
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).
frame, on llvmpipe — measured before the per-trace split, when the one image was still re-filled
per row).
**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
+33 -23
View File
@@ -376,21 +376,26 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
trigger that has stopped firing never reaches another publish. A depth change is therefore also
how the memory comes back.
- **The phosphor density is one *image*, and that is a different display from the layers above, not a
- **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
every acquisition is deposited into and 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 `columns * rows * 4` bytes, one cell per device pixel of the row's axes (2.2 MB over the
four rows of the probe's run), so a thousand acquisitions cost what one costs. 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.
Four things hold it together, each of which is a rule:
**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
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
(`RowSet::signal_color()`, the same source the trace was built with and never a second one), so a
saturated cell is that signal's colour at full strength and two signals agreeing in a cell are two
quads of two colours rather than a blend.
Five things hold it together, each of which is a rule:
* **It is a trigger-mode display.** `apply_density()` (the tick) states `Held` and `Capturing` as
the states it is on in; `Live`, `Waiting`, `Paused` and a file are not, and turning it off drops
the grid **at once** rather than at an acquisition a quiet trigger never sends (the rule
the grids **at once** rather than at an acquisition a quiet trigger never sends (the rule
`set_persist_depth()` follows). So it goes away with the trigger, and a re-enable starts from an
empty image rather than a stale one at another scale.
* **One deposit per acquisition, one frame *after* the frame that frames it.** `Plot::accumulate_density()`
@@ -403,21 +408,26 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
* **The switch deposits what is already on screen.** `on && !density_was_on` sets the same pending
flag, so a user who turns persistence on while an event is held sees it instead of waiting for a
latch that `single` mode may never send.
* **The brightness ceiling is the fade's steady state, and the ramp is linear in sweeps.** The app
states `DensityConfig::full_scale` from the fade it configured -- `1/(1 - factor)` for the
acquisition fade, the library's default two sweeps for the time one, which has no fixed steady
state (it settles where the rate and the constant meet) -- and leaves `gamma` at 1.0. A ceiling
at *one* sweep is the defect a user reported as "the colour fading is not applied, it looks
white": a single sweep was already full ink, everything above it clamped to the same pixel, and
a display of one sweep and a display of five were the same white. It is also the one half that
the *grid's* own numbers cannot see -- `ink`/`peak` were right while the picture was flat -- so
the assertion is a **framebuffer** one: `a_phosphor_density_is_drawn_under_the_traces` measures a
cell at a quarter of the scale against one at the ceiling and against the background
(`empty=21 full=83 quarter=54`), which is the reading the eye asked for.
* **It is the plot's, so a figure rebuild resets it** -- the grid lives on each `Plot`, and
* **No trace is drawn over it, and the newest acquisition is inside it.** `Renderer::draw_traces()`
returns at once while a density is on: the trace ink *is* the image, so the acquisition just
deposited is a sweep in it and fades with the rest. Drawing the polyline too put a solid,
unfading stroke over its own faint ghost -- the reported "persistent image + last acquisition",
where the last acquisition never faded in time mode -- and the assertion is a **framebuffer** one:
`a_phosphor_density_is_the_trace_in_its_own_colour` reads the same pixel before and after one
time constant and requires it dimmer (`full=224 -> faded=115`, the trace's own `0xE0` green),
which a stroke over the density could not do.
* **The brightness ceiling is the trace's own brightness, and the ramp is linear in sweeps.** The
app leaves `DensityConfig::full_scale` at the library's default of **one sweep** and `gamma` at
1.0, so the acquisition just deposited reads as the trace it is a picture of and a signal crossing
the same place every acquisition settles *at* the trace's colour (the ceiling clamps), rather than
a fraction of a brighter steady state. The old ceiling -- the fade's steady state, `1/(1-factor)`
for the acquisition fade -- is the gain offset a user reported between the latest acquisition and
the persistent image, and the "dimmer by design" headroom that made the ramp visible is reversed
here.
* **It is the plot's, so a figure rebuild resets it** -- the grids live on each `Plot`, and
`rebuild_all()` builds new ones. That is the one state a structural change (a row added, a signal
switched off, a trace recoloured) does not carry: it costs the accumulated image and nothing else,
and the alternative -- keeping the grid in the `RowSet` -- is what would make it not a plot
and the alternative -- keeping the grids in the `RowSet` -- is what would make it not a plot
feature. The layout is unaffected either way: the density reserves no space and nothing on it can
be picked, exactly as `set_invalid_spans()` does.
* **The session carries it.** `trigger_density`, `trigger_density_decay`, `trigger_density_tau` and
+35 -27
View File
@@ -1980,53 +1980,61 @@ 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 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 cells=2226150 ink=226596.0 peak=1.0000 ratio=1.000000
[density-probe] step-acquisitions enabled=1 fade=acquisitions tau=1.000 factor=0.500 rows=4 cells=2226150 ink=262722.0 peak=1.5000 ratio=1.159429
[density-probe] time-before enabled=1 fade=time tau=1.000 factor=0.500 rows=4 cells=2226150 ink=213362.2 peak=1.2182 ratio=1.000000
[density-probe] step-time enabled=1 fade=time tau=1.000 factor=0.500 rows=4 cells=2226150 ink=78491.6 peak=0.4481 ratio=0.367879
[density-probe] frame cost: tick before=0.74 ms (163 frames) after=9.99 ms (12 frames) draw before=2.09 ms (157) after=20.91 ms (7)
[udpscope] density: frames=504 rows=4 cells=2226150 grid_bytes=8904600 ink=632845.4 peak=2.8 upload_bytes=1081350
[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
```
The **phosphor density** (`Plot::set_density()`, the trigger pane's `persistence` switch): one
screen-sized intensity image per row, every acquisition accumulated into it, drawn behind 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 rows the display is on, which is the same source the library's tests read -- and the
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.
- **`off` is the display without it**: `rows=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**: `peak=1.0000` exactly, and `ink=226596` is the cells that sweep covered over
four rows. 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.
- **`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
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.
- **`step-acquisitions`** deposits one more sweep: `peak=1.0000 -> 1.5000`, which is `1 + 0.5 x 1` -- the
panel's factor 0.5 applied to what was there, then the new sweep's own pass on top. `ink` rises from
`226596` to `262722` for the same reason: the old image halved and one sweep landed on it. The ratio is
panel's factor 0.5 applied to what was there, then the new sweep's own pass on top. The ratio is
**not** a constant to expect -- the acquisition on screen can move between the two reads, so what it says
is "the old image halved and a pass landed on it"; the exact relation is the *library's* case
(`a_density_halves_per_acquisition_and_keeps_the_newest_brightest`), where the same frame is deposited
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, and the pair is the reading that says the *panel's* constant reaches the grid.
fade (`peak=2.1359`), 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.74 -> 9.99 ms** and **draw 2.09 -> 20.91 ms**: the per-frame
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.
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.
- **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, over the rows that moved -- and not the last frame's, for the same
reason.
one frame uploaded -- one byte a cell **per trace**, over the grids that moved -- and not the last frame's,
for the same reason.
**Read `peak` as the display's ceiling**: with the default `full_scale` of 1.0 a cell at 1.0 is full ink, so
a steady display under the acquisition fade sits between 1 and 2, and a `peak` well above that (the
per-hit deposit read 189 after two sweeps) is the defect this probe's `on` leg now catches.
**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`
well above that (the per-hit deposit read 189 after two sweeps) is the defect this probe's `on` leg now
catches.
**And `N = 1` is the display without the feature**, which is the same command at
`UDPSCOPE_PROBE_PERSIST_DEPTH=1`:
+6 -4
View File
@@ -115,8 +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 every triggered acquisition is accumulated into, drawn
behind the traces, fading by a per-acquisition factor or by a time constant. It states the display
density**: one screen-sized intensity image **per trace**, in that trace's own colour, 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
trigger on the mock's `square` with one signal per row — the arrangement the persistence probe uses
— and only then switches it on, so the acquisition already on screen is the first image. Four legs,
@@ -131,8 +132,9 @@ precedent, not an exception.
kill $PY
```
**The assertions, in order:** `off` reads `rows=0 cells=0 ink=0.0 peak=0.0000` (there is no grid at
all); `on` reads `rows=4` — every row has its own image — and **`peak=1.0000`**, one sweep's ink
**The assertions, in order:** `off` reads `rows=0 traces=0 cells=0 ink=0.0 peak=0.0000` (there is no
grid at all); `on` reads `rows=4` — every row has its own image, one per trace in its own colour,
so `traces=4` with one signal a row — and **`peak=1.0000`**, one sweep's ink
being one per cell it covers; `step-acquisitions` reads **`peak=1.5000`**, which is the panel's
0.5 applied to the old image plus the new sweep's own pass; and `step-time` reads
**`ratio=0.367879`**, one step of `dt = tau` through the plot's own `decay_density()`. A `peak` in
+11 -8
View File
@@ -123,16 +123,19 @@
<section id="persistence">
<title>Persistence: how often the trace has been here</title>
<p><gui>persistence</gui> — the switch in the trigger panel's <gui>Advanced</gui> block — keeps
one <em>image</em> of the last acquisitions instead of their traces. Every acquisition is
accumulated into it, so a cell's brightness means how many of them passed through it: the
sweep on screen is the brightest, the ones behind it add on top, and where several agree the
ink is brighter still. It is off by default.</p>
an <em>image</em> of the last acquisitions instead of their traces. Every acquisition is
accumulated into it, the one on screen included, so a cell's brightness means how many of them
passed through it: the sweep on screen is the brightest, the ones behind it add on top, and
where several agree the ink is brighter still. There is no stroke drawn over the image — the
acquisition on screen <em>is</em> its newest ink — so it fades with the rest in both modes. Each
signal is kept as its own image in that signal's own colour, so where two signals cross the same
place you see both, not one blended line. It is off by default.</p>
<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>: the image is
the size of the plot on screen, so a display of a thousand acquisitions costs what a display of
one costs, and there is no count to choose. <em>And it fades</em>, in one of two ways —
<gui>fades by</gui> picks them:</p>
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>
<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
+33 -25
View File
@@ -3384,6 +3384,7 @@ void apply_density(App& app, DisplayMode display, bool live_display) {
app.density_was_on = on;
int rows_on = 0;
int traces_on = 0;
unsigned long long cells = 0;
double ink = 0.0;
double peak = 0.0;
@@ -3396,18 +3397,14 @@ void apply_density(App& app, DisplayMode display, bool live_display) {
: plot::DensityDecay::PerAcquisition;
cfg.time_constant_s = static_cast<float>(tc.density_tau_s);
cfg.per_acquisition = static_cast<float>(tc.density_factor);
// The brightness ceiling: the ink this fade settles at, so the newest acquisition is a
// *fraction* of full ink and the display reaches white only where the acquisitions agree. For a
// per-acquisition fade that is the geometric sum `1/(1-f)` exactly. A time fade has no fixed
// one -- it settles where the trigger's rate and the constant meet, which is the point of it --
// so it keeps the library's default of two sweeps, and a fast trigger then reads brighter than
// a slow one the way a phosphor's does. The gamma is left at the library's 1.0: the cell is a
// count of sweeps, so half as many sweeps must read as half the ink. (With the ceiling at one
// sweep -- the library's first default -- one sweep was already full white and the fade was
// invisible, which is exactly what a user reported seeing.)
cfg.full_scale = tc.density_decay == PersistDecay::Time
? 2.0f
: static_cast<float>(1.0 / std::max(1.0 - tc.density_factor, 0.05));
// The brightness ceiling is left at the library's default of **one sweep** -- the trace's own
// brightness -- so the acquisition just deposited reads as the trace it is a picture of and a
// signal crossing the same place every acquisition settles at the trace's colour rather than
// brighter. The fade's steady state is deliberately *not* the ceiling: it put a gain offset
// between the newest sweep and the image behind it (the newest at a fraction of the settled
// brightness), which is what a user reported as a gain offset between the latest acquisition
// and the persistent image, and the "dimmer by design" headroom that made the ramp visible is
// the wrong call at this brightness scale -- reversed here.
// The plot drops what it held when this turns the display off, so the step below only ever
// speaks about an image that is on screen.
p->set_density(cfg);
@@ -3415,16 +3412,26 @@ void apply_density(App& app, DisplayMode display, bool live_display) {
++rows_on;
// Time first, then the deposit: under `PerAcquisition` the decay is the accumulate path's own,
// so this is a no-op there; under `TimeConstant` it is this frame's fade and the deposit lands
// on top of it at full ink.
// on top of it at full ink. Either way the *newest* acquisition is inside the image, so it
// fades with the rest -- the renderer draws no solid trace over the density.
p->decay_density(dt);
if (deposit) p->accumulate_density();
cells += static_cast<unsigned long long>(p->density_grid().cell_count());
ink += p->density_grid().ink();
peak = std::max(peak, static_cast<double>(p->density_grid().peak()));
// One grid per trace, each in its own signal's colour, so the row's figures are the **sum over
// its traces** and `cells` scales with the signal count as well as the row count. `peak` is the
// brightest of them, which is the reading that says whether any one trace is saturating.
for (plot::u32 t = 0; t < p->trace_count(); ++t) {
const plot::DensityGrid& g = p->density_grid(t);
if (g.cell_count() == 0u) continue; // an unbound slot holds no grid
++traces_on;
cells += static_cast<unsigned long long>(g.cell_count());
ink += g.ink();
peak = std::max(peak, static_cast<double>(g.peak()));
}
}
app.density_pending = false;
if (rows_on == 0) return; // nothing was on this frame: the dump's figures stay the last real ones
app.dbg_density_rows = rows_on;
app.dbg_density_traces = traces_on;
app.dbg_density_cells = cells;
app.dbg_density_grid_bytes = cells * sizeof(plot::f32);
app.dbg_density_ink = ink;
@@ -6804,15 +6811,16 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
{
// The phosphor density, as of the last tick it was **on** (the exit probe switches the
// trigger off before this runs, so the plots hold nothing by now -- see `App::dbg_density_*`).
// `frames` is how many ticks drew an image, `cells`/`grid_bytes` its resolution and memory
// over the rows it was on, `ink`/`peak` the numbers the plot's own readers report, and
// `upload_bytes` the most any one frame uploaded -- one byte a cell, on the frames whose grid
// moved; a frame whose grid did not move uploads nothing at all.
std::printf("[udpscope] density: frames=%d rows=%d cells=%llu grid_bytes=%llu ink=%.1f "
"peak=%.1f upload_bytes=%lld\n",
app.dbg_density_frames, app.dbg_density_rows, app.dbg_density_cells,
app.dbg_density_grid_bytes, app.dbg_density_ink, app.dbg_density_peak,
app.dbg_density_upload_bytes);
// `frames` is how many ticks drew an image; `rows`/`traces` how many plots and how many of
// their traces held a grid (one per trace, in the trace's own colour); `cells`/`grid_bytes`
// its resolution and memory over them, `ink`/`peak` the numbers the plot's own readers
// report, and `upload_bytes` the most any one frame uploaded -- one byte a cell, on the frames
// whose grids moved; a frame whose grids did not move uploads nothing at all.
std::printf("[udpscope] density: frames=%d rows=%d traces=%d cells=%llu grid_bytes=%llu "
"ink=%.1f peak=%.1f upload_bytes=%lld\n",
app.dbg_density_frames, app.dbg_density_rows, app.dbg_density_traces,
app.dbg_density_cells, app.dbg_density_grid_bytes, app.dbg_density_ink,
app.dbg_density_peak, app.dbg_density_upload_bytes);
}
}
return result;
+4 -4
View File
@@ -217,11 +217,11 @@
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 one screen-sized image (the plotter's `Plot::set_density()`). The
first is a count and the second is a decay, which is why neither is a mode of the
other. -->
acquisition into an image (one screen-sized 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">
<property name="tooltip-text" translatable="yes">what the trigger mode does with the acquisitions it takes: the last few kept as dimmer traces, and a persistence image every acquisition is accumulated into</property>
<property name="tooltip-text" translatable="yes">what the trigger mode does with the acquisitions it takes: the last few kept as dimmer traces, and a persistence image (one per signal, in its own colour) every acquisition is accumulated into</property>
<child type="label">
<!-- A box rather than a plain label, for the search block's reason: the contextual
help's "(i)" joins its last child and a hint hung on the expander's own *child* is
+1
View File
@@ -291,6 +291,7 @@ struct App {
/// off, which turns the display off with it -- a dump that asked the plots would say `rows=0`
/// about a run whose whole tail had the image up.
int dbg_density_rows = 0;
int dbg_density_traces = 0;
unsigned long long dbg_density_cells = 0;
unsigned long long dbg_density_grid_bytes = 0;
double dbg_density_ink = 0.0;
+23 -14
View File
@@ -6102,10 +6102,12 @@ double density_probe_tau() {
return s > 0.0 ? std::min(s, 3600.0) : 1.0;
}
/// What the density holds over every row it is on: the rows, the cells, and the two numbers the
/// plot reports. A row with the display off contributes nothing -- it has no grid at all.
/// What the density holds over every row it is on: the rows, the traces (one grid each), the cells,
/// and the two numbers the plot reports. A row with the display off contributes nothing -- it has no
/// grid at all.
struct DensityReading {
int rows = 0;
int traces = 0;
unsigned long long cells = 0;
double ink = 0.0;
double peak = 0.0;
@@ -6116,12 +6118,18 @@ DensityReading density_read(const App& a) {
for (int i = 0; i < a.rows->row_count(); ++i) {
const RowSet::Row& row = a.rows->row(i);
if (row.plot == nullptr || !row.plot->density().enabled) continue;
const plot::DensityGrid& g = row.plot->density_grid();
++r.rows;
// **One grid per trace**, each in its trace's own colour, so the row's cells/ink are the sum
// over its traces and the peak the brightest of them.
for (plot::u32 t = 0; t < row.plot->trace_count(); ++t) {
const plot::DensityGrid& g = row.plot->density_grid(t);
if (g.cell_count() == 0u) continue; // an unbound slot holds no grid
++r.traces;
r.cells += static_cast<unsigned long long>(g.cell_count());
r.ink += g.ink();
r.peak = std::max(r.peak, static_cast<double>(g.peak()));
}
}
return r;
}
@@ -6129,12 +6137,18 @@ DensityReading density_read(const App& a) {
/// plotter's own accumulation (its per-acquisition fade happens inside it, before the deposit), and
/// the time step is `decay_density(tau)` -- the same call the frame tick makes with a frame's `dt`.
double density_step(App& a, bool by_time) {
double before = 0.0;
// The ink summed over every trace's grid (one grid per trace): the same total `density_read()`
// reports, so a step's ratio is off one reading.
const auto ink = [&]() {
double total = 0.0;
for (int i = 0; i < a.rows->row_count(); ++i) {
const RowSet::Row& row = a.rows->row(i);
if (row.plot == nullptr || !row.plot->density().enabled) continue;
before += row.plot->density_grid().ink();
for (plot::u32 t = 0; t < row.plot->trace_count(); ++t) total += row.plot->density_grid(t).ink();
}
return total;
};
const double before = ink();
for (int i = 0; i < a.rows->row_count(); ++i) {
plot::Plot* p = a.rows->row(i).plot;
if (p == nullptr || !p->density().enabled) continue;
@@ -6144,12 +6158,7 @@ double density_step(App& a, bool by_time) {
p->accumulate_density();
}
}
double after = 0.0;
for (int i = 0; i < a.rows->row_count(); ++i) {
const RowSet::Row& row = a.rows->row(i);
if (row.plot == nullptr || !row.plot->density().enabled) continue;
after += row.plot->density_grid().ink();
}
const double after = ink();
return before > 0.0 ? after / before : 0.0;
}
@@ -6180,11 +6189,11 @@ void density_probe_report(App& a, const char* what, double ratio) {
tc = a.model->trigger();
}
std::fprintf(stderr,
"[density-probe] %s enabled=%d fade=%s tau=%.3f factor=%.3f rows=%d cells=%llu "
"ink=%.1f peak=%.4f ratio=%.6f\n",
"[density-probe] %s enabled=%d fade=%s tau=%.3f factor=%.3f rows=%d traces=%d "
"cells=%llu ink=%.1f peak=%.4f ratio=%.6f\n",
what, tc.density ? 1 : 0,
tc.density_decay == PersistDecay::Time ? "time" : "acquisitions", tc.density_tau_s,
tc.density_factor, r.rows, r.cells, r.ink, r.peak, ratio);
tc.density_factor, r.rows, r.traces, r.cells, r.ink, r.peak, ratio);
std::fflush(stderr);
}
+10 -8
View File
@@ -756,11 +756,12 @@ 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 every acquisition is accumulated into, drawn
// behind 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 (N traces
// against one image, and no count to choose). It applies in trigger mode only, which the body
// already is: the whole block goes insensitive with the trigger off, the panel's own rule.
// The phosphor *density*: one screen-sized 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
// choose). It applies in trigger mode only, which the body already is: the whole block goes
// insensitive with the trigger off, the panel's own rule.
app.trigger_density_switch = gtk_switch_new();
gtk_widget_set_valign(app.trigger_density_switch, GTK_ALIGN_CENTER);
gtk_widget_set_halign(app.trigger_density_switch, GTK_ALIGN_END);
@@ -771,9 +772,10 @@ GtkWidget* build_trigger_panel(App& app) {
}
gtk_widget_set_tooltip_text(
app.trigger_density_switch,
"accumulate every triggered acquisition into one screen-sized image, drawn behind 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");
"accumulate every triggered acquisition into a screen-sized 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");
g_signal_connect(app.trigger_density_switch, "notify::active",
G_CALLBACK(on_density_toggled), &app);
// Not `row_for()`: it expands its child, and an expanded switch is a wide pill that reads as a