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

@@ -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