Compare commits
1
Commits
738205293e
...
1b8b55d3ee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b8b55d3ee |
No files matched your search
@@ -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
|
(`FrameStats::slope_traces`), so the look does not change as a window narrows past the
|
||||||
threshold;
|
threshold;
|
||||||
- a **phosphor density** a host can turn on for a plot (`Plot::set_density()`, an image rather
|
- 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
|
than a set of retained traces): **one grid per trace**, in that trace's own colour, one `f32`
|
||||||
traces, into which each acquisition is deposited -- 1.0 per cell a trace crosses, so brightness
|
cell per device pixel of the axes rect, drawn *in place of* the traces -- a host may not also
|
||||||
is how often it passed -- and which fades either by a **time constant** in stream seconds or by
|
draw them over it -- into which each acquisition is deposited, 1.0 per cell a trace crosses, so
|
||||||
a **per-acquisition factor**. Memory is the screen's (a 640x360 viewport's axes is 179 349
|
brightness is how often it passed. It fades either by a **time constant** in stream seconds or by
|
||||||
cells, 717 kB) whatever the trigger has done, the deposit walks the axes' columns rather than
|
a **per-acquisition factor**, and the newest acquisition is deposited like every other, so it
|
||||||
the samples, and a frame whose grid did not move costs one draw call
|
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`;
|
(`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
|
- 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);
|
(`Style::marker_below_samples`, 50 by default, `marker_px` across, in the trace's own colour);
|
||||||
|
|||||||
@@ -327,27 +327,34 @@ way**, against the 191 the sparse frame gave before. Across the whole sweep from
|
|||||||
samples a pixel the riser holds 71 and the level holds 191, so there is no step at the threshold
|
samples a pixel the riser holds 71 and the level holds 191, so there is no step at the threshold
|
||||||
left to see.
|
left to see.
|
||||||
|
|
||||||
## A phosphor density accumulates into a screen-sized grid
|
## A phosphor density is the trace, one grid per trace
|
||||||
|
|
||||||
`Plot::set_density()` gives a plot a **phosphor density**: an image of how often its traces have
|
`Plot::set_density()` gives a plot a **phosphor density**: an image of how often its traces have
|
||||||
passed, drawn behind them, fading by a time constant or by an acquisition count. It is the
|
passed, drawn *in place of them* -- one grid per trace, in that trace's own colour -- fading by a
|
||||||
library's because the library is the side that knows a trace's device geometry; a host that
|
time constant or by an acquisition count. It is the library's because the library is the side that
|
||||||
built one itself would be re-rasterising traces the renderer has already staged.
|
knows a trace's device geometry; a host that built one itself would be re-rasterising traces the
|
||||||
|
renderer has already staged.
|
||||||
|
|
||||||
The split from the grading above is what it costs: grading measures *one* frame's dwell and
|
The split from the grading above is what it costs: grading measures *one* frame's dwell and
|
||||||
throws it away, while the density **keeps** what it measured — that is the whole point of it, and
|
throws it away, while the density **keeps** what it measured — that is the whole point of it, and
|
||||||
also why it cannot be a per-frame accumulator.
|
also why it cannot be a per-frame accumulator.
|
||||||
|
|
||||||
- **The grid is the screen's, not the data's.** One `f32` cell per device pixel of the axes rect
|
- **The grid is the screen's, not the data's, and there is one per trace.** One `f32` cell per
|
||||||
(`DensityGrid`, `plot/plot/density.hpp`): the axes of a 640x360 viewport is a **573x313** grid
|
device pixel of the axes rect (`DensityGrid`, `plot/plot/density.hpp`): the axes of a 640x360
|
||||||
of **179 349 cells** and **717 396 bytes**, and a thousand acquisitions through it cost the
|
viewport is a **584x458** grid of **267 472 cells** and **1 069 888 bytes**, and a thousand
|
||||||
same. A cell's value is a **number of sweeps**: a trace covering it during one
|
acquisitions through it cost the same. A plot holds one of these for **each trace it draws**, so
|
||||||
|
the display's memory is that figure times the traces on screen — four signals in one 800x480 row
|
||||||
|
are four grids, 6 MB; a row of eight at 1920x1080 is 66 MB. The colour is the reason: a density
|
||||||
|
that had one hue for the plot could not say *which* trace put the ink there, and two signals
|
||||||
|
agreeing in a cell would read as one blended pixel. `Plot::density_grid(trace)` returns a trace's
|
||||||
|
image; a trace with no series (an unbound slot) holds no grid at all.
|
||||||
|
- **A cell's value is a number of sweeps**, and a trace covering it during one
|
||||||
`Plot::accumulate_density()` leaves 1.0, **once however many of its own samples crossed it**.
|
`Plot::accumulate_density()` leaves 1.0, **once however many of its own samples crossed it**.
|
||||||
Counting the hits instead puts a single dense sweep at the ceiling of the brightness scale and
|
Counting the hits instead puts a single dense sweep at the ceiling of the brightness scale and
|
||||||
hides the sweeps behind it, and a cell would then mean "how much the beam moved here" rather
|
hides the sweeps behind it, and a cell would then mean "how much the beam moved here" rather
|
||||||
than "how many acquisitions were here". The dedupe is a stamp per grid *row* (one `u64`, a few
|
than "how many acquisitions were here". The dedupe is a stamp per grid *row* (one `u64`, a few
|
||||||
kB), keyed by (trace, column), so two sweeps agreeing on a cell still count twice and the two
|
kB, shared by every trace's deposit), keyed by (trace, column), so two sweeps agreeing on a cell
|
||||||
segments that share a column boundary do not.
|
still count twice and the two segments that share a column boundary do not.
|
||||||
- **The deposit is one pass per cell, not one per sample.** A trace's visible window is mapped to
|
- **The deposit is one pass per cell, not one per sample.** A trace's visible window is mapped to
|
||||||
sample indices and gathered exactly as `Renderer::draw_traces()` maps it (`neighbours()`, the
|
sample indices and gathered exactly as `Renderer::draw_traces()` maps it (`neighbours()`, the
|
||||||
same 64-spp gate for the LOD level), then walked as a polyline: for each grid column the
|
same 64-spp gate for the LOD level), then walked as a polyline: for each grid column the
|
||||||
@@ -355,38 +362,47 @@ also why it cannot be a per-frame accumulator.
|
|||||||
over hundreds of columns therefore costs *columns*, plus one row of ink per column the trace
|
over hundreds of columns therefore costs *columns*, plus one row of ink per column the trace
|
||||||
crossed — the deposit is bounded by the screen, not by the data, which is what makes an
|
crossed — the deposit is bounded by the screen, not by the data, which is what makes an
|
||||||
accumulation affordable at the trigger's own rate.
|
accumulation affordable at the trigger's own rate.
|
||||||
- **The brightness ceiling is the fade's own steady state, and the ramp is linear in sweeps.**
|
- **The newest acquisition is deposited like every other, and no trace is drawn over the image.**
|
||||||
`DensityConfig::full_scale` defaults to `1/(1 - 0.5)` -- the geometric sum of the default
|
`Renderer::draw_traces()` returns at once while a density is on: the trace ink *is* the image, so
|
||||||
per-acquisition fade -- so one sweep reads half ink and the display reaches white only where
|
the acquisition just drawn is a sweep *inside* it and fades with the rest. Drawing the polyline
|
||||||
acquisitions agree, and `gamma` defaults to 1.0, because a cell's value is a count and half as
|
on top as well would put a solid, unfading stroke over its own faint ghost — the "persistent
|
||||||
many sweeps must read as half the ink. Both defaults were wrong first and the *eye* found it: with
|
image + last acquisition" a user reported, where the newest acquisition never faded.
|
||||||
the ceiling at a single sweep and `gamma` at 0.45 (the value `Style::intensity_gamma` wants for a
|
- **The brightness ceiling is the trace's own brightness, and the ramp is linear in sweeps.**
|
||||||
*dwell fraction*) every cell one sweep touched was already full white, so a display of one sweep
|
`DensityConfig::full_scale` defaults to **one sweep**, so a cell with one acquisition's ink is
|
||||||
and a display of five looked the same. The case that would have caught it is a **framebuffer**
|
drawn in its trace's colour at that colour's own alpha, and a signal crossing the same place
|
||||||
reading, not the grid's numbers -- `a_phosphor_density_is_drawn_under_the_traces` now measures a
|
every acquisition settles *exactly* at the colour its trace would have drawn — the settlement is
|
||||||
cell at a quarter of the scale against one at the ceiling and against the background
|
a clamp, not a gain. `gamma` defaults to 1.0, because a cell's value is a count and half as many
|
||||||
(`empty=16 full=83 quarter=54` at the current defaults, where the old ones read `199` for the
|
sweeps must read as half the ink. A ceiling at the *fade's steady state* (`1/(1-0.5)=2`) was the
|
||||||
first two alike).
|
first default and is a legitimate alternative: it makes the newest sweep a fraction of the
|
||||||
|
settled brightness so the fade's ramp is visible, at the cost of a gain offset between the newest
|
||||||
|
sweep and the image behind it. The case that reads either one off the *screen* rather than the
|
||||||
|
grid is `a_phosphor_density_is_the_trace_in_its_own_colour`.
|
||||||
- **The scale is applied once, on the CPU.** `DensityConfig::full_scale` and `gamma` map a cell's
|
- **The scale is applied once, on the CPU.** `DensityConfig::full_scale` and `gamma` map a cell's
|
||||||
count to a luminance byte when the grid changed, and the shaders (`kDensityVS`/`kDensityFS`)
|
count to a luminance byte when the grid changed, and the shaders (`kDensityVS`/`kDensityFS`)
|
||||||
only tint that byte. The one derivation is deliberate: a value a host reads back through
|
only tint that byte with the trace's colour. The one derivation is deliberate: a value a host
|
||||||
`DensityGrid::ink()`/`peak()` and the picture it sees come from the same numbers.
|
reads back through `DensityGrid::ink()`/`peak()` and the picture it sees come from the same
|
||||||
- **Per frame, it is one texture upload and one draw call -- or one draw call.** The upload is one
|
numbers.
|
||||||
byte per cell (**179 kB** for the axes above); a frame whose grid did not move skips the
|
- **Per frame, it is one texture upload and one draw call *per trace* — or one draw call each.**
|
||||||
conversion and the upload entirely and pays the quad alone (`FrameStats::density_upload_bytes`
|
The upload is one byte per cell (**179 kB** for the axes above); the one texture is re-filled per
|
||||||
reads 0 for it). The decay is what makes the grid move every frame under `TimeConstant`, and
|
trace, and a frame whose grids did not move skips the conversion and the uploads entirely and
|
||||||
what makes it stand still between acquisitions under `PerAcquisition`.
|
pays the quads alone (`FrameStats::density_upload_bytes` reads 0 for it). The decay is what makes
|
||||||
|
the grids move every frame under `TimeConstant`, and what makes them stand still between
|
||||||
|
acquisitions under `PerAcquisition`.
|
||||||
|
|
||||||
`a_density_grid_is_the_axes_in_device_pixels` reads the resolution and the memory back
|
`a_density_grid_is_the_axes_in_device_pixels` reads the resolution and the memory back
|
||||||
(**584x458 = 267 472 cells, 1 069 888 bytes**, `ink=584 peak=1` for one flat sweep — the ink of
|
(**584x458 = 267 472 cells, 1 069 888 bytes**, `ink=584 peak=1` for one flat sweep — the ink of
|
||||||
one sweep *is* the columns it covers),
|
one sweep *is* the columns it covers),
|
||||||
`a_density_halves_per_acquisition_and_keeps_the_newest_brightest` reads the count schedule
|
`a_density_halves_per_acquisition_and_keeps_the_newest_brightest` reads the count schedule
|
||||||
(584 → 876 → 1022 on a 584-column grid, i.e. `1 + 1/2 + 1/4` of the width, `peak_steady=2.000`),
|
(584 → 876 → 1022 on a 584-column grid, i.e. `1 + 1/2 + 1/4` of the width, `peak_steady=2.000`),
|
||||||
`a_density_fades_by_its_time_constant` reads `exp(-dt/tau)` off the ink, and
|
`a_density_fades_by_its_time_constant` reads `exp(-dt/tau)` off the ink,
|
||||||
`a_phosphor_density_is_drawn_under_the_traces` reads the frame: `cells=179349 uploaded=179349`
|
`a_density_holds_one_grid_per_trace` reads two traces' grids apart (**267 472 cells each,
|
||||||
on the frame the grid moved and `uploaded=0` on the identical one after it, with the trace's
|
ink 584/584**, in two different rows), and
|
||||||
stroke keeping its own colour over the ink (`min_red < 40`, where a density drawn on top would
|
`a_phosphor_density_is_the_trace_in_its_own_colour` reads the frame: `cells=179349 uploaded=179349`
|
||||||
put the red at ~190).
|
on the frame the grid moved and `uploaded=0` on the identical one after it; a saturated cell is the
|
||||||
|
trace's own colour exactly (`full=224`, the trace's `0xE0` green, against `bg=25`, where the old
|
||||||
|
near-white tint read ~190 red); one time constant dims it (`faded=115`); and a steady state
|
||||||
|
(`steady=224`) against a quarter of it (`quarter=138`) keeps the trace's hue, red near the
|
||||||
|
background's.
|
||||||
|
|
||||||
## One sample or a statistic of many
|
## One sample or a statistic of many
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ Colours (neither logical nor device — nothing to scale):
|
|||||||
| `measure_fill` | `0xFFB020` at α 0.15 | the measurement band |
|
| `measure_fill` | `0xFFB020` at α 0.15 | the measurement band |
|
||||||
| `invalid_fill` | `0x8A94A6` at α 0.18 | a span the host declared invalid (`Plot::set_invalid_spans()`) |
|
| `invalid_fill` | `0x8A94A6` at α 0.18 | a span the host declared invalid (`Plot::set_invalid_spans()`) |
|
||||||
| `invalid_repaired_fill` | `0x8A94A6` at α 0.08 | the same span once the host has marked it repaired |
|
| `invalid_repaired_fill` | `0x8A94A6` at α 0.08 | the same span once the host has marked it repaired |
|
||||||
| `density` | `0xDFF2FF` at α 0.85 | the tint a plot's phosphor density is drawn in (`Plot::set_density()`); the alpha is the ceiling of the image's brightness scale |
|
|
||||||
| `crosshair` | `0xE6EBF2` | crosshair lines and the readout panel's border (hue-neutral by design; trace markers keep each trace's own colour) |
|
| `crosshair` | `0xE6EBF2` | crosshair lines and the readout panel's border (hue-neutral by design; trace markers keep each trace's own colour) |
|
||||||
|
|
||||||
The scale:
|
The scale:
|
||||||
|
|||||||
@@ -35,6 +35,14 @@ enum class DensityDecay {
|
|||||||
/// A plot's phosphor density: whether the grid is on, how it fades, and how a cell's count is
|
/// A plot's phosphor density: whether the grid is on, how it fades, and how a cell's count is
|
||||||
/// turned into brightness. Plain data, replaced wholesale by `Plot::set_density()`.
|
/// turned into brightness. Plain data, replaced wholesale by `Plot::set_density()`.
|
||||||
///
|
///
|
||||||
|
/// The image is **one grid per trace**, drawn in that trace's own colour (`Trace::color`), because
|
||||||
|
/// a density that had one hue for the whole plot could not say *which* trace put the ink there --
|
||||||
|
/// two signals agreeing in a cell would be one blended pixel, and a display of several traces
|
||||||
|
/// could not be read back to the trace it came from. A cell's brightness is a count of sweeps in
|
||||||
|
/// *that trace's* colour: `full_scale` (default one sweep, the trace's own brightness) is where it
|
||||||
|
/// saturates, and a signal crossing the same place every acquisition settles exactly at the colour
|
||||||
|
/// its trace would have drawn.
|
||||||
|
///
|
||||||
/// Its reason to exist as a *plot* feature rather than a host's overlay is the same one
|
/// Its reason to exist as a *plot* feature rather than a host's overlay is the same one
|
||||||
/// `Plot::set_invalid_spans()` has: the plot is what knows a trace's device geometry -- the
|
/// `Plot::set_invalid_spans()` has: the plot is what knows a trace's device geometry -- the
|
||||||
/// visible window about to be drawn, on the axes rect the layout settled -- so the host would
|
/// visible window about to be drawn, on the axes rect the layout settled -- so the host would
|
||||||
@@ -56,15 +64,21 @@ struct DensityConfig {
|
|||||||
/// what a phosphor's afterglow looks like sweep to sweep: 1, 0.5, 0.25, 0.125 and by the
|
/// what a phosphor's afterglow looks like sweep to sweep: 1, 0.5, 0.25, 0.125 and by the
|
||||||
/// seventh the layer is under 1 %.
|
/// seventh the layer is under 1 %.
|
||||||
f32 per_acquisition = 0.5f;
|
f32 per_acquisition = 0.5f;
|
||||||
/// The cell value that reads as **full brightness**, in accumulated sweeps. The default is the
|
/// The cell value that reads as **full brightness**, in accumulated sweeps -- and the one field
|
||||||
/// ink the **default fade settles at** -- `1/(1 - 0.5)`, the geometric sum of
|
/// a caller reads *off its own trace* rather than off the fade. A cell at this value is drawn in
|
||||||
/// `PerAcquisition`'s factor -- and that is the rule worth keeping: a ceiling at *one* sweep
|
/// its trace's own colour at that colour's own alpha; the default of **one sweep** is therefore
|
||||||
/// puts the newest acquisition at full white and everything above it clamps to the same pixel,
|
/// the ink the newest acquisition leaves, so the acquisition just drawn reads exactly as the
|
||||||
/// so a display of one sweep and a display of five read alike and the fade is invisible
|
/// trace it is a picture of (a signal crossing the same place every acquisition settles at the
|
||||||
/// (measured: a cell at a quarter of the scale rendered 104 of 255 while one at the ceiling
|
/// trace's brightness, where a ceiling above one sweep would settle *brighter* than the trace and
|
||||||
/// rendered 156, but with the ceiling at one sweep both were 199). A caller whose fade settles
|
/// leave a gain offset between the newest sweep and the image behind it). Everything above the
|
||||||
/// elsewhere wants its own sum here.
|
/// ceiling clamps to the same pixel: where several acquisitions agree, the cell is the trace's
|
||||||
f32 full_scale = 2.0f;
|
/// colour at full strength and no brighter.
|
||||||
|
///
|
||||||
|
/// A caller that wants the fade's own ramp on screen instead -- the newest sweep a *fraction* of
|
||||||
|
/// the steady state, so the eye can rank the sweeps -- sets this to the sum the fade settles at
|
||||||
|
/// (for `PerAcquisition`'s factor `f` that is `1/(1-f)`) and accepts the gain offset; that was
|
||||||
|
/// this field's first default, and it is a legitimate choice, not the one a phosphor makes.
|
||||||
|
f32 full_scale = 1.0f;
|
||||||
/// The gamma applied to the normalised cell value before it becomes a luminance. **1.0 by
|
/// The gamma applied to the normalised cell value before it becomes a luminance. **1.0 by
|
||||||
/// default: linear in sweeps**, because that is what a count means -- a cell covered by half the
|
/// default: linear in sweeps**, because that is what a count means -- a cell covered by half the
|
||||||
/// acquisitions is half the ink. Under 1 lifts the faint end, and that is what
|
/// acquisitions is half the ink. Under 1 lifts the faint end, and that is what
|
||||||
@@ -79,10 +93,13 @@ struct DensityConfig {
|
|||||||
/// resolution is the screen's and not the data's -- the whole point of accumulating into a grid
|
/// resolution is the screen's and not the data's -- the whole point of accumulating into a grid
|
||||||
/// rather than keeping the acquisitions that were drawn into it. Memory is therefore
|
/// rather than keeping the acquisitions that were drawn into it. Memory is therefore
|
||||||
/// `columns() * rows() * 4` bytes whatever the trigger has done: an 800x480 axes is 1.5 MB and a
|
/// `columns() * rows() * 4` bytes whatever the trigger has done: an 800x480 axes is 1.5 MB and a
|
||||||
/// 1920x1080 one is 8.3 MB, and a thousand acquisitions cost the same as one. The renderer adds
|
/// 1920x1080 one is 8.3 MB, and a thousand acquisitions cost the same as one. A plot holds **one
|
||||||
/// one byte per cell as the luminance it uploads (`DensityGrid::cells()` is `f32` because a
|
/// grid per trace** (see `DensityConfig`), so a display's memory is that figure times the number of
|
||||||
/// **count** is what decays and what `ink()`/`peak()` report; the byte is derived per changed
|
/// traces actually drawn -- a plot's unbound slots hold no grid -- and four signals in one 800x480
|
||||||
/// frame, not stored here).
|
/// row are four grids, 6 MB, while a 1920x1080 row of eight is 66 MB. The renderer adds one byte
|
||||||
|
/// per cell as the luminance it uploads (`DensityGrid::cells()`
|
||||||
|
/// is `f32` because a **count** is what decays and what `ink()`/`peak()` report; the byte is
|
||||||
|
/// derived per changed frame, not stored here).
|
||||||
///
|
///
|
||||||
/// A cell's value is a **number of sweeps**: every visible trace of the plot that covers that cell
|
/// A cell's value is a **number of sweeps**: every visible trace of the plot that covers that cell
|
||||||
/// during one `Plot::accumulate_density()` leaves 1.0 in it -- once, however many of that trace's
|
/// during one `Plot::accumulate_density()` leaves 1.0 in it -- once, however many of that trace's
|
||||||
|
|||||||
+53
-39
@@ -590,13 +590,16 @@ class Plot {
|
|||||||
/// default -- is the plot exactly as it was: no grid is allocated, no geometry is deposited
|
/// default -- is the plot exactly as it was: no grid is allocated, no geometry is deposited
|
||||||
/// and no frame uploads anything.
|
/// and no frame uploads anything.
|
||||||
///
|
///
|
||||||
/// On, the plot keeps a **screen-sized intensity grid** behind its traces (see `DensityGrid`
|
/// On, the plot keeps a **screen-sized intensity grid behind each of its traces**, in that
|
||||||
/// for the resolution, the per-cell meaning and the memory cost) and deposits the acquisition
|
/// trace's own colour (see `DensityGrid` for the resolution and per-cell meaning, and
|
||||||
/// on screen into it with `accumulate_density()`: brightness means how often a trace passed,
|
/// `DensityConfig` for the per-trace colour and the memory the grids cost), and deposits the
|
||||||
/// monochrome, and every cell fades as the display's own clock moves on. It is deliberately
|
/// acquisition on screen into it with `accumulate_density()`. While a density is on the
|
||||||
/// *not* a set of retained traces -- the library has the trace geometry in hand, so a density
|
/// renderer draws **no solid trace** over it: the trace ink *is* the image, so the newest
|
||||||
/// costs one grid whatever the trigger has done, where retaining the acquisitions would cost
|
/// acquisition is deposited like every other and fades with the rest -- a trace left drawn on
|
||||||
/// a window copy each.
|
/// 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:
|
/// 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
|
/// 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);
|
void set_density(const DensityConfig& config);
|
||||||
/// The configuration, as set.
|
/// The configuration, as set.
|
||||||
const DensityConfig& density() const noexcept { return density_; }
|
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 accumulated image **of a trace**, for a host that wants to draw it or read it back. There
|
||||||
/// the first `accumulate_density()` on a laid-out plot.
|
/// is one per trace, in trace order, because each is drawn in that trace's own colour (see
|
||||||
const DensityGrid& density_grid() const noexcept { return density_grid_; }
|
/// `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,
|
/// Deposits **the acquisition on screen** into each trace's grid: every visible trace of this
|
||||||
/// over the x window its own axis is showing, 1.0 into every cell it covers -- **once per cell
|
/// plot, the newest acquisition included, over the x window its own axis is showing, 1.0 into
|
||||||
/// per sweep**, however many of that trace's own samples crossed it, so a cell's value is how
|
/// every cell it covers -- **once per cell per sweep**, however many of that trace's own samples
|
||||||
/// many traces have covered it since the last decay and not how much the beam moved (see
|
/// crossed it, so a cell's value is how many traces have covered it since the last decay and not
|
||||||
/// `DensityGrid`; the dwell question is `Style::intensity_grading`'s). Called once per
|
/// how much the beam moved (see `DensityGrid`; the dwell question is
|
||||||
/// *acquisition*, which is the cadence the grid counts in: a
|
/// `Style::intensity_grading`'s). Called once per *acquisition*, which is the cadence the grids
|
||||||
/// caller that deposited every frame would add one sweep sixty times a second, and the
|
/// count in: a caller that deposited every frame would add one sweep sixty times a second, and
|
||||||
/// per-acquisition fade would have nothing to measure itself against.
|
/// 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 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
|
/// 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
|
/// 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.
|
/// the renderer for `build_regions()`' reason -- one x mapping, the frame's own.
|
||||||
void build_invalid_spans();
|
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
|
/// `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
|
/// 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
|
/// plot's last layout, and left with `deposit_density_segment()`. A trace with no series, no
|
||||||
/// visible window or non-positive `dx()` deposits nothing.
|
/// visible window or non-positive `dx()` deposits nothing.
|
||||||
void deposit_density_trace(const Trace& trace);
|
void deposit_density_trace(const Trace& trace, DensityGrid& grid);
|
||||||
/// Deposits one already-mapped segment of a trace polyline: for every grid column the segment
|
/// Deposits one already-mapped segment of a trace polyline into `grid`: for every grid column
|
||||||
/// crosses, the rows it covers in that column, +1 each. The columns it also *clips*: a column
|
/// the segment crosses, the rows it covers in that column, +1 each. The columns it also *clips*:
|
||||||
/// whose slice of the segment lies entirely outside the axes is skipped, so a trace off the
|
/// a column whose slice of the segment lies entirely outside the axes is skipped, so a trace off
|
||||||
/// top of the plot deposits nothing rather than a band along the edge, and a segment with both
|
/// the top of the plot deposits nothing rather than a band along the edge, and a segment with
|
||||||
/// ends past an edge keeps the part that is inside. A degenerate segment (`x0 == x1`, or a
|
/// 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.
|
/// non-finite coordinate) fills the single column it is in.
|
||||||
void deposit_density_segment(f32 x0, f32 y0, f32 x1, f32 y1) noexcept;
|
void deposit_density_segment(f32 x0, f32 y0, f32 x1, f32 y1, DensityGrid& grid) noexcept;
|
||||||
/// Grows `density_grid_` to the last layout's axes rect in device pixels, clearing it when the
|
/// Grows `density_grids_` -- one grid per trace -- to the last layout's axes rect in device
|
||||||
/// size moved (a *different* screen is a different image), and returns whether the grid is
|
/// pixels, clearing a grid when the size moved (a *different* screen is a different image), and
|
||||||
/// usable. The one place the grid is allocated.
|
/// returns whether any grid is usable. The one place the grids are allocated.
|
||||||
bool size_density_grid();
|
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
|
/// Exact statistics of `region` over `series`, or an invalid result when the
|
||||||
/// interval covers no sample. The one place the region arithmetic lives.
|
/// interval covers no sample. The one place the region arithmetic lives.
|
||||||
static RegionStats evaluate_region_stats(const Region& region, const data::Series& series);
|
static RegionStats evaluate_region_stats(const Region& region, const data::Series& series);
|
||||||
@@ -1062,20 +1074,22 @@ class Plot {
|
|||||||
std::vector<Annotation> annotations_;
|
std::vector<Annotation> annotations_;
|
||||||
std::vector<Region> regions_;
|
std::vector<Region> regions_;
|
||||||
std::vector<InvalidSpan> invalid_spans_;
|
std::vector<InvalidSpan> invalid_spans_;
|
||||||
/// The phosphor density: its configuration, the grid it accumulates into, and the scratch the
|
/// The phosphor density: its configuration, the grids it accumulates into, and the scratch the
|
||||||
/// one visible window being deposited is gathered into. The grid is a member and not a local
|
/// one visible window being deposited is gathered into. **One grid per trace** -- the image is
|
||||||
/// because it is the *display's* memory -- it outlives the frames it was drawn in, which is the
|
/// screen-sized but each trace's is tinted with that trace's own colour (see `DensityConfig`), so
|
||||||
/// whole point of accumulating into a screen-sized image rather than retaining traces.
|
/// 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_{};
|
DensityConfig density_{};
|
||||||
DensityGrid density_grid_{};
|
std::vector<DensityGrid> density_grids_;
|
||||||
/// Scratch for `deposit_density_trace()`: interleaved (x, y) of one gathered window, re-used
|
/// 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
|
/// across traces and frames so a steady-state accumulation allocates nothing. Bounded by the
|
||||||
/// window, not the series, as `Renderer::gather_` is.
|
/// window, not the series, as `Renderer::gather_` is.
|
||||||
std::vector<f32> density_points_;
|
std::vector<f32> density_points_;
|
||||||
/// The per-sweep dedupe's stamps, one per grid **row**: the key of the last (trace, column) to
|
/// The per-sweep dedupe's stamps, one per grid **row** and shared by every trace's deposit: the
|
||||||
/// deposit at that row, so one acquisition's own samples cannot count a cell twice (see
|
/// key of the last (trace, column) to deposit at that row, so one acquisition's own samples
|
||||||
/// `deposit_density_segment()`). One `u64` a row -- a few kB at any window -- and not one per
|
/// cannot count a cell twice (see `deposit_density_segment()`). One `u64` a row -- a few kB at
|
||||||
/// cell, which would double the grid's memory for a comparison.
|
/// any window -- and not one per cell, which would double a grid's memory for a comparison.
|
||||||
std::vector<u64> density_stamp_;
|
std::vector<u64> density_stamp_;
|
||||||
/// The sweep's own number, bumped once per trace deposited: the high half of every stamp key, so
|
/// 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.
|
/// every trace of a plot, and every acquisition, deposits exactly once per cell it covers.
|
||||||
|
|||||||
@@ -48,10 +48,11 @@ struct FrameStats {
|
|||||||
u32 staged_segments = 0; ///< line segments staged, all colour groups
|
u32 staged_segments = 0; ///< line segments staged, all colour groups
|
||||||
u32 staged_markers = 0; ///< marker shapes staged over the traces
|
u32 staged_markers = 0; ///< marker shapes staged over the traces
|
||||||
u32 draw_calls = 0; ///< draw calls issued, line and text
|
u32 draw_calls = 0; ///< draw calls issued, line and text
|
||||||
/// The phosphor density's own cells, when one was drawn: `Plot::density_grid()`'s resolution,
|
/// The phosphor density's own cells, when one was drawn: the **sum** of every trace's
|
||||||
/// zero for a plot with the density off. It is what the *frame* drew, so a host reading the
|
/// `Plot::density_grid()` resolution, zero for a plot with the density off. It is what the
|
||||||
/// density's cost off a frame reads it here rather than off the grid, which says what is
|
/// *frame* drew, so a host reading the density's cost off a frame reads it here rather than off
|
||||||
/// accumulated and not what was spent.
|
/// the grids, which say what is accumulated and not what was spent -- and it is the number that
|
||||||
|
/// scales with the trace count, since the image is one grid per trace (see `DensityConfig`).
|
||||||
u32 density_cells = 0;
|
u32 density_cells = 0;
|
||||||
/// The bytes of that image the frame **uploaded**: `columns * rows` when the grid had changed
|
/// The bytes of that image the frame **uploaded**: `columns * rows` when the grid had changed
|
||||||
/// since the last frame and 0 when it had not. It is the one field that separates the two
|
/// since the last frame and 0 when it had not. It is the one field that separates the two
|
||||||
@@ -524,18 +525,23 @@ class Renderer {
|
|||||||
void draw_plot(Plot& plot, const PlotLayout& layout);
|
void draw_plot(Plot& plot, const PlotLayout& layout);
|
||||||
|
|
||||||
void draw_grid(const Plot& plot);
|
void draw_grid(const Plot& plot);
|
||||||
/// The plot's **phosphor density** (`Plot::set_density()`), when one is on: the accumulated
|
/// The plot's **phosphor density** (`Plot::set_density()`), when one is on: one textured quad per
|
||||||
/// image as one textured quad over the axes rect, in `Style::density`'s monochrome tint. Drawn
|
/// trace over the axes rect, each in **that trace's own colour** -- a saturated cell is the trace
|
||||||
/// over the grid and the invalid-span bands and **under the traces**, which is what makes it a
|
/// at its own brightness -- drawn over the grid and the invalid-span bands. The traces themselves
|
||||||
/// record of where the traces have been rather than a second set of them.
|
/// are not drawn while it is on (`draw_traces()` returns), so this is the whole of the data ink
|
||||||
|
/// and the newest acquisition is inside it.
|
||||||
///
|
///
|
||||||
/// The image reaches the GPU as an `GL_R8` texture, one texel per cell, converted here from the
|
/// Each trace's image reaches the GPU as an `GL_R8` texture, one texel per cell, converted here
|
||||||
/// grid's counts -- the scale, the gamma and the tint are the plot's and this stage derives
|
/// from the grid's counts -- the scale and the gamma are the plot's and this stage derives
|
||||||
/// none of them, so the numbers a host reads back and the picture it sees are one derivation.
|
/// neither, so the numbers a host reads back and the picture it sees are one derivation. A frame
|
||||||
/// A frame whose grid did not change (nothing accumulated, nothing decayed -- the steady state
|
/// whose grids did not change skips the conversion *and* the uploads and costs the draw calls,
|
||||||
/// of a display that has stopped being swept) skips the conversion *and* the upload and costs
|
/// which `FrameStats::density_upload_bytes` says per frame.
|
||||||
/// the one draw call, which `FrameStats::density_upload_bytes` says per frame.
|
|
||||||
void draw_density(const Plot& plot, const PlotLayout& layout);
|
void draw_density(const Plot& plot, const PlotLayout& layout);
|
||||||
|
/// One grid's quad: fills the shared `GL_R8` texture from `grid` (skipping when its revision and
|
||||||
|
/// address are the ones already uploaded) and draws it over the axes rect in `tint`. The one
|
||||||
|
/// place the density conversion lives, shared by every trace of `draw_density()`.
|
||||||
|
void draw_density_grid(const DensityConfig& config, const DensityGrid& grid, Color tint,
|
||||||
|
const PlotLayout& layout);
|
||||||
/// `Plot::invalid_spans()`, as `PlotLayout::invalid_spans` measured them: one fill per span,
|
/// `Plot::invalid_spans()`, as `PlotLayout::invalid_spans` measured them: one fill per span,
|
||||||
/// in `style_.invalid_fill` or — for a span the host has marked repaired —
|
/// in `style_.invalid_fill` or — for a span the host has marked repaired —
|
||||||
/// `style_.invalid_repaired_fill`. Over the grid and under everything else inside the axes, so
|
/// `style_.invalid_repaired_fill`. Over the grid and under everything else inside the axes, so
|
||||||
@@ -702,10 +708,12 @@ class Renderer {
|
|||||||
render::TextAtlas atlas_;
|
render::TextAtlas atlas_;
|
||||||
render::LineProgram line_program_;
|
render::LineProgram line_program_;
|
||||||
render::TextProgram text_program_;
|
render::TextProgram text_program_;
|
||||||
/// The phosphor density's pipeline, its one attribute-less vertex array and the texture the
|
/// The phosphor density's pipeline, its one attribute-less vertex array and the texture a grid is
|
||||||
/// grid is uploaded into. The texture is `GL_R8`, one texel per cell of the plot's grid, and
|
/// uploaded into. The texture is `GL_R8`, one texel per cell of the plot's grid, and is re-filled
|
||||||
/// re-created when that grid's size moves -- a resized plot is a differently sized image, and
|
/// for each trace in turn -- a trace's grid is drawn before the next trace's replaces it -- so a
|
||||||
/// the plot has already dropped what the old one held.
|
/// row of several traces still costs the one texture. It is re-created when the grid's size moves
|
||||||
|
/// (a resized plot is a differently sized image, and the plot has already dropped what the old one
|
||||||
|
/// held).
|
||||||
render::DensityProgram density_program_;
|
render::DensityProgram density_program_;
|
||||||
gl::VertexArray density_vao_;
|
gl::VertexArray density_vao_;
|
||||||
gl::Texture density_texture_;
|
gl::Texture density_texture_;
|
||||||
@@ -714,8 +722,12 @@ class Renderer {
|
|||||||
std::vector<u8> density_pixels_;
|
std::vector<u8> density_pixels_;
|
||||||
u32 density_w_ = 0;
|
u32 density_w_ = 0;
|
||||||
u32 density_h_ = 0;
|
u32 density_h_ = 0;
|
||||||
/// The grid revision the texture currently holds. Compared per frame, so a frame whose grid
|
/// The grid the texture currently holds, by address, and its revision. Compared per frame and per
|
||||||
/// did not move skips the conversion *and* the upload -- the pause in a persistence display.
|
/// trace, so a frame whose grids did not move skips the conversion *and* the upload -- the pause
|
||||||
|
/// in a persistence display. The address is part of the key because the texture is shared: two
|
||||||
|
/// traces of one plot have two grids with two revisions, and without the address a skip would
|
||||||
|
/// draw one trace's image under another's colour.
|
||||||
|
const DensityGrid* density_uploaded_grid_ = nullptr;
|
||||||
u64 density_uploaded_revision_ = 0;
|
u64 density_uploaded_revision_ = 0;
|
||||||
render::LineVertexArray line_vao_;
|
render::LineVertexArray line_vao_;
|
||||||
render::TextVertexArray text_vao_;
|
render::TextVertexArray text_vao_;
|
||||||
|
|||||||
@@ -77,14 +77,6 @@ struct Style {
|
|||||||
/// different kinds of thing; and still above the threshold where the difference from *no* band
|
/// different kinds of thing; and still above the threshold where the difference from *no* band
|
||||||
/// is visible, which is the floor this alpha is chosen against.
|
/// is visible, which is the floor this alpha is chosen against.
|
||||||
Color invalid_repaired_fill = Color::hex(0x8A94A6, 0.08f);
|
Color invalid_repaired_fill = Color::hex(0x8A94A6, 0.08f);
|
||||||
/// The tint a plot's **phosphor density** is drawn in (`Plot::set_density()`): the grid's
|
|
||||||
/// luminance comes from the accumulated counts, and this is the monochrome hue it is multiplied
|
|
||||||
/// into -- the density says *how often*, so it must not also say *which signal*, and a trace
|
|
||||||
/// colour here would make a ghost read as a trace that is not there. A near-white neutral, one
|
|
||||||
/// step brighter than `invalid_fill`'s grey, because this is ink the plot *has* and a hole is
|
|
||||||
/// ink it is missing. The alpha is the whole brightness scale's ceiling -- the shader multiplies
|
|
||||||
/// it by each cell's own value -- so under 1 it puts a lid on how bright the display can get.
|
|
||||||
Color density = Color::hex(0xDFF2FF, 0.85f);
|
|
||||||
/// The crosshair's colour: its two lines and the readout panel's border (spec §15). Markers are
|
/// The crosshair's colour: its two lines and the readout panel's border (spec §15). Markers are
|
||||||
/// deliberately *not* this colour -- they carry each trace's own, because a marker says *which*
|
/// deliberately *not* this colour -- they carry each trace's own, because a marker says *which*
|
||||||
/// trace and the line says *where*.
|
/// trace and the line says *where*.
|
||||||
|
|||||||
+94
-37
@@ -355,13 +355,26 @@ void Plot::set_density(const DensityConfig& config) {
|
|||||||
// firing never reaches another accumulation -- so the image goes now rather than at an event
|
// firing never reaches another accumulation -- so the image goes now rather than at an event
|
||||||
// that may not come. The same rule, for the same reason, as the app's `set_persist_depth()`.
|
// that may not come. The same rule, for the same reason, as the app's `set_persist_depth()`.
|
||||||
if (density_.enabled && !config.enabled) {
|
if (density_.enabled && !config.enabled) {
|
||||||
density_grid_.cells_.clear();
|
for (DensityGrid& grid : density_grids_) {
|
||||||
density_grid_.cells_.shrink_to_fit();
|
grid.cells_.clear();
|
||||||
density_grid_.columns_ = 0u;
|
grid.cells_.shrink_to_fit();
|
||||||
density_grid_.rows_ = 0u;
|
grid.columns_ = 0u;
|
||||||
++density_grid_.revision_;
|
grid.rows_ = 0u;
|
||||||
|
++grid.revision_;
|
||||||
|
}
|
||||||
|
density_grids_.clear();
|
||||||
|
density_grids_.shrink_to_fit();
|
||||||
}
|
}
|
||||||
|
// The two fields the renderer turns a cell's count into a luminance with: a change to either is
|
||||||
|
// a change to every cell's *byte*, and a grid's own revision does not see it. A host that states
|
||||||
|
// its configuration every frame (udpscope does) must not force a re-upload per frame, so this
|
||||||
|
// bumps only when one of the two actually moved -- and then a re-upload is owed.
|
||||||
|
const bool rescale = density_.enabled && config.enabled &&
|
||||||
|
(density_.full_scale != config.full_scale || density_.gamma != config.gamma);
|
||||||
density_ = config;
|
density_ = config;
|
||||||
|
if (rescale) {
|
||||||
|
for (DensityGrid& grid : density_grids_) ++grid.revision_;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Plot::decay_density(f64 dt_s) {
|
void Plot::decay_density(f64 dt_s) {
|
||||||
@@ -373,56 +386,94 @@ void Plot::decay_density(f64 dt_s) {
|
|||||||
const f32 factor =
|
const f32 factor =
|
||||||
static_cast<f32>(std::exp(-dt_s / static_cast<f64>(density_.time_constant_s)));
|
static_cast<f32>(std::exp(-dt_s / static_cast<f64>(density_.time_constant_s)));
|
||||||
if (!(factor < 1.0f)) return;
|
if (!(factor < 1.0f)) return;
|
||||||
for (f32& cell : density_grid_.cells_) cell *= factor;
|
for (DensityGrid& grid : density_grids_) {
|
||||||
++density_grid_.revision_;
|
if (grid.cells_.empty()) continue;
|
||||||
|
for (f32& cell : grid.cells_) cell *= factor;
|
||||||
|
++grid.revision_;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Plot::clear_density() noexcept {
|
void Plot::clear_density() noexcept {
|
||||||
if (density_grid_.cells_.empty()) return;
|
for (DensityGrid& grid : density_grids_) {
|
||||||
std::fill(density_grid_.cells_.begin(), density_grid_.cells_.end(), 0.0f);
|
if (grid.cells_.empty()) continue;
|
||||||
++density_grid_.revision_;
|
std::fill(grid.cells_.begin(), grid.cells_.end(), 0.0f);
|
||||||
|
++grid.revision_;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Plot::size_density_grid() {
|
bool Plot::size_density_grids() {
|
||||||
const Rect& axes = layout_.axes;
|
const Rect& axes = layout_.axes;
|
||||||
if (!(axes.w >= 1.0f) || !(axes.h >= 1.0f)) return false;
|
if (!(axes.w >= 1.0f) || !(axes.h >= 1.0f)) return false;
|
||||||
const u32 columns = static_cast<u32>(std::ceil(axes.w));
|
const u32 columns = static_cast<u32>(std::ceil(axes.w));
|
||||||
const u32 rows = static_cast<u32>(std::ceil(axes.h));
|
const u32 rows = static_cast<u32>(std::ceil(axes.h));
|
||||||
if (columns == 0u || rows == 0u) return false;
|
if (columns == 0u || rows == 0u) return false;
|
||||||
if (columns != density_grid_.columns_ || rows != density_grid_.rows_) {
|
// **One grid per trace**, because a density is drawn in the colour of the trace that laid it
|
||||||
// A grid of another size is another image, and there is nothing honest to carry over from
|
// down (see `DensityConfig`). The vector tracks the trace list, so a trace added after the first
|
||||||
// one to the other: the cells are device pixels, so a resize moves every one of them. Drop
|
// accumulation gets its own empty grid here while the grids already sized keep what they held.
|
||||||
// what it held rather than resample it -- the display restarts, which is what a scope does
|
if (density_grids_.size() != traces_.size()) density_grids_.resize(traces_.size());
|
||||||
// when its screen changes.
|
// The per-sweep dedupe's stamps are one per grid **row** and shared by every trace's deposit:
|
||||||
density_grid_.cells_.assign(static_cast<usize>(columns) * rows, 0.0f);
|
// the key a cell is deduped against carries the trace's number (see
|
||||||
// The per-sweep dedupe's stamps are sized with the grid and cleared with it: a stamp is only
|
// `deposit_density_segment()`), so a stamp left by one trace's sweep never matches another's, and
|
||||||
// ever compared for equality against the key of the sweep being deposited, and every key has a
|
// a zeroed stamp -- every key has a non-zero trace number in its high half -- matches nothing.
|
||||||
// non-zero trace number in its high half (`density_trace_` starts at 0 and is pre-incremented),
|
if (density_stamp_.size() != rows) density_stamp_.assign(rows, 0u);
|
||||||
// so a zeroed stamp matches nothing.
|
bool usable = false;
|
||||||
density_stamp_.assign(rows, 0u);
|
for (usize i = 0u; i < traces_.size(); ++i) {
|
||||||
density_grid_.columns_ = columns;
|
DensityGrid& grid = density_grids_[i];
|
||||||
density_grid_.rows_ = rows;
|
if (!traces_[i].visible()) {
|
||||||
++density_grid_.revision_;
|
// A trace with nothing to draw gets no image: the grid is per *trace*, and a slot with no
|
||||||
|
// series is not one. Dropping it here is also what keeps the display's memory proportional
|
||||||
|
// to the traces actually on screen rather than to a plot's fixed trace slots -- a row of four
|
||||||
|
// signals out of eight slots holds four grids, not eight.
|
||||||
|
if (!grid.cells_.empty()) {
|
||||||
|
grid.cells_.clear();
|
||||||
|
grid.cells_.shrink_to_fit();
|
||||||
|
grid.columns_ = 0u;
|
||||||
|
grid.rows_ = 0u;
|
||||||
|
++grid.revision_;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (columns != grid.columns_ || rows != grid.rows_) {
|
||||||
|
// A grid of another size is another image, and there is nothing honest to carry over from
|
||||||
|
// one to the other: the cells are device pixels, so a resize moves every one of them. Drop
|
||||||
|
// what it held rather than resample it -- the display restarts, which is what a scope does
|
||||||
|
// when its screen changes.
|
||||||
|
grid.cells_.assign(static_cast<usize>(columns) * rows, 0.0f);
|
||||||
|
grid.columns_ = columns;
|
||||||
|
grid.rows_ = rows;
|
||||||
|
++grid.revision_;
|
||||||
|
}
|
||||||
|
usable = usable || !grid.cells_.empty();
|
||||||
}
|
}
|
||||||
return !density_grid_.cells_.empty();
|
return usable;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Plot::accumulate_density() {
|
void Plot::accumulate_density() {
|
||||||
if (!density_.enabled) return;
|
if (!density_.enabled) return;
|
||||||
if (!size_density_grid()) return;
|
if (!size_density_grids()) return;
|
||||||
// The fade before the deposit, never after: under `PerAcquisition` the acquisition just drawn
|
// The fade before the deposit, never after: under `PerAcquisition` the acquisition just drawn
|
||||||
// is the brightest one and each older one is the factor down, which is what "the N-th back is
|
// is the brightest one and each older one is the factor down, which is what "the N-th back is
|
||||||
// gone" means. TimeConstant fades in `decay_density()` instead, because its fade is the clock's
|
// gone" means. TimeConstant fades in `decay_density()` instead, because its fade is the clock's
|
||||||
// and not the acquisition's.
|
// and not the acquisition's.
|
||||||
if (density_.decay == DensityDecay::PerAcquisition) {
|
if (density_.decay == DensityDecay::PerAcquisition) {
|
||||||
const f32 factor = std::clamp(density_.per_acquisition, 0.0f, 1.0f);
|
const f32 factor = std::clamp(density_.per_acquisition, 0.0f, 1.0f);
|
||||||
for (f32& cell : density_grid_.cells_) cell *= factor;
|
for (DensityGrid& grid : density_grids_) {
|
||||||
|
if (grid.cells_.empty()) continue;
|
||||||
|
for (f32& cell : grid.cells_) cell *= factor;
|
||||||
|
++grid.revision_;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const Trace& trace : traces_) deposit_density_trace(trace);
|
// **Every visible trace of the acquisition on screen, the newest included.** The current
|
||||||
++density_grid_.revision_;
|
// acquisition is deposited here like any other, so it is inside the image and fades with it --
|
||||||
|
// there is no second, solid trace drawn over the density (see `Renderer::draw_traces()`), which
|
||||||
|
// is what a report of "the last acquisition never fades" turned out to be. One grid each, so two
|
||||||
|
// traces that agree read as two colours at full strength where they overlap and not as one
|
||||||
|
// blended one.
|
||||||
|
const usize n = std::min(density_grids_.size(), traces_.size());
|
||||||
|
for (usize i = 0u; i < n; ++i) deposit_density_trace(traces_[i], density_grids_[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Plot::deposit_density_trace(const Trace& trace) {
|
void Plot::deposit_density_trace(const Trace& trace, DensityGrid& grid) {
|
||||||
if (!trace.visible()) return;
|
if (!trace.visible()) return;
|
||||||
const data::Series& series = *trace.series;
|
const data::Series& series = *trace.series;
|
||||||
const u64 n = series.size();
|
const u64 n = series.size();
|
||||||
@@ -470,7 +521,7 @@ void Plot::deposit_density_trace(const Trace& trace) {
|
|||||||
// gap -- is deposited as a degenerate segment instead, because a place a trace *was* is ink
|
// gap -- is deposited as a degenerate segment instead, because a place a trace *was* is ink
|
||||||
// however short its stay.
|
// however short its stay.
|
||||||
const auto flush_run = [&]() {
|
const auto flush_run = [&]() {
|
||||||
if (run == 1u) deposit_density_segment(px, py, px, py);
|
if (run == 1u) deposit_density_segment(px, py, px, py, grid);
|
||||||
};
|
};
|
||||||
for (usize k = 0; k + 1u < density_points_.size(); k += 2u) {
|
for (usize k = 0; k + 1u < density_points_.size(); k += 2u) {
|
||||||
// x is gathered relative to the visible range's minimum (see `Series::gather()`), so the
|
// x is gathered relative to the visible range's minimum (see `Series::gather()`), so the
|
||||||
@@ -490,17 +541,18 @@ void Plot::deposit_density_trace(const Trace& trace) {
|
|||||||
run = 0u;
|
run = 0u;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (run > 0u) deposit_density_segment(px, py, sx, sy);
|
if (run > 0u) deposit_density_segment(px, py, sx, sy, grid);
|
||||||
px = sx;
|
px = sx;
|
||||||
py = sy;
|
py = sy;
|
||||||
++run;
|
++run;
|
||||||
}
|
}
|
||||||
flush_run();
|
flush_run();
|
||||||
|
++grid.revision_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Plot::deposit_density_segment(f32 x0, f32 y0, f32 x1, f32 y1) noexcept {
|
void Plot::deposit_density_segment(f32 x0, f32 y0, f32 x1, f32 y1, DensityGrid& grid) noexcept {
|
||||||
const u32 columns = density_grid_.columns_;
|
const u32 columns = grid.columns_;
|
||||||
const u32 rows = density_grid_.rows_;
|
const u32 rows = grid.rows_;
|
||||||
if (columns == 0u || rows == 0u) return;
|
if (columns == 0u || rows == 0u) return;
|
||||||
if (density_stamp_.size() < rows) return; // sized with the grid; nothing deposits before that
|
if (density_stamp_.size() < rows) return; // sized with the grid; nothing deposits before that
|
||||||
if (!std::isfinite(x0) || !std::isfinite(y0) || !std::isfinite(x1) || !std::isfinite(y1)) return;
|
if (!std::isfinite(x0) || !std::isfinite(y0) || !std::isfinite(x1) || !std::isfinite(y1)) return;
|
||||||
@@ -557,7 +609,7 @@ void Plot::deposit_density_segment(f32 x0, f32 y0, f32 x1, f32 y1) noexcept {
|
|||||||
// *(trace, column)*, so two sweeps that agree on a cell still count twice, and the two segments
|
// *(trace, column)*, so two sweeps that agree on a cell still count twice, and the two segments
|
||||||
// that share a column's boundary do not.
|
// that share a column's boundary do not.
|
||||||
const u64 key = (static_cast<u64>(density_trace_) << 32) | static_cast<u64>(static_cast<u32>(c));
|
const u64 key = (static_cast<u64>(density_trace_) << 32) | static_cast<u64>(static_cast<u32>(c));
|
||||||
f32* column_cells = density_grid_.cells_.data() + static_cast<usize>(c);
|
f32* column_cells = grid.cells_.data() + static_cast<usize>(c);
|
||||||
for (i32 r = r0; r <= r1; ++r) {
|
for (i32 r = r0; r <= r1; ++r) {
|
||||||
const usize row = static_cast<usize>(r);
|
const usize row = static_cast<usize>(r);
|
||||||
if (density_stamp_[row] == key) continue; // this sweep has already put its ink here
|
if (density_stamp_[row] == key) continue; // this sweep has already put its ink here
|
||||||
@@ -567,6 +619,11 @@ void Plot::deposit_density_segment(f32 x0, f32 y0, f32 x1, f32 y1) noexcept {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DensityGrid& Plot::empty_density_grid() noexcept {
|
||||||
|
static const DensityGrid empty{};
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
|
||||||
f64 Plot::x_axis_origin(u32 axis) const {
|
f64 Plot::x_axis_origin(u32 axis) const {
|
||||||
return axis < x_axes_.size() ? x_axes_[axis].origin : 0.0;
|
return axis < x_axes_.size() ? x_axes_[axis].origin : 0.0;
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-11
@@ -208,6 +208,7 @@ void Renderer::release_gl() noexcept {
|
|||||||
density_pixels_.clear();
|
density_pixels_.clear();
|
||||||
density_w_ = 0;
|
density_w_ = 0;
|
||||||
density_h_ = 0;
|
density_h_ = 0;
|
||||||
|
density_uploaded_grid_ = nullptr;
|
||||||
density_uploaded_revision_ = 0;
|
density_uploaded_revision_ = 0;
|
||||||
lines_.clear();
|
lines_.clear();
|
||||||
glyphs_under_.clear();
|
glyphs_under_.clear();
|
||||||
@@ -528,7 +529,8 @@ void Renderer::draw_plot(Plot& plot, const PlotLayout& layout) {
|
|||||||
// overlay, so everything a frame draws inside the axes is drawn on top of it.
|
// overlay, so everything a frame draws inside the axes is drawn on top of it.
|
||||||
draw_invalid_spans(layout);
|
draw_invalid_spans(layout);
|
||||||
// ... the density included: it is trace ink and not chrome, so it goes over the bands exactly
|
// ... the density included: it is trace ink and not chrome, so it goes over the bands exactly
|
||||||
// as the traces do, and under them for the same reason they are under the data.
|
// where the traces did -- and, while it is on, *instead* of them (`draw_traces()` returns) -- and
|
||||||
|
// under them for the same reason they are under the data.
|
||||||
draw_density(plot, layout);
|
draw_density(plot, layout);
|
||||||
clear_clip();
|
clear_clip();
|
||||||
draw_axis_lines(plot);
|
draw_axis_lines(plot);
|
||||||
@@ -1368,6 +1370,14 @@ u32 Renderer::stage_trace_slopes(const Plot& plot, const Trace& trace, const f32
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::draw_traces(const Plot& plot, const PlotLayout& layout) {
|
void Renderer::draw_traces(const Plot& plot, const PlotLayout& layout) {
|
||||||
|
// **No solid trace while the density is on.** The trace ink *is* the density image (see
|
||||||
|
// `Plot::set_density()` and `draw_density()`): every visible trace was deposited, the newest
|
||||||
|
// acquisition included, so drawing the polyline as well would put a solid, unfading stroke over
|
||||||
|
// its own faint image -- the "persistent image + last acquisition" a report described, where the
|
||||||
|
// newest acquisition never faded because it was never only the image. The traces still exist --
|
||||||
|
// the legend, the crosshair readout and the measurements read them -- they are simply not drawn
|
||||||
|
// as lines.
|
||||||
|
if (plot.density().enabled) return;
|
||||||
// Both dimensions: a zero-width rect selects no level, and a zero-height one
|
// Both dimensions: a zero-width rect selects no level, and a zero-height one
|
||||||
// would flatten every trace onto a single row -- a frame that looks like a
|
// would flatten every trace onto a single row -- a frame that looks like a
|
||||||
// rendering choice rather than the collapsed plot it is.
|
// rendering choice rather than the collapsed plot it is.
|
||||||
@@ -1753,17 +1763,43 @@ void Renderer::draw_invalid_spans(const PlotLayout& layout) {
|
|||||||
|
|
||||||
void Renderer::draw_density(const Plot& plot, const PlotLayout& layout) {
|
void Renderer::draw_density(const Plot& plot, const PlotLayout& layout) {
|
||||||
const DensityConfig& config = plot.density();
|
const DensityConfig& config = plot.density();
|
||||||
const DensityGrid& grid = plot.density_grid();
|
if (!config.enabled) {
|
||||||
const u32 columns = grid.columns();
|
// A plot that has stopped drawing a density is not holding a stale one: forget what the shared
|
||||||
const u32 rows = grid.rows();
|
// texture holds, so a re-enable -- which drops the grids and rebuilds them, possibly at the same
|
||||||
if (!config.enabled || columns == 0u || rows == 0u) return;
|
// addresses and with fresh revisions -- re-uploads rather than trusting a cache entry that
|
||||||
|
// happens to match.
|
||||||
|
density_uploaded_grid_ = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!(layout.axes.w > 0.0f) || !(layout.axes.h > 0.0f)) return;
|
if (!(layout.axes.w > 0.0f) || !(layout.axes.h > 0.0f)) return;
|
||||||
if (!density_program_.valid()) return; // a pipeline that never linked draws nothing
|
if (!density_program_.valid()) return; // a pipeline that never linked draws nothing
|
||||||
|
// One quad per trace, in that trace's own colour: the plot holds one grid per trace (see
|
||||||
|
// `DensityConfig`), so where two traces agree the viewer sees both colours, and a saturated cell
|
||||||
|
// is the colour of the trace whose ink filled it and not a blend. A trace with no visible series
|
||||||
|
// was deposited into nothing (its grid is empty), so it is skipped.
|
||||||
|
for (u32 i = 0u; i < plot.trace_count(); ++i) {
|
||||||
|
const Trace& trace = plot.trace(i);
|
||||||
|
if (!trace.visible()) continue;
|
||||||
|
draw_density_grid(config, plot.density_grid(i), trace.color, layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Renderer::draw_density_grid(const DensityConfig& config, const DensityGrid& grid, Color tint,
|
||||||
|
const PlotLayout& layout) {
|
||||||
|
const u32 columns = grid.columns();
|
||||||
|
const u32 rows = grid.rows();
|
||||||
|
if (columns == 0u || rows == 0u) return;
|
||||||
|
|
||||||
// The image as the shader reads it: one byte per cell, `full_scale` and `gamma` already
|
// The image as the shader reads it: one byte per cell, `full_scale` and `gamma` already
|
||||||
// applied. The conversion lives here rather than in the fragment shader so that a cell's
|
// applied. The conversion lives here rather than in the fragment shader so that a cell's
|
||||||
// *count* has exactly one reading -- the plot's, which is also what a host reads back through
|
// *count* has exactly one reading -- the plot's, which is also what a host reads back through
|
||||||
// `DensityGrid::ink()`/`peak()`, and which decays once per frame on the CPU anyway.
|
// `DensityGrid::ink()`/`peak()`, and which decays once per frame on the CPU anyway.
|
||||||
|
//
|
||||||
|
// **One texture, re-filled per trace.** The cache is keyed by the grid's own address as well as
|
||||||
|
// its revision, so a plot drawn twice with nothing changed (the steady state of a display that
|
||||||
|
// has stopped being swept) skips the conversion *and* the upload -- while a row of several traces
|
||||||
|
// re-fills the one texture once per trace, which is what a figure of several rows already cost
|
||||||
|
// before the density was per trace.
|
||||||
const bool resized = columns != density_w_ || rows != density_h_;
|
const bool resized = columns != density_w_ || rows != density_h_;
|
||||||
if (resized) {
|
if (resized) {
|
||||||
if (!density_texture_.create_r8(columns, rows, nullptr)) {
|
if (!density_texture_.create_r8(columns, rows, nullptr)) {
|
||||||
@@ -1776,9 +1812,9 @@ void Renderer::draw_density(const Plot& plot, const PlotLayout& layout) {
|
|||||||
density_texture_.set_filter(gl::GL_NEAREST, gl::GL_NEAREST);
|
density_texture_.set_filter(gl::GL_NEAREST, gl::GL_NEAREST);
|
||||||
density_w_ = columns;
|
density_w_ = columns;
|
||||||
density_h_ = rows;
|
density_h_ = rows;
|
||||||
density_uploaded_revision_ = 0u; // a fresh texture holds nothing
|
density_uploaded_grid_ = nullptr; // a fresh texture holds nothing
|
||||||
}
|
}
|
||||||
if (resized || grid.revision() != density_uploaded_revision_) {
|
if (resized || density_uploaded_grid_ != &grid || grid.revision() != density_uploaded_revision_) {
|
||||||
const f32 full = config.full_scale > 0.0f ? config.full_scale : 1.0f;
|
const f32 full = config.full_scale > 0.0f ? config.full_scale : 1.0f;
|
||||||
const f32 gamma = config.gamma > 0.0f ? config.gamma : 1.0f;
|
const f32 gamma = config.gamma > 0.0f ? config.gamma : 1.0f;
|
||||||
// The gamma through a 256-entry table rather than a `pow` per cell: the output *is* eight
|
// The gamma through a 256-entry table rather than a `pow` per cell: the output *is* eight
|
||||||
@@ -1804,9 +1840,10 @@ void Renderer::draw_density(const Plot& plot, const PlotLayout& layout) {
|
|||||||
gl::glGetIntegerv(gl::GL_UNPACK_ALIGNMENT, &alignment);
|
gl::glGetIntegerv(gl::GL_UNPACK_ALIGNMENT, &alignment);
|
||||||
density_texture_.upload_r8(columns, rows, density_pixels_.data());
|
density_texture_.upload_r8(columns, rows, density_pixels_.data());
|
||||||
gl::glPixelStorei(gl::GL_UNPACK_ALIGNMENT, alignment);
|
gl::glPixelStorei(gl::GL_UNPACK_ALIGNMENT, alignment);
|
||||||
|
density_uploaded_grid_ = &grid;
|
||||||
density_uploaded_revision_ = grid.revision();
|
density_uploaded_revision_ = grid.revision();
|
||||||
// += and not =: a figure draws a density per row, and what this field reports is the frame's
|
// += and not =: a figure draws a density per trace per row, and what this field reports is the
|
||||||
// upload, not the last row's.
|
// frame's upload, not the last one's.
|
||||||
stats_.density_upload_bytes += columns * rows;
|
stats_.density_upload_bytes += columns * rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1817,11 +1854,13 @@ void Renderer::draw_density(const Plot& plot, const PlotLayout& layout) {
|
|||||||
density_program_.set_viewport({viewport_.w, viewport_.h});
|
density_program_.set_viewport({viewport_.w, viewport_.h});
|
||||||
density_program_.set_rect({layout.axes.x, layout.axes.y},
|
density_program_.set_rect({layout.axes.x, layout.axes.y},
|
||||||
{static_cast<f32>(columns), static_cast<f32>(rows)});
|
{static_cast<f32>(columns), static_cast<f32>(rows)});
|
||||||
density_program_.set_color(style_.density);
|
// The tint is the trace's own colour: its alpha is the trace's, so a saturated cell reads as the
|
||||||
|
// trace at its own brightness, and the shader multiplies it by the cell's luminance.
|
||||||
|
density_program_.set_color(tint);
|
||||||
density_texture_.bind();
|
density_texture_.bind();
|
||||||
density_vao_.bind();
|
density_vao_.bind();
|
||||||
gl::glDrawArrays(gl::GL_TRIANGLE_STRIP, 0, 4);
|
gl::glDrawArrays(gl::GL_TRIANGLE_STRIP, 0, 4);
|
||||||
stats_.density_cells = columns * rows;
|
stats_.density_cells += columns * rows;
|
||||||
++stats_.draw_calls;
|
++stats_.draw_calls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -271,8 +271,9 @@ out vec4 o_color;
|
|||||||
void main() {
|
void main() {
|
||||||
// The cell's count was already turned into a luminance on the CPU, where the count, the
|
// The cell's count was already turned into a luminance on the CPU, where the count, the
|
||||||
// decay and the gamma live -- so this stage has no scale of its own that a host reading the
|
// decay and the gamma live -- so this stage has no scale of its own that a host reading the
|
||||||
// grid's numbers back could disagree with. All that is left is the tint: the density says how
|
// grid's numbers back could disagree with. All that is left is the tint: the density is drawn
|
||||||
// often a trace passed, never which one, so it is one hue for the whole image.
|
// once per trace, in that trace's own colour (the caller's `u_color`), so a saturated cell is
|
||||||
|
// the trace at its own brightness and a faint one is the same hue at a lower alpha.
|
||||||
float ink = texture(u_density, v_uv).r;
|
float ink = texture(u_density, v_uv).r;
|
||||||
o_color = vec4(u_color.rgb, u_color.a * ink);
|
o_color = vec4(u_color.rgb, u_color.a * ink);
|
||||||
}
|
}
|
||||||
|
|||||||
+93
-39
@@ -2852,9 +2852,9 @@ PLOT_TEST(a_density_grid_is_the_axes_in_device_pixels) {
|
|||||||
// deposits nothing at all.
|
// deposits nothing at all.
|
||||||
PLOT_CHECK(!f.plot.density().enabled);
|
PLOT_CHECK(!f.plot.density().enabled);
|
||||||
f.plot.accumulate_density();
|
f.plot.accumulate_density();
|
||||||
PLOT_CHECK(f.plot.density_grid().cell_count() == 0u);
|
PLOT_CHECK(f.plot.density_grid(0u).cell_count() == 0u);
|
||||||
PLOT_CHECK(f.plot.density_grid().ink() == 0.0);
|
PLOT_CHECK(f.plot.density_grid(0u).ink() == 0.0);
|
||||||
PLOT_CHECK(f.plot.density_grid().peak() == 0.0f);
|
PLOT_CHECK(f.plot.density_grid(0u).peak() == 0.0f);
|
||||||
|
|
||||||
plot::DensityConfig cfg;
|
plot::DensityConfig cfg;
|
||||||
cfg.enabled = true;
|
cfg.enabled = true;
|
||||||
@@ -2862,7 +2862,7 @@ PLOT_TEST(a_density_grid_is_the_axes_in_device_pixels) {
|
|||||||
PLOT_CHECK(f.plot.density().enabled);
|
PLOT_CHECK(f.plot.density().enabled);
|
||||||
f.plot.accumulate_density();
|
f.plot.accumulate_density();
|
||||||
|
|
||||||
const plot::DensityGrid& grid = f.plot.density_grid();
|
const plot::DensityGrid& grid = f.plot.density_grid(0u);
|
||||||
PLOT_CHECK(grid.columns() == columns);
|
PLOT_CHECK(grid.columns() == columns);
|
||||||
PLOT_CHECK(grid.rows() == rows);
|
PLOT_CHECK(grid.rows() == rows);
|
||||||
PLOT_CHECK(grid.cell_count() == static_cast<plot::usize>(columns) * rows);
|
PLOT_CHECK(grid.cell_count() == static_cast<plot::usize>(columns) * rows);
|
||||||
@@ -2895,9 +2895,9 @@ PLOT_TEST(a_density_grid_is_the_axes_in_device_pixels) {
|
|||||||
f.plot.set_viewport({0.0f, 0.0f, 320.0f, 240.0f});
|
f.plot.set_viewport({0.0f, 0.0f, 320.0f, 240.0f});
|
||||||
f.plot.layout();
|
f.plot.layout();
|
||||||
f.plot.accumulate_density();
|
f.plot.accumulate_density();
|
||||||
PLOT_CHECK(f.plot.density_grid().columns() < columns);
|
PLOT_CHECK(f.plot.density_grid(0u).columns() < columns);
|
||||||
PLOT_CHECK_NEAR(f.plot.density_grid().ink(),
|
PLOT_CHECK_NEAR(f.plot.density_grid(0u).ink(),
|
||||||
static_cast<double>(f.plot.density_grid().columns()), 1.0);
|
static_cast<double>(f.plot.density_grid(0u).columns()), 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per acquisition the image is multiplied by the factor *before* the new acquisition is
|
/// Per acquisition the image is multiplied by the factor *before* the new acquisition is
|
||||||
@@ -2915,30 +2915,30 @@ PLOT_TEST(a_density_halves_per_acquisition_and_keeps_the_newest_brightest) {
|
|||||||
f.plot.accumulate_density();
|
f.plot.accumulate_density();
|
||||||
// The grid is sized by the first accumulation, so the width is read after it -- and the ink
|
// The grid is sized by the first accumulation, so the width is read after it -- and the ink
|
||||||
// of one pass *is* the width: one segment across the window, one cell per column.
|
// of one pass *is* the width: one segment across the window, one cell per column.
|
||||||
const double width = static_cast<double>(f.plot.density_grid().columns());
|
const double width = static_cast<double>(f.plot.density_grid(0u).columns());
|
||||||
const double one = f.plot.density_grid().ink();
|
const double one = f.plot.density_grid(0u).ink();
|
||||||
PLOT_CHECK_NEAR(one, width, 1.0);
|
PLOT_CHECK_NEAR(one, width, 1.0);
|
||||||
|
|
||||||
f.plot.accumulate_density();
|
f.plot.accumulate_density();
|
||||||
const double two = f.plot.density_grid().ink();
|
const double two = f.plot.density_grid(0u).ink();
|
||||||
PLOT_CHECK_NEAR(two, width * 1.5, 2.0);
|
PLOT_CHECK_NEAR(two, width * 1.5, 2.0);
|
||||||
PLOT_CHECK_NEAR(f.plot.density_grid().peak(), 1.5f, 1e-3);
|
PLOT_CHECK_NEAR(f.plot.density_grid(0u).peak(), 1.5f, 1e-3);
|
||||||
|
|
||||||
f.plot.accumulate_density();
|
f.plot.accumulate_density();
|
||||||
const double three = f.plot.density_grid().ink();
|
const double three = f.plot.density_grid(0u).ink();
|
||||||
PLOT_CHECK_NEAR(three, width * 1.75, 2.0);
|
PLOT_CHECK_NEAR(three, width * 1.75, 2.0);
|
||||||
PLOT_CHECK_NEAR(f.plot.density_grid().peak(), 1.75f, 1e-3);
|
PLOT_CHECK_NEAR(f.plot.density_grid(0u).peak(), 1.75f, 1e-3);
|
||||||
|
|
||||||
// The steady state of a stream that keeps sweeping: 1 + 1/2 + 1/4 + ... = 2 passes.
|
// The steady state of a stream that keeps sweeping: 1 + 1/2 + 1/4 + ... = 2 passes.
|
||||||
for (int i = 0; i < 40; ++i) f.plot.accumulate_density();
|
for (int i = 0; i < 40; ++i) f.plot.accumulate_density();
|
||||||
PLOT_CHECK_NEAR(f.plot.density_grid().peak(), 2.0f, 1e-3);
|
PLOT_CHECK_NEAR(f.plot.density_grid(0u).peak(), 2.0f, 1e-3);
|
||||||
|
|
||||||
// A factor of 0 is "only the newest acquisition is on screen": the grid is the last pass.
|
// A factor of 0 is "only the newest acquisition is on screen": the grid is the last pass.
|
||||||
cfg.per_acquisition = 0.0f;
|
cfg.per_acquisition = 0.0f;
|
||||||
f.plot.set_density(cfg);
|
f.plot.set_density(cfg);
|
||||||
f.plot.accumulate_density();
|
f.plot.accumulate_density();
|
||||||
PLOT_CHECK_NEAR(f.plot.density_grid().ink(), width, 1.0);
|
PLOT_CHECK_NEAR(f.plot.density_grid(0u).ink(), width, 1.0);
|
||||||
PLOT_CHECK_NEAR(f.plot.density_grid().peak(), 1.0f, 1e-3);
|
PLOT_CHECK_NEAR(f.plot.density_grid(0u).peak(), 1.0f, 1e-3);
|
||||||
|
|
||||||
std::printf("[density] per-acquisition ink: 1=%.0f 2=%.0f 3=%.0f peak_steady=%.3f\n", one, two,
|
std::printf("[density] per-acquisition ink: 1=%.0f 2=%.0f 3=%.0f peak_steady=%.3f\n", one, two,
|
||||||
three, 2.0);
|
three, 2.0);
|
||||||
@@ -2955,40 +2955,40 @@ PLOT_TEST(a_density_fades_by_its_time_constant) {
|
|||||||
cfg.time_constant_s = 2.0f;
|
cfg.time_constant_s = 2.0f;
|
||||||
f.plot.set_density(cfg);
|
f.plot.set_density(cfg);
|
||||||
f.plot.accumulate_density();
|
f.plot.accumulate_density();
|
||||||
const double full = f.plot.density_grid().ink();
|
const double full = f.plot.density_grid(0u).ink();
|
||||||
|
|
||||||
// A frame that has not been measured yet (dt <= 0) does not wipe the grid, and neither does
|
// A frame that has not been measured yet (dt <= 0) does not wipe the grid, and neither does
|
||||||
// a frame under `PerAcquisition`, which fades in `accumulate_density()` instead.
|
// a frame under `PerAcquisition`, which fades in `accumulate_density()` instead.
|
||||||
f.plot.decay_density(0.0);
|
f.plot.decay_density(0.0);
|
||||||
PLOT_CHECK_NEAR(f.plot.density_grid().ink(), full, 1e-6);
|
PLOT_CHECK_NEAR(f.plot.density_grid(0u).ink(), full, 1e-6);
|
||||||
f.plot.decay_density(-1.0);
|
f.plot.decay_density(-1.0);
|
||||||
PLOT_CHECK_NEAR(f.plot.density_grid().ink(), full, 1e-6);
|
PLOT_CHECK_NEAR(f.plot.density_grid(0u).ink(), full, 1e-6);
|
||||||
|
|
||||||
// Quarter of a time constant: exp(-0.25).
|
// Quarter of a time constant: exp(-0.25).
|
||||||
f.plot.decay_density(0.5);
|
f.plot.decay_density(0.5);
|
||||||
const double quarter = full * std::exp(-0.25);
|
const double quarter = full * std::exp(-0.25);
|
||||||
PLOT_CHECK_NEAR(f.plot.density_grid().ink(), quarter, quarter * 1e-4);
|
PLOT_CHECK_NEAR(f.plot.density_grid(0u).ink(), quarter, quarter * 1e-4);
|
||||||
|
|
||||||
// Ten more constants and the display is gone to within a rounding: 4.5e-5 of the ink.
|
// Ten more constants and the display is gone to within a rounding: 4.5e-5 of the ink.
|
||||||
for (int i = 0; i < 10; ++i) f.plot.decay_density(2.0);
|
for (int i = 0; i < 10; ++i) f.plot.decay_density(2.0);
|
||||||
PLOT_CHECK(f.plot.density_grid().ink() < quarter * 1e-3);
|
PLOT_CHECK(f.plot.density_grid(0u).ink() < quarter * 1e-3);
|
||||||
PLOT_CHECK(f.plot.density_grid().peak() < 1e-3f);
|
PLOT_CHECK(f.plot.density_grid(0u).peak() < 1e-3f);
|
||||||
|
|
||||||
// A non-positive constant freezes the grid rather than fading it.
|
// A non-positive constant freezes the grid rather than fading it.
|
||||||
cfg.time_constant_s = 0.0f;
|
cfg.time_constant_s = 0.0f;
|
||||||
f.plot.set_density(cfg);
|
f.plot.set_density(cfg);
|
||||||
f.plot.accumulate_density();
|
f.plot.accumulate_density();
|
||||||
const double frozen = f.plot.density_grid().ink();
|
const double frozen = f.plot.density_grid(0u).ink();
|
||||||
f.plot.decay_density(100.0);
|
f.plot.decay_density(100.0);
|
||||||
PLOT_CHECK_NEAR(f.plot.density_grid().ink(), frozen, 1e-6);
|
PLOT_CHECK_NEAR(f.plot.density_grid(0u).ink(), frozen, 1e-6);
|
||||||
|
|
||||||
// And `PerAcquisition` fades in `accumulate_density()` alone: a frame that only passed
|
// And `PerAcquisition` fades in `accumulate_density()` alone: a frame that only passed
|
||||||
// changes nothing.
|
// changes nothing.
|
||||||
cfg.decay = plot::DensityDecay::PerAcquisition;
|
cfg.decay = plot::DensityDecay::PerAcquisition;
|
||||||
f.plot.set_density(cfg);
|
f.plot.set_density(cfg);
|
||||||
const double before = f.plot.density_grid().ink();
|
const double before = f.plot.density_grid(0u).ink();
|
||||||
f.plot.decay_density(1.0);
|
f.plot.decay_density(1.0);
|
||||||
PLOT_CHECK_NEAR(f.plot.density_grid().ink(), before, 1e-6);
|
PLOT_CHECK_NEAR(f.plot.density_grid(0u).ink(), before, 1e-6);
|
||||||
|
|
||||||
std::printf("[density] time-constant ink: full=%.0f quarter=%.1f frozen=%.0f\n", full, quarter,
|
std::printf("[density] time-constant ink: full=%.0f quarter=%.1f frozen=%.0f\n", full, quarter,
|
||||||
frozen);
|
frozen);
|
||||||
@@ -3002,26 +3002,26 @@ PLOT_TEST(switching_a_density_off_drops_the_image_at_once) {
|
|||||||
cfg.enabled = true;
|
cfg.enabled = true;
|
||||||
f.plot.set_density(cfg);
|
f.plot.set_density(cfg);
|
||||||
f.plot.accumulate_density();
|
f.plot.accumulate_density();
|
||||||
PLOT_CHECK(f.plot.density_grid().ink() > 0.0);
|
PLOT_CHECK(f.plot.density_grid(0u).ink() > 0.0);
|
||||||
|
|
||||||
f.plot.clear_density();
|
f.plot.clear_density();
|
||||||
PLOT_CHECK(f.plot.density_grid().ink() == 0.0);
|
PLOT_CHECK(f.plot.density_grid(0u).ink() == 0.0);
|
||||||
PLOT_CHECK(f.plot.density_grid().peak() == 0.0f);
|
PLOT_CHECK(f.plot.density_grid(0u).peak() == 0.0f);
|
||||||
// The clear keeps the allocation -- the grid is still the screen's -- so the next
|
// The clear keeps the allocation -- the grid is still the screen's -- so the next
|
||||||
// accumulation does not have to reallocate it.
|
// accumulation does not have to reallocate it.
|
||||||
PLOT_CHECK(f.plot.density_grid().cell_count() > 0u);
|
PLOT_CHECK(f.plot.density_grid(0u).cell_count() > 0u);
|
||||||
|
|
||||||
f.plot.accumulate_density();
|
f.plot.accumulate_density();
|
||||||
PLOT_CHECK(f.plot.density_grid().ink() > 0.0);
|
PLOT_CHECK(f.plot.density_grid(0u).ink() > 0.0);
|
||||||
|
|
||||||
cfg.enabled = false;
|
cfg.enabled = false;
|
||||||
f.plot.set_density(cfg);
|
f.plot.set_density(cfg);
|
||||||
PLOT_CHECK(f.plot.density_grid().cell_count() == 0u);
|
PLOT_CHECK(f.plot.density_grid(0u).cell_count() == 0u);
|
||||||
PLOT_CHECK(f.plot.density_grid().ink() == 0.0);
|
PLOT_CHECK(f.plot.density_grid(0u).ink() == 0.0);
|
||||||
|
|
||||||
// ... and an accumulation while it is off deposits nothing (it is not a hidden build-up).
|
// ... and an accumulation while it is off deposits nothing (it is not a hidden build-up).
|
||||||
f.plot.accumulate_density();
|
f.plot.accumulate_density();
|
||||||
PLOT_CHECK(f.plot.density_grid().cell_count() == 0u);
|
PLOT_CHECK(f.plot.density_grid(0u).cell_count() == 0u);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What is deposited is where the *trace* is, and nowhere else: a trace off the visible y range
|
/// What is deposited is where the *trace* is, and nowhere else: a trace off the visible y range
|
||||||
@@ -3034,14 +3034,14 @@ PLOT_TEST(a_density_deposits_only_where_the_trace_is) {
|
|||||||
cfg.enabled = true;
|
cfg.enabled = true;
|
||||||
off.plot.set_density(cfg);
|
off.plot.set_density(cfg);
|
||||||
off.plot.accumulate_density();
|
off.plot.accumulate_density();
|
||||||
PLOT_CHECK_NEAR(off.plot.density_grid().ink(), 0.0, 1e-9);
|
PLOT_CHECK_NEAR(off.plot.density_grid(0u).ink(), 0.0, 1e-9);
|
||||||
|
|
||||||
// ... and the same series inside the axis deposits one cell per column, in two rows: the
|
// ... and the same series inside the axis deposits one cell per column, in two rows: the
|
||||||
// first half at y = -1 and the second at y = +1, joined by a diagonal.
|
// first half at y = -1 and the second at y = +1, joined by a diagonal.
|
||||||
DensityFixture split(std::vector<f32>{-1.0f, 1.0f});
|
DensityFixture split(std::vector<f32>{-1.0f, 1.0f});
|
||||||
split.plot.set_density(cfg);
|
split.plot.set_density(cfg);
|
||||||
split.plot.accumulate_density();
|
split.plot.accumulate_density();
|
||||||
const plot::DensityGrid& grid = split.plot.density_grid();
|
const plot::DensityGrid& grid = split.plot.density_grid(0u);
|
||||||
PLOT_CHECK(grid.ink() >= static_cast<double>(grid.columns()));
|
PLOT_CHECK(grid.ink() >= static_cast<double>(grid.columns()));
|
||||||
const plot::Transform ty = split.plot.y_transform(split.ay);
|
const plot::Transform ty = split.plot.y_transform(split.ay);
|
||||||
const plot::Rect axes = split.plot.layout().axes;
|
const plot::Rect axes = split.plot.layout().axes;
|
||||||
@@ -3073,8 +3073,8 @@ PLOT_TEST(a_density_deposits_only_where_the_trace_is) {
|
|||||||
gap.plot.set_density(cfg);
|
gap.plot.set_density(cfg);
|
||||||
gap.plot.accumulate_density();
|
gap.plot.accumulate_density();
|
||||||
|
|
||||||
const plot::DensityGrid& whole_grid = whole.plot.density_grid();
|
const plot::DensityGrid& whole_grid = whole.plot.density_grid(0u);
|
||||||
const plot::DensityGrid& gap_grid = gap.plot.density_grid();
|
const plot::DensityGrid& gap_grid = gap.plot.density_grid(0u);
|
||||||
const plot::Transform tx = gap.plot.x_transform(gap.ax);
|
const plot::Transform tx = gap.plot.x_transform(gap.ax);
|
||||||
const plot::Rect gap_axes = gap.plot.layout().axes;
|
const plot::Rect gap_axes = gap.plot.layout().axes;
|
||||||
const plot::u32 column = static_cast<plot::u32>(std::clamp(
|
const plot::u32 column = static_cast<plot::u32>(std::clamp(
|
||||||
@@ -3085,5 +3085,59 @@ PLOT_TEST(a_density_deposits_only_where_the_trace_is) {
|
|||||||
PLOT_CHECK(gap_grid.ink() < whole_grid.ink());
|
PLOT_CHECK(gap_grid.ink() < whole_grid.ink());
|
||||||
|
|
||||||
std::printf("[density] off-range ink=%.0f split ink=%.0f gap ink=%.0f whole ink=%.0f\n",
|
std::printf("[density] off-range ink=%.0f split ink=%.0f gap ink=%.0f whole ink=%.0f\n",
|
||||||
off.plot.density_grid().ink(), grid.ink(), gap_grid.ink(), whole_grid.ink());
|
off.plot.density_grid(0u).ink(), grid.ink(), gap_grid.ink(), whole_grid.ink());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **One grid per trace.** A plot of two signals holds two images, each deposited from its own
|
||||||
|
/// trace and drawn in its own colour (the colour is the renderer's, and here the *count* is what is
|
||||||
|
/// read): the two traces at two levels leave ink in two different rows, so one grid holding one
|
||||||
|
/// trace's ink and not the other's is the reading that says the images are not shared.
|
||||||
|
PLOT_TEST(a_density_holds_one_grid_per_trace) {
|
||||||
|
plot::data::ArraySeries lo(std::vector<plot::f32>{-0.5f, -0.5f}, 0.0, 1.0);
|
||||||
|
plot::data::ArraySeries hi(std::vector<plot::f32>{0.5f, 0.5f}, 0.0, 1.0);
|
||||||
|
plot::Plot p;
|
||||||
|
p.set_viewport({0.0f, 0.0f, 640.0f, 480.0f});
|
||||||
|
const plot::u32 ax = p.add_x_axis({"t", {0.0, 1.0}, false, plot::AxisPlace::Bottom, false});
|
||||||
|
const plot::u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||||
|
PLOT_CHECK(p.add_trace({&lo, plot::Color::hex(0x4FA3FF), 1.5f, ax, ay, "lo"}) == 0u);
|
||||||
|
PLOT_CHECK(p.add_trace({&hi, plot::Color::hex(0xFF8A4F), 1.5f, ax, ay, "hi"}) == 1u);
|
||||||
|
p.set_x_range(ax, {0.0, 1.0});
|
||||||
|
p.set_y_range(ay, {-1.0, 1.0});
|
||||||
|
p.layout();
|
||||||
|
|
||||||
|
plot::DensityConfig cfg;
|
||||||
|
cfg.enabled = true;
|
||||||
|
p.set_density(cfg);
|
||||||
|
p.accumulate_density();
|
||||||
|
|
||||||
|
PLOT_CHECK(p.trace_count() == 2u);
|
||||||
|
const plot::DensityGrid& g0 = p.density_grid(0u);
|
||||||
|
const plot::DensityGrid& g1 = p.density_grid(1u);
|
||||||
|
// Both are sized to the one axes rect, and each holds exactly its own trace's one pass: the ink
|
||||||
|
// is the width in each grid, so neither picked up the other trace's ink (two traces would be two
|
||||||
|
// widths in a shared grid).
|
||||||
|
PLOT_CHECK(g0.cell_count() > 0u);
|
||||||
|
PLOT_CHECK(g1.cell_count() == g0.cell_count());
|
||||||
|
PLOT_CHECK_NEAR(g0.ink(), static_cast<double>(g0.columns()), 1.0);
|
||||||
|
PLOT_CHECK_NEAR(g1.ink(), static_cast<double>(g1.columns()), 1.0);
|
||||||
|
|
||||||
|
const plot::Transform ty = p.y_transform(ay);
|
||||||
|
const plot::Rect axes = p.layout().axes;
|
||||||
|
const auto row_of = [&](plot::f64 y) {
|
||||||
|
return static_cast<plot::u32>(std::floor(ty.to_screen_y(y) - axes.y));
|
||||||
|
};
|
||||||
|
const plot::u32 lo_row = row_of(-0.5);
|
||||||
|
const plot::u32 hi_row = row_of(0.5);
|
||||||
|
PLOT_CHECK(lo_row != hi_row);
|
||||||
|
PLOT_CHECK(g0.at(0u, lo_row) > 0.0f);
|
||||||
|
PLOT_CHECK(g1.at(0u, hi_row) > 0.0f);
|
||||||
|
PLOT_CHECK(g0.at(0u, hi_row) == 0.0f); // no trace but `lo` is in the lower grid
|
||||||
|
PLOT_CHECK(g1.at(0u, lo_row) == 0.0f);
|
||||||
|
|
||||||
|
// An out-of-range trace reads an empty image rather than anything borrowed.
|
||||||
|
PLOT_CHECK(p.density_grid(2u).cell_count() == 0u);
|
||||||
|
PLOT_CHECK(p.density_grid(9u).ink() == 0.0);
|
||||||
|
|
||||||
|
std::printf("[density] per-trace grids: cells=%zu each ink=%.0f/%.0f rows lo=%u hi=%u\n",
|
||||||
|
g0.cell_count(), g0.ink(), g1.ink(), lo_row, hi_row);
|
||||||
}
|
}
|
||||||
+121
-89
@@ -7520,17 +7520,17 @@ PLOT_TEST(the_box_zoom_frame_lands_exactly_where_it_says) {
|
|||||||
p.clear_selection_rect();
|
p.clear_selection_rect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The phosphor density is a *background* image drawn by the plot: it covers the axes rect in
|
/// The phosphor density **is the trace**, in the trace's own colour: it covers the axes rect in
|
||||||
/// the style's one tint, it is behind the traces -- a trace drawn over the ink is the trace's
|
/// `Trace::color`, the newest acquisition is deposited into it like every other -- so it fades, and
|
||||||
/// own colour and not a blend -- and it costs an upload on the frame the grid moved and nothing
|
/// no solid polyline is drawn over the ink -- and it costs an upload on the frame a grid moved and
|
||||||
/// on the frame it did not.
|
/// nothing on the frame it did not.
|
||||||
///
|
///
|
||||||
/// The trace is moved between the two accumulations (the same `ArraySeries`, committed to new
|
/// Three things are read off the *screen*, because neither the grid's counts nor a tint alone can
|
||||||
/// values, which is how a host re-points a trace it cannot retarget) so that the image holds a
|
/// make them: that a saturated cell is the trace's own colour and not a monochrome wash; that a
|
||||||
/// sweep at y = 0 *and* the trace is drawn at y = +0.5. Without that, a trace and its own ghost
|
/// cell a quarter of the way up the scale is dimmer *and the same hue*; and that the acquisition
|
||||||
/// would be in the same row and the density would be entirely hidden by the ink over it -- which
|
/// just deposited fades -- which is only true if it is *inside* the image and not a stroke drawn
|
||||||
/// is exactly the ordering being asserted, so the two have to be told apart by position.
|
/// on top of its own faint ghost (the "persistent image + last acquisition" report).
|
||||||
PLOT_TEST(a_phosphor_density_is_drawn_under_the_traces) {
|
PLOT_TEST(a_phosphor_density_is_the_trace_in_its_own_colour) {
|
||||||
plottest::HeadlessContext ctx(kW, kH);
|
plottest::HeadlessContext ctx(kW, kH);
|
||||||
plot::Status st;
|
plot::Status st;
|
||||||
PLOT_CHECK(ctx.create(&st));
|
PLOT_CHECK(ctx.create(&st));
|
||||||
@@ -7557,14 +7557,25 @@ PLOT_TEST(a_phosphor_density_is_drawn_under_the_traces) {
|
|||||||
PLOT_CHECK(r.stats().density_upload_bytes == 0u);
|
PLOT_CHECK(r.stats().density_upload_bytes == 0u);
|
||||||
const std::vector<plot::u8> without = read_frame(ctx);
|
const std::vector<plot::u8> without = read_frame(ctx);
|
||||||
|
|
||||||
|
const auto channel = [&](const std::vector<plot::u8>& f, plot::u32 x, plot::u32 y, int c) {
|
||||||
|
return static_cast<int>(f[(plot::usize(y) * kW + x) * 4u + static_cast<plot::usize>(c)]);
|
||||||
|
};
|
||||||
|
const plot::Transform tx = p.x_transform(ax);
|
||||||
|
const plot::Transform ty = p.y_transform(ay);
|
||||||
|
const plot::u32 mid_x = static_cast<plot::u32>(tx.to_screen_x(0.5));
|
||||||
|
const plot::u32 trace_row = static_cast<plot::u32>(ty.to_screen_y(0.0));
|
||||||
|
// A row well away from the sweep, and away from the trace in the no-density frame: the
|
||||||
|
// background to read every cell against, rather than a guessed one.
|
||||||
|
const plot::u32 blank_row = trace_row + 40u < kH ? trace_row + 40u : trace_row - 40u;
|
||||||
|
|
||||||
|
// **The newest acquisition is in the image, and it fades.** A time fade from the first deposit:
|
||||||
|
// one time constant must dim the same pixel, which a stroke drawn over the density could not do.
|
||||||
plot::DensityConfig cfg;
|
plot::DensityConfig cfg;
|
||||||
cfg.enabled = true;
|
cfg.enabled = true;
|
||||||
cfg.decay = plot::DensityDecay::PerAcquisition;
|
cfg.decay = plot::DensityDecay::TimeConstant;
|
||||||
cfg.per_acquisition = 0.5f;
|
cfg.time_constant_s = 1.0f; // the ceiling stays the library's one sweep -- the trace's own
|
||||||
p.set_density(cfg);
|
p.set_density(cfg);
|
||||||
p.accumulate_density(); // a sweep at y = 0
|
p.accumulate_density(); // the acquisition on screen, deposited like any other
|
||||||
s.reset(std::vector<plot::f32>{0.5f, 0.5f}, 0.0, 1.0);
|
|
||||||
p.accumulate_density(); // and the next one at y = +0.5, the trace's own row
|
|
||||||
|
|
||||||
r.begin_frame(kW, kH, kFrameRect);
|
r.begin_frame(kW, kH, kFrameRect);
|
||||||
r.draw(p);
|
r.draw(p);
|
||||||
@@ -7572,7 +7583,7 @@ PLOT_TEST(a_phosphor_density_is_drawn_under_the_traces) {
|
|||||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||||
dump_if_requested(r, "density.ppm");
|
dump_if_requested(r, "density.ppm");
|
||||||
const plot::FrameStats first = r.stats();
|
const plot::FrameStats first = r.stats();
|
||||||
const std::vector<plot::u8> with = read_frame(ctx);
|
const std::vector<plot::u8> fresh = read_frame(ctx);
|
||||||
|
|
||||||
// The image is drawn -- it changed pixels -- and every pixel it changed is inside the axes:
|
// The image is drawn -- it changed pixels -- and every pixel it changed is inside the axes:
|
||||||
// a density reserves no layout space and paints nothing over a label strip.
|
// a density reserves no layout space and paints nothing over a label strip.
|
||||||
@@ -7581,8 +7592,8 @@ PLOT_TEST(a_phosphor_density_is_drawn_under_the_traces) {
|
|||||||
for (plot::u32 y = 0u; y < kH; ++y) {
|
for (plot::u32 y = 0u; y < kH; ++y) {
|
||||||
for (plot::u32 x = 0u; x < kW; ++x) {
|
for (plot::u32 x = 0u; x < kW; ++x) {
|
||||||
const plot::usize at = (plot::usize(y) * kW + x) * 4u;
|
const plot::usize at = (plot::usize(y) * kW + x) * 4u;
|
||||||
if (with[at] == without[at] && with[at + 1u] == without[at + 1u] &&
|
if (fresh[at] == without[at] && fresh[at + 1u] == without[at + 1u] &&
|
||||||
with[at + 2u] == without[at + 2u]) {
|
fresh[at + 2u] == without[at + 2u]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
++differing;
|
++differing;
|
||||||
@@ -7598,81 +7609,101 @@ PLOT_TEST(a_phosphor_density_is_drawn_under_the_traces) {
|
|||||||
PLOT_CHECK(static_cast<plot::f32>(lo_y) >= axes.y - 1.0f);
|
PLOT_CHECK(static_cast<plot::f32>(lo_y) >= axes.y - 1.0f);
|
||||||
PLOT_CHECK(static_cast<plot::f32>(hi_y) <= axes.bottom() + 1.0f);
|
PLOT_CHECK(static_cast<plot::f32>(hi_y) <= axes.bottom() + 1.0f);
|
||||||
|
|
||||||
// The grid reached the GPU: one byte per cell, once, on the frame that moved.
|
// One trace, so one grid and one upload of one byte a cell -- and the *same* grid the plot
|
||||||
const plot::DensityGrid& grid = p.density_grid();
|
// reports, so the picture and the numbers are one derivation.
|
||||||
|
const plot::DensityGrid& grid = p.density_grid(0u);
|
||||||
PLOT_CHECK(first.density_cells == static_cast<plot::u32>(grid.cell_count()));
|
PLOT_CHECK(first.density_cells == static_cast<plot::u32>(grid.cell_count()));
|
||||||
PLOT_CHECK(first.density_upload_bytes == static_cast<plot::u32>(grid.cell_count()));
|
PLOT_CHECK(first.density_upload_bytes == static_cast<plot::u32>(grid.cell_count()));
|
||||||
|
|
||||||
// The trace is *over* the density: on the row it is drawn in, the pixel is the trace's own
|
// **The colour**: the cell the sweep is in is the trace's own colour at full strength -- one
|
||||||
// colour and not that colour blended with the image behind it.
|
// sweep is the ceiling, so a saturated cell *is* the trace -- and not the near-white monochrome
|
||||||
const plot::Transform tx = p.x_transform(ax);
|
// the density used to draw in. The trace has no red and plenty of green and blue; a monochrome
|
||||||
const plot::Transform ty = p.y_transform(ay);
|
// wash at full ink had all three high and near equal.
|
||||||
const plot::u32 mid_x = static_cast<plot::u32>(tx.to_screen_x(0.5));
|
const int bg_g = channel(fresh, mid_x, blank_row, 1);
|
||||||
const plot::u32 trace_row = static_cast<plot::u32>(ty.to_screen_y(0.5));
|
const int cell_r = channel(fresh, mid_x, trace_row, 0);
|
||||||
const plot::u32 ghost_row = static_cast<plot::u32>(ty.to_screen_y(0.0));
|
const int cell_g = channel(fresh, mid_x, trace_row, 1);
|
||||||
const auto frame_pixel = [&](const std::vector<plot::u8>& f, plot::u32 x, plot::u32 y,
|
const int cell_b = channel(fresh, mid_x, trace_row, 2);
|
||||||
plot::u8 out[4]) {
|
PLOT_CHECK(cell_r < 40); // the trace's colour has no red in it
|
||||||
const plot::usize at = (plot::usize(y) * kW + x) * 4u;
|
PLOT_CHECK(cell_g > 150); // ... and its green is the ink
|
||||||
for (int i = 0; i < 4; ++i) out[i] = f[at + static_cast<plot::usize>(i)];
|
PLOT_CHECK(cell_g > cell_b && cell_b > cell_r); // the hue is the trace's, not a neutral
|
||||||
};
|
PLOT_CHECK(cell_g > bg_g + 40);
|
||||||
plot::u8 px[4] = {};
|
// A saturated cell **is** the trace's colour: one sweep at the ceiling draws the trace's own
|
||||||
// The trace is drawn *over* the density, and the red channel is what says so: the trace's own
|
// rgba, so the pixel matches it exactly rather than merely resembling it.
|
||||||
// colour is 0x00E070 and carries none, while the density's tint (Style::density, a near-white)
|
const plot::u8* cell_px = fresh.data() + (plot::usize(trace_row) * kW + mid_x) * 4u;
|
||||||
// at full ink over it would put the red at roughly 190. So a red near zero on the rows the
|
PLOT_CHECK(pixel_is(cell_px, trace_color));
|
||||||
// stroke covers is the ordering, and a red near 190 would be the image painted on top. The
|
|
||||||
// stroke's centre lands between two pixel centres as often as on one and its edge is
|
|
||||||
// anti-aliased, so the *smallest* red over the rows it covers is the one to read.
|
|
||||||
int min_red = 255;
|
|
||||||
for (int dy = -2; dy <= 2; ++dy) {
|
|
||||||
const int y = std::clamp(static_cast<int>(trace_row) + dy, 0, static_cast<int>(kH) - 1);
|
|
||||||
frame_pixel(with, mid_x, static_cast<plot::u32>(y), px);
|
|
||||||
min_red = std::min(min_red, static_cast<int>(px[0]));
|
|
||||||
}
|
|
||||||
PLOT_CHECK(min_red < 40);
|
|
||||||
// ... and the row *above* it holds the ghost of the sweep before: ink that was not in the
|
|
||||||
// frame drawn without a density, and ink that is not the trace's colour.
|
|
||||||
frame_pixel(without, mid_x, ghost_row, px);
|
|
||||||
const plot::u8 ghost_without[4] = {px[0], px[1], px[2], px[3]};
|
|
||||||
frame_pixel(with, mid_x, ghost_row, px);
|
|
||||||
PLOT_CHECK(px[0] != ghost_without[0] || px[1] != ghost_without[1] ||
|
|
||||||
px[2] != ghost_without[2]);
|
|
||||||
PLOT_CHECK(!pixel_is(px, trace_color));
|
|
||||||
|
|
||||||
// **The screen half**: a cell at a quarter of what a full one holds must render measurably
|
// One time constant of the same pixel: dimmer, and still the trace's hue. This is the assertion
|
||||||
// fainter, and a full cell must not be the background. That is the assertion the *eye* makes --
|
// that the current acquisition is inside the density -- the fade `exp(-dt/tau)`.
|
||||||
// it is what "the colour fading is not applied, it looks white" turned out to be -- and nothing
|
const double before = grid.ink();
|
||||||
// in the grid's own numbers can make it: they were right while the picture was flat.
|
p.decay_density(1.0); // dt = tau, so the whole image is e^-1 of what it was
|
||||||
//
|
const double after = grid.ink();
|
||||||
// The row measured is the one the *ghost* is in (the sweep before the last), not the trace's own:
|
PLOT_CHECK_NEAR(after / before, std::exp(-1.0), 1e-4);
|
||||||
// a trace drawn over a cell hides the density in it by construction.
|
|
||||||
const auto channel = [&](const std::vector<plot::u8>& f, plot::u32 x, plot::u32 y, int c) {
|
|
||||||
return static_cast<int>(f[(plot::usize(y) * kW + x) * 4u + static_cast<plot::usize>(c)]);
|
|
||||||
};
|
|
||||||
const int ghost_full = channel(with, mid_x, ghost_row, 0);
|
|
||||||
// The background to compare against is a row *away* from both: the ghost's own row holds the
|
|
||||||
// trace in the frame drawn without a density (that is where the trace was before it moved), so
|
|
||||||
// the difference between the two frames there is the trace, not the ink.
|
|
||||||
const plot::u32 blank_row = ghost_row + 40u < kH ? ghost_row + 40u : ghost_row - 40u;
|
|
||||||
const int empty_red = channel(without, mid_x, blank_row, 0);
|
|
||||||
// A quarter of the ink: `exp(-dt/tau)` is the time fade's own rule, so `dt = tau*ln(4)` is one
|
|
||||||
// step to a quarter whatever one sweep deposited.
|
|
||||||
plot::DensityConfig fading = cfg;
|
|
||||||
fading.decay = plot::DensityDecay::TimeConstant;
|
|
||||||
fading.time_constant_s = 1.0f;
|
|
||||||
p.set_density(fading);
|
|
||||||
const double before = p.density_grid().ink();
|
|
||||||
p.decay_density(std::log(4.0));
|
|
||||||
const double after = p.density_grid().ink();
|
|
||||||
PLOT_CHECK_NEAR(after / before, 0.25, 1e-6);
|
|
||||||
r.begin_frame(kW, kH, kFrameRect);
|
r.begin_frame(kW, kH, kFrameRect);
|
||||||
r.draw(p);
|
r.draw(p);
|
||||||
r.end_frame();
|
r.end_frame();
|
||||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||||
const std::vector<plot::u8> faded = read_frame(ctx);
|
const std::vector<plot::u8> faded = read_frame(ctx);
|
||||||
const int ghost_quarter = channel(faded, mid_x, ghost_row, 0);
|
const int faded_g = channel(faded, mid_x, trace_row, 1);
|
||||||
PLOT_CHECK(ghost_full > empty_red + 20);
|
const int faded_r = channel(faded, mid_x, trace_row, 0);
|
||||||
PLOT_CHECK(ghost_full > ghost_quarter + 20);
|
PLOT_CHECK(faded_g < cell_g - 20); // the newest acquisition faded
|
||||||
PLOT_CHECK(ghost_quarter > empty_red);
|
PLOT_CHECK(faded_r < 40); // and it faded *as the trace's colour*, not toward a neutral
|
||||||
|
PLOT_CHECK(faded_g > bg_g); // ... to more than the background: still on screen
|
||||||
|
|
||||||
|
// **No solid trace over the image.** Move the series to y = +0.5 and draw *without* depositing:
|
||||||
|
// the plot draws no polyline while the density is on, so the +0.5 row is background and the only
|
||||||
|
// ink on screen is the sweep already in the image. A trace drawn over its own ghost -- the
|
||||||
|
// reported "persistent image + last acquisition" -- would put its colour at +0.5 here.
|
||||||
|
s.reset(std::vector<plot::f32>{0.5f, 0.5f}, 0.0, 1.0);
|
||||||
|
r.begin_frame(kW, kH, kFrameRect);
|
||||||
|
r.draw(p);
|
||||||
|
r.end_frame();
|
||||||
|
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||||
|
const std::vector<plot::u8> moved = read_frame(ctx);
|
||||||
|
const plot::u32 moved_row = static_cast<plot::u32>(ty.to_screen_y(0.5));
|
||||||
|
PLOT_CHECK(moved_row != trace_row);
|
||||||
|
// At +0.5 there is only the grid: the frame with the density is *identical* to the frame without
|
||||||
|
// one there, so nothing was drawn at the moved trace's own row -- no stroke over the image. (The
|
||||||
|
// row the sweep is in still holds its ink, so the image itself did not go away.)
|
||||||
|
PLOT_CHECK(channel(moved, mid_x, moved_row, 1) == channel(without, mid_x, moved_row, 1));
|
||||||
|
PLOT_CHECK(channel(moved, mid_x, moved_row, 0) == channel(without, mid_x, moved_row, 0));
|
||||||
|
PLOT_CHECK(channel(moved, mid_x, trace_row, 1) > bg_g + 20); // the image is still there
|
||||||
|
s.reset(std::vector<plot::f32>{0.0f, 0.0f}, 0.0, 1.0);
|
||||||
|
|
||||||
|
// **The steady state and a quarter of it, in the signal's colour.** Many acquisitions of a
|
||||||
|
// per-acquisition fade settle at `1/(1-f)` sweeps, which the one-sweep ceiling clamps to the
|
||||||
|
// trace's colour; a decay to a quarter of that ink is dimmer and keeps the hue.
|
||||||
|
plot::DensityConfig steady_cfg = cfg;
|
||||||
|
steady_cfg.decay = plot::DensityDecay::PerAcquisition;
|
||||||
|
steady_cfg.per_acquisition = 0.5f;
|
||||||
|
p.set_density(steady_cfg);
|
||||||
|
for (int i = 0; i < 24; ++i) p.accumulate_density();
|
||||||
|
r.begin_frame(kW, kH, kFrameRect);
|
||||||
|
r.draw(p);
|
||||||
|
r.end_frame();
|
||||||
|
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||||
|
const std::vector<plot::u8> steady = read_frame(ctx);
|
||||||
|
const int steady_g = channel(steady, mid_x, trace_row, 1);
|
||||||
|
const int steady_r = channel(steady, mid_x, trace_row, 0);
|
||||||
|
PLOT_CHECK(steady_g > bg_g + 60);
|
||||||
|
PLOT_CHECK(steady_r < 40);
|
||||||
|
// A quarter of the ink: `dt = tau*ln(4)` is one step of the time fade to a quarter.
|
||||||
|
plot::DensityConfig quarter_cfg = steady_cfg;
|
||||||
|
quarter_cfg.decay = plot::DensityDecay::TimeConstant;
|
||||||
|
quarter_cfg.time_constant_s = 1.0f;
|
||||||
|
p.set_density(quarter_cfg);
|
||||||
|
const double steady_ink = grid.ink();
|
||||||
|
p.decay_density(std::log(4.0));
|
||||||
|
PLOT_CHECK_NEAR(grid.ink() / steady_ink, 0.25, 1e-6);
|
||||||
|
r.begin_frame(kW, kH, kFrameRect);
|
||||||
|
r.draw(p);
|
||||||
|
r.end_frame();
|
||||||
|
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||||
|
const std::vector<plot::u8> quarter = read_frame(ctx);
|
||||||
|
const int quarter_g = channel(quarter, mid_x, trace_row, 1);
|
||||||
|
const int quarter_r = channel(quarter, mid_x, trace_row, 0);
|
||||||
|
PLOT_CHECK(steady_g > quarter_g + 20);
|
||||||
|
PLOT_CHECK(quarter_g > bg_g + 10);
|
||||||
|
PLOT_CHECK(quarter_r < 40); // still the signal's colour, not a neutral wash
|
||||||
|
|
||||||
// A frame with nothing new to deposit: the grid did not move, so the conversion and the upload
|
// A frame with nothing new to deposit: the grid did not move, so the conversion and the upload
|
||||||
// are skipped -- the frame costs the one draw call -- and the picture is bit-identical.
|
// are skipped -- the frame costs the one draw call -- and the picture is bit-identical.
|
||||||
@@ -7683,13 +7714,14 @@ PLOT_TEST(a_phosphor_density_is_drawn_under_the_traces) {
|
|||||||
PLOT_CHECK(r.stats().density_cells > 0u);
|
PLOT_CHECK(r.stats().density_cells > 0u);
|
||||||
PLOT_CHECK(r.stats().density_upload_bytes == 0u);
|
PLOT_CHECK(r.stats().density_upload_bytes == 0u);
|
||||||
const std::vector<plot::u8> again = read_frame(ctx);
|
const std::vector<plot::u8> again = read_frame(ctx);
|
||||||
PLOT_CHECK(again == faded);
|
PLOT_CHECK(again == quarter);
|
||||||
|
|
||||||
std::printf("[density gl] cells=%u uploaded=%u differing=%u ghost row=%u trace row=%u"
|
std::printf("[density gl] cells=%u uploaded=%u differing=%u trace row=%u blank row=%u"
|
||||||
" axes=%.0f,%.0f %.0fx%.0f\n",
|
" axes=%.0f,%.0f %.0fx%.0f\n",
|
||||||
first.density_cells, first.density_upload_bytes, differing, ghost_row, trace_row,
|
first.density_cells, first.density_upload_bytes, differing, trace_row, blank_row,
|
||||||
axes.x, axes.y, axes.w, axes.h);
|
axes.x, axes.y, axes.w, axes.h);
|
||||||
std::printf("[density gl] luminance red: empty=%d full=%d quarter=%d (ink %.0f -> %.0f)\n",
|
std::printf("[density gl] green: bg=%d full=%d faded=%d steady=%d quarter=%d"
|
||||||
empty_red, ghost_full, ghost_quarter, before, after);
|
" (fresh ink %.0f -> %.0f)\n",
|
||||||
|
bg_g, cell_g, faded_g, steady_g, quarter_g, before, after);
|
||||||
ctx.destroy();
|
ctx.destroy();
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user