Browse Source

Palette blending: Add a test and a benchmark

Extract palette blending into a library and adds
a basic test and a basic benchmark.
pull/8024/head
Gleb Mazovetskiy 10 months ago
parent
commit
b210581435
  1. 8
      Source/CMakeLists.txt
  2. 1
      Source/cursor.cpp
  3. 103
      Source/engine/palette.cpp
  4. 15
      Source/engine/palette.h
  5. 2
      Source/engine/render/blit_impl.hpp
  6. 2
      Source/engine/render/primitive_render.cpp
  7. 116
      Source/utils/palette_blending.cpp
  8. 43
      Source/utils/palette_blending.hpp
  9. 4
      test/CMakeLists.txt
  10. 37
      test/palette_blending_benchmark.cpp
  11. 64
      test/palette_blending_test.cpp

8
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

1
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"

103
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<SDL_Color, 256> logical_palette;
std::array<SDL_Color, 256> system_palette;
std::array<SDL_Color, 256> 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<SDL_Color, 256> &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<SDL_Color, 256> &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<SDL_Color, 256> &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

15
Source/engine/palette.h

@ -37,21 +37,6 @@ namespace devilution {
extern std::array<SDL_Color, 256> logical_palette;
extern std::array<SDL_Color, 256> system_palette;
extern std::array<SDL_Color, 256> 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();

2
Source/engine/render/blit_impl.hpp

@ -5,9 +5,9 @@
#include <execution>
#include <version>
#include "engine/palette.h"
#include "engine/render/light_render.hpp"
#include "utils/attributes.h"
#include "utils/palette_blending.hpp"
namespace devilution {

2
Source/engine/render/primitive_render.cpp

@ -4,10 +4,10 @@
#include <cstdint>
#include <cstring>
#include "engine/palette.h"
#include "engine/point.hpp"
#include "engine/size.hpp"
#include "engine/surface.hpp"
#include "utils/palette_blending.hpp"
namespace devilution {
namespace {

116
Source/utils/palette_blending.cpp

@ -0,0 +1,116 @@
#include "utils/palette_blending.hpp"
#include <cstdint>
#include <limits>
#include <SDL.h>
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<uint32_t>::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<uint8_t>((static_cast<int>(a.r) + static_cast<int>(b.r)) / 2),
.g = static_cast<uint8_t>((static_cast<int>(a.g) + static_cast<int>(b.g)) / 2),
.b = static_cast<uint8_t>((static_cast<int>(a.b) + static_cast<int>(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

43
Source/utils/palette_blending.hpp

@ -0,0 +1,43 @@
#pragma once
#include <cstdint>
#include <SDL.h>
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

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

37
test/palette_blending_benchmark.cpp

@ -0,0 +1,37 @@
#include "utils/palette_blending.hpp"
#include <SDL.h>
#include <benchmark/benchmark.h>
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

64
test/palette_blending_test.cpp

@ -0,0 +1,64 @@
#include "utils/palette_blending.hpp"
#include <algorithm>
#include <iostream>
#include <SDL.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "utils/str_cat.hpp"
void PrintTo(const SDL_Color &color, std::ostream *os)
{
*os << "("
<< static_cast<int>(color.r) << ", "
<< static_cast<int>(color.g) << ", "
<< static_cast<int>(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
Loading…
Cancel
Save