fix: bound the density grid, and make [notrace] a reading instead of a flood

The plotter caps a density grid at kDensityCellCap now (deps/plotter), so a 2 s
window with persistence on no longer scales the image with the window: the
probe's four one-signal rows read 1 044 714 cells / 4 178 856 bytes where they
read 2 226 150 / 8 904 600, and a moved frame uploads 1 044 714 bytes where it
uploaded 2 226 150. The probe's own legs are unchanged (on peak=1.0000,
step-acquisitions peak=1.5000, step-time ratio=0.367879).

The paint probe no longer counts a frame the density is drawing as *traceless*.
`Renderer::draw_traces()` returns while the ink is the image, so every frame of
a persistence display stages no trace: the dump read `traceless paints: 653
(637 after the first bind)` for a run whose real figure is 4 (0), and the
`[notrace]` line printed for each of those frames. It is now behind
UDPSCOPE_PROBE_NOTRACE -- a per-paint line is a log flood and not a reading --
while the counters it details stay in every build, and the line stays reachable
for the state it was written for (a bound row that draws nothing).

Docs corrected where they stated the old rule: the README, the trigger
invariants, the density probe's readings, the verification list (the new
variable) and the manual page behind the panel's tooltip.
This commit is contained in:
Martino Ferrari committed 2026-10-16 21:09:11 +02:00
1 parent da826f2cb5
commit ac054a30c9
11 files changed
+131 -76

No files matched your search

+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`