From 79395b2ae367f1adf420a6e236a2f4e648c206d9 Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sun, 29 Jun 2025 17:09:32 +0100 Subject: [PATCH] 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 ``` --- Source/CMakeLists.txt | 1 + Source/utils/palette_kd_tree.hpp | 98 +++++++++++++++++++------------- 2 files changed, 58 insertions(+), 41 deletions(-) diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index d1a5fd762..d067026c2 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -409,6 +409,7 @@ add_devilutionx_object_library(libdevilutionx_palette_blending ) target_link_dependencies(libdevilutionx_palette_blending PUBLIC DevilutionX::SDL + fmt::fmt libdevilutionx_strings ) diff --git a/Source/utils/palette_kd_tree.hpp b/Source/utils/palette_kd_tree.hpp index 519dc65fe..84308f964 100644 --- a/Source/utils/palette_kd_tree.hpp +++ b/Source/utils/palette_kd_tree.hpp @@ -9,6 +9,7 @@ #include #include +#include #include "utils/static_vector.hpp" #include "utils/str_cat.hpp" @@ -21,11 +22,11 @@ namespace devilution { -[[nodiscard]] inline uint32_t GetColorDistance(const SDL_Color &a, const std::array &b) +[[nodiscard]] inline uint32_t GetColorDistance(const std::array &a, const std::array &b) { - const int diffr = a.r - b[0]; - const int diffg = a.g - b[1]; - const int diffb = a.b - b[2]; + const int diffr = a[0] - b[0]; + const int diffg = a[1] - b[1]; + const int diffb = a[2] - b[2]; return (diffr * diffr) + (diffg * diffg) + (diffb * diffb); } @@ -58,6 +59,8 @@ constexpr size_t PaletteKdTreeDepth = 5; */ template struct PaletteKdTreeNode { + using RGB = std::array; + static constexpr unsigned Coord = (PaletteKdTreeDepth - RemainingDepth) % 3; PaletteKdTreeNode left; @@ -97,7 +100,7 @@ struct PaletteKdTreeNode { } } - [[maybe_unused]] void toGraphvizDot(size_t id, std::span values, std::string &dot) const + [[maybe_unused]] void toGraphvizDot(size_t id, std::span, 256> values, std::string &dot) const { StrAppend(dot, " node_", id, " [label=\""); if (Coord == 0) { @@ -123,27 +126,36 @@ struct PaletteKdTreeNode { */ template <> struct PaletteKdTreeNode { + using RGB = std::array; + // We use inclusive indices to allow for representing the full [0, 255] range. // An empty node is represented as [1, 0]. uint8_t valuesBegin; uint8_t valuesEndInclusive; - [[maybe_unused]] void toGraphvizDot(size_t id, std::span values, std::string &dot) const + [[maybe_unused]] void toGraphvizDot(size_t id, std::span, 256> values, std::string &dot) const { - StrAppend(dot, " node_", id, " [shape=box label=\""); - const uint8_t *it = values.data() + valuesBegin; - const uint8_t *const end = values.data() + valuesEndInclusive; - while (it <= end) { - StrAppend(dot, static_cast(*it), ", "); - ++it; - } - if (valuesBegin <= valuesEndInclusive) { - dot[dot.size() - 2] = '\"'; - dot[dot.size() - 1] = ']'; - dot += "\n"; - } else { - StrAppend(dot, "\"]\n"); + StrAppend(dot, " node_", id, R"( [shape=plain label=< + + )"); + const std::pair *const end = values.data() + valuesEndInclusive; + for (const std::pair *it = values.data() + valuesBegin; it <= end; ++it) { + const auto &[rgb, paletteIndex] = *it; + char hexColor[6]; + fmt::format_to(hexColor, "{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]); + StrAppend(dot, R"("); } + if (valuesBegin > valuesEndInclusive) StrAppend(dot, ""); + StrAppend(dot, "\n
"); + const bool useWhiteText = rgb[0] + rgb[1] + rgb[2] < 350; + if (useWhiteText) StrAppend(dot, R"()"); + StrAppend(dot, + static_cast(rgb[0]), " ", + static_cast(rgb[1]), " ", + static_cast(rgb[2]), R"(
)", + static_cast(paletteIndex)); + if (useWhiteText) StrAppend(dot, "
"); + StrAppend(dot, "
>]\n"); } }; @@ -167,9 +179,8 @@ public: * Colors between skipFrom and skipTo (inclusive) are skipped. */ explicit PaletteKdTree(const SDL_Color palette[256], int skipFrom, int skipTo) - : palette_(palette) { - populatePivots(skipFrom, skipTo); + populatePivots(palette, skipFrom, skipTo); StaticVector leafValues[NumLeaves]; for (int i = 0; i < 256; ++i) { if (i >= skipFrom && i <= skipTo) continue; @@ -186,7 +197,11 @@ public: } else { leaf.valuesBegin = static_cast(totalLen); leaf.valuesEndInclusive = static_cast(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(); } } @@ -206,7 +221,7 @@ public: uint8_t best; uint32_t bestDiff = std::numeric_limits::max(); findNearestNeighborVisit(tree_, rgb, bestDiff, best); - return best; + return values_[best].second; } [[maybe_unused]] [[nodiscard]] std::string toGraphvizDot() const @@ -242,16 +257,18 @@ private: } template - void maybeAddToSubdivisionForMedian( - const PaletteKdTreeNode &node, unsigned paletteIndex, + static void maybeAddToSubdivisionForMedian( + const PaletteKdTreeNode &node, + const SDL_Color palette[256], unsigned paletteIndex, std::span, N> out) { - const uint8_t color = node.getColorCoordinate(palette_[paletteIndex]); + const uint8_t color = node.getColorCoordinate(palette[paletteIndex]); if constexpr (N == 1) { out[0].emplace_back(color); } else { const bool isLeft = color < node.pivot; maybeAddToSubdivisionForMedian(node.child(isLeft), + palette, paletteIndex, isLeft ? out.template subspan<0, N / 2>() @@ -260,7 +277,7 @@ private: } template - void setPivotsRecursively( + static void setPivotsRecursively( PaletteKdTreeNode &node, std::span, N> values) { @@ -273,27 +290,27 @@ private: } template - void populatePivotsForTargetDepth(int skipFrom, int skipTo) + void populatePivotsForTargetDepth(const SDL_Color palette[256], int skipFrom, int skipTo) { constexpr size_t NumSubdivisions = 1U << TargetDepth; std::array, NumSubdivisions> subdivisions; const std::span, NumSubdivisions> subdivisionsSpan { subdivisions }; for (int i = 0; i < 256; ++i) { if (i >= skipFrom && i <= skipTo) continue; - maybeAddToSubdivisionForMedian(tree_, i, subdivisionsSpan); + maybeAddToSubdivisionForMedian(tree_, palette, i, subdivisionsSpan); } setPivotsRecursively(tree_, subdivisionsSpan); } template - void populatePivotsImpl(int skipFrom, int skipTo, std::index_sequence intSeq) // NOLINT(misc-unused-parameters) + void populatePivotsImpl(const SDL_Color palette[256], int skipFrom, int skipTo, std::index_sequence intSeq) // NOLINT(misc-unused-parameters) { - (populatePivotsForTargetDepth(skipFrom, skipTo), ...); + (populatePivotsForTargetDepth(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 {}); + populatePivotsImpl(palette, skipFrom, skipTo, std::make_index_sequence {}); } template @@ -307,7 +324,7 @@ private: // to the current best candidate vs the distance to the edge of the half-space represented // by the node. if (bestDiff == std::numeric_limits::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); } } @@ -315,21 +332,20 @@ private: void findNearestNeighborVisit(const PaletteKdTreeNode<0> &node, const RGB &rgb, uint32_t &bestDiff, uint8_t &best) const { - const uint8_t *it = values_.data() + node.valuesBegin; - const uint8_t *const end = values_.data() + node.valuesEndInclusive; + const std::pair *it = values_.data() + node.valuesBegin; + const std::pair *const end = values_.data() + node.valuesEndInclusive; while (it <= end) { - const uint8_t paletteIndex = *it++; - const uint32_t diff = GetColorDistance(palette_[paletteIndex], rgb); + const auto &[paletteColor, paletteIndex] = *it++; + const uint32_t diff = GetColorDistance(paletteColor, rgb); if (diff < bestDiff) { - best = paletteIndex; + best = static_cast(it - values_.data() - 1); bestDiff = diff; } } } - const SDL_Color *palette_; PaletteKdTreeNode tree_; - std::array values_; + std::array, 256> values_; }; } // namespace devilution