`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.
87 lines
3.0 KiB
C++
87 lines
3.0 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#include <plot/render/program.hpp>
|
|
|
|
#include <plot/render/shaders.hpp>
|
|
|
|
namespace plot::render {
|
|
|
|
bool LineProgram::create(Status* st) noexcept {
|
|
// plot::gl::Program::from_source() reports Unsupported while
|
|
// plot::gl::available() is false and GLFailure when the driver rejects the
|
|
// GLSL. Propagating it is the whole point of returning a bool here: the
|
|
// caller must not end up drawing with a program that never linked.
|
|
program_ = gl::Program::from_source(kLineVS, kLineFS, st);
|
|
return program_.id() != 0;
|
|
}
|
|
|
|
void LineProgram::set_scale(Vec2 data_to_px) const noexcept {
|
|
program_.set_uniform("u_scale", data_to_px);
|
|
}
|
|
|
|
void LineProgram::set_offset(Vec2 px) const noexcept { program_.set_uniform("u_offset", px); }
|
|
|
|
void LineProgram::set_viewport(Vec2 px) const noexcept { program_.set_uniform("u_viewport", px); }
|
|
|
|
void LineProgram::set_width(f32 px) const noexcept { program_.set_uniform("u_width", px); }
|
|
|
|
void LineProgram::set_dash(Vec2 on_off) const noexcept {
|
|
program_.set_uniform("u_dash", on_off);
|
|
}
|
|
|
|
void LineProgram::set_color(Color color) const noexcept { program_.set_uniform("u_color", color); }
|
|
|
|
void LineProgram::set_aa(f32 px) const noexcept { program_.set_uniform("u_aa", px); }
|
|
|
|
void LineProgram::set_snap(bool on) const noexcept {
|
|
program_.set_uniform("u_snap", on ? 1.0f : 0.0f);
|
|
}
|
|
|
|
bool TextProgram::create(Status* st) noexcept {
|
|
program_ = gl::Program::from_source(kTextVS, kTextFS, st);
|
|
if (program_.id() == 0) {
|
|
return false;
|
|
}
|
|
// The atlas is bound to texture unit 0 by plot::gl::Texture::bind(), so the
|
|
// sampler has to point there. It is set once, here, not per frame.
|
|
program_.use();
|
|
program_.set_uniform_int("u_atlas", 0);
|
|
return true;
|
|
}
|
|
|
|
void TextProgram::set_viewport(Vec2 px) const noexcept { program_.set_uniform("u_viewport", px); }
|
|
|
|
void TextProgram::set_px_range(Vec2 px) const noexcept { program_.set_uniform("u_px_range", px); }
|
|
|
|
void TextProgram::set_glyph_uv(Vec2 uv) const noexcept { program_.set_uniform("u_glyph_uv", uv); }
|
|
|
|
void TextProgram::set_texture_size(Vec2 texels) const noexcept {
|
|
program_.set_uniform("u_tex_size", texels);
|
|
}
|
|
|
|
bool DensityProgram::create(Status* st) noexcept {
|
|
program_ = gl::Program::from_source(kDensityVS, kDensityFS, st);
|
|
if (program_.id() == 0) {
|
|
return false;
|
|
}
|
|
// The grid texture is bound to texture unit 0 by plot::gl::Texture::bind(), so the sampler
|
|
// has to point there. Set once, here, not per frame -- the same shape as the text program's.
|
|
program_.use();
|
|
program_.set_uniform_int("u_density", 0);
|
|
return true;
|
|
}
|
|
|
|
void DensityProgram::set_viewport(Vec2 px) const noexcept { program_.set_uniform("u_viewport", px); }
|
|
|
|
void DensityProgram::set_rect(Vec2 origin, Vec2 size) const noexcept {
|
|
program_.set_uniform("u_origin", origin);
|
|
program_.set_uniform("u_size", size);
|
|
}
|
|
|
|
void DensityProgram::set_color(Color color) const noexcept {
|
|
program_.set_uniform("u_color", color);
|
|
}
|
|
|
|
} // namespace plot::render
|