diff --git a/doomsday/sdk/libcore/include/de/PointerSet b/doomsday/sdk/libcore/include/de/PointerSet new file mode 100644 index 0000000000..11a4ffe586 --- /dev/null +++ b/doomsday/sdk/libcore/include/de/PointerSet @@ -0,0 +1 @@ +#include "data/pointerset.h" diff --git a/doomsday/sdk/libcore/include/de/core/range.h b/doomsday/sdk/libcore/include/de/core/range.h index 8816fd7ec3..993ad7d2fe 100644 --- a/doomsday/sdk/libcore/include/de/core/range.h +++ b/doomsday/sdk/libcore/include/de/core/range.h @@ -151,6 +151,7 @@ struct Range } }; +typedef Range Rangeui16; typedef Range Rangei; typedef Range Rangeui; typedef Range Rangei64; diff --git a/doomsday/sdk/libcore/include/de/data/pointerset.h b/doomsday/sdk/libcore/include/de/data/pointerset.h new file mode 100644 index 0000000000..77a3634e79 --- /dev/null +++ b/doomsday/sdk/libcore/include/de/data/pointerset.h @@ -0,0 +1,86 @@ +/** @file pointerset.h Set of pointers. + * + * @authors Copyright (c) 2017 Jaakko Keränen + * + * @par License + * LGPL: http://www.gnu.org/licenses/lgpl.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. This program 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 Lesser + * General Public License for more details. You should have received a copy of + * the GNU Lesser General Public License along with this program; if not, see: + * http://www.gnu.org/licenses + */ + +#ifndef LIBDENG2_POINTERSET_H +#define LIBDENG2_POINTERSET_H + +#include "../libcore.h" +#include "../Range" +#include + +namespace de { + +/** + * Set of pointers. + * + * Light-weight class specifically designed to be used for observer audiences. Maintains + * a sorted vector of pointers. Insertions, deletions, and lookups are done with an + * O(log n) binary search. Insertions start at the middle to allow expansion to both + * directions. Removing individual pointers is allowed at any time. + */ +class DENG2_PUBLIC PointerSet +{ +public: + typedef void * Pointer; + typedef Pointer const * const_iterator; + typedef duint16 Flag; + + static Flag const AllowInsertionDuringIteration; + static Flag const BeingIterated; + +public: + PointerSet(); + PointerSet(PointerSet const &other); + PointerSet(PointerSet &&moved); + + ~PointerSet(); + + void insert(Pointer ptr); + void remove(Pointer ptr); + bool contains(Pointer ptr) const; + void clear(); + + PointerSet &operator = (PointerSet &&moved); + + inline void setFlags(Flag flags, FlagOpArg op = SetFlags) { + applyFlagOperation(_flags, flags, op); + } + + inline Flag flags() const { return _flags; } + inline int size() const { return _range.size(); } + inline Rangeui16 usedRange() const { return _range; } + inline int allocatedSize() const { return _size; } + inline const_iterator begin() const { return _pointers + _range.start; } + inline const_iterator end() const { return _pointers + _range.end; } + +protected: + Rangeui16 locate(Pointer ptr) const; + + inline Pointer at(duint16 pos) const { return _pointers[pos]; } + +private: + Pointer *_pointers; + duint16 _flags; + duint16 _size; + Rangeui16 _range; +}; + +} // namespace de + + +#endif // LIBDENG2_POINTERSET_H diff --git a/doomsday/sdk/libcore/src/data/pointerset.cpp b/doomsday/sdk/libcore/src/data/pointerset.cpp new file mode 100644 index 0000000000..9b8c016642 --- /dev/null +++ b/doomsday/sdk/libcore/src/data/pointerset.cpp @@ -0,0 +1,232 @@ +/** @file pointerset.cpp Set of pointers. + * + * @authors Copyright (c) 2017 Jaakko Keränen + * + * @par License + * LGPL: http://www.gnu.org/licenses/lgpl.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. This program 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 Lesser + * General Public License for more details. You should have received a copy of + * the GNU Lesser General Public License along with this program; if not, see: + * http://www.gnu.org/licenses + */ + +#include "de/PointerSet" +#include + +namespace de { + +static duint16 const POINTERSET_MIN_ALLOC = 2; +static duint16 const POINTERSET_MAX_SIZE = 0xffff; + +PointerSet::Flag const PointerSet::AllowInsertionDuringIteration = 0x1; +PointerSet::Flag const PointerSet::BeingIterated = 0x2; + +PointerSet::PointerSet() + : _pointers(nullptr) + , _flags (0) + , _size (0) +{} + +PointerSet::PointerSet(PointerSet const &other) + : _flags(other._flags) + , _size (other._size) + , _range(other._range) +{ + auto const bytes = sizeof(Pointer) * _size; + _pointers = reinterpret_cast(malloc(bytes)); + std::memcpy(_pointers, other._pointers, bytes); +} + +PointerSet::PointerSet(PointerSet &&moved) + : _pointers(moved._pointers) + , _flags (moved._flags) + , _size (moved._size) + , _range (moved._range) +{ + moved._pointers = nullptr; // taken +} + +PointerSet::~PointerSet() +{ + free(_pointers); +} + +void PointerSet::insert(Pointer ptr) +{ + if (!_pointers) + { + // Make a minimum allocation. + _size = POINTERSET_MIN_ALLOC; + _pointers = reinterpret_cast(calloc(sizeof(Pointer), _size)); + } + + if (_range.isEmpty()) + { + // Nothing is currently allocated. Place the first item in the middle. + duint16 const pos = _size / 2; + _pointers[pos] = ptr; + _range.start = pos; + _range.end = pos + 1; + } + else + { + auto const loc = locate(ptr); + if (!loc.isEmpty()) return; // Already got it. + + if (_flags & BeingIterated) + { + DENG2_ASSERT(_flags & AllowInsertionDuringIteration); + } + + // Do we need to expand? + if (_range.size() == _size) + { + DENG2_ASSERT(_size < POINTERSET_MAX_SIZE); + + duint const oldSize = _size; + _size = (_size < 0x8000? (_size * 2) : POINTERSET_MAX_SIZE); + _pointers = reinterpret_cast(realloc(_pointers, sizeof(Pointer) * _size)); + std::memset(_pointers + oldSize, 0, sizeof(Pointer) * (_size - oldSize)); + } + + // Addition to the ends with room to spare? + duint16 const pos = loc.start; + if (pos == _range.start && _range.start > 0) + { + _pointers[--_range.start] = ptr; + } + else if (pos == _range.end && _range.end < _size) + { + _pointers[_range.end++] = ptr; + } + else // Need to move first to make room. + { + duint16 const middle = (_range.start + _range.end + 1)/2; + if ((pos > middle && _range.end < _size) || // Less stuff to move toward the end. + _range.start == 0) + { + DENG2_ASSERT(_range.end < _size); + std::memmove(_pointers + pos + 1, + _pointers + pos, + sizeof(Pointer) * (_range.end - pos)); + _range.end++; + _pointers[pos] = ptr; + } + else + { + std::memmove(_pointers + _range.start - 1, + _pointers + _range.start, + sizeof(Pointer) * (pos - _range.start + 1)); + _range.start--; + _pointers[pos - 1] = ptr; + } + } + } +} + +void PointerSet::remove(Pointer ptr) +{ + auto const loc = locate(ptr); + if (!loc.isEmpty()) + { + DENG2_ASSERT(!_range.isEmpty()); + + // Removing the first or last item needs just a range adjustment. + if (loc.start == _range.start) + { + _pointers[_range.start++] = nullptr; + } + else if (loc.start == _range.end - 1 && + !(_flags & BeingIterated)) + { + _pointers[--_range.end] = nullptr; + } + else + { + // Move forward so that during iteration the future items won't be affected. + std::memmove(_pointers + _range.start + 1, + _pointers + _range.start, + sizeof(Pointer) * (loc.start - _range.start)); + _pointers[_range.start++] = nullptr; + } + + DENG2_ASSERT(_range.start <= _range.end); + } +} + +bool PointerSet::contains(Pointer ptr) const +{ + return !locate(ptr).isEmpty(); +} + +void PointerSet::clear() +{ + if (_pointers) + { + std::memset(_pointers, 0, sizeof(Pointer) * _size); + _range = Rangeui16(); + } +} + +PointerSet &PointerSet::operator = (PointerSet &&moved) +{ + free(_pointers); + _pointers = moved._pointers; + moved._pointers = nullptr; + + _flags = moved._flags; + _size = moved._size; + _range = moved._range; + return *this; +} + +Rangeui16 PointerSet::locate(Pointer ptr) const +{ + // We will narrow down the span until the pointer is found or we'll know where + // it would be if it were inserted. + Rangeui16 span = _range; + + while (!span.isEmpty()) + { + // Arrived at a single item? + if (span.size() == 1) + { + if (at(span.start) == ptr) + { + return span; // Found it. + } + // Then the ptr would go before or after this position. + if (ptr < at(span.start)) + { + return Rangeui16(span.start, span.start); + } + return Rangeui16(span.end, span.end); + } + + // Narrow down the search by a half. + Rangeui16 const rightHalf((span.start + span.end + 1) / 2, span.end); + Pointer const mid = at(rightHalf.start); + if (ptr == mid) + { + // Oh, it's here. + return Rangeui16(rightHalf.start, rightHalf.start + 1); + } + else if (ptr > mid) + { + span = rightHalf; + } + else + { + span = Rangeui16(span.start, rightHalf.start); + } + } + return span; +} + +} // namespace de diff --git a/doomsday/tests/CMakeLists.txt b/doomsday/tests/CMakeLists.txt index 480b903f6a..65df7535d3 100644 --- a/doomsday/tests/CMakeLists.txt +++ b/doomsday/tests/CMakeLists.txt @@ -13,6 +13,7 @@ if (DENG_ENABLE_TESTS) add_subdirectory (test_commandline) add_subdirectory (test_info) add_subdirectory (test_log) + add_subdirectory (test_pointerset) add_subdirectory (test_record) add_subdirectory (test_script) add_subdirectory (test_string) diff --git a/doomsday/tests/test_pointerset/CMakeLists.txt b/doomsday/tests/test_pointerset/CMakeLists.txt new file mode 100644 index 0000000000..35bed7cf1c --- /dev/null +++ b/doomsday/tests/test_pointerset/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required (VERSION 3.1) +project (DENG_TEST_POINTERSET) +include (../TestConfig.cmake) + +deng_test (test_pointerset main.cpp) diff --git a/doomsday/tests/test_pointerset/main.cpp b/doomsday/tests/test_pointerset/main.cpp new file mode 100644 index 0000000000..dbf88931bb --- /dev/null +++ b/doomsday/tests/test_pointerset/main.cpp @@ -0,0 +1,146 @@ +/** + * @file main.cpp + * + * PointerSet tests. @ingroup tests + * + * @author Copyright © 2017 Jaakko Keränen + * + * @par License + * GPL: http://www.gnu.org/licenses/gpl.html + * + * This program 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 2 of the License, or (at your + * option) any later version. This program 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 this program; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#include +#include + +using namespace de; + +void printSet(PointerSet const &pset) +{ + qDebug() << "[ Size:" << pset.size() << "/" << pset.allocatedSize() << "range:" + << pset.usedRange().asText(); + for (auto const p : pset) + { + qDebug() << " " << QString("%1").arg(reinterpret_cast(p), 16, 16, QChar('0')).toLatin1().constData(); + } + qDebug() << "]"; +} + +int main(int, char **) +{ + try + { + PointerSet::Pointer a = reinterpret_cast(0x1000); + PointerSet::Pointer b = reinterpret_cast(0x2000); + PointerSet::Pointer c = reinterpret_cast(0x3000); + PointerSet::Pointer d = reinterpret_cast(0x4000); + PointerSet::Pointer e = reinterpret_cast(0x5000); + + PointerSet pset; + qDebug() << "Empty PointerSet:"; + printSet(pset); + + pset.insert(a); + qDebug() << "Added one pointer:"; + printSet(pset); + + pset.insert(a); + qDebug() << "'a' is there?" << pset.contains(a); + qDebug() << "'b' should not be there?" << pset.contains(b); + + qDebug() << "Trying to remove a non-existing pointer."; + pset.remove(b); + printSet(pset); + + pset.remove(a); + qDebug() << "Removed the pointer:"; + printSet(pset); + + qDebug() << "Adding everything:"; + pset.insert(d); + pset.insert(a); + pset.insert(c); + pset.insert(b); + pset.insert(e); + printSet(pset); + + qDebug() << "Removing the ends:"; + pset.remove(a); + pset.remove(e); + printSet(pset); + + qDebug() << "Removing the middle:"; + pset.remove(c); + printSet(pset); + + qDebug() << "Adding everything again:"; + pset.insert(d); + pset.insert(a); + pset.insert(c); + pset.insert(b); + pset.insert(e); + printSet(pset); + + qDebug() << "Removing everything:"; + pset.remove(d); + pset.remove(a); + pset.remove(c); + pset.remove(b); + pset.remove(e); + printSet(pset); + + qDebug() << "Adding one:"; + pset.insert(e); + printSet(pset); + + qDebug() << "Adding another:"; + pset.insert(a); + printSet(pset); + + qDebug() << "Removing during iteration:"; + pset.insert(e); + pset.insert(d); + pset.insert(c); + pset.insert(b); + pset.insert(a); + pset.setFlags(PointerSet::BeingIterated); + for (auto i : pset) + { + if (i == c) + { + qDebug() << "Removing 'c'..."; + pset.remove(i); + } + if (i == a) + { + qDebug() << "Removing 'a'..."; + pset.remove(i); + } + if (i == e) + { + qDebug() << "Removing 'e'..."; + pset.remove(i); + } + pset.remove(d); + } + pset.setFlags(PointerSet::BeingIterated, false); + printSet(pset); + } + catch (Error const &err) + { + qWarning() << err.asText() << "\n"; + } + + qDebug() << "Exiting main()...\n"; + return 0; +}