From da826f2cb57e5015c162112175fd5b87fc0e0690 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Fri, 16 Oct 2026 20:21:57 +0200 Subject: [PATCH] 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. --- README.md | 26 +++++--- deps/plotter | 2 +- docs/claude/invariants-trigger-and-pinning.md | 56 ++++++++++------- docs/claude/probe-readings.md | 62 +++++++++++-------- docs/claude/verification.md | 10 +-- help/C/org.udpscope.app/trigger.page | 19 +++--- src/ui.cpp | 58 +++++++++-------- src/ui/trigger-panel.ui | 8 +-- src/ui_app.hpp | 1 + src/ui_probe.cpp | 51 ++++++++------- src/ui_trigger.cpp | 18 +++--- 11 files changed, 181 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index fea3c6e..418ceff 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/deps/plotter b/deps/plotter index 7382052..1b8b55d 160000 --- a/deps/plotter +++ b/deps/plotter @@ -1 +1 @@ -Subproject commit 738205293eca6e77d6876dbd0ad2df1a42f26791 +Subproject commit 1b8b55d3ee37d13a9d8533784a343ac1755b4153 diff --git a/docs/claude/invariants-trigger-and-pinning.md b/docs/claude/invariants-trigger-and-pinning.md index 1c8d7bb..e96c784 100644 --- a/docs/claude/invariants-trigger-and-pinning.md +++ b/docs/claude/invariants-trigger-and-pinning.md @@ -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 diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 1733be8..1c0dc45 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -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`: diff --git a/docs/claude/verification.md b/docs/claude/verification.md index b7d4ec4..823b23b 100644 --- a/docs/claude/verification.md +++ b/docs/claude/verification.md @@ -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 diff --git a/help/C/org.udpscope.app/trigger.page b/help/C/org.udpscope.app/trigger.page index 5f85d2b..7d2cd1a 100644 --- a/help/C/org.udpscope.app/trigger.page +++ b/help/C/org.udpscope.app/trigger.page @@ -123,16 +123,19 @@
Persistence: how often the trace has been here

persistence — the switch in the trigger panel's Advanced block — keeps - one image 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.

+ an image 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 is 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.

Two things make it different from multiple events beside it, and the panel offers - both because they answer different questions. It never runs out of room: 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. And it fades, in one of two ways — - fades by picks them:

+ both because they answer different questions. It never runs out of room: 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. And it fades, in one of two ways — fades by picks them:

acquisitions: every acquisition keeps per acquisition of what was there before it (0.5, the default, is the schedule a diff --git a/src/ui.cpp b/src/ui.cpp index 0f85ce4..efd928e 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -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(tc.density_tau_s); cfg.per_acquisition = static_cast(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(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(p->density_grid().cell_count()); - ink += p->density_grid().ink(); - peak = std::max(peak, static_cast(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(g.cell_count()); + ink += g.ink(); + peak = std::max(peak, static_cast(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; diff --git a/src/ui/trigger-panel.ui b/src/ui/trigger-panel.ui index 22ec6a9..a850a16 100644 --- a/src/ui/trigger-panel.ui +++ b/src/ui/trigger-panel.ui @@ -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. --> - 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 + 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