fix: persistence is the trace's own phosphor, not a density behind a stroke
The plotter now deposits every acquisition -- the newest included -- into one grid per trace and
draws no solid trace over it, each grid in that trace's colour and with a one-sweep ceiling (its own
commit, **1b8b55d**). This side needed almost no change of its own, because the colour was already
the trace's and the trace's colour is `RowSet::signal_color()` -- the invariants' one route -- but the
figures it reads back are now per trace:
- **The ceiling is left at the library's default of one sweep**, no longer derived from the fade.
Deriving it from the steady state is the gain offset the user reported between the latest
acquisition and the persistent image; a signal crossing the same place every acquisition now settles
at the trace's colour, at full strength, and the newest sweep reads as the trace it is a picture of.
- **`cells`/`grid_bytes` are the sum over the row's traces** -- one grid each, a trace with no series
holding none -- and both the probe and the exit dump carry `traces=` beside `rows=`. That is what
makes the per-signal memory figure read: **556 537 cells and 2.2 MB per signal**, at 8904600 bytes
over the four one-signal rows of the probe's run.
The probes, re-read on this build (the `verification.md` command, `--rate 500000 --batch 1000`):
[density-probe] off enabled=0 ... rows=0 traces=0 cells=0 ink=0.0 peak=0.0000
[density-probe] on enabled=1 ... rows=4 traces=4 cells=2226150 ink=226786.0 peak=1.0000
[density-probe] step-acquisitions ... peak=1.5000
[density-probe] time-before ... peak=2.1359
[density-probe] step-time ... ratio=0.367879
[density-probe] frame cost: tick 0.62 -> 6.96 ms (184/23 frames), draw 2.02 -> 6.16 ms (177/18)
[udpscope] density: frames=748 rows=4 traces=4 cells=2226150 grid_bytes=8904600 ink=489400.2 peak=2.2 upload_bytes=2226150
`on` `peak=1.0000`, `step-acquisitions` `peak=1.5000` and `step-time` `ratio=0.367879` are the legs
that were expected, unchanged; the *draw* mean is now **below** the run before the split (2.02 -> 6.16
against 2.09 -> 20.91), because the solid traces are no longer drawn over the image.
Docs updated with it: the trigger manual page (`persistence` is the trace, one image per signal, the
newest included), the README's persistence section, `invariants-trigger-and-pinning.md` (five rules,
the ceiling and the no-stroke rule among them), `verification.md` and `probe-readings.md` (the lines
above), the trigger panel's comment and tooltips, and the plotter submodule.
`cmake --build build --clean-first -j4`: **0 errors, the documented three warnings** (two
`MathNode::source`, one `-Woverlength-strings` at 12352); `make -C build test-ui` **13 ok**;
`make -C build test-help` **green** (15 pages); the self-check **0 `Gtk-WARNING`/`Gtk-CRITICAL`** and
`gtk warnings: 0`, two `buffer allocated:` lines.
This commit is contained in:
1 parent
5e32199597
commit
da826f2cb5
11 files changed
+181
-130
No files matched your search
@@ -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
|
||||
|
||||
@@ -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`:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user