fix: a density grid is bounded in cells, not by the window's pixels
A grid was the axes rect in device pixels, so dragging a window wide made the image wider with it -- and the per-frame pass over it is O(cells): the fade multiplies every cell, a host's ink()/peak() read every cell, and a frame the fade moved converts and uploads every cell as one byte. At 2x device scale a 1520x850 window's axes rect is 2877 device pixels wide, so four rows of one trace are 4.5 M cells and 4.9 MB uploaded a frame; measured headless on llvmpipe with a 2 s window, 12.9 ms of tick and 12.8 ms of draw a frame, every frame under a time fade -- a cost that follows the window and has no ceiling. The grid is now the axes rect up to kDensityCellCap (2^18 cells, a mebibyte of counts), scaled down uniformly past it so cells stay square and the picture keeps the axes' aspect, and magnified back over the rect with a linear filter: a cell is a count, so a fragment between two of them is between their two ink levels, which a point sample cannot give once a cell is several pixels wide. The same four rows at the cap are 1.05 M cells, 3.9 ms of tick and 5.6 ms of draw and 1.0 MB uploaded, while a grid under the cap is untouched -- a 640x360 viewport's 573x313 axes is still 179 349 cells, one a device pixel. The image's footprint is the axes rect rounded up to whole device pixels on both sides of the pipe, so a grid under the cap keeps the exact one-texel-a-pixel mapping it has always had (sx/sy come out exactly 1) and only a capped grid reads its cell index through a scale. Moving the decay itself to the GPU was considered and left out: the CPU grid is what ink()/peak() read, what the deposit adds to and what the per-sweep dedupe keys on, so a GPU image would have to be reconciled with it per frame -- a larger change than this bound, and not one to half-do. Tests: a_density_grid_is_capped_whatever_the_window reads the bound off a 3840x2160 viewport (261 504 cells of a 3784x2138 axes, aspect kept), a_capped_density_is_magnified_over_the_axes_rect reads the screen side of it (569x460 cells covering all 832 of the rect's columns), and the density cases now assert their capped figures. 399 cases, 0 failed.
This commit is contained in:
1 parent
1b8b55d3ee
commit
f79a97be9b
12 files changed
+414
-108
No files matched your search
+18
-14
@@ -590,8 +590,8 @@ class Plot {
|
||||
/// default -- is the plot exactly as it was: no grid is allocated, no geometry is deposited
|
||||
/// and no frame uploads anything.
|
||||
///
|
||||
/// On, the plot keeps a **screen-sized intensity grid behind each of its traces**, in that
|
||||
/// trace's own colour (see `DensityGrid` for the resolution and per-cell meaning, and
|
||||
/// On, the plot keeps an intensity grid behind each of its traces, in that trace's own colour
|
||||
/// (see `DensityGrid` for the resolution, the bound on it and the per-cell meaning, and
|
||||
/// `DensityConfig` for the per-trace colour and the memory the grids cost), and deposits the
|
||||
/// acquisition on screen into it with `accumulate_density()`. While a density is on the
|
||||
/// renderer draws **no solid trace** over it: the trace ink *is* the image, so the newest
|
||||
@@ -603,8 +603,10 @@ class Plot {
|
||||
///
|
||||
/// 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. Unlike a band it needs no one-pixel floor on its own size -- a cell *is* a device pixel
|
||||
/// by construction, which is the smallest mark the rule could ask for.
|
||||
/// it. A cell is a device pixel of that axes rect while the rect is under `kDensityCellCap` and
|
||||
/// a block of them past it, which the renderer scales back up with a linear filter -- the image
|
||||
/// reserves no space either way, so the bound changes its resolution and nothing about the
|
||||
/// layout.
|
||||
///
|
||||
/// **Disabling drops what was accumulated at once**, not at the next acquisition: a caller who
|
||||
/// has stopped paying for the display is not left holding a stale image of it, which is the
|
||||
@@ -640,11 +642,12 @@ class Plot {
|
||||
/// a layout: a plot that has never been laid out has no axes rect to size the grid to and
|
||||
/// deposits nothing.
|
||||
///
|
||||
/// The grid is sized to that axes rect, **one `f32` cell per device pixel**, and a grid of the
|
||||
/// wrong size is reallocated -- which drops what it held, because the image is a screen-sized
|
||||
/// one and a new screen is a new image. Under `DensityDecay::PerAcquisition` the fade happens
|
||||
/// here, before the deposit, so the acquisition just drawn is the brightest. A NaN sample is a
|
||||
/// gap: it deposits nothing and joins nothing, the library-wide policy.
|
||||
/// The grid is sized to that axes rect, **the rect in device pixels up to `kDensityCellCap`
|
||||
/// cells** (`DensityGrid`), and a grid of the wrong size is reallocated -- which drops what it
|
||||
/// held, because the image is of the axes rect and a new rect is a new image. Under
|
||||
/// `DensityDecay::PerAcquisition` the fade happens here, before the deposit, so the acquisition
|
||||
/// just drawn is the brightest. A NaN sample is a gap: it deposits nothing and joins nothing,
|
||||
/// the library-wide policy.
|
||||
void accumulate_density();
|
||||
|
||||
/// Fades the grid by `dt_s` of **stream seconds** -- the clock the x axis reads, so a caller
|
||||
@@ -1075,11 +1078,12 @@ class Plot {
|
||||
std::vector<Region> regions_;
|
||||
std::vector<InvalidSpan> invalid_spans_;
|
||||
/// The phosphor density: its configuration, the grids it accumulates into, and the scratch the
|
||||
/// one visible window being deposited is gathered into. **One grid per trace** -- the image is
|
||||
/// screen-sized but each trace's is tinted with that trace's own colour (see `DensityConfig`), so
|
||||
/// the memory is `traces * columns * rows * 4` bytes and not one plot's worth. The vector tracks
|
||||
/// `traces_` and outlives the frames its images were drawn in, which is the whole point of
|
||||
/// accumulating into a screen-sized image rather than retaining traces.
|
||||
/// one visible window being deposited is gathered into. **One grid per trace** -- the image is of
|
||||
/// the axes rect but each trace's is tinted with that trace's own colour (see `DensityConfig`), so
|
||||
/// the memory is `traces * columns * rows * 4` bytes and not one plot's worth -- and each is
|
||||
/// bounded by `kDensityCellCap`. The vector tracks `traces_` and outlives the frames its images
|
||||
/// were drawn in, which is the whole point of accumulating into an image rather than retaining
|
||||
/// traces.
|
||||
DensityConfig density_{};
|
||||
std::vector<DensityGrid> density_grids_;
|
||||
/// Scratch for `deposit_density_trace()`: interleaved (x, y) of one gathered window, re-used
|
||||
|
||||
Reference in new issue
Block a user