#include "utils/palette_kd_tree.hpp" #include #include #include #include #include #include #ifdef USE_SDL3 #include #elif defined(USE_SDL1) #include #else #include #endif #include "utils/static_vector.hpp" #include "utils/str_cat.hpp" #if DEVILUTIONX_PRINT_PALETTE_BLENDING_TREE_GRAPHVIZ #include #endif namespace devilution { namespace { template uint8_t GetColorComponent(const SDL_Color &); template <> inline uint8_t GetColorComponent<0>(const SDL_Color &c) { return c.r; } template <> inline uint8_t GetColorComponent<1>(const SDL_Color &c) { return c.g; } template <> inline uint8_t GetColorComponent<2>(const SDL_Color &c) { return c.b; } template [[nodiscard]] PaletteKdTreeNode<0> &LeafByIndex(PaletteKdTreeNode &node, uint8_t index) { if constexpr (RemainingDepth == 1) { return node.child(index % 2 == 0); } else { return LeafByIndex(node.child(index % 2 == 0), index / 2); } } template [[nodiscard]] uint8_t LeafIndexForColor(const PaletteKdTreeNode &node, const SDL_Color &color, uint8_t result = 0) { const bool isLeft = GetColorComponent::Coord>(color) < node.pivot; if constexpr (RemainingDepth == 1) { return (2 * result) + (isLeft ? 0 : 1); } else { return (2 * LeafIndexForColor(node.child(isLeft), color, result)) + (isLeft ? 0 : 1); } } struct MedianInfo { std::array counts = {}; uint16_t numValues = 0; }; [[nodiscard]] static uint8_t GetMedian(const MedianInfo &medianInfo) { const std::span counts = medianInfo.counts; const uint_fast16_t numValues = medianInfo.numValues; const auto medianTarget = static_cast((medianInfo.numValues + 1) / 2); uint_fast16_t partialSum = 0; uint_fast16_t i = 0; for (; partialSum < medianTarget && partialSum != numValues; ++i) { partialSum += counts[i]; } // Special cases: // 1. If the elements are empty, this will return 0. // 2. If all the elements are the same, this will be `value + 1` (rolling over to 0 if value is 256). // This means all the elements will be on one side of the pivot (left unless the value is 255). return static_cast(i); } template void MaybeAddToSubdivisionForMedian( const PaletteKdTreeNode &node, const SDL_Color palette[256], unsigned paletteIndex, std::span medianInfos) { const uint8_t color = GetColorComponent::Coord>(palette[paletteIndex]); if constexpr (N == 1) { MedianInfo &medianInfo = medianInfos[0]; ++medianInfo.counts[color]; ++medianInfo.numValues; } else { const bool isLeft = color < node.pivot; MaybeAddToSubdivisionForMedian(node.child(isLeft), palette, paletteIndex, isLeft ? medianInfos.template subspan<0, N / 2>() : medianInfos.template subspan()); } } template void SetPivotsRecursively( PaletteKdTreeNode &node, std::span medianInfos) { if constexpr (N == 1) { node.pivot = GetMedian(medianInfos[0]); } else { SetPivotsRecursively(node.left, medianInfos.template subspan<0, N / 2>()); SetPivotsRecursively(node.right, medianInfos.template subspan()); } } template void PopulatePivotsForTargetDepth(PaletteKdTreeNode &root, const SDL_Color palette[256], int skipFrom, int skipTo) { constexpr size_t NumSubdivisions = 1U << TargetDepth; std::array subdivisions = {}; const std::span subdivisionsSpan { subdivisions }; for (int i = 0; i < 256; ++i) { if (i >= skipFrom && i <= skipTo) continue; MaybeAddToSubdivisionForMedian(root, palette, i, subdivisionsSpan); } SetPivotsRecursively(root, subdivisionsSpan); } template void PopulatePivotsImpl(PaletteKdTreeNode &root, const SDL_Color palette[256], int skipFrom, int skipTo, std::index_sequence intSeq) // NOLINT(misc-unused-parameters) { (PopulatePivotsForTargetDepth(root, palette, skipFrom, skipTo), ...); } void PopulatePivots(PaletteKdTreeNode &root, const SDL_Color palette[256], int skipFrom, int skipTo) { PopulatePivotsImpl(root, palette, skipFrom, skipTo, std::make_index_sequence {}); } } // namespace PaletteKdTree::PaletteKdTree(const SDL_Color palette[256], int skipFrom, int skipTo) { PopulatePivots(tree_, palette, skipFrom, skipTo); StaticVector leafValues[NumLeaves]; for (int i = 0; i < 256; ++i) { if (i >= skipFrom && i <= skipTo) continue; leafValues[LeafIndexForColor(tree_, palette[i])].emplace_back(i); } size_t totalLen = 0; for (uint8_t leafIndex = 0; leafIndex < NumLeaves; ++leafIndex) { PaletteKdTreeNode<0> &leaf = LeafByIndex(tree_, leafIndex); const std::span values = leafValues[leafIndex]; if (values.empty()) { leaf.valuesBegin = 1; leaf.valuesEndInclusive = 0; } else { leaf.valuesBegin = static_cast(totalLen); leaf.valuesEndInclusive = static_cast(totalLen - 1 + values.size()); 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(); } } #if DEVILUTIONX_PRINT_PALETTE_BLENDING_TREE_GRAPHVIZ // To generate palette.dot.svg, run: // dot -O -Tsvg palette.dot FILE *out = std::fopen("palette.dot", "w"); std::string dot = toGraphvizDot(); std::fwrite(dot.data(), dot.size(), 1, out); std::fclose(out); #endif } std::string PaletteKdTree::toGraphvizDot() const { std::string dot = "graph palette_tree {\n rankdir=LR\n"; tree_.toGraphvizDot(0, values_, dot); dot.append("}\n"); return dot; } void PaletteKdTreeNode<0>::toGraphvizDot( size_t id, std::span::RGB, uint8_t>, 256> values, std::string &dot) const { 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; 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"); } } // namespace devilution