fix(data): the gather origin is the caller's, so a deep zoom draws on the sample
`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:
1 parent
59c2e45af7
commit
c2c8c596f3
15 files changed
+150
-95
No files matched your search
@@ -33,7 +33,7 @@ class ArraySeries final : public Series {
|
||||
Range x_bounds() const override;
|
||||
Range y_bounds() const override;
|
||||
const MinMaxPyramid& pyramid() const override;
|
||||
void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
||||
void gather(u32 level, u64 begin, u64 end, f64 x_origin, std::vector<f32>& out_xy,
|
||||
GatherMode mode = GatherMode::Envelope) const override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -59,12 +59,20 @@ class Series {
|
||||
|
||||
/// Appends interleaved (x, y) pairs for level `level`.
|
||||
///
|
||||
/// **x is relative to `x0()`**: the emitted value is `x_at(i) - x0()` as
|
||||
/// f32, never the absolute coordinate. A consumer reconstructs the absolute
|
||||
/// x in f64 (`x0() + emitted`) before mapping it to the screen; staging an
|
||||
/// absolute epoch-time x as f32 would quantise it to a grid coarser than
|
||||
/// the pixel (f32 carries 24 bits -- 1 s at 1.7e9), while the relative
|
||||
/// value `i*dx` is small and exact. y is unchanged.
|
||||
/// **x is relative to `x_origin`**: the emitted value is `x_at(i) - x_origin` as f32, never
|
||||
/// the absolute coordinate. A consumer reconstructs the absolute x in f64
|
||||
/// (`x_origin + emitted`) before mapping it to the screen; staging an absolute epoch-time x
|
||||
/// as f32 would quantise it to a grid coarser than the pixel (f32 carries 24 bits -- 1 s at
|
||||
/// 1.7e9). y is unchanged.
|
||||
///
|
||||
/// **The caller names the origin because only the caller knows the span the f32 has to be
|
||||
/// exact against.** The origin was `x0()` -- the start of the series' *retained window* --
|
||||
/// and a relative value's ULP is then a fraction of the whole window however little of it is
|
||||
/// on screen: 1.9 us over 20 s of retained data, which at a view 10 us wide is more than a
|
||||
/// hundred pixels between where a sample is *drawn* and the x a readout quotes for the same
|
||||
/// sample (`x_at()`, exact f64). Passing the visible range's minimum instead makes the error
|
||||
/// 2^-24 of the *visible span* -- a fixed 4e-5 of a pixel over a 700 px axis -- at every
|
||||
/// zoom. A caller with no view in mind may pass `x0()` and get the old behaviour.
|
||||
///
|
||||
/// Level 0 emits the raw samples of [begin, end). Level >= 1 emits one
|
||||
/// geometry element per *complete* level bucket wholly inside the retained
|
||||
@@ -85,7 +93,7 @@ class Series {
|
||||
/// level_for() picks), and never reaches outside the retained window -- nor
|
||||
/// outside [begin, end). Pairs are appended to out_xy, which is never
|
||||
/// cleared.
|
||||
virtual void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
||||
virtual void gather(u32 level, u64 begin, u64 end, f64 x_origin, std::vector<f32>& out_xy,
|
||||
GatherMode mode = GatherMode::Envelope) const = 0;
|
||||
|
||||
/// Absolute x of sample `i` (`x0() + i*dx()` on a uniform-grid series).
|
||||
@@ -121,7 +129,7 @@ class Series {
|
||||
|
||||
/// The shared body of the concrete gather() overrides: raw samples at level 0,
|
||||
/// bucket geometry above it (per `mode`).
|
||||
void gather_impl(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
||||
void gather_impl(u32 level, u64 begin, u64 end, f64 x_origin, std::vector<f32>& out_xy,
|
||||
GatherMode mode) const;
|
||||
|
||||
private:
|
||||
@@ -143,7 +151,7 @@ class Series {
|
||||
/// 0 is entirely this, and a level >= 1 gather uses it for the two partial edge
|
||||
/// buckets a pyramid of complete buckets cannot answer for. Precondition:
|
||||
/// begin < end, end <= size().
|
||||
void gather_raw(u64 begin, u64 end, std::vector<f32>& out_xy) const;
|
||||
void gather_raw(u64 begin, u64 end, f64 x_origin, std::vector<f32>& out_xy) const;
|
||||
};
|
||||
|
||||
} // namespace plot::data
|
||||
@@ -92,7 +92,7 @@ class StreamSeries final : public Series {
|
||||
Range x_bounds() const override;
|
||||
Range y_bounds() const override;
|
||||
const MinMaxPyramid& pyramid() const override;
|
||||
void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
||||
void gather(u32 level, u64 begin, u64 end, f64 x_origin, std::vector<f32>& out_xy,
|
||||
GatherMode mode = GatherMode::Envelope) const override;
|
||||
/// Always true: this series' `x_bounds()` is the retained ring window, which
|
||||
/// slides with every `acquire()` -- the property `XFollow::Live` follows.
|
||||
|
||||
@@ -53,8 +53,8 @@ class XYSeries final : public Series {
|
||||
Range x_bounds() const override;
|
||||
Range y_bounds() const override;
|
||||
const MinMaxPyramid& pyramid() const override;
|
||||
/// Same contract as `Series::gather`, with x emitted relative to `x0()`.
|
||||
void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
||||
/// Same contract as `Series::gather`, with x emitted relative to `x_origin`.
|
||||
void gather(u32 level, u64 begin, u64 end, f64 x_origin, std::vector<f32>& out_xy,
|
||||
GatherMode mode = GatherMode::Envelope) const override;
|
||||
|
||||
/// The sample's own x.
|
||||
|
||||
@@ -445,11 +445,12 @@ class Renderer {
|
||||
/// `origin_x` back in, in f64, before the single f32 narrowing (B1), so a
|
||||
/// large-magnitude x -- an epoch axis -- never loses sub-pixel precision.
|
||||
void stage_segment(f64 origin_x, f64 x0, f64 y0, f64 x1, f64 y1, bool log_x, bool log_y);
|
||||
/// Stages `n` interleaved `x,y` points of a gathered window (see
|
||||
/// `Series::gather`): x **relative to the series' `x0()`**, y absolute. The
|
||||
/// absolute data x is reconstructed in f64 (`series_x0 + rel`) and staged as
|
||||
/// Stages `n` interleaved `x,y` points of a gathered window (see `Series::gather`): x
|
||||
/// **relative to `origin_x`**, which the caller asked the gather for, y absolute. On linear
|
||||
/// axes that is already what the batch wants and the buffer is handed over as it is; a log
|
||||
/// axis reconstructs the absolute x in f64 per segment and stages
|
||||
/// `to_space(x_abs) - to_space(origin_x)` -- O(visible pixels), not O(epoch).
|
||||
void stage_xy(const f32* xy, u32 n, f64 series_x0, f64 origin_x, bool log_x, bool log_y);
|
||||
void stage_xy(const f32* xy, u32 n, f64 origin_x, bool log_x, bool log_y);
|
||||
/// Lays one run of text out at a framebuffer position and stages it into
|
||||
/// `batch`, shifting it into viewport space and tinting it `color` (the
|
||||
/// per-instance `a_colour`). Returns the pen past the run.
|
||||
|
||||
Reference in new issue
Block a user