diff --git a/src/inc/til/bitmap.h b/src/inc/til/bitmap.h index f992fe765b6..eff5f1f64d3 100644 --- a/src/inc/til/bitmap.h +++ b/src/inc/til/bitmap.h @@ -11,6 +11,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { namespace details { + template class _bitmap_const_iterator { public: @@ -20,7 +21,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" using pointer = typename const til::rectangle*; using reference = typename const til::rectangle&; - _bitmap_const_iterator(const dynamic_bitset<>& values, til::rectangle rc, ptrdiff_t pos) : + _bitmap_const_iterator(const dynamic_bitset& values, til::rectangle rc, ptrdiff_t pos) : _values(values), _rc(rc), _pos(pos), @@ -74,7 +75,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } private: - const dynamic_bitset<>& _values; + const dynamic_bitset& _values; const til::rectangle _rc; ptrdiff_t _pos; ptrdiff_t _nextPos; @@ -130,372 +131,459 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } } }; - } - - class bitmap - { - public: - using const_iterator = details::_bitmap_const_iterator; - bitmap() noexcept : - _sz{}, - _rc{}, - _bits{}, - _runs{} + template> + class bitmap { - } + public: + using allocator_type = Allocator; + using const_iterator = details::_bitmap_const_iterator; - bitmap(til::size sz) : - bitmap(sz, false) - { - } + private: + using run_allocator_type = typename std::allocator_traits::template rebind_alloc; - bitmap(til::size sz, bool fill) : - _sz(sz), - _rc(sz), - _bits(_sz.area()), - _runs{} - { - if (fill) + public: + explicit bitmap(const allocator_type& allocator) noexcept : + _alloc{ allocator }, + _sz{}, + _rc{}, + _bits{ _alloc }, + _runs{ _alloc } { - set_all(); } - } - constexpr bool operator==(const bitmap& other) const noexcept - { - return _sz == other._sz && - _rc == other._rc && - _bits == other._bits; - // _runs excluded because it's a cache of generated state. - } + bitmap() noexcept : + bitmap(allocator_type{}) + { + } - constexpr bool operator!=(const bitmap& other) const noexcept - { - return !(*this == other); - } + bitmap(til::size sz) : + bitmap(sz, false, allocator_type{}) + { + } - const_iterator begin() const - { - return const_iterator(_bits, _sz, 0); - } + bitmap(til::size sz, const allocator_type& allocator) : + bitmap(sz, false, allocator) + { + } - const_iterator end() const - { - return const_iterator(_bits, _sz, _sz.area()); - } + bitmap(til::size sz, bool fill, const allocator_type& allocator) : + _alloc{ allocator }, + _sz(sz), + _rc(sz), + _bits(_sz.area(), fill ? std::numeric_limits::max() : 0, _alloc), + _runs{ _alloc } + { + } - const std::vector& runs() const - { - // If we don't have cached runs, rebuild. - if (!_runs.has_value()) + bitmap(til::size sz, bool fill) : + bitmap(sz, fill, allocator_type{}) { - _runs.emplace(begin(), end()); } - // Return a reference to the runs. - return _runs.value(); - } + bitmap(const bitmap& other) : + _alloc{ std::allocator_traits::select_on_container_copy_construction(other._alloc) }, + _sz{ other._sz }, + _rc{ other._rc }, + _bits{ other._bits }, + _runs{ other._runs } + { + // copy constructor is required to call select_on_container_copy + } - // optional fill the uncovered area with bits. - void translate(const til::point delta, bool fill = false) - { - if (delta.x() == 0) - { - // fast path by using bit shifting - translate_y(delta.y(), fill); - return; - } - - // FUTURE: PERF: GH #4015: This could use in-place walk semantics instead of a temporary. - til::bitmap other{ _sz }; - - for (auto run : *this) - { - // Offset by the delta - run += delta; - - // Intersect with the bounds of our bitmap area - // as part of it could have slid out of bounds. - run &= _rc; - - // Set it into the new bitmap. - other.set(run); - } - - // If we were asked to fill... find the uncovered region. - if (fill) - { - // Original Rect of As. - // - // X <-- origin - // A A A A - // A A A A - // A A A A - // A A A A - const auto originalRect = _rc; - - // If Delta = (2, 2) - // Translated Rect of Bs. - // - // X <-- origin - // - // - // B B B B - // B B B B - // B B B B - // B B B B - const auto translatedRect = _rc + delta; - - // Subtract the B from the A one to see what wasn't filled by the move. - // C is the overlap of A and B: - // - // X <-- origin - // A A A A 1 1 1 1 - // A A A A 1 1 1 1 - // A A C C B B subtract 2 2 - // A A C C B B ---------> 2 2 - // B B B B A - B - // B B B B - // - // 1 and 2 are the spaces to fill that are "uncovered". - const auto fillRects = originalRect - translatedRect; - for (const auto& f : fillRects) + bitmap& operator=(const bitmap& other) + { + if constexpr (std::allocator_traits::propagate_on_container_copy_assignment::value) { - other.set(f); + _alloc = other._alloc; } + _sz = other._sz; + _rc = other._rc; + _bits = other._bits; + _runs = other._runs; + return *this; } - // Swap us with the temporary one. - std::swap(other, *this); - } + bitmap(bitmap&& other) noexcept : + _alloc{ std::move(other._alloc) }, + _sz{ std::move(other._sz) }, + _rc{ std::move(other._rc) }, + _bits{ std::move(other._bits) }, + _runs{ std::move(other._runs) } + { + } - void set(const til::point pt) - { - THROW_HR_IF(E_INVALIDARG, !_rc.contains(pt)); - _runs.reset(); // reset cached runs on any non-const method + bitmap& operator=(bitmap&& other) noexcept + { + if constexpr (std::allocator_traits::propagate_on_container_move_assignment::value) + { + _alloc = std::move(other._alloc); + } + _bits = std::move(other._bits); + _runs = std::move(other._runs); + _sz = std::move(other._sz); + _rc = std::move(other._rc); + return *this; + } - _bits.set(_rc.index_of(pt)); - } + ~bitmap() {} - void set(const til::rectangle rc) - { - THROW_HR_IF(E_INVALIDARG, !_rc.contains(rc)); - _runs.reset(); // reset cached runs on any non-const method + void swap(bitmap& other) + { + if constexpr (std::allocator_traits::propagate_on_container_swap::value) + { + std::swap(_alloc, other._alloc); + } + std::swap(_bits, other._bits); + std::swap(_runs, other._runs); + std::swap(_sz, other._sz); + std::swap(_rc, other._rc); + } - for (auto row = rc.top(); row < rc.bottom(); ++row) + constexpr bool operator==(const bitmap& other) const noexcept { - _bits.set(_rc.index_of(til::point{ rc.left(), row }), rc.width(), true); + return _sz == other._sz && + _rc == other._rc && + _bits == other._bits; + // _runs excluded because it's a cache of generated state. } - } - void set_all() noexcept - { - _runs.reset(); // reset cached runs on any non-const method - _bits.set(); - } + constexpr bool operator!=(const bitmap& other) const noexcept + { + return !(*this == other); + } - void reset_all() noexcept - { - _runs.reset(); // reset cached runs on any non-const method - _bits.reset(); - } + const_iterator begin() const + { + return const_iterator(_bits, _sz, 0); + } - // True if we resized. False if it was the same size as before. - // Set fill if you want the new region (on growing) to be marked dirty. - bool resize(til::size size, bool fill = false) - { - _runs.reset(); // reset cached runs on any non-const method + const_iterator end() const + { + return const_iterator(_bits, _sz, _sz.area()); + } - // Don't resize if it's not different - if (_sz != size) + const std::vector& runs() const { - // Make a new bitmap for the other side, empty initially. - auto newMap = bitmap(size, false); + // If we don't have cached runs, rebuild. + if (!_runs.has_value()) + { + _runs.emplace(begin(), end()); + } + + // Return a reference to the runs. + return _runs.value(); + } + + // optional fill the uncovered area with bits. + void translate(const til::point delta, bool fill = false) + { + if (delta.x() == 0) + { + // fast path by using bit shifting + translate_y(delta.y(), fill); + return; + } + + // FUTURE: PERF: GH #4015: This could use in-place walk semantics instead of a temporary. + bitmap other{ _sz, _alloc }; - // Copy any regions that overlap from this map to the new one. - // Just iterate our runs... - for (const auto& run : *this) + for (auto run : *this) { - // intersect them with the new map - // so we don't attempt to set bits that fit outside - // the new one. - const auto intersect = run & newMap._rc; + // Offset by the delta + run += delta; - // and if there is still anything left, set them. - if (!intersect.empty()) - { - newMap.set(intersect); - } + // Intersect with the bounds of our bitmap area + // as part of it could have slid out of bounds. + run &= _rc; + + // Set it into the new bitmap. + other.set(run); } - // Then, if we were requested to fill the new space on growing, - // find the space in the new rectangle that wasn't in the old - // and fill it up. + // If we were asked to fill... find the uncovered region. if (fill) { - // A subtraction will yield anything in the new that isn't - // a part of the old. - const auto newAreas = newMap._rc - _rc; - for (const auto& area : newAreas) + // Original Rect of As. + // + // X <-- origin + // A A A A + // A A A A + // A A A A + // A A A A + const auto originalRect = _rc; + + // If Delta = (2, 2) + // Translated Rect of Bs. + // + // X <-- origin + // + // + // B B B B + // B B B B + // B B B B + // B B B B + const auto translatedRect = _rc + delta; + + // Subtract the B from the A one to see what wasn't filled by the move. + // C is the overlap of A and B: + // + // X <-- origin + // A A A A 1 1 1 1 + // A A A A 1 1 1 1 + // A A C C B B subtract 2 2 + // A A C C B B ---------> 2 2 + // B B B B A - B + // B B B B + // + // 1 and 2 are the spaces to fill that are "uncovered". + const auto fillRects = originalRect - translatedRect; + for (const auto& f : fillRects) { - newMap.set(area); + other.set(f); } } - // Swap and return. - std::swap(newMap, *this); + // Swap us with the temporary one. + std::swap(other, *this); + } - return true; + void set(const til::point pt) + { + THROW_HR_IF(E_INVALIDARG, !_rc.contains(pt)); + _runs.reset(); // reset cached runs on any non-const method + + _bits.set(_rc.index_of(pt)); } - else + + void set(const til::rectangle rc) { - return false; + THROW_HR_IF(E_INVALIDARG, !_rc.contains(rc)); + _runs.reset(); // reset cached runs on any non-const method + + for (auto row = rc.top(); row < rc.bottom(); ++row) + { + _bits.set(_rc.index_of(til::point{ rc.left(), row }), rc.width(), true); + } } - } - constexpr bool one() const noexcept - { - return _bits.count() == 1; - } + void set_all() noexcept + { + _runs.reset(); // reset cached runs on any non-const method + _bits.set(); + } - constexpr bool any() const noexcept - { - return !none(); - } + void reset_all() noexcept + { + _runs.reset(); // reset cached runs on any non-const method + _bits.reset(); + } - constexpr bool none() const noexcept - { - return _bits.none(); - } + // True if we resized. False if it was the same size as before. + // Set fill if you want the new region (on growing) to be marked dirty. + bool resize(til::size size, bool fill = false) + { + _runs.reset(); // reset cached runs on any non-const method - constexpr bool all() const noexcept - { - return _bits.all(); - } + // Don't resize if it's not different + if (_sz != size) + { + // Make a new bitmap for the other side, empty initially. + bitmap newMap{ size, false, _alloc }; - constexpr til::size size() const noexcept - { - return _sz; - } + // Copy any regions that overlap from this map to the new one. + // Just iterate our runs... + for (const auto& run : *this) + { + // intersect them with the new map + // so we don't attempt to set bits that fit outside + // the new one. + const auto intersect = run & newMap._rc; + + // and if there is still anything left, set them. + if (!intersect.empty()) + { + newMap.set(intersect); + } + } - std::wstring to_string() const - { - std::wstringstream wss; - wss << std::endl - << L"Bitmap of size " << _sz.to_string() << " contains the following dirty regions:" << std::endl; - wss << L"Runs:" << std::endl; + // Then, if we were requested to fill the new space on growing, + // find the space in the new rectangle that wasn't in the old + // and fill it up. + if (fill) + { + // A subtraction will yield anything in the new that isn't + // a part of the old. + const auto newAreas = newMap._rc - _rc; + for (const auto& area : newAreas) + { + newMap.set(area); + } + } - for (auto& item : *this) - { - wss << L"\t- " << item.to_string() << std::endl; - } + // Swap and return. + std::swap(newMap, *this); - return wss.str(); - } + return true; + } + else + { + return false; + } + } - private: - void translate_y(ptrdiff_t delta_y, bool fill) - { - if (delta_y == 0) + constexpr bool one() const noexcept { - return; + return _bits.count() == 1; } - const auto bitShift = delta_y * _sz.width(); + constexpr bool any() const noexcept + { + return !none(); + } -#pragma warning(push) - // we can't depend on GSL here, so we use static_cast for explicit narrowing -#pragma warning(disable : 26472) - const auto newBits = static_cast(std::abs(bitShift)); -#pragma warning(pop) - const bool isLeftShift = bitShift > 0; + constexpr bool none() const noexcept + { + return _bits.none(); + } - if (newBits >= _bits.size()) + constexpr bool all() const noexcept { - if (fill) - { - set_all(); - } - else - { - reset_all(); - } - return; + return _bits.all(); } - if (isLeftShift) + constexpr til::size size() const noexcept { - // This operator doesn't modify the size of `_bits`: the - // new bits are set to 0. - _bits <<= newBits; + return _sz; } - else + + std::wstring to_string() const { - _bits >>= newBits; + std::wstringstream wss; + wss << std::endl + << L"Bitmap of size " << _sz.to_string() << " contains the following dirty regions:" << std::endl; + wss << L"Runs:" << std::endl; + + for (auto& item : *this) + { + wss << L"\t- " << item.to_string() << std::endl; + } + + return wss.str(); } - if (fill) + private: + void translate_y(ptrdiff_t delta_y, bool fill) { + if (delta_y == 0) + { + return; + } + + const auto bitShift = delta_y * _sz.width(); + +#pragma warning(push) + // we can't depend on GSL here, so we use static_cast for explicit narrowing +#pragma warning(disable : 26472) + const auto newBits = static_cast(std::abs(bitShift)); +#pragma warning(pop) + const bool isLeftShift = bitShift > 0; + + if (newBits >= _bits.size()) + { + if (fill) + { + set_all(); + } + else + { + reset_all(); + } + return; + } + if (isLeftShift) { - _bits.set(0, newBits, true); + // This operator doesn't modify the size of `_bits`: the + // new bits are set to 0. + _bits <<= newBits; } else { - _bits.set(_bits.size() - newBits, newBits, true); + _bits >>= newBits; } - } - _runs.reset(); // reset cached runs on any non-const method - } + if (fill) + { + if (isLeftShift) + { + _bits.set(0, newBits, true); + } + else + { + _bits.set(_bits.size() - newBits, newBits, true); + } + } - til::size _sz; - til::rectangle _rc; - dynamic_bitset<> _bits; + _runs.reset(); // reset cached runs on any non-const method + } + + allocator_type _alloc; + til::size _sz; + til::rectangle _rc; + dynamic_bitset _bits; - mutable std::optional> _runs; + mutable std::optional> _runs; #ifdef UNIT_TESTING - friend class ::BitmapTests; + friend class ::BitmapTests; #endif - }; + }; + + } + + using bitmap = ::til::details::bitmap<>; + + namespace pmr + { + using bitmap = ::til::details::bitmap>; + } } #ifdef __WEX_COMMON_H__ namespace WEX::TestExecution { - template<> - class VerifyOutputTraits<::til::bitmap> + template + class VerifyOutputTraits<::til::details::bitmap> { public: - static WEX::Common::NoThrowString ToString(const ::til::bitmap& rect) + static WEX::Common::NoThrowString ToString(const ::til::details::bitmap& rect) { return WEX::Common::NoThrowString(rect.to_string().c_str()); } }; - template<> - class VerifyCompareTraits<::til::bitmap, ::til::bitmap> + template + class VerifyCompareTraits<::til::details::bitmap, ::til::details::bitmap> { public: - static bool AreEqual(const ::til::bitmap& expected, const ::til::bitmap& actual) noexcept + static bool AreEqual(const ::til::details::bitmap& expected, const ::til::details::bitmap& actual) noexcept { return expected == actual; } - static bool AreSame(const ::til::bitmap& expected, const ::til::bitmap& actual) noexcept + static bool AreSame(const ::til::details::bitmap& expected, const ::til::details::bitmap& actual) noexcept { return &expected == &actual; } - static bool IsLessThan(const ::til::bitmap& expectedLess, const ::til::bitmap& expectedGreater) = delete; + static bool IsLessThan(const ::til::details::bitmap& expectedLess, const ::til::details::bitmap& expectedGreater) = delete; - static bool IsGreaterThan(const ::til::bitmap& expectedGreater, const ::til::bitmap& expectedLess) = delete; + static bool IsGreaterThan(const ::til::details::bitmap& expectedGreater, const ::til::details::bitmap& expectedLess) = delete; - static bool IsNull(const ::til::bitmap& object) noexcept + static bool IsNull(const ::til::details::bitmap& object) noexcept { - return object == til::bitmap{}; + return object == til::details::bitmap{}; } }; diff --git a/src/til/ut_til/BitmapTests.cpp b/src/til/ut_til/BitmapTests.cpp index 9be1b87e792..30fb4c545d7 100644 --- a/src/til/ut_til/BitmapTests.cpp +++ b/src/til/ut_til/BitmapTests.cpp @@ -13,14 +13,16 @@ class BitmapTests { TEST_CLASS(BitmapTests); + template void _checkBits(const til::rectangle& bitsOn, - const til::bitmap& map) + const til::details::bitmap& map) { _checkBits(std::vector{ bitsOn }, map); } + template void _checkBits(const std::vector& bitsOn, - const til::bitmap& map) + const til::details::bitmap& map) { Log::Comment(L"Check all bits in map."); // For every point in the map... @@ -838,7 +840,145 @@ class BitmapTests // 0 1 1 0 _ F F _ Log::Comment(L"Set up a bitmap with some runs."); - til::bitmap map{ til::size{ 4, 4 } }; + til::bitmap map{ til::size{ 4, 4 }, false }; + + // 0 0 0 0 |1 1|0 0 + // 0 0 0 0 0 0 0 0 + // 0 0 0 0 --> 0 0 0 0 + // 0 0 0 0 0 0 0 0 + map.set(til::rectangle{ til::point{ 0, 0 }, til::size{ 2, 1 } }); + + // 1 1 0 0 1 1 0 0 + // 0 0 0 0 0 0|1|0 + // 0 0 0 0 --> 0 0|1|0 + // 0 0 0 0 0 0|1|0 + map.set(til::rectangle{ til::point{ 2, 1 }, til::size{ 1, 3 } }); + + // 1 1 0 0 1 1 0|1| + // 0 0 1 0 0 0 1|1| + // 0 0 1 0 --> 0 0 1 0 + // 0 0 1 0 0 0 1 0 + map.set(til::rectangle{ til::point{ 3, 0 }, til::size{ 1, 2 } }); + + // 1 1 0 1 1 1 0 1 + // 0 0 1 1 |1|0 1 1 + // 0 0 1 0 --> 0 0 1 0 + // 0 0 1 0 0 0 1 0 + map.set(til::point{ 0, 1 }); + + // 1 1 0 1 1 1 0 1 + // 1 0 1 1 1 0 1 1 + // 0 0 1 0 --> 0 0 1 0 + // 0 0 1 0 0|1|1 0 + map.set(til::point{ 1, 3 }); + + Log::Comment(L"Building the expected run rectangles."); + + // Reminder, we're making 6 rectangle runs A-F like this: + // A A _ B + // C _ D D + // _ _ E _ + // _ F F _ + til::some expected; + expected.push_back(til::rectangle{ til::point{ 0, 0 }, til::size{ 2, 1 } }); + expected.push_back(til::rectangle{ til::point{ 3, 0 }, til::size{ 1, 1 } }); + expected.push_back(til::rectangle{ til::point{ 0, 1 }, til::size{ 1, 1 } }); + expected.push_back(til::rectangle{ til::point{ 2, 1 }, til::size{ 2, 1 } }); + expected.push_back(til::rectangle{ til::point{ 2, 2 }, til::size{ 1, 1 } }); + expected.push_back(til::rectangle{ til::point{ 1, 3 }, til::size{ 2, 1 } }); + + Log::Comment(L"Run the iterator and collect the runs."); + til::some actual; + for (auto run : map.runs()) + { + actual.push_back(run); + } + + Log::Comment(L"Verify they match what we expected."); + VERIFY_ARE_EQUAL(expected, actual); + + Log::Comment(L"Clear the map and iterate and make sure we get no results."); + map.reset_all(); + + expected.clear(); + actual.clear(); + for (auto run : map.runs()) + { + actual.push_back(run); + } + + Log::Comment(L"Verify they're empty."); + VERIFY_ARE_EQUAL(expected, actual); + + Log::Comment(L"Set point and validate runs updated."); + const til::point setPoint{ 2, 2 }; + expected.push_back(til::rectangle{ setPoint }); + map.set(setPoint); + + for (auto run : map.runs()) + { + actual.push_back(run); + } + VERIFY_ARE_EQUAL(expected, actual); + + Log::Comment(L"Set rectangle and validate runs updated."); + const til::rectangle setRect{ setPoint, til::size{ 2, 2 } }; + expected.clear(); + expected.push_back(til::rectangle{ til::point{ 2, 2 }, til::size{ 2, 1 } }); + expected.push_back(til::rectangle{ til::point{ 2, 3 }, til::size{ 2, 1 } }); + map.set(setRect); + + actual.clear(); + for (auto run : map.runs()) + { + actual.push_back(run); + } + VERIFY_ARE_EQUAL(expected, actual); + + Log::Comment(L"Set all and validate runs updated."); + expected.clear(); + expected.push_back(til::rectangle{ til::point{ 0, 0 }, til::size{ 4, 1 } }); + expected.push_back(til::rectangle{ til::point{ 0, 1 }, til::size{ 4, 1 } }); + expected.push_back(til::rectangle{ til::point{ 0, 2 }, til::size{ 4, 1 } }); + expected.push_back(til::rectangle{ til::point{ 0, 3 }, til::size{ 4, 1 } }); + map.set_all(); + + actual.clear(); + for (auto run : map.runs()) + { + actual.push_back(run); + } + VERIFY_ARE_EQUAL(expected, actual); + + Log::Comment(L"Resize and validate runs updated."); + const til::size newSize{ 3, 3 }; + expected.clear(); + expected.push_back(til::rectangle{ til::point{ 0, 0 }, til::size{ 3, 1 } }); + expected.push_back(til::rectangle{ til::point{ 0, 1 }, til::size{ 3, 1 } }); + expected.push_back(til::rectangle{ til::point{ 0, 2 }, til::size{ 3, 1 } }); + map.resize(newSize); + + actual.clear(); + for (auto run : map.runs()) + { + actual.push_back(run); + } + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(RunsWithPmr) + { + // This is a copy of the above test, but with a pmr::bitmap. + std::pmr::unsynchronized_pool_resource pool; + + // This map --> Those runs + // 1 1 0 1 A A _ B + // 1 0 1 1 C _ D D + // 0 0 1 0 _ _ E _ + // 0 1 1 0 _ F F _ + Log::Comment(L"Set up a PMR bitmap with some runs."); + + til::pmr::bitmap map{ til::size{ 4, 4 }, false, &pool }; // 0 0 0 0 |1 1|0 0 // 0 0 0 0 0 0 0 0