diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 0acdb6931..358d8c9ad 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -404,6 +404,13 @@ target_link_dependencies(libdevilutionx_monster PUBLIC libdevilutionx_txtdata ) +add_devilutionx_object_library(libdevilutionx_palette_blending + utils/palette_blending.cpp +) +target_link_dependencies(libdevilutionx_palette_blending PUBLIC + DevilutionX::SDL +) + add_devilutionx_object_library(libdevilutionx_parse_int utils/parse_int.cpp ) @@ -701,6 +708,7 @@ target_link_dependencies(libdevilutionx PUBLIC libdevilutionx_multiplayer libdevilutionx_options libdevilutionx_padmapper + libdevilutionx_palette_blending libdevilutionx_parse_int libdevilutionx_pathfinding libdevilutionx_pkware_encrypt diff --git a/Source/cursor.cpp b/Source/cursor.cpp index a877e083a..ea5885f8e 100644 --- a/Source/cursor.cpp +++ b/Source/cursor.cpp @@ -38,6 +38,7 @@ #include "utils/attributes.h" #include "utils/is_of.hpp" #include "utils/language.h" +#include "utils/palette_blending.hpp" #include "utils/sdl_bilinear_scale.hpp" #include "utils/surface_to_clx.hpp" #include "utils/utf8.hpp" diff --git a/Source/engine/palette.cpp b/Source/engine/palette.cpp index 3dcf97f9c..7a73d9a57 100644 --- a/Source/engine/palette.cpp +++ b/Source/engine/palette.cpp @@ -18,6 +18,7 @@ #include "hwcursor.hpp" #include "options.h" #include "utils/display.h" +#include "utils/palette_blending.hpp" #include "utils/sdl_compat.h" namespace devilution { @@ -26,15 +27,6 @@ std::array logical_palette; std::array system_palette; std::array orig_palette; -// This array is read from a lot on every frame. -// We do not use `std::array` here to improve debug build performance. -// In a debug build, `std::array` accesses are function calls. -Uint8 paletteTransparencyLookup[256][256]; - -#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT -uint16_t paletteTransparencyLookupBlack16[65536]; -#endif - namespace { /** Specifies whether the palette has max brightness. */ @@ -47,26 +39,6 @@ void LoadBrightness() GetOptions().Graphics.brightness.SetValue(brightnessValue - brightnessValue % 5); } -Uint8 FindBestMatchForColor(std::array &palette, SDL_Color color, int skipFrom, int skipTo) -{ - Uint8 best; - Uint32 bestDiff = SDL_MAX_UINT32; - for (int i = 0; i < 256; i++) { - if (i >= skipFrom && i <= skipTo) - continue; - int diffr = palette[i].r - color.r; - int diffg = palette[i].g - color.g; - int diffb = palette[i].b - color.b; - Uint32 diff = diffr * diffr + diffg * diffg + diffb * diffb; - - if (bestDiff > diff) { - best = i; - bestDiff = diff; - } - } - return best; -} - /** * @brief Generate lookup table for transparency * @@ -80,55 +52,6 @@ Uint8 FindBestMatchForColor(std::array &palette, SDL_Color color * @param skipTo Do not use colors between skipFrom and this index * @param toUpdate Only update the first n colors */ -void GenerateBlendedLookupTable(std::array &palette, int skipFrom, int skipTo, int toUpdate = 256) -{ - for (int i = 0; i < 256; i++) { - for (int j = 0; j < 256; j++) { - if (i == j) { // No need to calculate transparency between 2 identical colors - paletteTransparencyLookup[i][j] = j; - continue; - } - if (i > j) { // Half the blends will be mirror identical ([i][j] is the same as [j][i]), so simply copy the existing combination. - paletteTransparencyLookup[i][j] = paletteTransparencyLookup[j][i]; - continue; - } - if (i > toUpdate && j > toUpdate) { - continue; - } - - SDL_Color blendedColor; - blendedColor.r = ((int)palette[i].r + (int)palette[j].r) / 2; - blendedColor.g = ((int)palette[i].g + (int)palette[j].g) / 2; - blendedColor.b = ((int)palette[i].b + (int)palette[j].b) / 2; - Uint8 best = FindBestMatchForColor(palette, blendedColor, skipFrom, skipTo); - paletteTransparencyLookup[i][j] = best; - } - } - -#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT - for (unsigned i = 0; i < 256; ++i) { - for (unsigned j = 0; j < 256; ++j) { - const std::uint16_t index = i | (j << 8); - paletteTransparencyLookupBlack16[index] = paletteTransparencyLookup[0][i] | (paletteTransparencyLookup[0][j] << 8); - } - } -#endif -} - -#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT -void UpdateTransparencyLookupBlack16(int from, int to) -{ - for (int i = from; i <= to; i++) { - for (int j = 0; j < 256; j++) { - const std::uint16_t index = i | (j << 8); - const std::uint16_t reverseIndex = j | (i << 8); - paletteTransparencyLookupBlack16[index] = paletteTransparencyLookup[0][i] | (paletteTransparencyLookup[0][j] << 8); - paletteTransparencyLookupBlack16[reverseIndex] = paletteTransparencyLookup[0][j] | (paletteTransparencyLookup[0][i] << 8); - } - } -} -#endif - /** * @brief Cycle the given range of colors in the palette * @param from First color index of the range @@ -253,11 +176,11 @@ void LoadPalette(const char *pszFileName, bool blend /*= true*/) if (blend) { if (leveltype == DTYPE_CAVES || leveltype == DTYPE_CRYPT) { - GenerateBlendedLookupTable(orig_palette, 1, 31); + GenerateBlendedLookupTable(orig_palette.data(), 1, 31); } else if (leveltype == DTYPE_NEST) { - GenerateBlendedLookupTable(orig_palette, 1, 15); + GenerateBlendedLookupTable(orig_palette.data(), 1, 15); } else { - GenerateBlendedLookupTable(orig_palette, -1, -1); + GenerateBlendedLookupTable(orig_palette.data(), -1, -1); } } } @@ -462,23 +385,7 @@ void palette_update_quest_palette(int n) logical_palette[i] = orig_palette[i]; ApplyToneMapping(system_palette, logical_palette, 32); palette_update(0, 31); - // Update blended transparency, but only for the color that was updated - for (int j = 0; j < 256; j++) { - if (i == j) { // No need to calculate transparency between 2 identical colors - paletteTransparencyLookup[i][j] = j; - continue; - } - SDL_Color blendedColor; - blendedColor.r = ((int)logical_palette[i].r + (int)logical_palette[j].r) / 2; - blendedColor.g = ((int)logical_palette[i].g + (int)logical_palette[j].g) / 2; - blendedColor.b = ((int)logical_palette[i].b + (int)logical_palette[j].b) / 2; - Uint8 best = FindBestMatchForColor(logical_palette, blendedColor, 1, 31); - paletteTransparencyLookup[i][j] = paletteTransparencyLookup[j][i] = best; - } - -#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT - UpdateTransparencyLookupBlack16(i, i); -#endif + UpdateBlendedLookupTableSingleColor(i, logical_palette.data(), /*skipFrom=*/1, /*skipTo=*/31); } } // namespace devilution diff --git a/Source/engine/palette.h b/Source/engine/palette.h index 4c8e6bc83..e6993cd85 100644 --- a/Source/engine/palette.h +++ b/Source/engine/palette.h @@ -37,21 +37,6 @@ namespace devilution { extern std::array logical_palette; extern std::array system_palette; extern std::array orig_palette; -/** Lookup table for transparency */ -extern Uint8 paletteTransparencyLookup[256][256]; - -#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT -/** - * A lookup table from black for a pair of colors. - * - * For a pair of colors i and j, the index `i | (j << 8)` contains - * `paletteTransparencyLookup[0][i] | (paletteTransparencyLookup[0][j] << 8)`. - * - * On big-endian platforms, the indices are encoded as `j | (i << 8)`, while the - * value order remains the same. - */ -extern uint16_t paletteTransparencyLookupBlack16[65536]; -#endif void palette_update(int first = 0, int ncolor = 256); void palette_init(); diff --git a/Source/engine/render/blit_impl.hpp b/Source/engine/render/blit_impl.hpp index 9b0f93764..d8a7468cd 100644 --- a/Source/engine/render/blit_impl.hpp +++ b/Source/engine/render/blit_impl.hpp @@ -5,9 +5,9 @@ #include #include -#include "engine/palette.h" #include "engine/render/light_render.hpp" #include "utils/attributes.h" +#include "utils/palette_blending.hpp" namespace devilution { diff --git a/Source/engine/render/primitive_render.cpp b/Source/engine/render/primitive_render.cpp index 82506e0b5..ef7cd7a28 100644 --- a/Source/engine/render/primitive_render.cpp +++ b/Source/engine/render/primitive_render.cpp @@ -4,10 +4,10 @@ #include #include -#include "engine/palette.h" #include "engine/point.hpp" #include "engine/size.hpp" #include "engine/surface.hpp" +#include "utils/palette_blending.hpp" namespace devilution { namespace { diff --git a/Source/utils/palette_blending.cpp b/Source/utils/palette_blending.cpp new file mode 100644 index 000000000..6b22051cc --- /dev/null +++ b/Source/utils/palette_blending.cpp @@ -0,0 +1,116 @@ +#include "utils/palette_blending.hpp" + +#include +#include + +#include + +namespace devilution { + +// This array is read from a lot on every frame. +// We do not use `std::array` here to improve debug build performance. +// In a debug build, `std::array` accesses are function calls. +uint8_t paletteTransparencyLookup[256][256]; + +#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT +uint16_t paletteTransparencyLookupBlack16[65536]; +#endif + +namespace { + +struct RGB { + uint8_t r; + uint8_t g; + uint8_t b; +}; + +uint8_t FindBestMatchForColor(SDL_Color palette[256], RGB color, int skipFrom, int skipTo) +{ + uint8_t best; + uint32_t bestDiff = std::numeric_limits::max(); + for (int i = 0; i < 256; i++) { + if (i >= skipFrom && i <= skipTo) + continue; + const int diffr = palette[i].r - color.r; + const int diffg = palette[i].g - color.g; + const int diffb = palette[i].b - color.b; + const uint32_t diff = diffr * diffr + diffg * diffg + diffb * diffb; + + if (bestDiff > diff) { + best = i; + bestDiff = diff; + } + } + return best; +} + +RGB BlendColors(const SDL_Color &a, const SDL_Color &b) +{ + return RGB { + .r = static_cast((static_cast(a.r) + static_cast(b.r)) / 2), + .g = static_cast((static_cast(a.g) + static_cast(b.g)) / 2), + .b = static_cast((static_cast(a.b) + static_cast(b.b)) / 2), + }; +} + +} // namespace + +void GenerateBlendedLookupTable(SDL_Color palette[256], int skipFrom, int skipTo) +{ + for (unsigned i = 0; i < 256; i++) { + for (unsigned j = 0; j < 256; j++) { + if (i == j) { // No need to calculate transparency between 2 identical colors + paletteTransparencyLookup[i][j] = j; + continue; + } + if (i > j) { // Half the blends will be mirror identical ([i][j] is the same as [j][i]), so simply copy the existing combination. + paletteTransparencyLookup[i][j] = paletteTransparencyLookup[j][i]; + continue; + } + const uint8_t best = FindBestMatchForColor(palette, BlendColors(palette[i], palette[j]), skipFrom, skipTo); + paletteTransparencyLookup[i][j] = best; + } + } + +#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT + for (unsigned i = 0; i < 256; ++i) { + for (unsigned j = 0; j < 256; ++j) { + const uint16_t index = i | (j << 8U); + paletteTransparencyLookupBlack16[index] = paletteTransparencyLookup[0][i] | (paletteTransparencyLookup[0][j] << 8); + } + } +#endif +} + +void UpdateBlendedLookupTableSingleColor(unsigned i, SDL_Color palette[256], int skipFrom, int skipTo) +{ + // Update blended transparency, but only for the color that was updated + for (unsigned j = 0; j < 256; j++) { + if (i == j) { // No need to calculate transparency between 2 identical colors + paletteTransparencyLookup[i][j] = j; + continue; + } + const uint8_t best = FindBestMatchForColor(palette, BlendColors(palette[i], palette[j]), skipFrom, skipTo); + paletteTransparencyLookup[i][j] = paletteTransparencyLookup[j][i] = best; + } + +#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT + UpdateTransparencyLookupBlack16(i, i); +#endif +} + +#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT +void UpdateTransparencyLookupBlack16(unsigned from, unsigned to) +{ + for (unsigned i = from; i <= to; i++) { + for (unsigned j = 0; j < 256; j++) { + const uint16_t index = i | (j << 8U); + const uint16_t reverseIndex = j | (i << 8U); + paletteTransparencyLookupBlack16[index] = paletteTransparencyLookup[0][i] | (paletteTransparencyLookup[0][j] << 8); + paletteTransparencyLookupBlack16[reverseIndex] = paletteTransparencyLookup[0][j] | (paletteTransparencyLookup[0][i] << 8); + } + } +} +#endif + +} // namespace devilution diff --git a/Source/utils/palette_blending.hpp b/Source/utils/palette_blending.hpp new file mode 100644 index 000000000..7907dec0d --- /dev/null +++ b/Source/utils/palette_blending.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include + +namespace devilution { + +/** Lookup table for transparency */ +extern uint8_t paletteTransparencyLookup[256][256]; + +/** + * @brief Generate lookup table for transparency + * + * This is based of the same technique found in Quake2. + * + * To mimic 50% transparency we figure out what colors in the existing palette are the best match for the combination of any 2 colors. + * We save this into a lookup table for use during rendering. + * + * @param palette The colors to operate on + * @param skipFrom Do not use colors between this index and skipTo + * @param skipTo Do not use colors between skipFrom and this index + */ +void GenerateBlendedLookupTable(SDL_Color palette[256], int skipFrom, int skipTo); + +void UpdateBlendedLookupTableSingleColor(unsigned i, SDL_Color palette[256], int skipFrom, int skipTo); + +#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT +/** + * A lookup table from black for a pair of colors. + * + * For a pair of colors i and j, the index `i | (j << 8)` contains + * `paletteTransparencyLookup[0][i] | (paletteTransparencyLookup[0][j] << 8)`. + * + * On big-endian platforms, the indices are encoded as `j | (i << 8)`, while the + * value order remains the same. + */ +extern uint16_t paletteTransparencyLookupBlack16[65536]; + +void UpdateTransparencyLookupBlack16(unsigned from, unsigned to); +#endif + +} // namespace devilution diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index dc2ddafa8..a758e5eb0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -42,6 +42,7 @@ set(standalone_tests file_util_test format_int_test ini_test + palette_blending_test parse_int_test path_test vision_test @@ -58,6 +59,7 @@ set(benchmarks clx_render_benchmark crawl_benchmark dun_render_benchmark + palette_blending_benchmark path_benchmark ) @@ -104,6 +106,8 @@ target_link_dependencies(dun_render_benchmark PRIVATE libdevilutionx_so) target_link_dependencies(file_util_test PRIVATE libdevilutionx_file_util app_fatal_for_testing) target_link_dependencies(format_int_test PRIVATE libdevilutionx_format_int language_for_testing) target_link_dependencies(ini_test PRIVATE libdevilutionx_ini app_fatal_for_testing) +target_link_dependencies(palette_blending_test PRIVATE libdevilutionx_palette_blending DevilutionX::SDL libdevilutionx_strings GTest::gmock) +target_link_dependencies(palette_blending_benchmark PRIVATE libdevilutionx_palette_blending DevilutionX::SDL) target_link_dependencies(parse_int_test PRIVATE libdevilutionx_parse_int) target_link_dependencies(path_test PRIVATE libdevilutionx_pathfinding libdevilutionx_direction app_fatal_for_testing) target_link_dependencies(vision_test PRIVATE libdevilutionx_vision) diff --git a/test/palette_blending_benchmark.cpp b/test/palette_blending_benchmark.cpp new file mode 100644 index 000000000..93e320c80 --- /dev/null +++ b/test/palette_blending_benchmark.cpp @@ -0,0 +1,37 @@ +#include "utils/palette_blending.hpp" + +#include +#include + +namespace devilution { +namespace { + +void GeneratePalette(SDL_Color palette[256]) +{ + for (unsigned j = 0; j < 4; ++j) { + for (unsigned i = 0; i < 64; ++i) { + palette[j * 64 + i].r = i * std::max(j, 1U); + palette[j * 64 + i].g = i * j; + palette[j * 64 + i].b = i * 2; +#ifndef USE_SDL1 + palette[j * 64 + i].a = SDL_ALPHA_OPAQUE; +#endif + } + } +} + +void BM_GenerateBlendedLookupTable(benchmark::State &state) +{ + SDL_Color palette[256]; + GeneratePalette(palette); + for (auto _ : state) { + GenerateBlendedLookupTable(palette, /*skipFrom=*/-1, /*skipTo=*/-1); + int result = paletteTransparencyLookup[17][98]; + benchmark::DoNotOptimize(result); + } +} + +BENCHMARK(BM_GenerateBlendedLookupTable); + +} // namespace +} // namespace devilution diff --git a/test/palette_blending_test.cpp b/test/palette_blending_test.cpp new file mode 100644 index 000000000..73b0d79fc --- /dev/null +++ b/test/palette_blending_test.cpp @@ -0,0 +1,64 @@ +#include "utils/palette_blending.hpp" + +#include +#include + +#include +#include +#include + +#include "utils/str_cat.hpp" + +void PrintTo(const SDL_Color &color, std::ostream *os) +{ + *os << "(" + << static_cast(color.r) << ", " + << static_cast(color.g) << ", " + << static_cast(color.b) << ")"; +} + +namespace devilution { +namespace { + +MATCHER_P3(ColorIs, r, g, b, + StrCat(negation ? "isn't" : "is", " (", r, ", ", g, ", ", b, ")")) +{ + return arg.r == r && arg.g == g && arg.b == b; +} + +void GeneratePalette(SDL_Color palette[256]) +{ + for (unsigned j = 0; j < 4; ++j) { + for (unsigned i = 0; i < 64; ++i) { + palette[j * 64 + i].r = i * std::max(j, 1U); + palette[j * 64 + i].g = i * j; + palette[j * 64 + i].b = i * 2; +#ifndef USE_SDL1 + palette[j * 64 + i].a = SDL_ALPHA_OPAQUE; +#endif + } + } +} + +TEST(GenerateBlendedLookupTableTest, BasicTest) +{ + SDL_Color palette[256]; + GeneratePalette(palette); + + GenerateBlendedLookupTable(palette, /*skipFrom=*/-1, /*skipTo=*/-1); + + EXPECT_THAT(palette[17], ColorIs(17, 0, 34)); + EXPECT_THAT(palette[150], ColorIs(44, 44, 44)); + EXPECT_THAT(palette[86], ColorIs(22, 22, 44)); + EXPECT_EQ(paletteTransparencyLookup[17][150], 86); + EXPECT_EQ(paletteTransparencyLookup[150][17], 86); + + EXPECT_THAT(palette[27], ColorIs(27, 0, 54)); + EXPECT_THAT(palette[130], ColorIs(4, 4, 4)); + EXPECT_THAT(palette[15], ColorIs(15, 0, 30)); + EXPECT_EQ(paletteTransparencyLookup[27][130], 15); + EXPECT_EQ(paletteTransparencyLookup[130][27], 15); +} + +} // namespace +} // namespace devilution