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:
1 parent
738205293e
commit
1b8b55d3ee
12 files changed
+534
-299
No files matched your search
+53
-39
@@ -590,13 +590,16 @@ class Plot {
|
||||
/// default -- is the plot exactly as it was: no grid is allocated, no geometry is deposited
|
||||
/// and no frame uploads anything.
|
||||
///
|
||||
/// On, the plot keeps a **screen-sized intensity grid** behind its traces (see `DensityGrid`
|
||||
/// for the resolution, the per-cell meaning and the memory cost) and deposits the acquisition
|
||||
/// on screen into it with `accumulate_density()`: brightness means how often a trace passed,
|
||||
/// monochrome, and every cell fades as the display's own clock moves on. It is deliberately
|
||||
/// *not* a set of retained traces -- the library has the trace geometry in hand, so a density
|
||||
/// costs one grid whatever the trigger has done, where retaining the acquisitions would cost
|
||||
/// a window copy each.
|
||||
/// On, the plot keeps a **screen-sized intensity grid behind each of its traces**, in that
|
||||
/// trace's own colour (see `DensityGrid` for the resolution and per-cell meaning, and
|
||||
/// `DensityConfig` for the per-trace colour and the memory the grids cost), and deposits the
|
||||
/// acquisition on screen into it with `accumulate_density()`. While a density is on the
|
||||
/// renderer draws **no solid trace** over it: the trace ink *is* the image, so the newest
|
||||
/// acquisition is deposited like every other and fades with the rest -- a trace left drawn on
|
||||
/// top of its own density could never fade, which is what a report of "the last acquisition
|
||||
/// never fades" was. It is deliberately *not* a set of retained traces -- the library has the
|
||||
/// trace geometry in hand, so a density costs one grid per trace whatever the trigger has done,
|
||||
/// where retaining the acquisitions would cost a window copy each.
|
||||
///
|
||||
/// Like `set_invalid_spans()` it reserves **no layout space** and nothing on it can be picked:
|
||||
/// it is drawn inside the axes it was sized to, and a pointer neither finds it nor is found by
|
||||
@@ -611,18 +614,24 @@ class Plot {
|
||||
void set_density(const DensityConfig& config);
|
||||
/// The configuration, as set.
|
||||
const DensityConfig& density() const noexcept { return density_; }
|
||||
/// The accumulated image, for a host that wants to draw it or read it back. Empty (0x0) until
|
||||
/// the first `accumulate_density()` on a laid-out plot.
|
||||
const DensityGrid& density_grid() const noexcept { return density_grid_; }
|
||||
/// The accumulated image **of a trace**, for a host that wants to draw it or read it back. There
|
||||
/// is one per trace, in trace order, because each is drawn in that trace's own colour (see
|
||||
/// `DensityConfig`); an out-of-range trace reads an empty (0x0) image. Empty until the first
|
||||
/// `accumulate_density()` on a laid-out plot, and dropped by `set_density()` off.
|
||||
const DensityGrid& density_grid(u32 trace) const noexcept {
|
||||
return trace < density_grids_.size() ? density_grids_[trace] : empty_density_grid();
|
||||
}
|
||||
|
||||
/// Deposits **the acquisition on screen** into the grid: every visible trace of this plot,
|
||||
/// over the x window its own axis is showing, 1.0 into every cell it covers -- **once per cell
|
||||
/// per sweep**, however many of that trace's own samples crossed it, so a cell's value is how
|
||||
/// many traces have covered it since the last decay and not how much the beam moved (see
|
||||
/// `DensityGrid`; the dwell question is `Style::intensity_grading`'s). Called once per
|
||||
/// *acquisition*, which is the cadence the grid counts in: a
|
||||
/// caller that deposited every frame would add one sweep sixty times a second, and the
|
||||
/// per-acquisition fade would have nothing to measure itself against.
|
||||
/// Deposits **the acquisition on screen** into each trace's grid: every visible trace of this
|
||||
/// plot, the newest acquisition included, over the x window its own axis is showing, 1.0 into
|
||||
/// every cell it covers -- **once per cell per sweep**, however many of that trace's own samples
|
||||
/// crossed it, so a cell's value is how many traces have covered it since the last decay and not
|
||||
/// how much the beam moved (see `DensityGrid`; the dwell question is
|
||||
/// `Style::intensity_grading`'s). Called once per *acquisition*, which is the cadence the grids
|
||||
/// count in: a caller that deposited every frame would add one sweep sixty times a second, and
|
||||
/// the per-acquisition fade would have nothing to measure itself against. The renderer draws no
|
||||
/// polyline for a trace whose density is on (see `set_density()`), so this deposit is the whole
|
||||
/// of what the newest acquisition looks like -- and it fades with the rest.
|
||||
///
|
||||
/// The geometry is the frame's own. The visible x range is mapped onto sample indices through
|
||||
/// the series' own x (`Series::neighbours()`), never the `dx()` grid, and never the whole
|
||||
@@ -997,23 +1006,26 @@ class Plot {
|
||||
/// columns, with a one-device-pixel floor, and the state that picks its tint. Here and not in
|
||||
/// the renderer for `build_regions()`' reason -- one x mapping, the frame's own.
|
||||
void build_invalid_spans();
|
||||
/// Deposits one trace's visible window into the density grid: the same index window
|
||||
/// Deposits one trace's visible window into its own density grid: the same index window
|
||||
/// `Renderer::draw_traces()` maps -- `neighbours()` for the ends, the 64-spp gate for the LOD
|
||||
/// level -- gathered into `density_points_`, mapped to pixels through the transforms of the
|
||||
/// plot's last layout, and left with `deposit_density_segment()`. A trace with no series, no
|
||||
/// visible window or non-positive `dx()` deposits nothing.
|
||||
void deposit_density_trace(const Trace& trace);
|
||||
/// Deposits one already-mapped segment of a trace polyline: for every grid column the segment
|
||||
/// crosses, the rows it covers in that column, +1 each. The columns it also *clips*: a column
|
||||
/// whose slice of the segment lies entirely outside the axes is skipped, so a trace off the
|
||||
/// top of the plot deposits nothing rather than a band along the edge, and a segment with both
|
||||
/// ends past an edge keeps the part that is inside. A degenerate segment (`x0 == x1`, or a
|
||||
void deposit_density_trace(const Trace& trace, DensityGrid& grid);
|
||||
/// Deposits one already-mapped segment of a trace polyline into `grid`: for every grid column
|
||||
/// the segment crosses, the rows it covers in that column, +1 each. The columns it also *clips*:
|
||||
/// a column whose slice of the segment lies entirely outside the axes is skipped, so a trace off
|
||||
/// the top of the plot deposits nothing rather than a band along the edge, and a segment with
|
||||
/// both ends past an edge keeps the part that is inside. A degenerate segment (`x0 == x1`, or a
|
||||
/// non-finite coordinate) fills the single column it is in.
|
||||
void deposit_density_segment(f32 x0, f32 y0, f32 x1, f32 y1) noexcept;
|
||||
/// Grows `density_grid_` to the last layout's axes rect in device pixels, clearing it when the
|
||||
/// size moved (a *different* screen is a different image), and returns whether the grid is
|
||||
/// usable. The one place the grid is allocated.
|
||||
bool size_density_grid();
|
||||
void deposit_density_segment(f32 x0, f32 y0, f32 x1, f32 y1, DensityGrid& grid) noexcept;
|
||||
/// Grows `density_grids_` -- one grid per trace -- to the last layout's axes rect in device
|
||||
/// pixels, clearing a grid when the size moved (a *different* screen is a different image), and
|
||||
/// returns whether any grid is usable. The one place the grids are allocated.
|
||||
bool size_density_grids();
|
||||
/// The image an out-of-range trace reads (see `density_grid()`): a shared empty grid, so the
|
||||
/// accessor need not hand back a reference into nothing.
|
||||
static const DensityGrid& empty_density_grid() noexcept;
|
||||
/// Exact statistics of `region` over `series`, or an invalid result when the
|
||||
/// interval covers no sample. The one place the region arithmetic lives.
|
||||
static RegionStats evaluate_region_stats(const Region& region, const data::Series& series);
|
||||
@@ -1062,20 +1074,22 @@ class Plot {
|
||||
std::vector<Annotation> annotations_;
|
||||
std::vector<Region> regions_;
|
||||
std::vector<InvalidSpan> invalid_spans_;
|
||||
/// The phosphor density: its configuration, the grid it accumulates into, and the scratch the
|
||||
/// one visible window being deposited is gathered into. The grid is a member and not a local
|
||||
/// because it is the *display's* memory -- it outlives the frames it was drawn in, which is the
|
||||
/// whole point of accumulating into a screen-sized image rather than retaining traces.
|
||||
/// The phosphor density: its configuration, the grids it accumulates into, and the scratch the
|
||||
/// one visible window being deposited is gathered into. **One grid per trace** -- the image is
|
||||
/// screen-sized but each trace's is tinted with that trace's own colour (see `DensityConfig`), so
|
||||
/// the memory is `traces * columns * rows * 4` bytes and not one plot's worth. The vector tracks
|
||||
/// `traces_` and outlives the frames its images were drawn in, which is the whole point of
|
||||
/// accumulating into a screen-sized image rather than retaining traces.
|
||||
DensityConfig density_{};
|
||||
DensityGrid density_grid_{};
|
||||
std::vector<DensityGrid> density_grids_;
|
||||
/// Scratch for `deposit_density_trace()`: interleaved (x, y) of one gathered window, re-used
|
||||
/// across traces and frames so a steady-state accumulation allocates nothing. Bounded by the
|
||||
/// window, not the series, as `Renderer::gather_` is.
|
||||
std::vector<f32> density_points_;
|
||||
/// The per-sweep dedupe's stamps, one per grid **row**: the key of the last (trace, column) to
|
||||
/// deposit at that row, so one acquisition's own samples cannot count a cell twice (see
|
||||
/// `deposit_density_segment()`). One `u64` a row -- a few kB at any window -- and not one per
|
||||
/// cell, which would double the grid's memory for a comparison.
|
||||
/// The per-sweep dedupe's stamps, one per grid **row** and shared by every trace's deposit: the
|
||||
/// key of the last (trace, column) to deposit at that row, so one acquisition's own samples
|
||||
/// cannot count a cell twice (see `deposit_density_segment()`). One `u64` a row -- a few kB at
|
||||
/// any window -- and not one per cell, which would double a grid's memory for a comparison.
|
||||
std::vector<u64> density_stamp_;
|
||||
/// The sweep's own number, bumped once per trace deposited: the high half of every stamp key, so
|
||||
/// every trace of a plot, and every acquisition, deposits exactly once per cell it covers.
|
||||
|
||||
Reference in new issue
Block a user