#pragma once #include #include #include #include #include namespace plot::data { /// Single-producer / single-consumer streaming series. /// /// The producer thread only ever calls push()/push(ys, n) (and produced()): those /// touch the ring buffer and nothing else -- no lock, no pyramid, no GL, so the /// acquisition thread can never stall the renderer. The consumer thread calls /// acquire() to publish a coherent snapshot, then queries it. /// /// Indexing is the *retained window published by the last acquire()*: size(), /// reduce(), sum(), sumsq(), gather() and pyramid() all use it, and index 0 is /// the oldest sample still held. Once the producer has written more than /// capacity() samples the oldest ones are gone, so acquire() notices that the /// window's base moved and rebuilds the pyramid from the retained samples -- an /// incremental append would splice a dropped prefix onto a retained suffix and /// mis-align every bucket (bucket boundaries are absolute, not relative to the /// window). Until the ring saturates, acquire() is O(new samples); once it wraps, /// an acquire whose base moved costs O(capacity()). /// /// x0() is therefore the x of the oldest retained sample, x_bounds().min == x0(), /// and y_bounds() covers exactly the published samples. /// /// THREADING NOTE. The ring overwrites the oldest samples -- that is what keeps a /// live stream showing the newest data, and what the drop test pins down -- so a /// producer racing past capacity() can write the very slot the consumer is reading /// at the window's oldest edge. That read is not ordered against the write, so a /// race detector flags it: the value is still a single f32 (no tearing on /// supported targets) and the worst case is one stale or too-new sample at the /// oldest edge, corrected by the next acquire() once it sees the base moved. The /// pyramid itself never mixes windows, because acquire() derives the whole window /// from one count. Ingest that is race-free even at the edge needs the producer to /// stop before unread data (drop-on-full) or a claim protocol, i.e. a different /// contract from the one this class specifies. class StreamSeries final : public Series { public: explicit StreamSeries(u64 capacity, f64 x0, f64 dx); /// Producer thread only. void push(f32 y); /// Producer thread only. void push(const f32* ys, u64 n); /// Producer thread only: number of samples ever pushed (monotonic). u64 produced() const; /// Consumer thread only: snapshots the produced count and brings the pyramid /// and bounds up to it. void acquire(); u64 size() const override; f64 x0() const override; f64 dx() const override; 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& out_xy) const override; protected: void copy_raw(u64 begin, u64 end, f32* out) const override; private: RingBuffer ring_; f64 x0_ = 0.0; f64 dx_ = 1.0; u64 base_ = 0; // absolute index of retained index 0 in the published snapshot u64 end_ = 0; // absolute index one past the published snapshot MinMaxPyramid pyramid_; Range y_bounds_{}; }; } // namespace plot::data