Browse Source

Palette kd-tree: Store color values in leaves

Storing color values directly in leaves improves lookup speed,
at the cost of a bit more space and a slightly slower `BuildTree`.

Tree size: 360 bytes -> 1119 bytes.

```
Benchmark                                              Time             CPU      Time Old      Time New       CPU Old       CPU New
-----------------------------------------------------------------------------------------------------------------------------------
BM_GenerateBlendedLookupTable_pvalue                 0.0002          0.0002      U Test, Repetitions: 10 vs 10
BM_GenerateBlendedLookupTable_mean                  +0.0116         +0.0117       2245830       2271969       2245300       2271555
BM_GenerateBlendedLookupTable_median                +0.0115         +0.0116       2245978       2271854       2245441       2271588
BM_GenerateBlendedLookupTable_stddev                -0.3436         -0.6455           659           433           505           179
BM_GenerateBlendedLookupTable_cv                    -0.3512         -0.6496             0             0             0             0
BM_BuildTree_pvalue                                  0.0002          0.0002      U Test, Repetitions: 10 vs 10
BM_BuildTree_mean                                   +0.0636         +0.0649          6382          6788          6372          6786
BM_BuildTree_median                                 +0.0649         +0.0652          6375          6788          6371          6787
BM_BuildTree_stddev                                 -0.8562         +0.4121            23             3             2             3
BM_BuildTree_cv                                     -0.8648         +0.3260             0             0             0             0
BM_FindNearestNeighbor_pvalue                        0.0002          0.0002      U Test, Repetitions: 10 vs 10
BM_FindNearestNeighbor_mean                         -0.1665         -0.1662    2555103647    2129742669    2553963618    2129377560
BM_FindNearestNeighbor_median                       -0.1663         -0.1663    2554516344    2129730092    2553962695    2129360650
BM_FindNearestNeighbor_stddev                       -0.7871         +0.6518       1986207        422943        254256        419979
BM_FindNearestNeighbor_cv                           -0.7445         +0.9812             0             0             0             0
OVERALL_GEOMEAN                                     -0.0356         -0.0351             0             0             0             0
```
pull/8048/head
Gleb Mazovetskiy 9 months ago
parent
commit
79395b2ae3
  1. 1
      Source/CMakeLists.txt
  2. 98
      Source/utils/palette_kd_tree.hpp

1
Source/CMakeLists.txt

@ -409,6 +409,7 @@ add_devilutionx_object_library(libdevilutionx_palette_blending
) )
target_link_dependencies(libdevilutionx_palette_blending PUBLIC target_link_dependencies(libdevilutionx_palette_blending PUBLIC
DevilutionX::SDL DevilutionX::SDL
fmt::fmt
libdevilutionx_strings libdevilutionx_strings
) )

98
Source/utils/palette_kd_tree.hpp

