diff --git a/src/framebuffer2.h b/src/framebuffer2.h --- a/src/framebuffer2.h +++ b/src/framebuffer2.h @@ -32,15 +32,20 @@ struct Range class FrameBuffer { public: + /// Returns size of the buffer. virtual unsigned size() const = 0; + /// Returns a sample from given index. virtual double sample(unsigned i) const = 0; + /// Returns minimum and maximum of the buffer values. virtual Range limits() const = 0; }; /// Common base class for index and writable frame buffers class ResizableBuffer : public FrameBuffer { - /// Resize buffer + /// Resize the buffer. + /// + /// @important Resizing to same value is an error. virtual void resize(unsigned n) = 0; }; diff --git a/src/ringbuffer.cpp b/src/ringbuffer.cpp new file mode 100644 --- /dev/null +++ b/src/ringbuffer.cpp @@ -0,0 +1,172 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with serialplot. If not, see . +*/ + +#include + +#include "ringbuffer.h" + +RingBuffer::RingBuffer(unsigned n) +{ + _size = n; + data = new double[_size](); + headIndex = 0; + + limInvalid = false; + limCache = {0, 0}; +} + +RingBuffer::~RingBuffer() +{ + delete[] data; +} + +unsigned RingBuffer::size() const +{ + return _size; +} + +double RingBuffer::sample(unsigned i) const +{ + unsigned index = headIndex + i; + if (index >= _size) index -= _size; + return data[index]; +} + +Range RingBuffer::limits() const +{ + if (limInvalid) updateLimits(); + return limCache; +} + +void RingBuffer::resize(unsigned n) +{ + Q_ASSERT(n != _size); + + int offset = (int) n - (int) _size; + if (offset == 0) return; + + double* newData = new double[n]; + + // move data to new array + int fill_start = offset > 0 ? offset : 0; + + for (int i = fill_start; i < int(n); i++) + { + newData[i] = sample(i - offset); + } + + // fill the beginning of the new data + if (fill_start > 0) + { + for (int i = 0; i < fill_start; i++) + { + newData[i] = 0; + } + } + + // data is ready, clean up and re-point + delete data; + data = newData; + headIndex = 0; + _size = n; + + // invalidate bounding rectangle + limInvalid = true; +} + +void RingBuffer::addSamples(double* samples, unsigned n) +{ + unsigned shift = n; + if (shift < _size) + { + unsigned x = _size - headIndex; // distance of `head` to end + + if (shift <= x) // there is enough room at the end of array + { + for (unsigned i = 0; i < shift; i++) + { + data[i+headIndex] = samples[i]; + } + + if (shift == x) // we used all the room at the end + { + headIndex = 0; + } + else + { + headIndex += shift; + } + } + else // there isn't enough room + { + for (unsigned i = 0; i < x; i++) // fill the end part + { + data[i+headIndex] = samples[i]; + } + for (unsigned i = 0; i < (shift-x); i++) // continue from the beginning + { + data[i] = samples[i+x]; + } + headIndex = shift-x; + } + } + else // number of new samples equal or bigger than current size (doesn't fit) + { + int x = shift - _size; + for (unsigned i = 0; i < _size; i++) + { + data[i] = samples[i+x]; + } + headIndex = 0; + } + + // invalidate cache + limInvalid = true; +} + +void RingBuffer::clear() +{ + for (unsigned i=0; i < _size; i++) + { + data[i] = 0.; + } + + limCache = {0, 0}; + limInvalid = false; +} + +void RingBuffer::updateLimits() const +{ + limCache.start = data[0]; + limCache.end = data[0]; + + for (unsigned i = 0; i < _size; i++) + { + if (data[i] > limCache.end) + { + limCache.end = data[i]; + } + else if (data[i] < limCache.start) + { + limCache.start = data[i]; + } + } + + limInvalid = false; +} diff --git a/src/ringbuffer.h b/src/ringbuffer.h new file mode 100644 --- /dev/null +++ b/src/ringbuffer.h @@ -0,0 +1,50 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with serialplot. If not, see . +*/ + +#ifndef RINGBUFFER_H +#define RINGBUFFER_H + +// IMPORTANT TODO: rename to "framebuffer.h" when stream work is done. +#include "framebuffer2.h" + +/// A fast buffer implementation for storing data. +class RingBuffer : public WFrameBuffer +{ +public: + RingBuffer(unsigned n); + ~RingBuffer(); + + virtual unsigned size() const; + virtual double sample(unsigned i) const; + virtual Range limits() const; + virtual void resize(unsigned n); + virtual void addSamples(double* samples, unsigned n); + virtual void clear(); + +private: + unsigned _size; ///< size of `data` + double* data; ///< storage + unsigned headIndex; ///< indicates the actual `0` index of the ring buffer + + mutable bool limInvalid; ///< Indicates that limits needs to be re-calculated + mutable Range limCache; ///< Cache for limits() + void updateLimits() const; ///< Updates limits cache +}; + +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -29,6 +29,7 @@ add_executable(Test EXCLUDE_FROM_ALL ../src/source.cpp ../src/indexbuffer.cpp ../src/linindexbuffer.cpp + ../src/ringbuffer.cpp ) add_test(NAME test1 COMMAND Test) qt5_use_modules(Test Widgets) diff --git a/tests/test.cpp b/tests/test.cpp --- a/tests/test.cpp +++ b/tests/test.cpp @@ -24,6 +24,7 @@ #include "source.h" #include "indexbuffer.h" #include "linindexbuffer.h" +#include "ringbuffer.h" TEST_CASE("samplepack with no X", "[memory]") { @@ -236,3 +237,130 @@ TEST_CASE("LinIndexBuffer", "[memory, bu REQUIRE(buf.sample(0) == -5.0); REQUIRE(buf.sample(19) == 5.0); } + +TEST_CASE("RingBuffer sizing", "[memory, buffer]") +{ + RingBuffer buf(10); + + REQUIRE(buf.size() == 10); + + buf.resize(5); + REQUIRE(buf.size() == 5); + + buf.resize(15); + REQUIRE(buf.size() == 15); +} + +TEST_CASE("RingBuffer initial values should be 0", "[memory, buffer]") +{ + RingBuffer buf(10); + + for (unsigned i = 0; i < 10; i++) + { + REQUIRE(buf.sample(i) == 0.); + } +} + +TEST_CASE("RingBuffer data access", "[memory, buffer]") +{ + RingBuffer buf(10); + double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + buf.addSamples(values, 10); + + REQUIRE(buf.size() == 10); + for (unsigned i = 0; i < 10; i++) + { + REQUIRE(buf.sample(i) == values[i]); + } + + buf.addSamples(values, 5); + + REQUIRE(buf.size() == 10); + for (unsigned i = 0; i < 5; i++) + { + REQUIRE(buf.sample(i) == values[i+5]); + } + for (unsigned i = 5; i < 10; i++) + { + REQUIRE(buf.sample(i) == values[i-5]); + } +} + +TEST_CASE("making RingBuffer bigger should keep end values", "[memory, buffer]") +{ + RingBuffer buf(5); + double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + buf.addSamples(values, 5); + buf.resize(10); + + REQUIRE(buf.size() == 10); + for (unsigned i = 0; i < 5; i++) + { + REQUIRE(buf.sample(i) == 0); + } + for (unsigned i = 5; i < 10; i++) + { + REQUIRE(buf.sample(i) == values[i-5]); + } +} + +TEST_CASE("making RingBuffer smaller should keep end values", "[memory, buffer]") +{ + RingBuffer buf(10); + double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + buf.addSamples(values, 10); + buf.resize(5); + + REQUIRE(buf.size() == 5); + for (unsigned i = 0; i < 5; i++) + { + REQUIRE(buf.sample(i) == values[i+5]); + } +} + +TEST_CASE("RingBuffer limits", "[memory, buffer]") +{ + RingBuffer buf(10); + double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + auto lim = buf.limits(); + REQUIRE(lim.start == 0.); + REQUIRE(lim.end == 0.); + + buf.addSamples(values, 10); + lim = buf.limits(); + REQUIRE(lim.start == 1.); + REQUIRE(lim.end == 10.); + + buf.addSamples(&values[9], 1); + lim = buf.limits(); + REQUIRE(lim.start == 2.); + REQUIRE(lim.end == 10.); + + buf.addSamples(values, 9); + buf.addSamples(values, 1); + lim = buf.limits(); + REQUIRE(lim.start == 1.); + REQUIRE(lim.end == 9.); +} + +TEST_CASE("RingBuffer clear", "[memory, buffer]") +{ + RingBuffer buf(10); + double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + buf.addSamples(values, 10); + buf.clear(); + + REQUIRE(buf.size() == 10); + for (unsigned i = 0; i < 10; i++) + { + REQUIRE(buf.sample(i) == 0.); + } + auto lim = buf.limits(); + REQUIRE(lim.start == 0.); + REQUIRE(lim.end == 0.); +}