680577f55c
This commit replaces `std::vector<bool>` with `dynamic_bitset<>` by @pinam45 (https://github.com/pinam45/dynamic_bitset) and with `libpopcnt` for high-performance bit counting by @kimwalisch (https://github.com/kimwalisch/libpopcnt). * [x] In support of performance, incremental rendering, and Terminal "not speed enough" as well as my sanity relative to `std::vector<bool>` * [x] Tests updated and passed. * [x] `LICENSE`, `NOTICE`, and provenance files updated. * [x] I'm a core contributor. I discussed it with @DHowett-MSFT and cleared the licensing checks before pulling this in. ## Details `std::vector<bool>` provided by the Microsoft VC Runtime is incapable of a great many things. Many of the methods you come to expect off of `std::vector<T>` that are dutifully presented through the `bool` variant will spontaneously fail at some future date because it decides you allocated, resized, or manipulated the `vector<bool>` specialization in an unsupported manner. Half of the methods will straight up not work for filling/resizing in bulk. And you will tear your hair out as it will somehow magically forget the assignment of half the bits you gave it part way through an iteration then assert out and die. As such, to preserve my sanity, I searched for an alternative. I came across the self-contained header-only library `dynamic_bitset` by @pinam45 which appears to do as much of `boost::dynamic_bitset` as I wanted, but without including 400kg of boost libraries. It also has a nifty optional dependency on `libpopcnt` by @kimwalisch that will use processor-specific extensions for rapidly counting bits. @DHowett-MSFT and I briefly discussed how nice `popcnt` would have been on `std::vector<bool>` last week... and now we can have it. (To be fair, I don't believe I'm using it yet... but we'll be able to easily dial in `til::bitmap` soon and not worry about a performance hit if we do have to walk bits and count them thanks to `libpopcnt`.) This PR specifically focuses on swapping the dependencies out and ingesting the new libraries. We'll further tune `til::bitmap` in future pulls as necessary. ## Validation * [x] Ran the automated tests for bitmap. * [x] Ran the terminal manually and it looks fine still.
449 lines
14 KiB
C++
449 lines
14 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#pragma once
|
|
|
|
#ifdef UNIT_TESTING
|
|
class BitmapTests;
|
|
#endif
|
|
|
|
namespace til // Terminal Implementation Library. Also: "Today I Learned"
|
|
{
|
|
namespace details
|
|
{
|
|
class _bitmap_const_iterator
|
|
{
|
|
public:
|
|
using iterator_category = typename std::input_iterator_tag;
|
|
using value_type = typename const til::rectangle;
|
|
using difference_type = typename ptrdiff_t;
|
|
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) :
|
|
_values(values),
|
|
_rc(rc),
|
|
_pos(pos),
|
|
_end(rc.size().area())
|
|
{
|
|
_calculateArea();
|
|
}
|
|
|
|
_bitmap_const_iterator& operator++()
|
|
{
|
|
_pos = _nextPos;
|
|
_calculateArea();
|
|
return (*this);
|
|
}
|
|
|
|
_bitmap_const_iterator operator++(int)
|
|
{
|
|
const auto prev = *this;
|
|
++*this;
|
|
return prev;
|
|
}
|
|
|
|
constexpr bool operator==(const _bitmap_const_iterator& other) const noexcept
|
|
{
|
|
return _pos == other._pos && _values == other._values;
|
|
}
|
|
|
|
constexpr bool operator!=(const _bitmap_const_iterator& other) const noexcept
|
|
{
|
|
return !(*this == other);
|
|
}
|
|
|
|
constexpr bool operator<(const _bitmap_const_iterator& other) const noexcept
|
|
{
|
|
return _pos < other._pos;
|
|
}
|
|
|
|
constexpr bool operator>(const _bitmap_const_iterator& other) const noexcept
|
|
{
|
|
return _pos > other._pos;
|
|
}
|
|
|
|
constexpr reference operator*() const noexcept
|
|
{
|
|
return _run;
|
|
}
|
|
|
|
constexpr pointer operator->() const noexcept
|
|
{
|
|
return &_run;
|
|
}
|
|
|
|
private:
|
|
const dynamic_bitset<>& _values;
|
|
const til::rectangle _rc;
|
|
ptrdiff_t _pos;
|
|
ptrdiff_t _nextPos;
|
|
const ptrdiff_t _end;
|
|
til::rectangle _run;
|
|
|
|
void _calculateArea()
|
|
{
|
|
// Backup the position as the next one.
|
|
_nextPos = _pos;
|
|
|
|
// Seek forward until we find an on bit.
|
|
while (_nextPos < _end && !_values[_nextPos])
|
|
{
|
|
++_nextPos;
|
|
}
|
|
|
|
// If we haven't reached the end yet...
|
|
if (_nextPos < _end)
|
|
{
|
|
// pos is now at the first on bit.
|
|
const auto runStart = _rc.point_at(_nextPos);
|
|
|
|
// We'll only count up until the end of this row.
|
|
// a run can be a max of one row tall.
|
|
const ptrdiff_t rowEndIndex = _rc.index_of(til::point(_rc.right() - 1, runStart.y())) + 1;
|
|
|
|
// Find the length for the rectangle.
|
|
ptrdiff_t runLength = 0;
|
|
|
|
// We have at least 1 so start with a do/while.
|
|
do
|
|
{
|
|
++_nextPos;
|
|
++runLength;
|
|
} while (_nextPos < rowEndIndex && _values[_nextPos]);
|
|
// Keep going until we reach end of row, end of the buffer, or the next bit is off.
|
|
|
|
// Assemble and store that run.
|
|
_run = til::rectangle{ runStart, til::size{ runLength, static_cast<ptrdiff_t>(1) } };
|
|
}
|
|
else
|
|
{
|
|
// If we reached the end, set the pos because the run is empty.
|
|
_pos = _nextPos;
|
|
_run = til::rectangle{};
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
class bitmap
|
|
{
|
|
public:
|
|
using const_iterator = details::_bitmap_const_iterator;
|
|
|
|
bitmap() noexcept :
|
|
_sz{},
|
|
_rc{},
|
|
_bits{},
|
|
_dirty{},
|
|
_runs{}
|
|
{
|
|
}
|
|
|
|
bitmap(til::size sz) :
|
|
bitmap(sz, false)
|
|
{
|
|
}
|
|
|
|
bitmap(til::size sz, bool fill) :
|
|
_sz(sz),
|
|
_rc(sz),
|
|
_bits(_sz.area()),
|
|
_dirty(fill ? sz : til::rectangle{}),
|
|
_runs{}
|
|
{
|
|
if (fill)
|
|
{
|
|
set_all();
|
|
}
|
|
}
|
|
|
|
constexpr bool operator==(const bitmap& other) const noexcept
|
|
{
|
|
return _sz == other._sz &&
|
|
_rc == other._rc &&
|
|
_dirty == other._dirty && // dirty is before bits because it's a rough estimate of bits and a faster comparison.
|
|
_bits == other._bits;
|
|
// _runs excluded because it's a cache of generated state.
|
|
}
|
|
|
|
constexpr bool operator!=(const bitmap& other) const noexcept
|
|
{
|
|
return !(*this == other);
|
|
}
|
|
|
|
const_iterator begin() const
|
|
{
|
|
return const_iterator(_bits, _sz, 0);
|
|
}
|
|
|
|
const_iterator end() const
|
|
{
|
|
return const_iterator(_bits, _sz, _sz.area());
|
|
}
|
|
|
|
const std::vector<til::rectangle>& runs() const
|
|
{
|
|
// If we don't have cached runs, rebuild.
|
|
if (!_runs.has_value())
|
|
{
|
|
// If there's only one square dirty, quick save it off and be done.
|
|
if (one())
|
|
{
|
|
_runs.emplace({ _dirty });
|
|
}
|
|
else
|
|
{
|
|
_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)
|
|
{
|
|
// 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)
|
|
{
|
|
other.set(f);
|
|
}
|
|
}
|
|
|
|
// Swap us with the temporary one.
|
|
std::swap(other, *this);
|
|
}
|
|
|
|
void set(const til::point pt)
|
|
{
|
|
THROW_HR_IF(E_INVALIDARG, !_rc.contains(pt));
|
|
_runs.reset(); // reset cached runs on any non-const method
|
|
|
|
til::at(_bits, _rc.index_of(pt)) = true;
|
|
|
|
_dirty |= til::rectangle{ pt };
|
|
}
|
|
|
|
void set(const til::rectangle rc)
|
|
{
|
|
THROW_HR_IF(E_INVALIDARG, !_rc.contains(rc));
|
|
_runs.reset(); // reset cached runs on any non-const method
|
|
|
|
for (const auto pt : rc)
|
|
{
|
|
til::at(_bits, _rc.index_of(pt)) = true;
|
|
}
|
|
|
|
_dirty |= rc;
|
|
}
|
|
|
|
void set_all() noexcept
|
|
{
|
|
_runs.reset(); // reset cached runs on any non-const method
|
|
_bits.set();
|
|
_dirty = _rc;
|
|
}
|
|
|
|
void reset_all() noexcept
|
|
{
|
|
_runs.reset(); // reset cached runs on any non-const method
|
|
_bits.reset();
|
|
_dirty = {};
|
|
}
|
|
|
|
// 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
|
|
|
|
// Don't resize if it's not different
|
|
if (_sz != size)
|
|
{
|
|
// Make a new bitmap for the other side, empty initially.
|
|
auto newMap = bitmap(size, false);
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// Swap and return.
|
|
std::swap(newMap, *this);
|
|
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool one() const
|
|
{
|
|
return _dirty.size() == til::size{ 1, 1 };
|
|
}
|
|
|
|
constexpr bool any() const noexcept
|
|
{
|
|
return !none();
|
|
}
|
|
|
|
constexpr bool none() const noexcept
|
|
{
|
|
return _dirty.empty();
|
|
}
|
|
|
|
constexpr bool all() const noexcept
|
|
{
|
|
return _dirty == _rc;
|
|
}
|
|
|
|
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;
|
|
|
|
for (auto& item : *this)
|
|
{
|
|
wss << L"\t- " << item.to_string() << std::endl;
|
|
}
|
|
|
|
return wss.str();
|
|
}
|
|
|
|
private:
|
|
til::rectangle _dirty;
|
|
til::size _sz;
|
|
til::rectangle _rc;
|
|
dynamic_bitset<> _bits;
|
|
|
|
mutable std::optional<std::vector<til::rectangle>> _runs;
|
|
|
|
#ifdef UNIT_TESTING
|
|
friend class ::BitmapTests;
|
|
#endif
|
|
};
|
|
}
|
|
|
|
#ifdef __WEX_COMMON_H__
|
|
namespace WEX::TestExecution
|
|
{
|
|
template<>
|
|
class VerifyOutputTraits<::til::bitmap>
|
|
{
|
|
public:
|
|
static WEX::Common::NoThrowString ToString(const ::til::bitmap& rect)
|
|
{
|
|
return WEX::Common::NoThrowString(rect.to_string().c_str());
|
|
}
|
|
};
|
|
|
|
template<>
|
|
class VerifyCompareTraits<::til::bitmap, ::til::bitmap>
|
|
{
|
|
public:
|
|
static bool AreEqual(const ::til::bitmap& expected, const ::til::bitmap& actual) noexcept
|
|
{
|
|
return expected == actual;
|
|
}
|
|
|
|
static bool AreSame(const ::til::bitmap& expected, const ::til::bitmap& actual) noexcept
|
|
{
|
|
return &expected == &actual;
|
|
}
|
|
|
|
static bool IsLessThan(const ::til::bitmap& expectedLess, const ::til::bitmap& expectedGreater) = delete;
|
|
|
|
static bool IsGreaterThan(const ::til::bitmap& expectedGreater, const ::til::bitmap& expectedLess) = delete;
|
|
|
|
static bool IsNull(const ::til::bitmap& object) noexcept
|
|
{
|
|
return object == til::bitmap{};
|
|
}
|
|
};
|
|
|
|
};
|
|
#endif
|