@ -9,6 +9,7 @@
#include <utility> #include <utility>
#include <SDL.h> #include <SDL.h>
#include <fmt/format.h>
#include "utils/static_vector.hpp" #include "utils/static_vector.hpp"
#include "utils/str_cat.hpp" #include "utils/str_cat.hpp"
@ -21,11 +22,11 @@
namespace devilution { namespace devilution {
[[nodiscard]] inline uint32_t GetColorDistance(const SDL_Color &a, const std::array<uint8_t, 3> &b) [[nodiscard]] inline uint32_t GetColorDistance(const std::array<uint8_t, 3> &a, const std::array<uint8_t, 3> &b)
{ {
const int diffr = a.r - b[0]; const int diffr = a[0] - b[0];
const int diffg = a.g - b[1]; const int diffg = a[1] - b[1];
const int diffb = a.b - b[2]; const int diffb = a[2] - b[2];
return (diffr * diffr) + (diffg * diffg) + (diffb * diffb); return (diffr * diffr) + (diffg * diffg) + (diffb * diffb);
} }
@ -58,6 +59,8 @@ constexpr size_t PaletteKdTreeDepth = 5;
*/ */
template <size_t RemainingDepth> template <size_t RemainingDepth>
struct PaletteKdTreeNode { struct PaletteKdTreeNode {
using RGB = std::array<uint8_t, 3>;
static constexpr unsigned Coord = (PaletteKdTreeDepth - RemainingDepth) % 3; static constexpr unsigned Coord = (PaletteKdTreeDepth - RemainingDepth) % 3;
PaletteKdTreeNode<RemainingDepth - 1> left; PaletteKdTreeNode<RemainingDepth - 1> left;
@ -97,7 +100,7 @@ struct PaletteKdTreeNode {
} }
} }
[[maybe_unused]] void toGraphvizDot(size_t id, std::span<const uint8_t, 256> values, std::string &dot) const [[maybe_unused]] void toGraphvizDot(size_t id, std::span<const std::pair<RGB, uint8_t>, 256> values, std::string &dot) const
{ {
StrAppend(dot, " node_", id, " [label=\""); StrAppend(dot, " node_", id, " [label=\"");
if (Coord == 0) { if (Coord == 0) {
@ -123,27 +126,36 @@ struct PaletteKdTreeNode {
*/ */
template <> template <>
struct PaletteKdTreeNode</*RemainingDepth=*/0> { struct PaletteKdTreeNode</*RemainingDepth=*/0> {
using RGB = std::array<uint8_t, 3>;
// We use inclusive indices to allow for representing the full [0, 255] range. // We use inclusive indices to allow for representing the full [0, 255] range.
// An empty node is represented as [1, 0]. // An empty node is represented as [1, 0].
uint8_t valuesBegin; uint8_t valuesBegin;
uint8_t valuesEndInclusive; uint8_t valuesEndInclusive;
[[maybe_unused]] void toGraphvizDot(size_t id, std::span<const uint8_t, 256> values, std::string &dot) const [[maybe_unused]] void toGraphvizDot(size_t id, std::span<const std::pair<RGB, uint8_t>, 256> values, std::string &dot) const
{ {
StrAppend(dot, " node_", id, " [shape=box label=\""); StrAppend(dot, " node_", id, R"( [shape=plain label=<
const uint8_t *it = values.data() + valuesBegin; <table border="0" cellborder="0" cellspacing="0" cellpadding="2" style="ROUNDED">
const uint8_t *const end = values.data() + valuesEndInclusive; <tr>)");
while (it <= end) { const std::pair<RGB, uint8_t> *const end = values.data() + valuesEndInclusive;
StrAppend(dot, static_cast<int>(*it), ", "); for (const std::pair<RGB, uint8_t> *it = values.data() + valuesBegin; it <= end; ++it) {
++it; const auto &[rgb, paletteIndex] = *it;
} char hexColor[6];
if (valuesBegin <= valuesEndInclusive) { fmt::format_to(hexColor, "{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]);
dot[dot.size() - 2] = '\"'; StrAppend(dot, R"(<td balign="left" bgcolor="#)", std::string_view(hexColor, 6), "\">");
dot[dot.size() - 1] = ']'; const bool useWhiteText = rgb[0] + rgb[1] + rgb[2] < 350;
dot += "\n"; if (useWhiteText) StrAppend(dot, R"(<font color="white">)");
} else { StrAppend(dot,
StrAppend(dot, "\"]\n"); static_cast<int>(rgb[0]), " ",
static_cast<int>(rgb[1]), " ",
static_cast<int>(rgb[2]), R"(<br/>)",
static_cast<int>(paletteIndex));
if (useWhiteText) StrAppend(dot, "</font>");
StrAppend(dot, "</td>");
} }
if (valuesBegin > valuesEndInclusive) StrAppend(dot, "<td></td>");
StrAppend(dot, "</tr>\n </table>>]\n");
} }
}; };
@ -167,9 +179,8 @@ public:
* Colors between skipFrom and skipTo (inclusive) are skipped. * Colors between skipFrom and skipTo (inclusive) are skipped.
*/ */
explicit PaletteKdTree(const SDL_Color palette[256], int skipFrom, int skipTo) explicit PaletteKdTree(const SDL_Color palette[256], int skipFrom, int skipTo)
: palette_(palette)
{ {
populatePivots(skipFrom, skipTo); populatePivots(palette, skipFrom, skipTo);
StaticVector<uint8_t, 256> leafValues[NumLeaves]; StaticVector<uint8_t, 256> leafValues[NumLeaves];
for (int i = 0; i < 256; ++i) { for (int i = 0; i < 256; ++i) {
if (i >= skipFrom && i <= skipTo) continue; if (i >= skipFrom && i <= skipTo) continue;
@ -186,7 +197,11 @@ public:
} else { } else {
leaf.valuesBegin = static_cast<uint8_t>(totalLen); leaf.valuesBegin = static_cast<uint8_t>(totalLen);
leaf.valuesEndInclusive = static_cast<uint8_t>(totalLen - 1 + values.size()); leaf.valuesEndInclusive = static_cast<uint8_t>(totalLen - 1 + values.size());
std::copy(values.begin(), values.end(), values_.data() + totalLen);
for (size_t i = 0; i < values.size(); ++i) {
const uint8_t value = values[i];
values_[totalLen + i] = std::make_pair(RGB { palette[value].r, palette[value].g, palette[value].b }, value);
}
totalLen += values.size(); totalLen += values.size();
} }
} }
@ -206,7 +221,7 @@ public:
uint8_t best; uint8_t best;
uint32_t bestDiff = std::numeric_limits<uint32_t>::max(); uint32_t bestDiff = std::numeric_limits<uint32_t>::max();
findNearestNeighborVisit(tree_, rgb, bestDiff, best); findNearestNeighborVisit(tree_, rgb, bestDiff, best);
return best; return values_[best].second;
} }
[[maybe_unused]] [[nodiscard]] std::string toGraphvizDot() const [[maybe_unused]] [[nodiscard]] std::string toGraphvizDot() const
@ -242,16 +257,18 @@ private:
} }
template <size_t RemainingDepth, size_t N> template <size_t RemainingDepth, size_t N>
void maybeAddToSubdivisionForMedian( static void maybeAddToSubdivisionForMedian(
const PaletteKdTreeNode<RemainingDepth> &node, unsigned paletteIndex, const PaletteKdTreeNode<RemainingDepth> &node,
const SDL_Color palette[256], unsigned paletteIndex,
std::span<StaticVector<uint8_t, 256>, N> out) std::span<StaticVector<uint8_t, 256>, N> out)
{ {
const uint8_t color = node.getColorCoordinate(palette_[paletteIndex]); const uint8_t color = node.getColorCoordinate(palette[paletteIndex]);
if constexpr (N == 1) { if constexpr (N == 1) {
out[0].emplace_back(color); out[0].emplace_back(color);
} else { } else {
const bool isLeft = color < node.pivot; const bool isLeft = color < node.pivot;
maybeAddToSubdivisionForMedian(node.child(isLeft), maybeAddToSubdivisionForMedian(node.child(isLeft),
palette,
paletteIndex, paletteIndex,
isLeft isLeft
? out.template subspan<0, N / 2>() ? out.template subspan<0, N / 2>()
@ -260,7 +277,7 @@ private:
} }
template <size_t RemainingDepth, size_t N> template <size_t RemainingDepth, size_t N>
void setPivotsRecursively( static void setPivotsRecursively(
PaletteKdTreeNode<RemainingDepth> &node, PaletteKdTreeNode<RemainingDepth> &node,
std::span<StaticVector<uint8_t, 256>, N> values) std::span<StaticVector<uint8_t, 256>, N> values)
{ {
@ -273,27 +290,27 @@ private:
} }
template <size_t TargetDepth> template <size_t TargetDepth>
void populatePivotsForTargetDepth(int skipFrom, int skipTo) void populatePivotsForTargetDepth(const SDL_Color palette[256], int skipFrom, int skipTo)
{ {
constexpr size_t NumSubdivisions = 1U << TargetDepth; constexpr size_t NumSubdivisions = 1U << TargetDepth;
std::array<StaticVector<uint8_t, 256>, NumSubdivisions> subdivisions; std::array<StaticVector<uint8_t, 256>, NumSubdivisions> subdivisions;
const std::span<StaticVector<uint8_t, 256>, NumSubdivisions> subdivisionsSpan { subdivisions }; const std::span<StaticVector<uint8_t, 256>, NumSubdivisions> subdivisionsSpan { subdivisions };
for (int i = 0; i < 256; ++i) { for (int i = 0; i < 256; ++i) {
if (i >= skipFrom && i <= skipTo) continue; if (i >= skipFrom && i <= skipTo) continue;
maybeAddToSubdivisionForMedian(tree_, i, subdivisionsSpan); maybeAddToSubdivisionForMedian(tree_, palette, i, subdivisionsSpan);
} }
setPivotsRecursively(tree_, subdivisionsSpan); setPivotsRecursively(tree_, subdivisionsSpan);
} }
template <size_t... TargetDepths> template <size_t... TargetDepths>
void populatePivotsImpl(int skipFrom, int skipTo, std::index_sequence<TargetDepths...> intSeq) // NOLINT(misc-unused-parameters) void populatePivotsImpl(const SDL_Color palette[256], int skipFrom, int skipTo, std::index_sequence<TargetDepths...> intSeq) // NOLINT(misc-unused-parameters)
{ {
(populatePivotsForTargetDepth<TargetDepths>(skipFrom, skipTo), ...); (populatePivotsForTargetDepth<TargetDepths>(palette, skipFrom, skipTo), ...);
} }
void populatePivots(int skipFrom, int skipTo) void populatePivots(const SDL_Color palette[256], int skipFrom, int skipTo)
{ {
populatePivotsImpl(skipFrom, skipTo, std::make_index_sequence<PaletteKdTreeDepth> {}); populatePivotsImpl(palette, skipFrom, skipTo, std::make_index_sequence<PaletteKdTreeDepth> {});
} }
template <size_t RemainingDepth> template <size_t RemainingDepth>
@ -307,7 +324,7 @@ private:
// to the current best candidate vs the distance to the edge of the half-space represented // to the current best candidate vs the distance to the edge of the half-space represented
// by the node. // by the node.
if (bestDiff == std::numeric_limits<uint32_t>::max() if (bestDiff == std::numeric_limits<uint32_t>::max()
|| GetColorDistanceToPlane(node.pivot, coord) < GetColorDistance(palette_[best], rgb)) { || GetColorDistanceToPlane(node.pivot, coord) < GetColorDistance(values_[best].first, rgb)) {
findNearestNeighborVisit(node.child(coord >= node.pivot), rgb, bestDiff, best); findNearestNeighborVisit(node.child(coord >= node.pivot), rgb, bestDiff, best);
} }
} }
@ -315,21 +332,20 @@ private:
void findNearestNeighborVisit(const PaletteKdTreeNode<0> &node, const RGB &rgb, void findNearestNeighborVisit(const PaletteKdTreeNode<0> &node, const RGB &rgb,
uint32_t &bestDiff, uint8_t &best) const uint32_t &bestDiff, uint8_t &best) const
{ {
const uint8_t *it = values_.data() + node.valuesBegin; const std::pair<RGB, uint8_t> *it = values_.data() + node.valuesBegin;
const uint8_t *const end = values_.data() + node.valuesEndInclusive; const std::pair<RGB, uint8_t> *const end = values_.data() + node.valuesEndInclusive;
while (it <= end) { while (it <= end) {
const uint8_t paletteIndex = *it++; const auto &[paletteColor, paletteIndex] = *it++;
const uint32_t diff = GetColorDistance(palette_[paletteIndex], rgb); const uint32_t diff = GetColorDistance(paletteColor, rgb);
if (diff < bestDiff) { if (diff < bestDiff) {
best = paletteIndex; best = static_cast<uint8_t>(it - values_.data() - 1);
bestDiff = diff; bestDiff = diff;
} }
} }
} }
const SDL_Color *palette_;
PaletteKdTreeNode<PaletteKdTreeDepth> tree_; PaletteKdTreeNode<PaletteKdTreeDepth> tree_;
std::array<uint8_t, 256> values_; std::array<std::pair<RGB, uint8_t>, 256> values_;
}; };
} // namespace devilution } // namespace devilution

Loading…
Cancel
Save