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
@@ -339,15 +339,31 @@ The split from the grading above is what it costs: grading measures *one* frame'
|
||||
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.
|
||||
|
||||
- **The grid is the screen's, not the data's, and there is one per trace.** One `f32` cell per
|
||||
device pixel of the axes rect (`DensityGrid`, `plot/plot/density.hpp`): the axes of a 640x360
|
||||
viewport is a **584x458** grid of **267 472 cells** and **1 069 888 bytes**, and a thousand
|
||||
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.
|
||||
- **The grid is the screen's, not the data's — bounded — and there is one per trace.** The axes
|
||||
rect in device pixels (`DensityGrid`, `plot/plot/density.hpp`), one `f32` cell each, **up to
|
||||
`kDensityCellCap` = 262 144 cells** (2^18, a mebibyte of counts): under the cap the grid *is* the
|
||||
rect — the axes of a 640x360 viewport is a **573x313** grid of **179 349 cells** and **717 396
|
||||
bytes**, and a thousand acquisitions through it cost the same — and past it the rect is scaled
|
||||
down *uniformly*, so a cell stays square and the picture keeps the axes' aspect. A plot holds one
|
||||
of these for **each trace it draws**, so the display's memory is that figure times the traces on
|
||||
screen; the bound is per *grid*, so four traces hold at most 1 MiB each whatever the window is.
|
||||
The colour is the reason for the per-trace split: 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.
|
||||
|
||||
The cap is what keeps the resolution from following the window, and the window is not a fixed
|
||||
thing: a 1520x850-logical window at 2x device scale has a 2877-pixel-wide axes rect, and four
|
||||
rows of one trace are **4.5 M cells** — 18 MB of counts, every one of them walked per frame. The
|
||||
pass is `O(cells)` in all three places it happens, so an unbounded image makes the *frame* cost a
|
||||
function of the window: measured on llvmpipe (four traces, one a row, a 2 s window, a 3040x1700
|
||||
framebuffer) the grid pass alone was **12.9 ms of tick and 12.8 ms of draw a frame** and 4.9 MB
|
||||
uploaded, against **3.9 ms / 5.6 ms** and 1.0 MB at the cap. The renderer magnifies the capped
|
||||
image back over the rect with a **linear** filter, because a cell is a *count* — a continuous
|
||||
quantity — and a fragment between two of them is between their two ink levels, which is what a
|
||||
linear tap returns and a point sample cannot (it would show the cell grid as blocks once a cell
|
||||
is several pixels wide). A one-cell-tall feature is thereby spread over its neighbours, which is
|
||||
what any magnification of an image does.
|
||||
- **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**.
|
||||
Counting the hits instead puts a single dense sweep at the ceiling of the brightness scale and
|
||||
@@ -383,20 +399,37 @@ also why it cannot be a per-frame accumulator.
|
||||
reads back through `DensityGrid::ink()`/`peak()` and the picture it sees come from the same
|
||||
numbers.
|
||||
- **Per frame, it is one texture upload and one draw call *per trace* — or one draw call each.**
|
||||
The upload is one byte per cell (**179 kB** for the axes above); the one texture is re-filled per
|
||||
trace, and a frame whose grids did not move skips the conversion and the uploads entirely and
|
||||
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`.
|
||||
The upload is one byte per cell (**179 kB** for the axes above, and at most 256 kB a trace at
|
||||
`kDensityCellCap`, whatever the window); the one texture is re-filled per trace, and a frame whose
|
||||
grids did not move skips the conversion and the uploads entirely and 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 `TimeConstant` fade therefore does pay the conversion and the upload every frame, whatever the
|
||||
bound — a *bounded* bill rather than a free one. Applying the fade on the GPU instead was
|
||||
considered and is a **larger** change than this bound and was deliberately not bundled with it:
|
||||
the image would have to live in a ping-pong target the decay multiplies in place, while the
|
||||
*count* stays on the CPU, because that is what a host reads back through `ink()`/`peak()`, what
|
||||
the deposit adds whole swaths to, and what the per-sweep dedupe keys on — so the two would have
|
||||
to be reconciled per frame anyway. The bound is what makes that reconciliation a fixed cost.
|
||||
|
||||
`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
|
||||
one sweep *is* the columns it covers),
|
||||
(**578x453 = 261 834 cells, 1 047 336 bytes** on its 640x480 viewport, which is past the cap, and
|
||||
the 320x240 leg it ends on is the axes rect *exactly*, one cell a device pixel —
|
||||
`ink=578 peak=1` for one flat sweep, the ink of one sweep *is* the columns it covers),
|
||||
`a_density_grid_is_capped_whatever_the_window` reads the bound itself off a 3840x2160 viewport
|
||||
(**681x384 = 261 504 cells = 1 046 016 bytes of a 3784x2138 axes**: the cells never exceed
|
||||
`kDensityCellCap`, the grid is within 1 % of the whole budget and its aspect is the axes' — the
|
||||
uniform scaling), and `a_capped_density_is_magnified_over_the_axes_rect` reads the *screen* side of
|
||||
it (**569x460 = 261 740 cells of an 832x673 axes**: the image covers all **832** of the rect's
|
||||
columns rather than the 569 cells it holds — the assertion a quad drawn at the grid's own size
|
||||
fails — the sweep stays on the row its transform maps it to, and its ink is most of the trace's own
|
||||
colour, `green=195` against a background of `65`, the rest being the magnification's blend),
|
||||
`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`),
|
||||
(578 → 867 → 1012 on a 578-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,
|
||||
`a_density_holds_one_grid_per_trace` reads two traces' grids apart (**267 472 cells each,
|
||||
ink 584/584**, in two different rows), and
|
||||
`a_density_holds_one_grid_per_trace` reads two traces' grids apart (**261 834 cells each,
|
||||
ink 578/578**, in two different rows), and
|
||||
`a_phosphor_density_is_the_trace_in_its_own_colour` reads the frame: `cells=179349 uploaded=179349`
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user