feat: a plot can carry a phosphor density, one image for every acquisition
`Plot::set_density()` gives a plot a **phosphor density**: a screen-sized intensity image behind its traces, into which each acquisition is deposited and which fades by a time constant or by a per-acquisition factor. It is the library's and not a host's because the library is the side that knows a trace's device geometry -- a host would be re-rasterising traces the renderer has already staged, at a second derivation of the same mapping. It is an **image and not a set of retained traces**, and that is the whole of its cost: one `f32` cell per device pixel of the axes rect (`DensityGrid`), a cell's value a *number of sweeps*, so a display of a thousand acquisitions costs what one costs and there is no depth to get wrong. Measured on a 640x360 viewport's axes: **573x313 = 179 349 cells, 717 396 bytes** of grid, and one byte per cell uploaded on the frame the grid moved. - `Plot::accumulate_density()` deposits the acquisition on screen: every visible trace's window is mapped to sample indices and gathered exactly as `Renderer::draw_traces()` maps it, then walked column by column -- so the deposit is bounded by the axes' columns and not by the samples behind them -- and a cell takes **1.0 once per trace per sweep**, however many of that trace's own samples crossed it. Counting the hits instead put a single dense sweep at the ceiling of the brightness scale and hid the sweeps behind it, and it made a cell mean "how much the beam moved here" rather than "how many acquisitions were here"; the dedupe is a stamp per grid *row* (one `u64`, keyed by trace and column) so two sweeps agreeing on a cell still count twice. Dwell is a different question the library already answers (`Style::intensity_grading`). - `decay_density(dt_s)` fades by the clock, `clear_density()` and switching the density off drop the image at once. `DensityConfig` carries `full_scale` and `gamma`, applied on the CPU when the grid changed: the value a host reads back through `ink()`/`peak()` and the picture it sees are one derivation, and a frame whose grid did not move skips the conversion and the upload entirely (`FrameStats::density_cells`/`density_upload_bytes`, the latter the *frame's* total over every row, which one row's assignment used to overwrite). - The renderer draws it as one attribute-less quad over the axes rect (`kDensityVS`/`kDensityFS`, `DensityProgram`), over the grid and the invalid-span bands and **under the traces**, inside the axes' clip: it reserves no layout space and nothing on it can be picked, exactly as `Plot::set_invalid_spans()` does. `Style::density` is its tint. - The two fades are the two questions a persistence display can be asked -- "still visible a second later" and "the N-th back is gone" -- so the choice is the caller's. Six cases, all in the suite: four plot-level ones (the grid's resolution and memory, the per-acquisition schedule **584 -> 876 -> 1022** ink with `peak_steady=2.000`, `exp(-dt/tau)` off the ink, and the deposit's own geometry including a NaN as a gap) and one on the real driver (`a_phosphor_density_is_drawn_under_the_traces`: `cells=179349 uploaded=179349` on the moved frame and `uploaded=0` on the identical one after it, with the trace's stroke keeping its own colour over the ink). The suite is **396 cases, 0 failed** and all four ctest entries pass.
This commit is contained in:
1 parent
3ad02e5120
commit
6a6915e64e
17 files changed
+1199
-13
No files matched your search
@@ -41,8 +41,8 @@ ctest --test-dir build/debug --output-on-failure
|
||||
|
||||
ctest registers four tests: `plot_tests` (the test binary), the two header/umbrella build
|
||||
checks (`plot_headers_self_contained`, `plot_umbrella_covers_public_headers`) and
|
||||
`plot_umbrella_check`. The `plot_tests` binary itself runs **377 cases** in the `linux-debug`
|
||||
preset; its harness prints one line per case and a final `377 cases, 0 failed` summary line.
|
||||
`plot_umbrella_check`. The `plot_tests` binary itself runs **396 cases** in the `linux-debug`
|
||||
preset; its harness prints one line per case and a final `396 cases, 0 failed` summary line.
|
||||
The case counts per file are the `PLOT_TEST(...)` declarations under `tests/` — recount them
|
||||
(`grep -c '^PLOT_TEST(' tests/test_*.cpp`) after adding or removing cases.
|
||||
|
||||
@@ -50,7 +50,7 @@ The case counts per file are the `PLOT_TEST(...)` declarations under `tests/`
|
||||
is for working *on* a case rather than for checking the library: most of the render cases
|
||||
create their own EGL context and read pixels back, so the whole binary is far slower than
|
||||
the one case being edited and its one line of measured output scrolls past 376 others. The
|
||||
summary line still counts the whole suite, so a filtered run says `377 cases, 0 failed`
|
||||
summary line still counts the whole suite, so a filtered run says `396 cases, 0 failed`
|
||||
having run one — it is never evidence that the suite passes. Unset, every case runs.
|
||||
|
||||
## Install layout
|
||||
@@ -126,7 +126,7 @@ per-file table and the floor verdict live in [docs/coverage.md](coverage.md), wh
|
||||
`tools/coverage.sh` rewrites from the very run that measured them. Re-run the command for the
|
||||
live numbers.
|
||||
|
||||
**One thing to know about the coverage preset:** it builds **360 of the 377 cases**. The 17
|
||||
**One thing to know about the coverage preset:** it builds **379 of the 396 cases**. The 17
|
||||
that are missing are the adapter cases that need an adapter target to exist — 11 in
|
||||
`test_imgui_widget.cpp` and 6 in `test_glfw_host.cpp` — and the coverage preset turns the
|
||||
adapters off. The toolkit-free adapter seams still run there: `test_qt_pointer.cpp` (5 cases)
|
||||
|
||||
Reference in new issue
Block a user