diff --git a/README.md b/README.md index 418ceff..50c2e46 100644 --- a/README.md +++ b/README.md @@ -1271,8 +1271,10 @@ snapshots there and then, because a trigger that has stopped firing never reache **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**) +turns on a **phosphor density** — the plotter's intensity image (`Plot::set_density()`, the axes rect +in device pixels **per trace, in that trace's own signal colour**, bounded by the plotter's +`kDensityCellCap`: past **262 144 cells** — a mebibyte of counts — it scales the rect down uniformly +and magnifies the image back over it with a linear filter) 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 @@ -1280,20 +1282,24 @@ the last acquisition, and the last one never fades in time mode" turned out to b 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 -on screen — and it goes away with the trigger, on a pause and on an opened file, because the image -is an image *of* a stream of acquisitions. Like the invalid-span bands it reserves no layout space -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 — measured before the per-trace split, when the one image was still re-filled -per row). +with the acquisitions**: the cost is bounded, and the bound is the grid's — at most a mebibyte of +counts a signal, so the app's four signals hold 4 MiB between them however wide the window, and the +probe's four one-signal rows read `cells=1044714` (4 178 856 bytes, 1 044 714 of them uploaded on a +moved frame) — 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 on screen — and it goes away with the trigger, on a pause and on an opened file, +because the image is an image *of* a stream of acquisitions. Like the invalid-span bands it reserves +no layout space 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 there and in the plotter's +`docs/data-and-performance.md`, where the density's own pass is priced headless at a fixed geometry — +the app's in-run A/B of it is a mean against a mean and the "before" side includes the run's own +startup frames. A 3040x1700 framebuffer with four rows of one trace and a 2 s window (the frozen +configuration: at 2x device scale that is a 1520x850 window) read **4.5 M cells a trace, 12.9 ms of +tick and 12.8 ms of draw a frame and 4.9 MB uploaded** on llvmpipe, and reads **1.0 M cells, 3.9 / +5.6 ms and 1.0 MB** at the cap. **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 1b8b55d..f79a97b 160000 --- a/deps/plotter +++ b/deps/plotter @@ -1 +1 @@ -Subproject commit 1b8b55d3ee37d13a9d8533784a343ac1755b4153 +Subproject commit f79a97be9bd1faa604991c6371b07d74a32302bf diff --git a/docs/claude/invariants-trigger-and-pinning.md b/docs/claude/invariants-trigger-and-pinning.md index e96c784..c56527e 100644 --- a/docs/claude/invariants-trigger-and-pinning.md +++ b/docs/claude/invariants-trigger-and-pinning.md @@ -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 diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 1c0dc45..777032d 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -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` diff --git a/docs/claude/verification.md b/docs/claude/verification.md index 823b23b..e91556e 100644 --- a/docs/claude/verification.md +++ b/docs/claude/verification.md @@ -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 diff --git a/help/C/org.udpscope.app/trigger.page b/help/C/org.udpscope.app/trigger.page index 7d2cd1a..19f89b1 100644 --- a/help/C/org.udpscope.app/trigger.page +++ b/help/C/org.udpscope.app/trigger.page @@ -133,9 +133,10 @@
Two things make it different from