From ee8ca1c00563c29d2a366d3459c19e14d87bb493 Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sat, 15 Jun 2024 13:12:46 +0100 Subject: [PATCH] Greatly simplify outline rendering The new algorithm is a lot less code, slightly faster, and results in a smaller binary (-40 KiB on rg99). The previous algorithm filled all the pixels around every solid pixel. The new algorithm only fills pixels that will be visible. We first collect the outline pixels into an array (which may contain a small amount of duplicates). Then, we render the entire array in a single loop. This turns out to be slightly faster than rendering inline, at the cost of ~4 KiB of stack (basically free). To collect the pixels, we go through the CLX sprite, keeping track of the solid runs in the current row, and the filled pixels on the line above and the line below. To be able to quickly test the pixels above and below, we introduce a new data structure, `StaticBitVector`. It is similar to a bitset, except the size is determined at runtime (capacity is fixed), and it supports quick updates of entire subspans. --- Source/engine/render/clx_render.cpp | 503 +++++++--------------------- Source/utils/static_bit_vector.hpp | 92 +++++ Source/utils/static_vector.hpp | 8 + test/CMakeLists.txt | 16 +- test/static_bit_vector_test.cpp | 30 ++ 5 files changed, 267 insertions(+), 382 deletions(-) create mode 100644 Source/utils/static_bit_vector.hpp create mode 100644 test/static_bit_vector_test.cpp diff --git a/Source/engine/render/clx_render.cpp b/Source/engine/render/clx_render.cpp index 9d75f3560..cf068911e 100644 --- a/Source/engine/render/clx_render.cpp +++ b/Source/engine/render/clx_render.cpp @@ -6,12 +6,16 @@ #include "clx_render.hpp" #include +#include #include +#include "engine/point.hpp" #include "engine/render/blit_impl.hpp" #include "engine/render/scrollrt.h" #include "utils/attributes.h" #include "utils/clx_decode.hpp" +#include "utils/static_bit_vector.hpp" +#include "utils/static_vector.hpp" #ifdef DEBUG_CLX #include @@ -250,420 +254,159 @@ void DoRenderBackwards( } } -template -DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderOutlineForPixel(uint8_t *dst, int dstPitch, uint8_t color) -{ - if constexpr (North) - dst[-dstPitch] = color; - if constexpr (West) - dst[-1] = color; - if constexpr (East) - dst[1] = color; - if constexpr (South) - dst[dstPitch] = color; -} - -template -DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderOutlineForPixel(uint8_t *dst, int dstPitch, uint8_t srcColor, uint8_t color) -{ - if constexpr (SkipColorIndexZero) { - if (srcColor == 0) - return; - } - RenderOutlineForPixel(dst, dstPitch, color); -} - -template -DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderOutlineForPixels(uint8_t *dst, int dstPitch, int width, uint8_t color) -{ - if constexpr (North) - std::memset(dst - dstPitch, color, width); - - if constexpr (West && East) - std::memset(dst - 1, color, width + 2); - else if constexpr (West) - std::memset(dst - 1, color, width); - else if constexpr (East) - std::memset(dst + 1, color, width); - - if constexpr (South) - std::memset(dst + dstPitch, color, width); -} +constexpr size_t MaxOutlinePixels = 1024; +constexpr size_t MaxOutlineSpriteWidth = 256; +using OutlinePixels = StaticVector, MaxOutlinePixels>; +using OutlineRowSolidRuns = StaticVector, MaxOutlineSpriteWidth / 2 + 1>; -template -DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderOutlineForPixels(uint8_t *dst, int dstPitch, int width, const uint8_t *src, uint8_t color) +void PopulateOutlinePixelsForRow( + const StaticBitVector &rowBelow, + const StaticBitVector &rowAbove, + const OutlineRowSolidRuns &solidRuns, + int y, OutlinePixels &result) { - if constexpr (SkipColorIndexZero) { - while (width-- > 0) - RenderOutlineForPixel(dst++, dstPitch, *src++, color); - } else { - RenderOutlineForPixels(dst, dstPitch, width, color); - } -} - -template -void RenderClxOutlinePixelsCheckFirstColumn( - uint8_t *dst, int dstPitch, int dstX, - const uint8_t *src, uint8_t width, uint8_t color) -{ - if (dstX == -1) { - if constexpr (Fill) { - RenderOutlineForPixel( - dst++, dstPitch, color); - } else { - RenderOutlineForPixel( - dst++, dstPitch, *src++, color); - } - --width; - } - if (width > 0) { - if constexpr (Fill) { - RenderOutlineForPixel(dst++, dstPitch, color); - } else { - RenderOutlineForPixel(dst++, dstPitch, *src++, color); - } - --width; - } - if (width > 0) { - if constexpr (Fill) { - RenderOutlineForPixels(dst, dstPitch, width, color); - } else { - RenderOutlineForPixels(dst, dstPitch, width, src, color); - } - } -} - -template -void RenderClxOutlinePixelsCheckLastColumn( - uint8_t *dst, int dstPitch, int dstX, int dstW, - const uint8_t *src, uint8_t width, uint8_t color) -{ - const bool lastPixel = dstX != dstW; - const bool oobPixel = dstX + width == dstW + 1; - const int numSpecialPixels = (lastPixel ? 1 : 0) + (oobPixel ? 1 : 0); - if (width > numSpecialPixels) { - width -= numSpecialPixels; - if constexpr (Fill) { - RenderOutlineForPixels(dst, dstPitch, width, color); - } else { - RenderOutlineForPixels(dst, dstPitch, width, src, color); - src += width; - } - dst += width; - } - if (lastPixel) { - if constexpr (Fill) { - RenderOutlineForPixel(dst++, dstPitch, color); - } else { - RenderOutlineForPixel(dst++, dstPitch, *src++, color); - } - } - if (oobPixel) { - if constexpr (Fill) { - RenderOutlineForPixel(dst, dstPitch, color); - } else { - RenderOutlineForPixel(dst, dstPitch, *src, color); + for (auto [xBegin, xEnd] : solidRuns) { + result.emplace_back(static_cast(xBegin - 1), static_cast(y)); + for (int xi = xBegin; xi < xEnd; ++xi) { + if (!rowAbove.test(xi)) { + result.emplace_back(static_cast(xi), static_cast(y - 1)); + } + if (!rowBelow.test(xi)) { + result.emplace_back(static_cast(xi), static_cast(y + 1)); + } } + result.emplace_back(static_cast(xEnd), static_cast(y)); } } -template -void RenderClxOutlinePixels( - uint8_t *dst, int dstPitch, int dstX, int dstW, - const uint8_t *src, uint8_t width, uint8_t color) +void AppendOutlineRowSolidRuns(int x, int w, OutlineRowSolidRuns &solidRuns) { - if constexpr (SkipColorIndexZero && Fill) { - if (*src == 0) - return; - } - - if constexpr (CheckFirstColumn) { - if (dstX <= 0) { - RenderClxOutlinePixelsCheckFirstColumn( - dst, dstPitch, dstX, src, width, color); - return; - } - } - if constexpr (CheckLastColumn) { - if (dstX + width >= dstW) { - RenderClxOutlinePixelsCheckLastColumn( - dst, dstPitch, dstX, dstW, src, width, color); - return; - } - } - - if constexpr (Fill) { - RenderOutlineForPixels(dst, dstPitch, width, color); + if (solidRuns.empty() || solidRuns.back().second != x) { + solidRuns.emplace_back(x, x + w); } else { - RenderOutlineForPixels(dst, dstPitch, width, src, color); + solidRuns.back().second = static_cast(x + w); } } -template -const uint8_t *RenderClxOutlineRowClipped( // NOLINT(readability-function-cognitive-complexity) - const Surface &out, Point position, const uint8_t *src, std::size_t srcWidth, - ClipX clipX, uint8_t color, SkipSize &skipSize) +template +OutlinePixels GetOutline(ClxSprite sprite) // NOLINT(readability-function-cognitive-complexity) { - int_fast16_t remainingWidth = clipX.width; - uint8_t v; + const unsigned width = sprite.width(); + assert(width < MaxOutlineSpriteWidth); + OutlinePixels result; + StaticBitVector rows[3] = { + StaticBitVector(width), + StaticBitVector(width), + StaticBitVector(width) + }; + StaticBitVector *rowAbove = &rows[0]; + StaticBitVector *row = &rows[1]; + StaticBitVector *rowBelow = &rows[2]; - auto *dst = &out[position]; - const auto dstPitch = out.pitch(); + int x = 0; + int y = sprite.height() - 1; - const auto renderPixels = [&](bool fill, uint8_t w) { - if (fill) { - RenderClxOutlinePixels( - dst, dstPitch, position.x, out.w(), src, w, color); - ++src; - } else { - RenderClxOutlinePixels( - dst, dstPitch, position.x, out.w(), src, w, color); - src += v; - } - dst += w; - }; + OutlineRowSolidRuns solidRuns[2]; + OutlineRowSolidRuns *solidRunAbove = &solidRuns[0]; + OutlineRowSolidRuns *solidRun = &solidRuns[1]; - if constexpr (ClipWidth) { - auto remainingLeftClip = clipX.left - skipSize.xOffset; - if (skipSize.xOffset > clipX.left) { - position.x += static_cast(skipSize.xOffset - clipX.left); - dst += skipSize.xOffset - clipX.left; - } - while (remainingLeftClip > 0) { - v = static_cast(*src++); + const uint8_t *src = sprite.pixelData(); + const uint8_t *const end = src + sprite.pixelDataSize(); + while (src < end) { + while (x < static_cast(width)) { + const auto v = static_cast(*src++); + uint8_t w; if (IsClxOpaque(v)) { - const bool fill = IsClxOpaqueFill(v); - v = fill ? GetClxOpaqueFillWidth(v) : GetClxOpaquePixelsWidth(v); - if (v > remainingLeftClip) { - const uint8_t overshoot = v - remainingLeftClip; - renderPixels(fill, overshoot); - position.x += overshoot; + if constexpr (SkipColorIndexZero) { + if (IsClxOpaqueFill(v)) { + w = GetClxOpaqueFillWidth(v); + const auto color = static_cast(*src++); + if (color != 0) { + AppendOutlineRowSolidRuns(x, w, *solidRunAbove); + } + } else { + w = GetClxOpaquePixelsWidth(v); + bool prevZero = solidRunAbove->empty() || solidRunAbove->back().second != x; + for (unsigned i = 0; i < w; ++i) { + const auto color = static_cast(src[i]); + if (color == 0) { + if (!prevZero) ++solidRunAbove->back().second; + prevZero = true; + } else { + if (prevZero) solidRunAbove->emplace_back(x + i, x + i); + ++solidRunAbove->back().second; + prevZero = false; + } + } + src += w; + } } else { - src += fill ? 1 : v; + if (IsClxOpaqueFill(v)) { + w = GetClxOpaqueFillWidth(v); + ++src; + } else { + w = GetClxOpaquePixelsWidth(v); + src += w; + } + AppendOutlineRowSolidRuns(x, w, *solidRunAbove); } } else { - if (v > remainingLeftClip) { - const uint8_t overshoot = v - remainingLeftClip; - dst += overshoot; - position.x += overshoot; - } + w = v; } - remainingLeftClip -= v; + x += w; } - remainingWidth += remainingLeftClip; - } else { - position.x += static_cast(skipSize.xOffset); - dst += skipSize.xOffset; - remainingWidth -= skipSize.xOffset; - } - while (remainingWidth > 0) { - v = static_cast(*src++); - if (IsClxOpaque(v)) { - const bool fill = IsClxOpaqueFill(v); - v = fill ? GetClxOpaqueFillWidth(v) : GetClxOpaquePixelsWidth(v); - if constexpr (ClipWidth) { - renderPixels(fill, std::min(remainingWidth, static_cast(v))); - } else { - renderPixels(fill, v); + for (const auto &[xBegin, xEnd] : *solidRunAbove) { + rowAbove->set(xBegin, xEnd - xBegin); + } + PopulateOutlinePixelsForRow(*rowBelow, *rowAbove, *solidRun, y + 1, result); + + // (0, 1, 2) => (2, 0, 1) + std::swap(row, rowBelow); + std::swap(row, rowAbove); + rowAbove->reset(); + + std::swap(solidRunAbove, solidRun); + solidRunAbove->clear(); + + if (x > static_cast(width)) { + // Transparent overrun. + const unsigned numWholeTransparentLines = x / width; + if (numWholeTransparentLines > 1) { + PopulateOutlinePixelsForRow(*rowBelow, *rowAbove, *solidRun, y, result); + solidRun->clear(); + row->reset(); } + if (numWholeTransparentLines > 2) rowBelow->reset(); + y -= static_cast(numWholeTransparentLines); + x = static_cast(x % width); } else { - dst += v; - } - remainingWidth -= v; - position.x += v; - } - - if constexpr (ClipWidth) { - remainingWidth += clipX.right; - if (remainingWidth > 0) { - skipSize.xOffset = static_cast(srcWidth) - remainingWidth; - return SkipRestOfLineWithOverrun(src, static_cast(srcWidth), skipSize); + --y; + x = 0; } } - skipSize = GetSkipSize(remainingWidth, static_cast(srcWidth)); - - return src; + rowAbove->reset(); + PopulateOutlinePixelsForRow(*rowBelow, *rowAbove, *solidRun, y + 1, result); + return result; } template -void RenderClxOutlineClippedY(const Surface &out, Point position, RenderSrc src, // NOLINT(readability-function-cognitive-complexity) - uint8_t color) +void RenderClxOutline(const Surface &out, Point position, ClxSprite sprite, uint8_t color) { - // Skip the bottom clipped lines. - const int dstHeight = out.h(); - SkipSize skipSize = { 0, SkipLinesForRenderBackwardsWithOverrun(position, src, dstHeight) }; - if (src.begin == src.end) - return; - - const ClipX clipX = { 0, 0, static_cast(src.width) }; - - if (position.y == dstHeight) { - // After-bottom line - can only draw north. - src.begin = RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - position.y -= static_cast(skipSize.wholeLines); - } - if (src.begin == src.end) - return; - - if (position.y + 1 == dstHeight) { - // Bottom line - cannot draw south. - src.begin = RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - position.y -= static_cast(skipSize.wholeLines); - } - - while (position.y > 0 && src.begin != src.end) { - src.begin = RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - position.y -= static_cast(skipSize.wholeLines); - } - if (src.begin == src.end) - return; - - if (position.y == 0) { - src.begin = RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - position.y -= static_cast(skipSize.wholeLines); - } - if (src.begin == src.end) - return; - - if (position.y == -1) { - // Special case: the top of the sprite is 1px below the last line, render just the outline above. - RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - } -} - -template -void RenderClxOutlineClippedXY(const Surface &out, Point position, RenderSrc src, // NOLINT(readability-function-cognitive-complexity) - uint8_t color) -{ - // Skip the bottom clipped lines. - const int dstHeight = out.h(); - SkipSize skipSize = { 0, SkipLinesForRenderBackwardsWithOverrun(position, src, dstHeight) }; - if (src.begin == src.end) - return; - - ClipX clipX = CalculateClipX(position.x, src.width, out); - if (clipX.width < 0) - return; - if (clipX.left > 0) { - --clipX.left, ++clipX.width; - } else if (clipX.right > 0) { - --clipX.right, ++clipX.width; - } - position.x += static_cast(clipX.left); - - if (position.y == dstHeight) { - // After-bottom line - can only draw north. - if (position.x <= 0) { - src.begin = RenderClxOutlineRowClipped(out, position, src.begin, src.width, clipX, color, skipSize); - } else if (position.x + clipX.width >= out.w()) { - src.begin = RenderClxOutlineRowClipped(out, position, src.begin, src.width, clipX, color, skipSize); - } else { - src.begin = RenderClxOutlineRowClipped(out, position, src.begin, src.width, clipX, color, skipSize); - } - position.y -= static_cast(skipSize.wholeLines); - } - if (src.begin == src.end) - return; - - if (position.y + 1 == dstHeight) { - // Bottom line - cannot draw south. - if (position.x <= 0) { - src.begin = RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - } else if (position.x + clipX.width >= out.w()) { - src.begin = RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - } else { - src.begin = RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - } - position.y -= static_cast(skipSize.wholeLines); - } - - if (position.x <= 0) { - while (position.y > 0 && src.begin != src.end) { - src.begin = RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - position.y -= static_cast(skipSize.wholeLines); - } - } else if (position.x + clipX.width >= out.w()) { - while (position.y > 0 && src.begin != src.end) { - src.begin = RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - position.y -= static_cast(skipSize.wholeLines); + const OutlinePixels pixels = GetOutline(sprite); + position.y -= sprite.height() - 1; + if (position.x > 0 && position.x + sprite.width() < out.w() + && position.y > 0 && position.y + sprite.height() < out.h()) { + for (auto [x, y] : pixels) { + *out.at(position.x + x, position.y + y) = color; } } else { - while (position.y > 0 && src.begin != src.end) { - src.begin = RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - position.y -= static_cast(skipSize.wholeLines); - } - } - if (src.begin == src.end) - return; - - if (position.y == 0) { - if (position.x <= 0) { - src.begin = RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - } else if (position.x + clipX.width >= out.w()) { - src.begin = RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - } else { - src.begin = RenderClxOutlineRowClipped( - out, position, src.begin, src.width, clipX, color, skipSize); - } - position.y -= static_cast(skipSize.wholeLines); - } - if (src.begin == src.end) - return; - - if (position.y == -1) { - // Before-top line - can only draw south. - if (position.x <= 0) { - src.begin = RenderClxOutlineRowClipped(out, position, src.begin, src.width, clipX, color, skipSize); - } else if (position.x + clipX.width >= out.w()) { - src.begin = RenderClxOutlineRowClipped(out, position, src.begin, src.width, clipX, color, skipSize); - } else { - src.begin = RenderClxOutlineRowClipped(out, position, src.begin, src.width, clipX, color, skipSize); + for (auto [x, y] : pixels) { + out.SetPixel(Point(position.x + x, position.y + y), color); } } } -template -void RenderClxOutline(const Surface &out, Point position, const uint8_t *src, std::size_t srcSize, - std::size_t srcWidth, uint8_t color) -{ - RenderSrc srcForBackwards { src, src + srcSize, static_cast(srcWidth) }; - if (position.x > 0 && position.x + static_cast(srcWidth) < static_cast(out.w())) { - RenderClxOutlineClippedY(out, position, srcForBackwards, color); - } else { - RenderClxOutlineClippedXY(out, position, srcForBackwards, color); - } -} - void ClxApplyTrans(ClxSprite sprite, const uint8_t *trn) { // A bit of a hack but this is the only place in the code where we need mutable sprites. @@ -840,12 +583,12 @@ void ClxDrawBlendedTRN(const Surface &out, Point position, ClxSprite clx, const void ClxDrawOutline(const Surface &out, uint8_t col, Point position, ClxSprite clx) { - RenderClxOutline(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), col); + RenderClxOutline(out, position, clx, col); } void ClxDrawOutlineSkipColorZero(const Surface &out, uint8_t col, Point position, ClxSprite clx) { - RenderClxOutline(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), col); + RenderClxOutline(out, position, clx, col); } } // namespace devilution diff --git a/Source/utils/static_bit_vector.hpp b/Source/utils/static_bit_vector.hpp new file mode 100644 index 000000000..964c93058 --- /dev/null +++ b/Source/utils/static_bit_vector.hpp @@ -0,0 +1,92 @@ +#include +#include +#include + +#include "utils/attributes.h" + +namespace devilution { + +/** + * @brief A stack-allocated bit-vector with a fixed capacity and a fixed size < capacity. + * + * @tparam N capacity. + */ +template // NOLINT(google-runtime-int) +class StaticBitVector { +public: + explicit StaticBitVector(size_t size) + : size_(size) + , num_words_(size / BitsPerWord + ((size % BitsPerWord) == 0 ? 0 : 1)) + { + } + + [[nodiscard]] bool test(size_t pos) const { return (word(pos) & maskBit(pos)) != 0; } + + void set() { std::fill_n(words_, num_words_, ~Word { 0 }); } + void set(size_t pos) { word(pos) |= maskBit(pos); } + + // Sets all bits in the [begin, begin+length) range to 1. + // Length must not be zero. + void set(size_t begin, size_t length) + { + DVL_ASSUME(length != 0); + const size_t firstWord = begin / BitsPerWord; + const size_t lastWord = (begin + length - 1) / BitsPerWord; + const Word firstWordMask = onesFrom(begin); + const Word lastWordMask = onesUpTo(begin + length - 1); + if (firstWord == lastWord) { + words_[firstWord] |= firstWordMask & lastWordMask; + return; + } + words_[firstWord] |= firstWordMask; + std::fill(&words_[firstWord + 1], &words_[lastWord], ~Word { 0 }); + words_[lastWord] |= lastWordMask; + } + + void reset() { std::fill_n(words_, num_words_, Word { 0 }); } + void reset(size_t pos) { word(pos) &= ~maskBit(pos); } + + // Sets all bits in the [begin, begin+length) range to 0. + // Length must not be zero. + void reset(size_t begin, size_t length) + { + DVL_ASSUME(length != 0); + const size_t firstWord = begin / BitsPerWord; + const size_t lastWord = (begin + length - 1) / BitsPerWord; + const Word firstWordMask = onesFrom(begin); + const Word lastWordMask = onesUpTo(begin + length - 1); + if (firstWord == lastWord) { + words_[firstWord] &= ~(firstWordMask & lastWordMask); + return; + } + words_[firstWord] &= ~firstWordMask; + std::fill(&words_[firstWord + 1], &words_[lastWord], Word { 0 }); + words_[lastWord] &= ~lastWordMask; + } + +private: + static constexpr Word onesFrom(size_t begin) + { + return (~Word { 0 }) << (begin % BitsPerWord); + } + static constexpr Word onesUpTo(size_t last) + { + return (~Word { 0 }) >> (BitsPerWord - (last % BitsPerWord) - 1); + } + + static constexpr Word maskBit(size_t pos) + { + return Word { 1 } << (pos % BitsPerWord); + } + Word word(size_t pos) const { return words_[pos / BitsPerWord]; } + Word &word(size_t pos) { return words_[pos / BitsPerWord]; } + + static constexpr unsigned BitsPerWord = CHAR_BIT * sizeof(Word); + static constexpr unsigned MaxNumWords = N / BitsPerWord + ((N % BitsPerWord) == 0 ? 0 : 1); + Word words_[MaxNumWords] = {}; + + size_t size_; + size_t num_words_; +}; + +} // namespace devilution diff --git a/Source/utils/static_vector.hpp b/Source/utils/static_vector.hpp index bf89e5ba4..92c7ebccb 100644 --- a/Source/utils/static_vector.hpp +++ b/Source/utils/static_vector.hpp @@ -75,6 +75,14 @@ public: return *data_[pos].ptr(); } + void clear() + { + for (std::size_t pos = 0; pos < size_; ++pos) { + std::destroy_at(data_[pos].ptr()); + } + size_ = 0; + } + ~StaticVector() { for (std::size_t pos = 0; pos < size_; ++pos) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index cf5266c0f..53cfd46ee 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -44,17 +44,29 @@ set(tests utf8_test writehero_test ) +set( + standalone_tests + static_bit_vector_test +) include(Fixtures.cmake) -foreach(test_target ${tests}) +foreach(test_target ${tests} ${standalone_tests}) add_executable(${test_target} "${test_target}.cpp") gtest_discover_tests(${test_target}) - target_link_libraries(${test_target} PRIVATE test_main) set_target_properties(${test_target} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) if(GPERF) target_link_libraries(${test_target} PUBLIC ${GPERFTOOLS_LIBRARIES}) endif() endforeach() +foreach(test_target ${tests}) + target_link_libraries(${test_target} PRIVATE test_main) +endforeach() + +foreach(test_target ${standalone_tests}) + target_link_libraries(${test_target} GTest::gtest_main) + target_include_directories(${test_target} PRIVATE "${PROJECT_SOURCE_DIR}/Source") +endforeach() + target_include_directories(writehero_test PRIVATE ../3rdParty/PicoSHA2) diff --git a/test/static_bit_vector_test.cpp b/test/static_bit_vector_test.cpp new file mode 100644 index 000000000..bd7501303 --- /dev/null +++ b/test/static_bit_vector_test.cpp @@ -0,0 +1,30 @@ +#include + +#include "utils/static_bit_vector.hpp" + +namespace devilution { +namespace { + +TEST(StaticBitVectorTest, SetSingleTest) +{ + StaticBitVector<500> v(100); + EXPECT_FALSE(v.test(97)); + v.set(97); + EXPECT_TRUE(v.test(97)); +} + +TEST(StaticBitVectorTest, SetRangeTest) +{ + StaticBitVector<500> v(100); + EXPECT_FALSE(v.test(97)); + v.set(50, 50); + for (size_t i = 0; i < 50; ++i) { + EXPECT_FALSE(v.test(i)) << i; + } + for (size_t i = 50; i < 100; ++i) { + EXPECT_TRUE(v.test(i)) << i; + } +} + +} // namespace +} // namespace devilution