From 3ad02e5120b5e2b8fb0a6d1a3ff4661b1d07325c Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Thu, 15 Oct 2026 16:30:05 +0200 Subject: [PATCH] feat: a cleared ring can resume its absolute count at a given origin A derived ring that is emptied and refilled from its source's retained window has to come back numbered as the source is. `clear()` restarts it at 0, which leaves every absolute index -- `base()`, `produced()`, `copy_from()` -- short by that origin for a whole window's worth of data. Co-Authored-By: Claude Opus 4.6 --- include/plot/data/ring_buffer.hpp | 12 ++++++++++ tests/test_ring_buffer.cpp | 37 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/include/plot/data/ring_buffer.hpp b/include/plot/data/ring_buffer.hpp index 9fcc4a0..f3776eb 100644 --- a/include/plot/data/ring_buffer.hpp +++ b/include/plot/data/ring_buffer.hpp @@ -127,6 +127,18 @@ class RingBuffer { /// pushing). void clear() noexcept { produced_.store(0, std::memory_order_relaxed); } + /// Consumer thread only: discards every element and resumes the absolute count + /// at `absolute`, so the next push() lands there (not safe while a producer is + /// pushing). + /// + /// This is for a *recompute*: a ring whose contents are derived from another is + /// emptied and refilled from a given absolute origin, and it must come back + /// numbered as its source is. clear() would restart it at 0 and leave every + /// absolute index -- base(), produced(), copy_from() -- off by that origin. + void clear_to(u64 absolute) noexcept { + produced_.store(absolute, std::memory_order_relaxed); + } + private: u64 cap_; u64 mask_; diff --git a/tests/test_ring_buffer.cpp b/tests/test_ring_buffer.cpp index 1fc61c3..2452660 100644 --- a/tests/test_ring_buffer.cpp +++ b/tests/test_ring_buffer.cpp @@ -21,6 +21,43 @@ PLOT_TEST(ring_clear_resets) { PLOT_CHECK_NEAR(r[0], 42.0); } +PLOT_TEST(ring_clear_to_resumes_the_absolute_count) { + // A recompute: a derived ring is emptied and refilled from the absolute origin its source + // retains, so it has to come back numbered as the source is. `clear()` restarts at 0, which + // leaves a refilled ring reporting a produced() a whole window behind an identically fed one. + plot::data::RingBuffer src(8); + plot::data::RingBuffer derived(8); + for (int i = 0; i < 20; ++i) { + src.push(f32(i)); + derived.push(f32(i) * 2.0f); + } + PLOT_CHECK(src.base() == 12); + + derived.clear_to(src.base()); + for (u64 i = 0; i < src.size(); ++i) derived.push(src[i] * 3.0f); + + // Numbered as the source: same produced(), same base(), and the retained windows pair up. + PLOT_CHECK(derived.produced() == src.produced()); + PLOT_CHECK(derived.base() == src.base()); + PLOT_CHECK(derived.size() == src.size()); + PLOT_CHECK_NEAR(derived[0], 36.0); // 12 * 3 + PLOT_CHECK_NEAR(derived[7], 57.0); // 19 * 3 + // And the absolute reader agrees with the windowed one, which is what a snapshot reads. + f32 at = 0.0f; + derived.copy_from(19, 1, &at); + PLOT_CHECK_NEAR(at, 57.0); +} + +PLOT_TEST(ring_clear_to_zero_is_clear) { + plot::data::RingBuffer r(8); + for (int i = 0; i < 5; ++i) r.push(f32(i)); + r.clear_to(0); + PLOT_CHECK(r.size() == 0); + PLOT_CHECK(r.produced() == 0); + r.push(42.0f); + PLOT_CHECK_NEAR(r[0], 42.0); +} + PLOT_TEST(ring_capacity_rounds_up_to_a_power_of_two) { // The capacity is rounded up so slot addressing stays a mask, and capacity() // reports the rounded value -- the one callers must size for (R4).