From 1ae72d49031a9863bb0578d66f6c1ccc9345d7ca Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Thu, 8 Oct 2026 19:32:47 +0200 Subject: [PATCH] feat: a retained slot can be backfilled without pushing --- include/plot/data/ring_buffer.hpp | 18 ++++++++++++++++++ tests/test_ring_buffer.cpp | 24 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/include/plot/data/ring_buffer.hpp b/include/plot/data/ring_buffer.hpp index 0af584f..9fcc4a0 100644 --- a/include/plot/data/ring_buffer.hpp +++ b/include/plot/data/ring_buffer.hpp @@ -64,6 +64,24 @@ class RingBuffer { for (u64 i = 0; i < n; ++i) push(vs[i]); } + /// Producer thread only: rewrites the slot at absolute index `absolute`, without pushing. + /// Returns false when that index is not retained -- already overwritten, or not yet + /// produced -- in which case nothing is written. + /// + /// This is for *backfill*: a slot pushed as a placeholder and corrected when the data that + /// belongs there arrives late. It is the same race class as `push()` -- one relaxed store + /// into a slot a consumer may be reading, and T is lock-free by the class's static_assert, + /// so a reader sees the old value or the new one and never a torn one. It deliberately does + /// not touch `produced_`: the count fixes which indices a snapshot reads, and an amend + /// changes a value inside that window rather than extending it. + bool amend(u64 absolute, T v) { + const u64 p = produced_.load(std::memory_order_relaxed); + if (absolute >= p) return false; + if (p > cap_ && absolute < p - cap_) return false; + buf_[static_cast(absolute & mask_)].store(v, std::memory_order_relaxed); + return true; + } + /// Consumer: retained element count, saturating at capacity(). u64 size() const noexcept { const u64 p = produced_.load(std::memory_order_acquire); diff --git a/tests/test_ring_buffer.cpp b/tests/test_ring_buffer.cpp index 9313677..1fc61c3 100644 --- a/tests/test_ring_buffer.cpp +++ b/tests/test_ring_buffer.cpp @@ -35,3 +35,27 @@ PLOT_TEST(ring_capacity_rounds_up_to_a_power_of_two) { PLOT_CHECK_NEAR(r[0], 872.0); // oldest retained PLOT_CHECK_NEAR(r[127], 999.0); // newest } + +PLOT_TEST(ring_amend_rewrites_a_retained_slot) { + // Backfill: a slot pushed as a placeholder is rewritten once the data that belongs there + // arrives. The index is absolute, because that is the only coordinate a late packet can + // compute -- a retained-window index would have moved by the time it got here. + plot::data::RingBuffer r(8); + for (int i = 0; i < 6; ++i) r.push(f32(i)); + PLOT_CHECK(r.amend(3, 42.0f)); + PLOT_CHECK_NEAR(r[3], 42.0); + PLOT_CHECK(r.produced() == 6); // an amend is not a push +} + +PLOT_TEST(ring_amend_refuses_an_index_the_ring_no_longer_holds) { + // The window slides on the producer, so a late amend may arrive after its slot was + // overwritten. Writing it anyway would corrupt whatever sample now owns that slot, so the + // bounds test is the whole safety of the operation and the caller reads the result. + plot::data::RingBuffer r(8); + for (int i = 0; i < 20; ++i) r.push(f32(i)); + PLOT_CHECK(r.base() == 12); + PLOT_CHECK(!r.amend(3, 42.0f)); // aged out + PLOT_CHECK(!r.amend(20, 42.0f)); // never produced + PLOT_CHECK_NEAR(r[0], 12.0); // and nothing moved + PLOT_CHECK_NEAR(r[7], 19.0); +}