fix(data): the gather origin is the caller's, so a deep zoom draws on the sample
CI / debug (tests + all adapters) (push) Canceled after 0s
CI / coverage (floor + measured table) (push) Canceled after 0s
CI / asan (AddressSanitizer) (push) Canceled after 0s

`Series::gather()` stages x as f32 relative to `x0()` -- the start of the
*retained window* -- so the value's ULP was a fraction of data the view need
not even hold: 1.9 us over a 20 s ring. A readout quotes `x_at()` in exact
f64, so at a 10 us view across 700 px that put more than a hundred pixels
between where a sample is drawn and the x reported for it.

The origin is now a parameter, placed before `out_xy` so every call site is a
type error rather than a silent change of behaviour. `Renderer::draw_traces`
passes the visible range's minimum, which makes the error a fixed 2^-24 of the
span on screen -- 4e-5 of a pixel over a 700 px axis -- at every zoom.

`stage_xy()` loses its second origin as a consequence: the gather origin and
the draw origin are now always the same value, so the linear case hands the
gathered buffer straight to `add_polyline_xy()` with no per-point copy.

`Plot::crosshair_point()` names the query itself, the one origin that cannot
lose precision for a caller that might.

Guarded by `a_gather_origin_at_the_view_keeps_a_deep_zoom_on_the_sample_it_quotes`,
which measures the error in pixels both ways: under 0.05 px with the view's
minimum, over 10 px with `x0()`.
This commit is contained in:
Martino Ferrari committed 2026-10-05 18:43:50 +02:00
1 parent 59c2e45af7
commit c2c8c596f3
15 files changed
+150 -95

No files matched your search

+4 -4
View File
@@ -100,16 +100,16 @@ u64 XYSeries::snap_index(f64 x) const {
: hi; // ties go to the lower index
}
void XYSeries::gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
void XYSeries::gather(u32 level, u64 begin, u64 end, f64 x_origin, std::vector<f32>& out_xy,
GatherMode mode) const {
const u64 n = size();
const u64 b = std::min(begin, n);
const u64 e = std::min(std::max(end, b), n);
if (b >= e) return;
// x emitted relative to x0(): see Series::gather(). The x array is f64, so
// x emitted relative to the caller's origin: see Series::gather(). The x array is f64, so
// the subtraction happens wide and only the small, exact offset narrows.
const f64 origin = x0();
const f64 origin = x_origin;
if (level == 0) {
for (u64 i = b; i < e; ++i) {
@@ -127,7 +127,7 @@ void XYSeries::gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
// `begin`, `last` one past the last bucket wholly before `end`, and the two
// edge spans around them are read from the raw arrays -- so no emitted
// geometry comes from outside [begin, end), and the emitted x values are the
// array's own coordinates relative to x0().
// array's own coordinates relative to the caller's origin.
const u64 span = u64{kBucket} << level;
const u64 lend = pyramid().level_size(level);
const u64 first = std::min(ceil_div(b, span), lend);