Browse Source

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.
pull/7145/head
Gleb Mazovetskiy 2 years ago committed by Anders Jenbo
parent
commit
ee8ca1c005
  1. 503
      Source/engine/render/clx_render.cpp
  2. 92
      Source/utils/static_bit_vector.hpp
  3. 8
      Source/utils/static_vector.hpp
  4. 16
      test/CMakeLists.txt
  5. 30
      test/static_bit_vector_test.cpp

503
Source/engine/render/clx_render.cpp

@ -6,12 +6,16 @@
#include "clx_render.hpp"
#include <algorithm>
#include <bitset>
#include <cstdint>
#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 <fmt/format.h>
@ -250,420 +254,159 @@ void DoRenderBackwards(
}
}
template <bool North, bool West, bool South, bool East>
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 <bool North, bool West, bool South, bool East, bool SkipColorIndexZero = true>
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<North, West, South, East>(dst, dstPitch, color);
}
template <bool North, bool West, bool South, bool East>
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<PointOf<int16_t>, MaxOutlinePixels>;
using OutlineRowSolidRuns = StaticVector<std::pair<int16_t, int16_t>, MaxOutlineSpriteWidth / 2 + 1>;
template <bool North, bool West, bool South, bool East, bool SkipColorIndexZero>
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<MaxOutlineSpriteWidth> &rowBelow,
const StaticBitVector<MaxOutlineSpriteWidth> &rowAbove,
const OutlineRowSolidRuns &solidRuns,
int y, OutlinePixels &result)
{
if constexpr (SkipColorIndexZero) {
while (width-- > 0)
RenderOutlineForPixel<North, West, South, East>(dst++, dstPitch, *src++, color);
} else {
RenderOutlineForPixels<North, West, South, East>(dst, dstPitch, width, color);
}
}
template <bool Fill, bool North, bool West, bool South, bool East, bool SkipColorIndexZero>
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</*North=*/false, /*West=*/false, /*South=*/false, East>(
dst++, dstPitch, color);
} else {
RenderOutlineForPixel</*North=*/false, /*West=*/false, /*South=*/false, East, SkipColorIndexZero>(
dst++, dstPitch, *src++, color);
}
--width;
}
if (width > 0) {
if constexpr (Fill) {
RenderOutlineForPixel<North, /*West=*/false, South, East>(dst++, dstPitch, color);
} else {
RenderOutlineForPixel<North, /*West=*/false, South, East, SkipColorIndexZero>(dst++, dstPitch, *src++, color);
}
--width;
}
if (width > 0) {
if constexpr (Fill) {
RenderOutlineForPixels<North, West, South, East>(dst, dstPitch, width, color);
} else {
RenderOutlineForPixels<North, West, South, East, SkipColorIndexZero>(dst, dstPitch, width, src, color);
}
}
}
template <bool Fill, bool North, bool West, bool South, bool East, bool SkipColorIndexZero>
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<North, West, South, East>(dst, dstPitch, width, color);
} else {
RenderOutlineForPixels<North, West, South, East, SkipColorIndexZero>(dst, dstPitch, width, src, color);
src += width;
}
dst += width;
}
if (lastPixel) {
if constexpr (Fill) {
RenderOutlineForPixel<North, West, South, /*East=*/false>(dst++, dstPitch, color);
} else {
RenderOutlineForPixel<North, West, South, /*East=*/false, SkipColorIndexZero>(dst++, dstPitch, *src++, color);
}
}
if (oobPixel) {
if constexpr (Fill) {
RenderOutlineForPixel</*North=*/false, West, /*South=*/false, /*East=*/false>(dst, dstPitch, color);
} else {
RenderOutlineForPixel</*North=*/false, West, /*South=*/false, /*East=*/false, SkipColorIndexZero>(dst, dstPitch, *src, color);
for (auto [xBegin, xEnd] : solidRuns) {
result.emplace_back(static_cast<int16_t>(xBegin - 1), static_cast<int16_t>(y));
for (int xi = xBegin; xi < xEnd; ++xi) {
if (!rowAbove.test(xi)) {
result.emplace_back(static_cast<int16_t>(xi), static_cast<int16_t>(y - 1));
}
if (!rowBelow.test(xi)) {
result.emplace_back(static_cast<int16_t>(xi), static_cast<int16_t>(y + 1));
}
}
result.emplace_back(static_cast<int16_t>(xEnd), static_cast<int16_t>(y));
}
}
template <bool Fill, bool North, bool West, bool South, bool East, bool SkipColorIndexZero, bool CheckFirstColumn, bool CheckLastColumn>
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<Fill, North, West, South, East, SkipColorIndexZero>(
dst, dstPitch, dstX, src, width, color);
return;
}
}
if constexpr (CheckLastColumn) {
if (dstX + width >= dstW) {
RenderClxOutlinePixelsCheckLastColumn<Fill, North, West, South, East, SkipColorIndexZero>(
dst, dstPitch, dstX, dstW, src, width, color);
return;
}
}
if constexpr (Fill) {
RenderOutlineForPixels<North, West, South, East>(dst, dstPitch, width, color);
if (solidRuns.empty() || solidRuns.back().second != x) {
solidRuns.emplace_back(x, x + w);
} else {
RenderOutlineForPixels<North, West, South, East, SkipColorIndexZero>(dst, dstPitch, width, src, color);
solidRuns.back().second = static_cast<int16_t>(x + w);
}
}
template <bool North, bool West, bool South, bool East, bool SkipColorIndexZero,
bool ClipWidth = false, bool CheckFirstColumn = false, bool CheckLastColumn = false>
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 <bool SkipColorIndexZero>
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<MaxOutlineSpriteWidth> rows[3] = {
StaticBitVector<MaxOutlineSpriteWidth>(width),
StaticBitVector<MaxOutlineSpriteWidth>(width),
StaticBitVector<MaxOutlineSpriteWidth>(width)
};
StaticBitVector<MaxOutlineSpriteWidth> *rowAbove = &rows[0];
StaticBitVector<MaxOutlineSpriteWidth> *row = &rows[1];
StaticBitVector<MaxOutlineSpriteWidth> *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</*Fill=*/true, North, West, South, East, SkipColorIndexZero, CheckFirstColumn, CheckLastColumn>(
dst, dstPitch, position.x, out.w(), src, w, color);
++src;
} else {
RenderClxOutlinePixels</*Fill=*/false, North, West, South, East, SkipColorIndexZero, CheckFirstColumn, CheckLastColumn>(
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<int>(skipSize.xOffset - clipX.left);
dst += skipSize.xOffset - clipX.left;
}
while (remainingLeftClip > 0) {
v = static_cast<uint8_t>(*src++);
const uint8_t *src = sprite.pixelData();
const uint8_t *const end = src + sprite.pixelDataSize();
while (src < end) {
while (x < static_cast<int>(width)) {
const auto v = static_cast<uint8_t>(*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<uint8_t>(*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<uint8_t>(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<int>(skipSize.xOffset);
dst += skipSize.xOffset;
remainingWidth -= skipSize.xOffset;
}
while (remainingWidth > 0) {
v = static_cast<uint8_t>(*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<int_fast16_t>(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<int>(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<int16_t>(numWholeTransparentLines);
x = static_cast<int16_t>(x % width);
} else {
dst += v;
}
remainingWidth -= v;
position.x += v;
}
if constexpr (ClipWidth) {
remainingWidth += clipX.right;
if (remainingWidth > 0) {
skipSize.xOffset = static_cast<int_fast16_t>(srcWidth) - remainingWidth;
return SkipRestOfLineWithOverrun(src, static_cast<int_fast16_t>(srcWidth), skipSize);
--y;
x = 0;
}
}
skipSize = GetSkipSize(remainingWidth, static_cast<int_fast16_t>(srcWidth));
return src;
rowAbove->reset();
PopulateOutlinePixelsForRow(*rowBelow, *rowAbove, *solidRun, y + 1, result);
return result;
}
template <bool SkipColorIndexZero>
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<decltype(ClipX {}.width)>(src.width) };
if (position.y == dstHeight) {
// After-bottom line - can only draw north.
src.begin = RenderClxOutlineRowClipped</*North=*/true, /*West=*/false, /*South=*/false, /*East=*/false, SkipColorIndexZero>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(skipSize.wholeLines);
}
if (src.begin == src.end)
return;
if (position.y + 1 == dstHeight) {
// Bottom line - cannot draw south.
src.begin = RenderClxOutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/false, /*East=*/true, SkipColorIndexZero>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(skipSize.wholeLines);
}
while (position.y > 0 && src.begin != src.end) {
src.begin = RenderClxOutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(skipSize.wholeLines);
}
if (src.begin == src.end)
return;
if (position.y == 0) {
src.begin = RenderClxOutlineRowClipped</*North=*/false, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(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</*North=*/false, /*West=*/false, /*South=*/true, /*East=*/false, SkipColorIndexZero>(
out, position, src.begin, src.width, clipX, color, skipSize);
}
}
template <bool SkipColorIndexZero>
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<int>(clipX.left);
if (position.y == dstHeight) {
// After-bottom line - can only draw north.
if (position.x <= 0) {
src.begin = RenderClxOutlineRowClipped</*North=*/true, /*West=*/false, /*South=*/false, /*East=*/false, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/true, /*CheckLastColumn=*/false>(out, position, src.begin, src.width, clipX, color, skipSize);
} else if (position.x + clipX.width >= out.w()) {
src.begin = RenderClxOutlineRowClipped</*North=*/true, /*West=*/false, /*South=*/false, /*East=*/false, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/false, /*CheckLastColumn=*/true>(out, position, src.begin, src.width, clipX, color, skipSize);
} else {
src.begin = RenderClxOutlineRowClipped</*North=*/true, /*West=*/false, /*South=*/false, /*East=*/false, SkipColorIndexZero,
/*ClipWidth=*/true>(out, position, src.begin, src.width, clipX, color, skipSize);
}
position.y -= static_cast<int>(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</*North=*/true, /*West=*/true, /*South=*/false, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/true, /*CheckLastColumn=*/false>(
out, position, src.begin, src.width, clipX, color, skipSize);
} else if (position.x + clipX.width >= out.w()) {
src.begin = RenderClxOutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/false, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/false, /*CheckLastColumn=*/true>(
out, position, src.begin, src.width, clipX, color, skipSize);
} else {
src.begin = RenderClxOutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/false, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true>(
out, position, src.begin, src.width, clipX, color, skipSize);
}
position.y -= static_cast<int>(skipSize.wholeLines);
}
if (position.x <= 0) {
while (position.y > 0 && src.begin != src.end) {
src.begin = RenderClxOutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/true, /*CheckLastColumn=*/false>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(skipSize.wholeLines);
}
} else if (position.x + clipX.width >= out.w()) {
while (position.y > 0 && src.begin != src.end) {
src.begin = RenderClxOutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/false, /*CheckLastColumn=*/true>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(skipSize.wholeLines);
const OutlinePixels pixels = GetOutline<SkipColorIndexZero>(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</*North=*/true, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(skipSize.wholeLines);
}
}
if (src.begin == src.end)
return;
if (position.y == 0) {
if (position.x <= 0) {
src.begin = RenderClxOutlineRowClipped</*North=*/false, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/true, /*CheckLastColumn=*/false>(
out, position, src.begin, src.width, clipX, color, skipSize);
} else if (position.x + clipX.width >= out.w()) {
src.begin = RenderClxOutlineRowClipped</*North=*/false, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/false, /*CheckLastColumn=*/true>(
out, position, src.begin, src.width, clipX, color, skipSize);
} else {
src.begin = RenderClxOutlineRowClipped</*North=*/false, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true>(
out, position, src.begin, src.width, clipX, color, skipSize);
}
position.y -= static_cast<int>(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</*North=*/false, /*West=*/false, /*South=*/true, /*East=*/false, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/true, /*CheckLastColumn=*/false>(out, position, src.begin, src.width, clipX, color, skipSize);
} else if (position.x + clipX.width >= out.w()) {
src.begin = RenderClxOutlineRowClipped</*North=*/false, /*West=*/false, /*South=*/true, /*East=*/false, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/false, /*CheckLastColumn=*/true>(out, position, src.begin, src.width, clipX, color, skipSize);
} else {
src.begin = RenderClxOutlineRowClipped</*North=*/false, /*West=*/false, /*South=*/true, /*East=*/false, SkipColorIndexZero,
/*ClipWidth=*/true>(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 <bool SkipColorIndexZero>
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<uint_fast16_t>(srcWidth) };
if (position.x > 0 && position.x + static_cast<int>(srcWidth) < static_cast<int>(out.w())) {
RenderClxOutlineClippedY<SkipColorIndexZero>(out, position, srcForBackwards, color);
} else {
RenderClxOutlineClippedXY<SkipColorIndexZero>(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</*SkipColorIndexZero=*/false>(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), col);
RenderClxOutline</*SkipColorIndexZero=*/false>(out, position, clx, col);
}
void ClxDrawOutlineSkipColorZero(const Surface &out, uint8_t col, Point position, ClxSprite clx)
{
RenderClxOutline</*SkipColorIndexZero=*/true>(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), col);
RenderClxOutline</*SkipColorIndexZero=*/true>(out, position, clx, col);
}
} // namespace devilution

92
Source/utils/static_bit_vector.hpp

@ -0,0 +1,92 @@
#include <algorithm>
#include <climits>
#include <cstddef>
#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 <size_t N, typename Word = unsigned long> // 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

8
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) {

16
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)

30
test/static_bit_vector_test.cpp

@ -0,0 +1,30 @@
#include <gtest/gtest.h>
#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
Loading…
Cancel
Save