feat: a phosphor density is the trace, one grid per trace in its own colour

A user looked at the display and reported three things at once: "there is clearly a gain offset
between latest acquisition and persistent image", "persistent instead of being brighter + opaque is
just gray/white", and "last acquisition does not seem to be affected, it looks like persistent image
+ last acquisition (so for example last acquisition never fade in time mode)". What they add up to is
that this was not a phosphor yet: it was a **density with a solid trace drawn over it**.

Four changes, and the first is the one the others follow from:

- **The current acquisition is deposited like every other, and no solid trace is drawn over the
  image.** `Renderer::draw_traces()` returns at once while a density is on: the trace ink *is* the
  image, so the newest acquisition is a sweep inside it and fades with it. Drawing the polyline as
  well 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.
- **One grid per trace, in that trace's own colour.** `Plot::density_grid(trace)` is one screen-sized
  `f32` grid per trace, deposited from that trace and drawn in `Trace::color`, because a density that
  had one hue for the plot could not say *which* trace put the ink there -- two signals agreeing in a
  cell would be one blended pixel. The display's memory is therefore `traces * columns * rows * 4`
  bytes rather than one plot's worth (a trace with no series, an unbound slot, holds no grid), which
  is the one cost this change adds: four signals in one 800x480 row are four grids, 6 MB.
- **The ceiling is the trace's own brightness, not the fade's steady state.** `full_scale` now
  defaults to **one sweep**: a cell with one acquisition's ink is drawn in its trace's colour at that
  colour's own alpha, and a signal crossing the same place every acquisition settles *at* the colour
  its trace would have drawn (the ceiling clamps) rather than a fraction of a brighter steady state --
  the gain offset the user reported. `Style::density`'s near-white 0.85 tint is gone with it, because
  there is no one tint any more. A caller that wants the fade's ramp on screen instead still sets
  `full_scale` to the sum the fade settles at and accepts the offset; that is a legitimate choice, just
  not the one a phosphor makes.

The reading is a **framebuffer** one, because neither `ink`/`peak` nor a tint can make a colour or a
fade: `a_phosphor_density_is_the_trace_in_its_own_colour` deposits one sweep, checks the saturated
cell is *exactly* the trace's colour (`full=224`, the trace's `0xE0` green, against `bg=25` -- the old
monochrome tint read ~190 red), then fades it one time constant and checks the same pixel is dimmer
(`faded=115`) and still the trace's hue -- which a stroke over the density could not do. It moves the
trace and draws without depositing to prove no polyline is drawn (`moved` equals the no-density frame
at the moved row), and reads a steady state against a quarter of it (`steady=224`, `quarter=138`), in
the signal's colour throughout. `a_density_holds_one_grid_per_trace` reads two traces' grids apart
(`267 472 cells each, ink 584/584`, in two rows). The grid's own cases are unchanged where they should
be (`a_density_halves_per_acquisition_and_keeps_the_newest_brightest` `peak_steady=2.000`,
`a_density_fades_by_its_time_constant` `exp(-dt/tau)`).

Suite: **397 cases, 0 failed**, 4/4 ctest entries.
This commit is contained in:
Martino Ferrari committed 2026-10-16 20:21:38 +02:00
1 parent 738205293e
commit 1b8b55d3ee
12 files changed
+534 -299

No files matched your search

+8 -6
View File
@@ -59,12 +59,14 @@ run touched a GPU, so read it as an upper bound and a record of the software pat
(`FrameStats::slope_traces`), so the look does not change as a window narrows past the
threshold;
- a **phosphor density** a host can turn on for a plot (`Plot::set_density()`, an image rather
than a set of retained traces): one `f32` cell per device pixel of the axes rect, behind the
traces, into which each acquisition is deposited -- 1.0 per cell a trace crosses, so brightness
is how often it passed -- and which fades either by a **time constant** in stream seconds or by
a **per-acquisition factor**. Memory is the screen's (a 640x360 viewport's axes is 179 349
cells, 717 kB) whatever the trigger has done, the deposit walks the axes' columns rather than
the samples, and a frame whose grid did not move costs one draw call
than a set of retained traces): **one grid per trace**, in that trace's own colour, one `f32`
cell per device pixel of the axes rect, drawn *in place of* the traces -- a host may not also
draw them over it -- into which each acquisition is deposited, 1.0 per cell a trace crosses, so
brightness is how often it passed. It fades either by a **time constant** in stream seconds or by
a **per-acquisition factor**, and the newest acquisition is deposited like every other, so it
fades too. Memory is the screen's times the traces on screen (a 640x360 viewport's axes is
179 349 cells per trace, 717 kB), the deposit walks the axes' columns rather than
the samples, and a frame whose grids did not move costs one draw call per trace
(`FrameStats::density_cells`/`density_upload_bytes`); see `docs/data-and-performance.md`;
- sample **markers** when a window is sparse enough that its samples are the shape
(`Style::marker_below_samples`, 50 by default, `marker_px` across, in the trace's own colour);