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
@@ -48,6 +48,17 @@ struct FrameStats {
|
||||
u32 staged_segments = 0; ///< line segments staged, all colour groups
|
||||
u32 staged_markers = 0; ///< marker shapes staged over the traces
|
||||
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,
|
||||
/// zero for a plot with the density off. It is what the *frame* drew, so a host reading the
|
||||
/// density's cost off a frame reads it here rather than off the grid, which says what is
|
||||
/// accumulated and not what was spent.
|
||||
u32 density_cells = 0;
|
||||
/// 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
|
||||
/// costs of a density -- the conversion and upload on the frame that moved, the single draw
|
||||
/// call on the frames that did not -- which is exactly the pair a host comparing frame times
|
||||
/// with the display on and off needs to tell apart.
|
||||
u32 density_upload_bytes = 0;
|
||||
};
|
||||
|
||||
/// Draws a `Plot` into whatever framebuffer is bound.
|
||||
@@ -93,8 +104,8 @@ class Renderer {
|
||||
Renderer(const Renderer&) = delete;
|
||||
Renderer& operator=(const Renderer&) = delete;
|
||||
|
||||
/// Destroys every GL object this renderer created: both programs, both vertex
|
||||
/// arrays and their instance buffers, and the glyph atlas texture. The
|
||||
/// Destroys every GL object this renderer created: all three programs, the vertex arrays and
|
||||
/// their instance buffers, the glyph atlas texture and the density's own texture. The
|
||||
/// renderer is left uninitialised (`ready() == false`) and `init()` can be
|
||||
/// called again on the same object.
|
||||
///
|
||||
@@ -513,6 +524,18 @@ class Renderer {
|
||||
void draw_plot(Plot& plot, const PlotLayout& layout);
|
||||
|
||||
void draw_grid(const Plot& plot);
|
||||
/// The plot's **phosphor density** (`Plot::set_density()`), when one is on: the accumulated
|
||||
/// image as one textured quad over the axes rect, in `Style::density`'s monochrome tint. Drawn
|
||||
/// over the grid and the invalid-span bands and **under the traces**, which is what makes it a
|
||||
/// record of where the traces have been rather than a second set of them.
|
||||
///
|
||||
/// The image reaches the GPU as an `GL_R8` texture, one texel per cell, converted here from the
|
||||
/// grid's counts -- the scale, the gamma and the tint 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.
|
||||
/// A frame whose grid did not change (nothing accumulated, nothing decayed -- the steady state
|
||||
/// of a display that has stopped being swept) skips the conversion *and* the upload and costs
|
||||
/// the one draw call, which `FrameStats::density_upload_bytes` says per frame.
|
||||
void draw_density(const Plot& plot, const PlotLayout& layout);
|
||||
/// `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 —
|
||||
/// `style_.invalid_repaired_fill`. Over the grid and under everything else inside the axes, so
|
||||
@@ -679,6 +702,21 @@ class Renderer {
|
||||
render::TextAtlas atlas_;
|
||||
render::LineProgram line_program_;
|
||||
render::TextProgram text_program_;
|
||||
/// The phosphor density's pipeline, its one attribute-less vertex array and the texture the
|
||||
/// grid is uploaded into. The texture is `GL_R8`, one texel per cell of the plot's grid, and
|
||||
/// re-created when that 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_;
|
||||
gl::VertexArray density_vao_;
|
||||
gl::Texture density_texture_;
|
||||
/// The luminance image as uploaded, re-used across frames so a steady-state frame reallocates
|
||||
/// nothing: one byte per cell.
|
||||
std::vector<u8> density_pixels_;
|
||||
u32 density_w_ = 0;
|
||||
u32 density_h_ = 0;
|
||||
/// The grid revision the texture currently holds. Compared per frame, so a frame whose grid
|
||||
/// did not move skips the conversion *and* the upload -- the pause in a persistence display.
|
||||
u64 density_uploaded_revision_ = 0;
|
||||
render::LineVertexArray line_vao_;
|
||||
render::TextVertexArray text_vao_;
|
||||
|
||||
|
||||
Reference in new issue
Block a user