From b6eacc8338fa4e7df0b478df6bd5673ead285900 Mon Sep 17 00:00:00 2001 From: Georg Hagen Date: Tue, 29 Apr 2025 21:19:11 +0200 Subject: [PATCH] Add first RingBuffer draft version --- openVulkanoCpp/Data/Containers/RingBuffer.hpp | 487 ++++++++++++++++++ tests/Data/Containers/RingBufferTest.cpp | 179 +++++++ 2 files changed, 666 insertions(+) create mode 100644 openVulkanoCpp/Data/Containers/RingBuffer.hpp create mode 100644 tests/Data/Containers/RingBufferTest.cpp diff --git a/openVulkanoCpp/Data/Containers/RingBuffer.hpp b/openVulkanoCpp/Data/Containers/RingBuffer.hpp new file mode 100644 index 0000000..d0dc9df --- /dev/null +++ b/openVulkanoCpp/Data/Containers/RingBuffer.hpp @@ -0,0 +1,487 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include "Base/Utils.hpp" +#include +#include +#include +#include + +namespace OpenVulkano +{ + namespace internal_detail + { + /** + * \internal + * This class is not intended for public use. Use RingBuffer instead. + */ + template + class NPCRingBufferBase + { + protected: + size_t count = 0, head = 0; + + ~NPCRingBufferBase() { assert(count == 0); } + + [[nodiscard]] size_t HeadId() const { return head; } + private: + [[nodiscard]] IMPL& Impl() { return *static_cast(this); } + [[nodiscard]] const IMPL& Impl() const { return *static_cast(this); } + + //region Iterators + private: + template + class ForwardIteratorBase + { + using BufferType = std::conditional_t; + using ValueType = std::conditional_t; + + BufferType* buffer; + size_t index; + int64_t remainder; + + public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = ValueType*; + using reference = ValueType&; + + ForwardIteratorBase(BufferType* buf, size_t startIdx, int64_t remainder) + : buffer(buf), index(startIdx), remainder(remainder) {} + + reference operator*() const { return buffer->data()[index]; } + pointer operator->() const { return &buffer->data()[index]; } + + ForwardIteratorBase& operator++() + { + if (remainder > 1) + { + if (index == buffer->capacity() - 1) + index = 0; + else + ++index; + } + remainder--; + return *this; + } + + ForwardIteratorBase operator++(int) + { + ForwardIteratorBase tmp = *this; + ++(*this); + return tmp; + } + + ForwardIteratorBase& operator--() + { + if (index == 0) + index = buffer->capacity() - 1; + else + --index; + remainder++; + return *this; + } + + ForwardIteratorBase operator--(int) + { + ForwardIteratorBase tmp = *this; + --(*this); + return tmp; + } + + friend bool operator==(const ForwardIteratorBase& a, const ForwardIteratorBase& b) + { + return a.buffer == b.buffer && a.index == b.index && a.remainder == b.remainder; + } + + friend bool operator!=(const ForwardIteratorBase& a, const ForwardIteratorBase& b) + { + return !(a == b); + } + }; + + template + class ReverseIteratorBase + { + using BufferType = std::conditional_t; + using ValueType = std::conditional_t; + + BufferType* buffer; + size_t index; + int64_t remainder; + + public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = ValueType*; + using reference = ValueType&; + + ReverseIteratorBase(BufferType* buf, size_t startIdx, int64_t remainder) + : buffer(buf), index(startIdx), remainder(remainder) {} + + reference operator*() const { return buffer->data()[index]; } + pointer operator->() const { return &buffer->data()[index]; } + + ReverseIteratorBase& operator++() + { + if (remainder > 1) + { + if (index == 0) + index = buffer->capacity() - 1; + else + --index; + } + remainder--; + return *this; + } + + ReverseIteratorBase operator++(int) + { + ReverseIteratorBase tmp = *this; + ++(*this); + return tmp; + } + + ReverseIteratorBase& operator--() + { + if (index == buffer->capacity() - 1) + index = 0; + else + ++index; + remainder++; + return *this; + } + + ReverseIteratorBase operator--(int) + { + ReverseIteratorBase tmp = *this; + --(*this); + return tmp; + } + + friend bool operator==(const ReverseIteratorBase& a, const ReverseIteratorBase& b) + { + return a.buffer == b.buffer && a.index == b.index && a.remainder == b.remainder; + } + + friend bool operator!=(const ReverseIteratorBase& a, const ReverseIteratorBase& b) + { + return !(a == b); + } + }; + + public: + using iterator = ForwardIteratorBase; + using const_iterator = ForwardIteratorBase; + + iterator begin() { return iterator(this, Impl().TailId(), count); } + iterator end() { return iterator(this, HeadId(), 0); } + + const_iterator begin() const { return cbegin(); } + const_iterator end() const { return cend(); } + + const_iterator cbegin() const { return const_iterator(this, Impl().TailId(), count); } + const_iterator cend() const { return const_iterator(this, HeadId(), 0); } + + using reverse_iterator = ReverseIteratorBase; + using const_reverse_iterator = ReverseIteratorBase; + + reverse_iterator rbegin() { return reverse_iterator(this, HeadId(), count); } + reverse_iterator rend() { return reverse_iterator(this, Impl().TailId(), 0); } + + const_reverse_iterator rbegin() const { return crbegin(); } + const_reverse_iterator rend() const { return crend(); } + + const_reverse_iterator crbegin() const { return const_reverse_iterator(this, HeadId(), count); } + const_reverse_iterator crend() const { return const_reverse_iterator(this, Impl().TailId(), 0); } + //endregion + + public: + [[nodiscard]] size_t capacity() const { return Impl().Capacity(); } + [[nodiscard]] T* data() { return Impl().Data(); } + [[nodiscard]] const T* data() const { return Impl().Data(); } + + public: + [[nodiscard]] size_t Count() const { return count; } + [[nodiscard]] size_t Size() const { return count; } + [[nodiscard]] bool IsEmpty() const { return count == 0; } + [[nodiscard]] bool HasFree() const { return Count() != capacity(); } + + T PopFront() + { + if (IsEmpty()) throw std::underflow_error("RingBuffer is empty"); + + T value = std::move(data()[head]); + data()[head].~T(); + if (head == 0) head = capacity() - 1; + else head--; + count--; + return value; + } + + T PopBack() + { + if (IsEmpty()) throw std::underflow_error("RingBuffer is empty"); + size_t tail = Impl().TailId(); + T value = std::move(data()[tail]); + data()[tail].~T(); + count--; + return value; + } + + void Clear() + { + while(count) + { + data()[head].~T(); + if (head == 0) head = capacity() - 1; + else head--; + count--; + } + } + + [[nodiscard]] T& Back() { return data()[Impl().TailId()]; } + [[nodiscard]] const T& Back() const { return data()[Impl().TailId()]; } + + [[nodiscard]] T& Front() { return data()[head]; } + [[nodiscard]] const T& Front() const { return data()[head]; } + + //region Insertion + void PushNoOverwrite(const T& value) { if (!HasFree()) throw std::overflow_error("RingBuffer is full"); Push(value); } + void PushNoOverwrite(T&& value) { if (!HasFree()) throw std::overflow_error("RingBuffer is full"); Push(std::move(value)); } + template + void EmplaceNoOverwrite(Args&&... args) + { + if (!HasFree()) throw std::overflow_error("RingBuffer is full"); + Emplace(std::forward(args...)); + } + + void Push(const T& value) + { + Impl().IncrementHead(); + if (HasFree()) count++; + else data()[head].~T(); + new (&data()[head]) T(value); + } + + void Push(T&& value) + { + Impl().IncrementHead(); + if (HasFree()) count++; + else data()[head].~T(); + new (&data()[head]) T(std::move(value)); + } + + template + void Emplace(Args&&... args) + { + Impl().IncrementHead(); + if (HasFree()) count++; + else data()[head].~T(); + new (&data()[head]) T(std::forward(args)...); + } + + [[nodiscard]] std::optional PushFront(const T& value) + { + if (HasFree()) + { + count++; + Impl().IncrementHead(); + new (&data()[head]) T(value); + return std::nullopt; + } + Impl().IncrementHead(); + std::optional oldData(std::move(Front())); + data()[head].~T(); + new (&data()[head]) T(value); + return oldData; + } + + [[nodiscard]] std::optional PushFront(T&& value) + { + if (HasFree()) + { + count++; + Impl().IncrementHead(); + new (&data()[head]) T(std::move(value)); + return std::nullopt; + } + Impl().IncrementHead(); + std::optional oldData(std::move(Front())); + data()[head].~T(); + new (&data()[head]) T(std::move(value)); + return oldData; + } + + template + [[nodiscard]] std::optional EmplaceFront(Args&&... args) + { + if (HasFree()) + { + count++; + Impl().IncrementHead(); + new (&data()[head]) T(std::forward(args)...); + return std::nullopt; + } + Impl().IncrementHead(); + std::optional oldData(std::move(Front())); + data()[head].~T(); + new (&data()[head]) T(std::forward(args)...); + return oldData; + } + + std::optional PushBack(const T& value) + { + if (HasFree()) + { + count++; + new (&data()[Impl().TailId()]) T(value); + return std::nullopt; + } + size_t tail = Impl().TailId(); + std::optional oldData(std::move(data()[tail])); + data()[tail].~T(); + new (&data()[tail]) T(value); + return oldData; + } + + std::optional PushBack(T&& value) + { + if (HasFree()) + { + count++; + new (&data()[Impl().TailId()]) T(std::move(value)); + return std::nullopt; + } + size_t tail = Impl().TailId(); + std::optional oldData(std::move(data()[tail])); + data()[tail].~T(); + new (&data()[tail]) T(std::move(value)); + return oldData; + } + + template + std::optional EmplaceBack(Args&&... args) + { + if (HasFree()) + { + count++; + new (&data()[Impl().TailId()]) T(std::forward(args)...); + return std::nullopt; + } + size_t tail = Impl().TailId(); + std::optional oldData(std::move(data()[tail])); + data()[tail].~T(); + new (&data()[tail]) T(std::forward(args)...); + return oldData; + } + //endregion + + [[nodiscard]] T& at(size_t idx) { if (idx >= Size()) throw std::range_error("Out of bounds"); return (*this)[idx]; } + [[nodiscard]] const T& at(size_t idx) const { if (idx >= Size()) throw std::range_error("Out of bounds"); return (*this)[idx]; } + + [[nodiscard]] T& operator[](size_t idx) + { + return data()[Impl().Index(idx)]; + } + + [[nodiscard]] const T& operator[](size_t idx) const + { + return data()[Impl().Index(idx)]; + } + + void Fill(const T& value) + { + while(HasFree()) { Push(value); } + } + }; + } + + template::max()> + class RingBuffer; + + template + class RingBuffer::max()> final : public internal_detail::NPCRingBufferBase::max()>> + { + typedef internal_detail::NPCRingBufferBase::max()>> Parent; + friend Parent; + + struct RawFreeDeleter { void operator()(void* ptr) const { ::operator delete(ptr); } }; + + std::unique_ptr m_data; + size_t m_capacity; + + [[nodiscard]] size_t TailId() const + { + if (Parent::IsEmpty()) [[unlikely]] return Parent::head; + return (Parent::HeadId() + 1 + Capacity() - Parent::Count()) % Capacity(); + } + + [[nodiscard]] size_t IncrementHead() + { + if (Parent::IsEmpty()) [[unlikely]] return Parent::head; + return (Parent::head = (Parent::HeadId() + 1) % Capacity()); + } + + [[nodiscard]] size_t Index(size_t i) const { return (TailId() + i) % Capacity(); } + public: + RingBuffer(const size_t size = 10): m_data(static_cast(::operator new(sizeof(T) * size))), m_capacity(size) {} + + ~RingBuffer() { Parent::Clear(); } + + [[nodiscard]] size_t Capacity() const { return m_capacity; } + [[nodiscard]] T* Data() { return m_data.get(); } + [[nodiscard]] const T* Data() const { return m_data.get(); } + }; + + template + class RingBuffer final : public internal_detail::NPCRingBufferBase> + { + typedef internal_detail::NPCRingBufferBase> Parent; + friend Parent; + constexpr static bool POW2 = (Utils::IsPow2(SIZE) && SIZE > 0); + constexpr static size_t MASK = SIZE - 1; + + using StorageType = typename std::aligned_storage::type; + StorageType m_data[SIZE]; // Uninitialized raw storage + + [[nodiscard]] size_t Index(size_t i) const + { + if constexpr (POW2) + return (Parent::HeadId() - (Parent::Count() - 1) - i) & MASK; + else + return (TailId() + i) % Capacity(); + } + + [[nodiscard]] size_t TailId() const + { + if (Parent::IsEmpty()) [[unlikely]] return Parent::head; + if constexpr (POW2) + return (Parent::HeadId() - (Parent::Count() - 1)) & MASK; + else + return (Parent::HeadId() + 1 + Capacity() - Parent::Count()) % Capacity(); + } + + [[nodiscard]] size_t IncrementHead() + { + if (Parent::IsEmpty()) [[unlikely]] return Parent::head; + if constexpr (POW2) + return (Parent::head = (Parent::HeadId() + 1) & MASK); + else + return (Parent::head = (Parent::HeadId() + 1) % Capacity()); + } + public: + ~RingBuffer() { Parent::Clear(); } + + [[nodiscard]] size_t Capacity() const { return SIZE; } + [[nodiscard]] T* Data() { return reinterpret_cast(m_data); } + [[nodiscard]] const T* Data() const { return reinterpret_cast(m_data); } + }; +} \ No newline at end of file diff --git a/tests/Data/Containers/RingBufferTest.cpp b/tests/Data/Containers/RingBufferTest.cpp new file mode 100644 index 0000000..27d7d6b --- /dev/null +++ b/tests/Data/Containers/RingBufferTest.cpp @@ -0,0 +1,179 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include +#include "Data/Containers/RingBuffer.hpp" +#include +#include + +using namespace OpenVulkano; + +// Helper to track construction/destruction +struct Tracked { + static inline int ctorCount = 0; + static inline int dtorCount = 0; + int value; + + Tracked(int v = 0) : value(v) { ++ctorCount; } + Tracked(const Tracked& other) : value(other.value) { ++ctorCount; } + Tracked(Tracked&& other) noexcept : value(other.value) { ++ctorCount; } + ~Tracked() { ++dtorCount; } + + Tracked& operator=(const Tracked&) = default; + Tracked& operator=(Tracked&&) = default; + + friend bool operator==(const Tracked& lhs, const Tracked& rhs) { + return lhs.value == rhs.value; + } +}; + +void reset_tracking() { + Tracked::ctorCount = 0; + Tracked::dtorCount = 0; +} + +TEST_CASE("RingBuffer Tracking Destruction", "[RingBuffer][Tracked]") { + reset_tracking(); + + { + RingBuffer buf; + buf.Push(Tracked(1)); + buf.Push(Tracked(2)); + buf.PopFront(); + buf.Clear(); + } + + REQUIRE(Tracked::dtorCount == Tracked::ctorCount); +} + +TEST_CASE("RingBuffer Push and Pop Operations", "[RingBuffer]") { + RingBuffer buf; + + buf.Push(1); + buf.Push(2); + buf.Push(3); + + REQUIRE(buf.Count() == 3); + REQUIRE_FALSE(buf.HasFree()); + + SECTION("PopFront/Back correctness") { + REQUIRE(buf.PopBack() == 1); + REQUIRE(buf.PopFront() == 3); + REQUIRE(buf.Count() == 1); + } + + SECTION("Clear empties the buffer") { + buf.Clear(); + REQUIRE(buf.IsEmpty()); + REQUIRE(buf.Count() == 0); + } +} + +TEST_CASE("PushBack and PushFront Overwrite Logic", "[RingBuffer]") { + RingBuffer buf; + buf.Push(1); + buf.Push(2); + + auto overwrittenFront = buf.PushFront(3); + REQUIRE(overwrittenFront.has_value()); + REQUIRE(overwrittenFront.value() == 1); + + auto overwrittenBack = buf.PushBack(4); + REQUIRE(overwrittenBack.has_value()); + REQUIRE(overwrittenBack.value() == 2); + + REQUIRE(buf.Front() == 3); + REQUIRE(buf.Back() == 4); +} + +TEST_CASE("at() bounds checking", "[RingBuffer]") { + RingBuffer buf; + buf.Push(100); + buf.Push(200); + + REQUIRE(buf.at(0) == 100); + REQUIRE(buf.at(1) == 200); + REQUIRE_THROWS_AS(buf.at(2), std::range_error); +} + +TEST_CASE("Index-based Access and Wraparound", "[RingBuffer]") { + RingBuffer buf; + + buf.Push(1); + buf.Push(2); + buf.Push(3); + REQUIRE(buf[0] == 1); + REQUIRE(buf[1] == 2); + REQUIRE(buf[2] == 3); + + buf.PopBack(); // Remove 1 + buf.Push(4); // Overwrites oldest (2) + + REQUIRE(buf[0] == 2); // Wrap-around behavior depends on ring position +} + +TEST_CASE("Emplace and EmplaceBack/Front work correctly", "[RingBuffer]") { + RingBuffer buf; + + buf.Emplace("first"); + buf.EmplaceBack("second"); + + REQUIRE(buf.Count() == 2); + REQUIRE(buf[1] == "first"); + REQUIRE(buf[0] == "second"); + + auto overwritten = buf.EmplaceFront("new"); + REQUIRE(overwritten.has_value()); + REQUIRE(overwritten.value() == "second"); + REQUIRE(buf.Front() == "new"); +} + +TEST_CASE("Iterators forward and reverse", "[RingBuffer]") { + RingBuffer buf; + buf.Push(10); + buf.Push(20); + buf.Push(30); + + std::vector forward; + for (int val : buf) forward.push_back(val); + REQUIRE(forward == std::vector{10, 20, 30}); + + std::vector reverse; + for (auto it = buf.rbegin(); it != buf.rend(); ++it) + reverse.push_back(*it); + REQUIRE(reverse == std::vector{30, 20, 10}); +} + +TEST_CASE("Const correctness in iterators", "[RingBuffer][Const]") { + RingBuffer buf; + buf.Push(5); + buf.Push(6); + + const auto& constBuf = buf; + + std::ostringstream oss; + for (auto it = constBuf.cbegin(); it != constBuf.cend(); ++it) { + oss << *it << " "; + } + + REQUIRE(oss.str() == "5 6 "); +} + +TEST_CASE("Dynamic RingBuffer behaves like static", "[RingBuffer][Dynamic]") { + RingBuffer buf(5); + + for (int i = 0; i < 5; ++i) + buf.Push(i * 10); + + REQUIRE(buf.Count() == 5); + REQUIRE_FALSE(buf.HasFree()); + REQUIRE(buf.Front() == 40); + REQUIRE(buf.Back() == 0); + + auto val = buf.PopBack(); + REQUIRE(val == 0); + REQUIRE(buf.Count() == 4); +}