From 392087f26b92681c8242ab7efdcb338fae0e9276 Mon Sep 17 00:00:00 2001 From: Peter Thoman Date: Tue, 30 Apr 2013 03:44:54 +0200 Subject: [PATCH 1/5] Added xBRZ library --- GPU/GPU.vcxproj | 2 + ext/xbrz/config.h | 40 ++ ext/xbrz/xbrz.cpp | 1185 +++++++++++++++++++++++++++++++++++++++++++++ ext/xbrz/xbrz.h | 80 +++ 4 files changed, 1307 insertions(+) create mode 100644 ext/xbrz/config.h create mode 100644 ext/xbrz/xbrz.cpp create mode 100644 ext/xbrz/xbrz.h diff --git a/GPU/GPU.vcxproj b/GPU/GPU.vcxproj index e7afcb5d7f..4a83118eef 100644 --- a/GPU/GPU.vcxproj +++ b/GPU/GPU.vcxproj @@ -129,6 +129,7 @@ + @@ -148,6 +149,7 @@ + diff --git a/ext/xbrz/config.h b/ext/xbrz/config.h new file mode 100644 index 0000000000..ae11f7a0f5 --- /dev/null +++ b/ext/xbrz/config.h @@ -0,0 +1,40 @@ +// **************************************************************************** +// * This file is part of the HqMAME project. It is distributed under * +// * GNU General Public License: http://www.gnu.org/licenses/gpl.html * +// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * +// * * +// * Additionally and as a special exception, the author gives permission * +// * to link the code of this program with the MAME library (or with modified * +// * versions of MAME that use the same license as MAME), and distribute * +// * linked combinations including the two. You must obey the GNU General * +// * Public License in all respects for all of the code used other than MAME. * +// * If you modify this file, you may extend this exception to your version * +// * of the file, but you are not obligated to do so. If you do not wish to * +// * do so, delete this exception statement from your version. * +// **************************************************************************** + +#ifndef XBRZ_CONFIG_HEADER_284578425345 +#define XBRZ_CONFIG_HEADER_284578425345 + +//do NOT include any headers here! used by xBRZ_dll!!! + +namespace xbrz +{ +struct ScalerCfg +{ + ScalerCfg() : + luminanceWeight_(1), + equalColorTolerance_(30), + dominantDirectionThreshold(3.6), + steepDirectionThreshold(2.2), + newTestAttribute_(0) {} + + double luminanceWeight_; + double equalColorTolerance_; + double dominantDirectionThreshold; + double steepDirectionThreshold; + double newTestAttribute_; //unused; test new parameters +}; +} + +#endif \ No newline at end of file diff --git a/ext/xbrz/xbrz.cpp b/ext/xbrz/xbrz.cpp new file mode 100644 index 0000000000..8f4a70de6e --- /dev/null +++ b/ext/xbrz/xbrz.cpp @@ -0,0 +1,1185 @@ +// **************************************************************************** +// * This file is part of the HqMAME project. It is distributed under * +// * GNU General Public License: http://www.gnu.org/licenses/gpl.html * +// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * +// * * +// * Additionally and as a special exception, the author gives permission * +// * to link the code of this program with the MAME library (or with modified * +// * versions of MAME that use the same license as MAME), and distribute * +// * linked combinations including the two. You must obey the GNU General * +// * Public License in all respects for all of the code used other than MAME. * +// * If you modify this file, you may extend this exception to your version * +// * of the file, but you are not obligated to do so. If you do not wish to * +// * do so, delete this exception statement from your version. * +// **************************************************************************** + +#include "xbrz.h" +#include +#include + +namespace +{ +template inline +unsigned char getByte(uint32_t val) { return static_cast((val >> (8 * N)) & 0xff); } + +// adjusted for RGBA +// - Durante +inline unsigned char getRed (uint32_t val) { return getByte<3>(val); } +inline unsigned char getGreen(uint32_t val) { return getByte<2>(val); } +inline unsigned char getBlue (uint32_t val) { return getByte<1>(val); } + +template inline +T abs(T value) +{ + static_assert(std::is_signed::value, ""); + return value < 0 ? -value : value; +} + +const uint32_t redMask = 0x00ff0000; +const uint32_t greenMask = 0x0000ff00; +const uint32_t blueMask = 0x000000ff; + +template inline +void alphaBlend(uint32_t& dst, uint32_t col) //blend color over destination with opacity N / M +{ + static_assert(N < 256, "possible overflow of (col & redMask) * N"); + static_assert(M < 256, "possible overflow of (col & redMask ) * N + (dst & redMask ) * (M - N)"); + static_assert(0 < N && N < M, ""); + //dst = (redMask & ((col & redMask ) * N + (dst & redMask ) * (M - N)) / M) | //this works because 8 upper bits are free + // (greenMask & ((col & greenMask) * N + (dst & greenMask) * (M - N)) / M) | + // (blueMask & ((col & blueMask ) * N + (dst & blueMask ) * (M - N)) / M); + + // the upper 8 bits are not free in our case, so we need to do this differently + // could probably be MUCH faster + // - Durante + uint8_t a = (((col ) >> 24) * N + ((dst ) >> 24) * (M - N) ) / M; + uint8_t r = (((col & redMask) >> 16) * N + ((dst & redMask) >> 16) * (M - N) ) / M; + uint8_t g = (((col & greenMask) >> 8) * N + ((dst & greenMask) >> 8) * (M - N) ) / M; + uint8_t b = (((col & blueMask) ) * N + ((dst & blueMask) ) * (M - N) ) / M; + + dst = (a << 24) | (r << 16) | (g << 8) | (b << 0); +} + +inline +uint32_t alphaBlend2(uint32_t pix1, uint32_t pix2, double alpha) +{ + return (redMask & static_cast((pix1 & redMask ) * alpha + (pix2 & redMask ) * (1 - alpha))) | + (greenMask & static_cast((pix1 & greenMask) * alpha + (pix2 & greenMask) * (1 - alpha))) | + (blueMask & static_cast((pix1 & blueMask ) * alpha + (pix2 & blueMask ) * (1 - alpha))); +} + + +uint32_t* byteAdvance( uint32_t* ptr, int bytes) { return reinterpret_cast< uint32_t*>(reinterpret_cast< char*>(ptr) + bytes); } +const uint32_t* byteAdvance(const uint32_t* ptr, int bytes) { return reinterpret_cast(reinterpret_cast(ptr) + bytes); } + + +//fill block with the given color +inline +void fillBlock(uint32_t* trg, int pitch, uint32_t col, int blockWidth, int blockHeight) +{ + //for (int y = 0; y < blockHeight; ++y, trg = byteAdvance(trg, pitch)) + // std::fill(trg, trg + blockWidth, col); + + for (int y = 0; y < blockHeight; ++y, trg = byteAdvance(trg, pitch)) + for (int x = 0; x < blockWidth; ++x) + trg[x] = col; +} + +inline +void fillBlock(uint32_t* trg, int pitch, uint32_t col, int n) { fillBlock(trg, pitch, col, n, n); } + + +#ifdef _MSC_VER +#define FORCE_INLINE __forceinline +#elif defined __GNUC__ +#define FORCE_INLINE __attribute__((always_inline)) inline +#else +#define FORCE_INLINE inline +#endif + + +enum RotationDegree //clock-wise +{ + ROT_0, + ROT_90, + ROT_180, + ROT_270 +}; + +//calculate input matrix coordinates after rotation at compile time +template +struct MatrixRotation; + +template +struct MatrixRotation +{ + static const size_t I_old = I; + static const size_t J_old = J; +}; + +template //(i, j) = (row, col) indices, N = size of (square) matrix +struct MatrixRotation +{ + static const size_t I_old = N - 1 - MatrixRotation(rotDeg - 1), I, J, N>::J_old; //old coordinates before rotation! + static const size_t J_old = MatrixRotation(rotDeg - 1), I, J, N>::I_old; // +}; + + +template +class OutputMatrix +{ +public: + OutputMatrix(uint32_t* out, int outWidth) : //access matrix area, top-left at position "out" for image with given width + out_(out), + outWidth_(outWidth) {} + + template + uint32_t& ref() const + { + static const size_t I_old = MatrixRotation::I_old; + static const size_t J_old = MatrixRotation::J_old; + return *(out_ + J_old + I_old * outWidth_); + } + +private: + uint32_t* out_; + const int outWidth_; +}; + + +template inline +T square(T value) { return value * value; } + + +/* +inline +void rgbtoLuv(uint32_t c, double& L, double& u, double& v) +{ + //http://www.easyrgb.com/index.php?X=MATH&H=02#text2 + double r = getRed (c) / 255.0; + double g = getGreen(c) / 255.0; + double b = getBlue (c) / 255.0; + + if ( r > 0.04045 ) + r = std::pow(( ( r + 0.055 ) / 1.055 ) , 2.4); + else + r /= 12.92; + if ( g > 0.04045 ) + g = std::pow(( ( g + 0.055 ) / 1.055 ) , 2.4); + else + g /= 12.92; + if ( b > 0.04045 ) + b = std::pow(( ( b + 0.055 ) / 1.055 ) , 2.4); + else + b /= 12.92; + + r *= 100; + g *= 100; + b *= 100; + + double x = 0.4124564 * r + 0.3575761 * g + 0.1804375 * b; + double y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b; + double z = 0.0193339 * r + 0.1191920 * g + 0.9503041 * b; + //--------------------- + double var_U = 4 * x / ( x + 15 * y + 3 * z ); + double var_V = 9 * y / ( x + 15 * y + 3 * z ); + double var_Y = y / 100; + + if ( var_Y > 0.008856 ) var_Y = std::pow(var_Y , 1.0/3 ); + else var_Y = 7.787 * var_Y + 16.0 / 116; + + const double ref_X = 95.047; //Observer= 2°, Illuminant= D65 + const double ref_Y = 100.000; + const double ref_Z = 108.883; + + const double ref_U = ( 4 * ref_X ) / ( ref_X + ( 15 * ref_Y ) + ( 3 * ref_Z ) ); + const double ref_V = ( 9 * ref_Y ) / ( ref_X + ( 15 * ref_Y ) + ( 3 * ref_Z ) ); + + L = ( 116 * var_Y ) - 16; + u = 13 * L * ( var_U - ref_U ); + v = 13 * L * ( var_V - ref_V ); +} +*/ + +inline +void rgbtoLab(uint32_t c, unsigned char& L, signed char& A, signed char& B) +{ + //code: http://www.easyrgb.com/index.php?X=MATH + //test: http://www.workwithcolor.com/color-converter-01.htm + //------RGB to XYZ------ + double r = getRed (c) / 255.0; + double g = getGreen(c) / 255.0; + double b = getBlue (c) / 255.0; + + r = r > 0.04045 ? std::pow(( r + 0.055 ) / 1.055, 2.4) : r / 12.92; + r = g > 0.04045 ? std::pow(( g + 0.055 ) / 1.055, 2.4) : g / 12.92; + r = b > 0.04045 ? std::pow(( b + 0.055 ) / 1.055, 2.4) : b / 12.92; + + r *= 100; + g *= 100; + b *= 100; + + double x = 0.4124564 * r + 0.3575761 * g + 0.1804375 * b; + double y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b; + double z = 0.0193339 * r + 0.1191920 * g + 0.9503041 * b; + //------XYZ to Lab------ + const double refX = 95.047; // + const double refY = 100.000; //Observer= 2°, Illuminant= D65 + const double refZ = 108.883; // + double var_X = x / refX; + double var_Y = y / refY; + double var_Z = z / refZ; + + var_X = var_X > 0.008856 ? std::pow(var_X, 1.0 / 3) : 7.787 * var_X + 4.0 / 29; + var_Y = var_Y > 0.008856 ? std::pow(var_Y, 1.0 / 3) : 7.787 * var_Y + 4.0 / 29; + var_Z = var_Z > 0.008856 ? std::pow(var_Z, 1.0 / 3) : 7.787 * var_Z + 4.0 / 29; + + L = static_cast(116 * var_Y - 16); + A = static_cast< signed char>(500 * (var_X - var_Y)); + B = static_cast< signed char>(200 * (var_Y - var_Z)); +}; + + +inline +double distLAB(uint32_t pix1, uint32_t pix2) +{ + unsigned char L1 = 0; //[0, 100] + signed char a1 = 0; //[-128, 127] + signed char b1 = 0; //[-128, 127] + rgbtoLab(pix1, L1, a1, b1); + + unsigned char L2 = 0; + signed char a2 = 0; + signed char b2 = 0; + rgbtoLab(pix2, L2, a2, b2); + + //----------------------------- + //http://www.easyrgb.com/index.php?X=DELT + + //Delta E/CIE76 + return std::sqrt(square(1.0 * L1 - L2) + + square(1.0 * a1 - a2) + + square(1.0 * b1 - b2)); +} + + +/* +inline +void rgbtoHsl(uint32_t c, double& h, double& s, double& l) +{ + //http://www.easyrgb.com/index.php?X=MATH&H=18#text18 + const int r = getRed (c); + const int g = getGreen(c); + const int b = getBlue (c); + + const int varMin = numeric::min(r, g, b); + const int varMax = numeric::max(r, g, b); + const int delMax = varMax - varMin; + + l = (varMax + varMin) / 2.0 / 255.0; + + if (delMax == 0) //gray, no chroma... + { + h = 0; + s = 0; + } + else + { + s = l < 0.5 ? + delMax / (1.0 * varMax + varMin) : + delMax / (2.0 * 255 - varMax - varMin); + + double delR = ((varMax - r) / 6.0 + delMax / 2.0) / delMax; + double delG = ((varMax - g) / 6.0 + delMax / 2.0) / delMax; + double delB = ((varMax - b) / 6.0 + delMax / 2.0) / delMax; + + if (r == varMax) + h = delB - delG; + else if (g == varMax) + h = 1 / 3.0 + delR - delB; + else if (b == varMax) + h = 2 / 3.0 + delG - delR; + + if (h < 0) + h += 1; + if (h > 1) + h -= 1; + } +} + +inline +double distHSL(uint32_t pix1, uint32_t pix2, double lightningWeight) +{ + double h1 = 0; + double s1 = 0; + double l1 = 0; + rgbtoHsl(pix1, h1, s1, l1); + double h2 = 0; + double s2 = 0; + double l2 = 0; + rgbtoHsl(pix2, h2, s2, l2); + + //HSL is in cylindric coordinatates where L represents height, S radius, H angle, + //however we interpret the cylinder as a bi-conic solid with top/bottom radius 0, middle radius 1 + assert(0 <= h1 && h1 <= 1); + assert(0 <= h2 && h2 <= 1); + + double r1 = l1 < 0.5 ? + l1 * 2 : + 2 - l1 * 2; + + double x1 = r1 * s1 * std::cos(h1 * 2 * numeric::pi); + double y1 = r1 * s1 * std::sin(h1 * 2 * numeric::pi); + double z1 = l1; + + double r2 = l2 < 0.5 ? + l2 * 2 : + 2 - l2 * 2; + + double x2 = r2 * s2 * std::cos(h2 * 2 * numeric::pi); + double y2 = r2 * s2 * std::sin(h2 * 2 * numeric::pi); + double z2 = l2; + + return 255 * std::sqrt(square(x1 - x2) + square(y1 - y2) + square(lightningWeight * (z1 - z2))); +} +*/ + + +inline +double distRGB(uint32_t pix1, uint32_t pix2) +{ + const double r_diff = static_cast(getRed (pix1)) - getRed (pix2); + const double g_diff = static_cast(getGreen(pix1)) - getGreen(pix2); + const double b_diff = static_cast(getBlue (pix1)) - getBlue (pix2); + + //euklidean RGB distance + return std::sqrt(square(r_diff) + square(g_diff) + square(b_diff)); +} + + +inline +double distNonLinearRGB(uint32_t pix1, uint32_t pix2) +{ + //non-linear rgb: http://www.compuphase.com/cmetric.htm + const double r_diff = static_cast(getRed (pix1)) - getRed (pix2); + const double g_diff = static_cast(getGreen(pix1)) - getGreen(pix2); + const double b_diff = static_cast(getBlue (pix1)) - getBlue (pix2); + + const double r_avg = (static_cast(getRed(pix1)) + getRed(pix2)) / 2; + return std::sqrt((2 + r_avg / 255) * square(r_diff) + 4 * square(g_diff) + (2 + (255 - r_avg) / 255) * square(b_diff)); +} + + +inline +double distYCbCr(uint32_t pix1, uint32_t pix2, double lumaWeight) +{ + //http://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion + //YCbCr conversion is a matrix multiplication => take advantage of linearity by subtracting first! + const int r_diff = static_cast(getRed (pix1)) - getRed (pix2); //we may delay division by 255 to after matrix multiplication + const int g_diff = static_cast(getGreen(pix1)) - getGreen(pix2); // + const int b_diff = static_cast(getBlue (pix1)) - getBlue (pix2); //substraction for int is noticeable faster than for double! + + const double k_b = 0.0722; //ITU-R BT.709 conversion + const double k_r = 0.2126; // + const double k_g = 1 - k_b - k_r; + + const double scale_b = 0.5 / (1 - k_b); + const double scale_r = 0.5 / (1 - k_r); + + const double y = k_r * r_diff + k_g * g_diff + k_b * b_diff; //[!], analog YCbCr! + const double c_b = scale_b * (b_diff - y); + const double c_r = scale_r * (r_diff - y); + + //we skip division by 255 to have similar range like other distance functions + return std::sqrt(square(lumaWeight * y) + square(c_b) + square(c_r)); +} + + +inline +double distYUV(uint32_t pix1, uint32_t pix2, double luminanceWeight) +{ + //perf: it's not worthwhile to buffer the YUV-conversion, the direct code is faster by ~ 6% + //since RGB -> YUV conversion is essentially a matrix multiplication, we can calculate the RGB diff before the conversion (distributive property) + const double r_diff = static_cast(getRed (pix1)) - getRed (pix2); + const double g_diff = static_cast(getGreen(pix1)) - getGreen(pix2); + const double b_diff = static_cast(getBlue (pix1)) - getBlue (pix2); + + //http://en.wikipedia.org/wiki/YUV#Conversion_to.2Ffrom_RGB + const double w_b = 0.114; + const double w_r = 0.299; + const double w_g = 1 - w_r - w_b; + + const double u_max = 0.436; + const double v_max = 0.615; + + const double scale_u = u_max / (1 - w_b); + const double scale_v = v_max / (1 - w_r); + + double y = w_r * r_diff + w_g * g_diff + w_b * b_diff;//value range: 255 * [-1, 1] + double u = scale_u * (b_diff - y); //value range: 255 * 2 * u_max * [-1, 1] + double v = scale_v * (r_diff - y); //value range: 255 * 2 * v_max * [-1, 1] + +#ifndef NDEBUG + const double eps = 0.5; +#endif + assert(std::abs(y) <= 255 + eps); + assert(std::abs(u) <= 255 * 2 * u_max + eps); + assert(std::abs(v) <= 255 * 2 * v_max + eps); + + return std::sqrt(square(luminanceWeight * y) + square(u) + square(v)); +} + + +inline +double colorDist(uint32_t pix1, uint32_t pix2, double luminanceWeight) +{ + if (pix1 == pix2) //about 8% perf boost + return 0; + + //return distHSL(pix1, pix2, luminanceWeight); + //return distRGB(pix1, pix2); + //return distLAB(pix1, pix2); + //return distNonLinearRGB(pix1, pix2); + //return distYUV(pix1, pix2, luminanceWeight); + + return distYCbCr(pix1, pix2, luminanceWeight); +} + + +enum BlendType +{ + BLEND_NONE = 0, + BLEND_NORMAL, //a normal indication to blend + BLEND_DOMINANT, //a strong indication to blend + //attention: BlendType must fit into the value range of 2 bit!!! +}; + +struct BlendResult +{ + BlendType + /**/blend_f, blend_g, + /**/blend_j, blend_k; +}; + + +struct Kernel_4x4 //kernel for preprocessing step +{ + uint32_t + /**/a, b, c, d, + /**/e, f, g, h, + /**/i, j, k, l, + /**/m, n, o, p; +}; + +/* +input kernel area naming convention: +----------------- +| A | B | C | D | +----|---|---|---| +| E | F | G | H | //evalute the four corners between F, G, J, K +----|---|---|---| //input pixel is at position F +| I | J | K | L | +----|---|---|---| +| M | N | O | P | +----------------- +*/ +FORCE_INLINE //detect blend direction +BlendResult preProcessCorners(const Kernel_4x4& ker, const xbrz::ScalerCfg& cfg) //result: F, G, J, K corners of "GradientType" +{ + BlendResult result = {}; + + if ((ker.f == ker.g && + ker.j == ker.k) || + (ker.f == ker.j && + ker.g == ker.k)) + return result; + + auto dist = [&](uint32_t col1, uint32_t col2) { return colorDist(col1, col2, cfg.luminanceWeight_); }; + + const int weight = 4; + double jg = dist(ker.i, ker.f) + dist(ker.f, ker.c) + dist(ker.n, ker.k) + dist(ker.k, ker.h) + weight * dist(ker.j, ker.g); + double fk = dist(ker.e, ker.j) + dist(ker.j, ker.o) + dist(ker.b, ker.g) + dist(ker.g, ker.l) + weight * dist(ker.f, ker.k); + + if (jg < fk) //test sample: 70% of values max(jg, fk) / min(jg, fk) are between 1.1 and 3.7 with median being 1.8 + { + const bool dominantGradient = cfg.dominantDirectionThreshold * jg < fk; + if (ker.f != ker.g && ker.f != ker.j) + result.blend_f = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; + + if (ker.k != ker.j && ker.k != ker.g) + result.blend_k = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; + } + else if (fk < jg) + { + const bool dominantGradient = cfg.dominantDirectionThreshold * fk < jg; + if (ker.j != ker.f && ker.j != ker.k) + result.blend_j = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; + + if (ker.g != ker.f && ker.g != ker.k) + result.blend_g = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; + } + return result; +} + +struct Kernel_3x3 +{ + uint32_t + /**/a, b, c, + /**/d, e, f, + /**/g, h, i; +}; + +#define DEF_GETTER(x) template uint32_t inline get_##x(const Kernel_3x3& ker) { return ker.x; } +//we cannot and NEED NOT write "ker.##x" since ## concatenates preprocessor tokens but "." is not a token +DEF_GETTER(a) DEF_GETTER(b) DEF_GETTER(c) +DEF_GETTER(d) DEF_GETTER(e) DEF_GETTER(f) +DEF_GETTER(g) DEF_GETTER(h) DEF_GETTER(i) +#undef DEF_GETTER + +#define DEF_GETTER(x, y) template <> inline uint32_t get_##x(const Kernel_3x3& ker) { return ker.y; } +DEF_GETTER(a, g) DEF_GETTER(b, d) DEF_GETTER(c, a) +DEF_GETTER(d, h) DEF_GETTER(e, e) DEF_GETTER(f, b) +DEF_GETTER(g, i) DEF_GETTER(h, f) DEF_GETTER(i, c) +#undef DEF_GETTER + +#define DEF_GETTER(x, y) template <> inline uint32_t get_##x(const Kernel_3x3& ker) { return ker.y; } +DEF_GETTER(a, i) DEF_GETTER(b, h) DEF_GETTER(c, g) +DEF_GETTER(d, f) DEF_GETTER(e, e) DEF_GETTER(f, d) +DEF_GETTER(g, c) DEF_GETTER(h, b) DEF_GETTER(i, a) +#undef DEF_GETTER + +#define DEF_GETTER(x, y) template <> inline uint32_t get_##x(const Kernel_3x3& ker) { return ker.y; } +DEF_GETTER(a, c) DEF_GETTER(b, f) DEF_GETTER(c, i) +DEF_GETTER(d, b) DEF_GETTER(e, e) DEF_GETTER(f, h) +DEF_GETTER(g, a) DEF_GETTER(h, d) DEF_GETTER(i, g) +#undef DEF_GETTER + + +//compress four blend types into a single byte +inline BlendType getTopL (unsigned char b) { return static_cast(0x3 & b); } +inline BlendType getTopR (unsigned char b) { return static_cast(0x3 & (b >> 2)); } +inline BlendType getBottomR(unsigned char b) { return static_cast(0x3 & (b >> 4)); } +inline BlendType getBottomL(unsigned char b) { return static_cast(0x3 & (b >> 6)); } + +inline void setTopL (unsigned char& b, BlendType bt) { b |= bt; } //buffer is assumed to be initialized before preprocessing! +inline void setTopR (unsigned char& b, BlendType bt) { b |= (bt << 2); } +inline void setBottomR(unsigned char& b, BlendType bt) { b |= (bt << 4); } +inline void setBottomL(unsigned char& b, BlendType bt) { b |= (bt << 6); } + +inline bool blendingNeeded(unsigned char b) { return b != 0; } + +template inline +unsigned char rotateBlendInfo(unsigned char b) { return b; } +template <> inline unsigned char rotateBlendInfo(unsigned char b) { return ((b << 2) | (b >> 6)) & 0xff; } +template <> inline unsigned char rotateBlendInfo(unsigned char b) { return ((b << 4) | (b >> 4)) & 0xff; } +template <> inline unsigned char rotateBlendInfo(unsigned char b) { return ((b << 6) | (b >> 2)) & 0xff; } + + +#ifndef NDEBUG +int debugPixelX = -1; +int debugPixelY = 84; +bool breakIntoDebugger = false; +#endif + + +/* +input kernel area naming convention: +------------- +| A | B | C | +----|---|---| +| D | E | F | //input pixel is at position E +----|---|---| +| G | H | I | +------------- +*/ +template +FORCE_INLINE //perf: quite worth it! +void scalePixel(const Kernel_3x3& ker, + uint32_t* target, int trgWidth, + unsigned char blendInfo, //result of preprocessing all four corners of pixel "e" + const xbrz::ScalerCfg& cfg) +{ +#define a get_a(ker) +#define b get_b(ker) +#define c get_c(ker) +#define d get_d(ker) +#define e get_e(ker) +#define f get_f(ker) +#define g get_g(ker) +#define h get_h(ker) +#define i get_i(ker) + +#ifndef NDEBUG + if (breakIntoDebugger) + __debugbreak(); //__asm int 3; +#endif + + const unsigned char blend = rotateBlendInfo(blendInfo); + + if (getBottomR(blend) >= BLEND_NORMAL) + { + auto eq = [&](uint32_t col1, uint32_t col2) { return colorDist(col1, col2, cfg.luminanceWeight_) < cfg.equalColorTolerance_; }; + auto dist = [&](uint32_t col1, uint32_t col2) { return colorDist(col1, col2, cfg.luminanceWeight_); }; + + const bool doLineBlend = [&]() -> bool + { + if (getBottomR(blend) >= BLEND_DOMINANT) + return true; + + //make sure there is no second blending in an adjacent rotation for this pixel: handles insular pixels, mario eyes + if (getTopR(blend) != BLEND_NONE && !eq(e, g)) //but support double-blending for 90° corners + return false; + if (getBottomL(blend) != BLEND_NONE && !eq(e, c)) + return false; + + //no full blending for L-shapes; blend corner only (handles "mario mushroom eyes") + if (eq(g, h) && eq(h , i) && eq(i, f) && eq(f, c) && !eq(e, i)) + return false; + + return true; + }(); + + const uint32_t px = dist(e, f) <= dist(e, h) ? f : h; //choose most similar color + + OutputMatrix out(target, trgWidth); + + if (doLineBlend) + { + const double fg = dist(f, g); //test sample: 70% of values max(fg, hc) / min(fg, hc) are between 1.1 and 3.7 with median being 1.9 + const double hc = dist(h, c); // + + const bool haveShallowLine = cfg.steepDirectionThreshold * fg <= hc && e != g && d != g; + const bool haveSteepLine = cfg.steepDirectionThreshold * hc <= fg && e != c && b != c; + + if (haveShallowLine) + { + if (haveSteepLine) + Scaler::blendLineSteepAndShallow(px, out); + else + Scaler::blendLineShallow(px, out); + } + else + { + if (haveSteepLine) + Scaler::blendLineSteep(px, out); + else + Scaler::blendLineDiagonal(px,out); + } + } + else + Scaler::blendCorner(px, out); + } + +#undef a +#undef b +#undef c +#undef d +#undef e +#undef f +#undef g +#undef h +#undef i +} + + +template //scaler policy: see "Scaler2x" reference implementation +void scaleImage(const uint32_t* src, uint32_t* trg, int srcWidth, int srcHeight, const xbrz::ScalerCfg& cfg, int yFirst, int yLast) +{ + yFirst = std::max(yFirst, 0); + yLast = std::min(yLast, srcHeight); + if (yFirst >= yLast || srcWidth <= 0) + return; + + const int trgWidth = srcWidth * Scaler::scale; + + //"use" space at the end of the image as temporary buffer for "on the fly preprocessing": we even could use larger area of + //"sizeof(uint32_t) * srcWidth * (yLast - yFirst)" bytes without risk of accidental overwriting before accessing + const int bufferSize = srcWidth; + unsigned char* preProcBuffer = reinterpret_cast(trg + yLast * Scaler::scale * trgWidth) - bufferSize; + std::fill(preProcBuffer, preProcBuffer + bufferSize, 0); + static_assert(BLEND_NONE == 0, ""); + + //initialize preprocessing buffer for first row: detect upper left and right corner blending + //this cannot be optimized for adjacent processing stripes; we must not allow for a memory race condition! + if (yFirst > 0) + { + const int y = yFirst - 1; + + const uint32_t* s_m1 = src + srcWidth * std::max(y - 1, 0); + const uint32_t* s_0 = src + srcWidth * y; //center line + const uint32_t* s_p1 = src + srcWidth * std::min(y + 1, srcHeight - 1); + const uint32_t* s_p2 = src + srcWidth * std::min(y + 2, srcHeight - 1); + + for (int x = 0; x < srcWidth; ++x) + { + const int x_m1 = std::max(x - 1, 0); + const int x_p1 = std::min(x + 1, srcWidth - 1); + const int x_p2 = std::min(x + 2, srcWidth - 1); + + Kernel_4x4 ker = {}; //perf: initialization is negligable + ker.a = s_m1[x_m1]; //read sequentially from memory as far as possible + ker.b = s_m1[x]; + ker.c = s_m1[x_p1]; + ker.d = s_m1[x_p2]; + + ker.e = s_0[x_m1]; + ker.f = s_0[x]; + ker.g = s_0[x_p1]; + ker.h = s_0[x_p2]; + + ker.i = s_p1[x_m1]; + ker.j = s_p1[x]; + ker.k = s_p1[x_p1]; + ker.l = s_p1[x_p2]; + + ker.m = s_p2[x_m1]; + ker.n = s_p2[x]; + ker.o = s_p2[x_p1]; + ker.p = s_p2[x_p2]; + + const BlendResult res = preProcessCorners(ker, cfg); + /* + preprocessing blend result: + --------- + | F | G | //evalute corner between F, G, J, K + ----|---| //input pixel is at position F + | J | K | + --------- + */ + setTopR(preProcBuffer[x], res.blend_j); + + if (x + 1 < srcWidth) + setTopL(preProcBuffer[x + 1], res.blend_k); + } + } + //------------------------------------------------------------------------------------ + + for (int y = yFirst; y < yLast; ++y) + { + uint32_t* out = trg + Scaler::scale * y * trgWidth; //consider MT "striped" access + + const uint32_t* s_m1 = src + srcWidth * std::max(y - 1, 0); + const uint32_t* s_0 = src + srcWidth * y; //center line + const uint32_t* s_p1 = src + srcWidth * std::min(y + 1, srcHeight - 1); + const uint32_t* s_p2 = src + srcWidth * std::min(y + 2, srcHeight - 1); + + unsigned char blend_xy1 = 0; //corner blending for current (x, y + 1) position + + for (int x = 0; x < srcWidth; ++x, out += Scaler::scale) + { +#ifndef NDEBUG + breakIntoDebugger = debugPixelX == x && debugPixelY == y; +#endif + //all those bounds checks have only insignificant impact on performance! + const int x_m1 = std::max(x - 1, 0); //perf: prefer array indexing to additional pointers! + const int x_p1 = std::min(x + 1, srcWidth - 1); + const int x_p2 = std::min(x + 2, srcWidth - 1); + + //evaluate the four corners on bottom-right of current pixel + unsigned char blend_xy = 0; //for current (x, y) position + { + Kernel_4x4 ker = {}; //perf: initialization is negligable + ker.a = s_m1[x_m1]; //read sequentially from memory as far as possible + ker.b = s_m1[x]; + ker.c = s_m1[x_p1]; + ker.d = s_m1[x_p2]; + + ker.e = s_0[x_m1]; + ker.f = s_0[x]; + ker.g = s_0[x_p1]; + ker.h = s_0[x_p2]; + + ker.i = s_p1[x_m1]; + ker.j = s_p1[x]; + ker.k = s_p1[x_p1]; + ker.l = s_p1[x_p2]; + + ker.m = s_p2[x_m1]; + ker.n = s_p2[x]; + ker.o = s_p2[x_p1]; + ker.p = s_p2[x_p2]; + + const BlendResult res = preProcessCorners(ker, cfg); + /* + preprocessing blend result: + --------- + | F | G | //evalute corner between F, G, J, K + ----|---| //current input pixel is at position F + | J | K | + --------- + */ + blend_xy = preProcBuffer[x]; + setBottomR(blend_xy, res.blend_f); //all four corners of (x, y) have been determined at this point due to processing sequence! + + setTopR(blend_xy1, res.blend_j); //set 2nd known corner for (x, y + 1) + preProcBuffer[x] = blend_xy1; //store on current buffer position for use on next row + + blend_xy1 = 0; + setTopL(blend_xy1, res.blend_k); //set 1st known corner for (x + 1, y + 1) and buffer for use on next column + + if (x + 1 < srcWidth) //set 3rd known corner for (x + 1, y) + setBottomL(preProcBuffer[x + 1], res.blend_g); + } + + //fill block of size scale * scale with the given color + fillBlock(out, trgWidth * sizeof(uint32_t), s_0[x], Scaler::scale); //place *after* preprocessing step, to not overwrite the results while processing the the last pixel! + + //blend four corners of current pixel + if (blendingNeeded(blend_xy)) //good 20% perf-improvement + { + Kernel_3x3 ker = {}; //perf: initialization is negligable + + ker.a = s_m1[x_m1]; //read sequentially from memory as far as possible + ker.b = s_m1[x]; + ker.c = s_m1[x_p1]; + + ker.d = s_0[x_m1]; + ker.e = s_0[x]; + ker.f = s_0[x_p1]; + + ker.g = s_p1[x_m1]; + ker.h = s_p1[x]; + ker.i = s_p1[x_p1]; + + scalePixel(ker, out, trgWidth, blend_xy, cfg); + scalePixel(ker, out, trgWidth, blend_xy, cfg); + scalePixel(ker, out, trgWidth, blend_xy, cfg); + scalePixel(ker, out, trgWidth, blend_xy, cfg); + } + } + } +} + + +struct Scaler2x +{ + static const int scale = 2; + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 4>(out.template ref(), col); + alphaBlend<3, 4>(out.template ref(), col); + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 4>(out.template ref<0, scale - 1>(), col); + alphaBlend<3, 4>(out.template ref<1, scale - 1>(), col); + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 4>(out.template ref<1, 0>(), col); + alphaBlend<1, 4>(out.template ref<0, 1>(), col); + alphaBlend<5, 6>(out.template ref<1, 1>(), col); //[!] fixes 7/8 used in xBR + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 2>(out.template ref<1, 1>(), col); + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaBlend<21, 100>(out.template ref<1, 1>(), col); //exact: 1 - pi/4 = 0.2146018366 + } +}; + + +struct Scaler3x +{ + static const int scale = 3; + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 4>(out.template ref(), col); + alphaBlend<1, 4>(out.template ref(), col); + + alphaBlend<3, 4>(out.template ref(), col); + out.template ref() = col; + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 4>(out.template ref<0, scale - 1>(), col); + alphaBlend<1, 4>(out.template ref<2, scale - 2>(), col); + + alphaBlend<3, 4>(out.template ref<1, scale - 1>(), col); + out.template ref<2, scale - 1>() = col; + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 4>(out.template ref<2, 0>(), col); + alphaBlend<1, 4>(out.template ref<0, 2>(), col); + alphaBlend<3, 4>(out.template ref<2, 1>(), col); + alphaBlend<3, 4>(out.template ref<1, 2>(), col); + out.template ref<2, 2>() = col; + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 8>(out.template ref<1, 2>(), col); + alphaBlend<1, 8>(out.template ref<2, 1>(), col); + alphaBlend<7, 8>(out.template ref<2, 2>(), col); + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaBlend<45, 100>(out.template ref<2, 2>(), col); //exact: 0.4545939598 + //alphaBlend<14, 1000>(out.template ref<2, 1>(), col); //0.01413008627 -> negligable + //alphaBlend<14, 1000>(out.template ref<1, 2>(), col); //0.01413008627 + } +}; + + +struct Scaler4x +{ + static const int scale = 4; + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 4>(out.template ref(), col); + alphaBlend<1, 4>(out.template ref(), col); + + alphaBlend<3, 4>(out.template ref(), col); + alphaBlend<3, 4>(out.template ref(), col); + + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 4>(out.template ref<0, scale - 1>(), col); + alphaBlend<1, 4>(out.template ref<2, scale - 2>(), col); + + alphaBlend<3, 4>(out.template ref<1, scale - 1>(), col); + alphaBlend<3, 4>(out.template ref<3, scale - 2>(), col); + + out.template ref<2, scale - 1>() = col; + out.template ref<3, scale - 1>() = col; + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaBlend<3, 4>(out.template ref<3, 1>(), col); + alphaBlend<3, 4>(out.template ref<1, 3>(), col); + alphaBlend<1, 4>(out.template ref<3, 0>(), col); + alphaBlend<1, 4>(out.template ref<0, 3>(), col); + alphaBlend<1, 3>(out.template ref<2, 2>(), col); //[!] fixes 1/4 used in xBR + out.template ref<3, 3>() = out.template ref<3, 2>() = out.template ref<2, 3>() = col; + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 2>(out.template ref(), col); + alphaBlend<1, 2>(out.template ref(), col); + out.template ref() = col; + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaBlend<68, 100>(out.template ref<3, 3>(), col); //exact: 0.6848532563 + alphaBlend< 9, 100>(out.template ref<3, 2>(), col); //0.08677704501 + alphaBlend< 9, 100>(out.template ref<2, 3>(), col); //0.08677704501 + } +}; + + +struct Scaler5x +{ + static const int scale = 5; + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 4>(out.template ref(), col); + alphaBlend<1, 4>(out.template ref(), col); + alphaBlend<1, 4>(out.template ref(), col); + + alphaBlend<3, 4>(out.template ref(), col); + alphaBlend<3, 4>(out.template ref(), col); + + out.template ref() = col; + out.template ref() = col; + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 4>(out.template ref<0, scale - 1>(), col); + alphaBlend<1, 4>(out.template ref<2, scale - 2>(), col); + alphaBlend<1, 4>(out.template ref<4, scale - 3>(), col); + + alphaBlend<3, 4>(out.template ref<1, scale - 1>(), col); + alphaBlend<3, 4>(out.template ref<3, scale - 2>(), col); + + out.template ref<2, scale - 1>() = col; + out.template ref<3, scale - 1>() = col; + out.template ref<4, scale - 1>() = col; + out.template ref<4, scale - 2>() = col; + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 4>(out.template ref<0, scale - 1>(), col); + alphaBlend<1, 4>(out.template ref<2, scale - 2>(), col); + alphaBlend<3, 4>(out.template ref<1, scale - 1>(), col); + + alphaBlend<1, 4>(out.template ref(), col); + alphaBlend<1, 4>(out.template ref(), col); + alphaBlend<3, 4>(out.template ref(), col); + + out.template ref<2, scale - 1>() = col; + out.template ref<3, scale - 1>() = col; + + out.template ref() = col; + out.template ref() = col; + + out.template ref<4, scale - 1>() = col; + + alphaBlend<2, 3>(out.template ref<3, 3>(), col); + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaBlend<1, 8>(out.template ref(), col); + alphaBlend<1, 8>(out.template ref(), col); + alphaBlend<1, 8>(out.template ref(), col); + + alphaBlend<7, 8>(out.template ref<4, 3>(), col); + alphaBlend<7, 8>(out.template ref<3, 4>(), col); + + out.template ref<4, 4>() = col; + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaBlend<86, 100>(out.template ref<4, 4>(), col); //exact: 0.8631434088 + alphaBlend<23, 100>(out.template ref<4, 3>(), col); //0.2306749731 + alphaBlend<23, 100>(out.template ref<3, 4>(), col); //0.2306749731 + //alphaBlend<8, 1000>(out.template ref<4, 2>(), col); //0.008384061834 -> negligable + //alphaBlend<8, 1000>(out.template ref<2, 4>(), col); //0.008384061834 + } +}; +} + + +void xbrz::scale(size_t factor, const uint32_t* src, uint32_t* trg, int srcWidth, int srcHeight, const xbrz::ScalerCfg& cfg, int yFirst, int yLast) +{ + switch (factor) + { + case 2: + return scaleImage(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 3: + return scaleImage(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 4: + return scaleImage(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 5: + return scaleImage(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + } + assert(false); +} + + +bool xbrz::equalColor(uint32_t col1, uint32_t col2, double luminanceWeight, double equalColorTolerance) +{ + return colorDist(col1, col2, luminanceWeight) < equalColorTolerance; +} + + +void xbrz::nearestNeighborScale(const uint32_t* src, int srcWidth, int srcHeight, int srcPitch, + uint32_t* trg, int trgWidth, int trgHeight, int trgPitch, + SliceType st, int yFirst, int yLast) +{ + if (srcPitch < srcWidth * static_cast(sizeof(uint32_t)) || + trgPitch < trgWidth * static_cast(sizeof(uint32_t))) + { + assert(false); + return; + } + + switch (st) + { + case NN_SCALE_SLICE_SOURCE: + //nearest-neighbor (going over source image - fast for upscaling, since source is read only once + yFirst = std::max(yFirst, 0); + yLast = std::min(yLast, srcHeight); + if (yFirst >= yLast || trgWidth <= 0 || trgHeight <= 0) return; + + for (int y = yFirst; y < yLast; ++y) + { + //mathematically: ySrc = floor(srcHeight * yTrg / trgHeight) + // => search for integers in: [ySrc, ySrc + 1) * trgHeight / srcHeight + + //keep within for loop to support MT input slices! + const int yTrg_first = ( y * trgHeight + srcHeight - 1) / srcHeight; //=ceil(y * trgHeight / srcHeight) + const int yTrg_last = ((y + 1) * trgHeight + srcHeight - 1) / srcHeight; //=ceil(((y + 1) * trgHeight) / srcHeight) + const int blockHeight = yTrg_last - yTrg_first; + + if (blockHeight > 0) + { + const uint32_t* srcLine = byteAdvance(src, y * srcPitch); + uint32_t* trgLine = byteAdvance(trg, yTrg_first * trgPitch); + int xTrg_first = 0; + + for (int x = 0; x < srcWidth; ++x) + { + int xTrg_last = ((x + 1) * trgWidth + srcWidth - 1) / srcWidth; + const int blockWidth = xTrg_last - xTrg_first; + if (blockWidth > 0) + { + xTrg_first = xTrg_last; + fillBlock(trgLine, trgPitch, srcLine[x], blockWidth, blockHeight); + trgLine += blockWidth; + } + } + } + } + break; + + case NN_SCALE_SLICE_TARGET: + //nearest-neighbor (going over target image - slow for upscaling, since source is read multiple times missing out on cache! Fast for similar image sizes!) + yFirst = std::max(yFirst, 0); + yLast = std::min(yLast, trgHeight); + if (yFirst >= yLast || srcHeight <= 0 || srcWidth <= 0) return; + + for (int y = yFirst; y < yLast; ++y) + { + uint32_t* trgLine = byteAdvance(trg, y * trgPitch); + const int ySrc = srcHeight * y / trgHeight; + const uint32_t* srcLine = byteAdvance(src, ySrc * srcPitch); + for (int x = 0; x < trgWidth; ++x) + { + const int xSrc = srcWidth * x / trgWidth; + trgLine[x] = srcLine[xSrc]; + } + } + break; + } +} diff --git a/ext/xbrz/xbrz.h b/ext/xbrz/xbrz.h new file mode 100644 index 0000000000..11572d7c61 --- /dev/null +++ b/ext/xbrz/xbrz.h @@ -0,0 +1,80 @@ +// **************************************************************************** +// * This file is part of the HqMAME project. It is distributed under * +// * GNU General Public License: http://www.gnu.org/licenses/gpl.html * +// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * +// * * +// * Additionally and as a special exception, the author gives permission * +// * to link the code of this program with the MAME library (or with modified * +// * versions of MAME that use the same license as MAME), and distribute * +// * linked combinations including the two. You must obey the GNU General * +// * Public License in all respects for all of the code used other than MAME. * +// * If you modify this file, you may extend this exception to your version * +// * of the file, but you are not obligated to do so. If you do not wish to * +// * do so, delete this exception statement from your version. * +// **************************************************************************** + +#ifndef XBRZ_HEADER_3847894708239054 +#define XBRZ_HEADER_3847894708239054 + +#include //size_t +#include //uint32_t +#include +#include "config.h" + +namespace xbrz +{ +/* +------------------------------------------------------------------------- +| xBRZ: "Scale by rules" - high quality image upscaling filter by Zenju | +------------------------------------------------------------------------- +using a modified approach of xBR: +http://board.byuu.org/viewtopic.php?f=10&t=2248 +- new rule set preserving small image features +- support multithreading +- support 64 bit architectures +*/ + +/* +-> map source (srcWidth * srcHeight) to target (scale * width x scale * height) image, optionally processing rows [yFirst, yLast) only +-> color format: ARGB (BGRA byte order) +-> optional source/target pitch in bytes! + +THREAD-SAFETY: - parts of the same image may be scaled by multiple threads as long as the [yFirst, yLast) ranges do not overlap! + - there is a minor inefficiency for the first row of a slice, so avoid processing single rows only +*/ +void scale(size_t factor, //valid range: 2 - 5 + const uint32_t* src, uint32_t* trg, int srcWidth, int srcHeight, + const ScalerCfg& cfg = ScalerCfg(), + int yFirst = 0, int yLast = std::numeric_limits::max()); //slice of source image + +void nearestNeighborScale(const uint32_t* src, int srcWidth, int srcHeight, + uint32_t* trg, int trgWidth, int trgHeight); + +enum SliceType +{ + NN_SCALE_SLICE_SOURCE, + NN_SCALE_SLICE_TARGET, +}; +void nearestNeighborScale(const uint32_t* src, int srcWidth, int srcHeight, int srcPitch, //pitch in bytes! + uint32_t* trg, int trgWidth, int trgHeight, int trgPitch, + SliceType st, int yFirst, int yLast); + +//parameter tuning +bool equalColor(uint32_t col1, uint32_t col2, double luminanceWeight, double equalColorTolerance); + + + + + +//########################### implementation ########################### +inline +void nearestNeighborScale(const uint32_t* src, int srcWidth, int srcHeight, + uint32_t* trg, int trgWidth, int trgHeight) +{ + nearestNeighborScale(src, srcWidth, srcHeight, srcWidth * sizeof(uint32_t), + trg, trgWidth, trgHeight, trgWidth * sizeof(uint32_t), + NN_SCALE_SLICE_TARGET, 0, trgHeight); +} +} + +#endif From 04d0a6196844cb489e7c761005f56eed9d17e567 Mon Sep 17 00:00:00 2001 From: Peter Thoman Date: Tue, 30 Apr 2013 03:46:27 +0200 Subject: [PATCH 2/5] Added config option for xBR texture scaling --- Core/Config.cpp | 3 +++ Core/Config.h | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Core/Config.cpp b/Core/Config.cpp index faf8d26457..fb4f91981d 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -113,6 +113,8 @@ void Config::Load(const char *iniFileName) #else graphics->Get("MipMap", &bMipMap, false); #endif + graphics->Get("XBRZTexScalingLevel", &iXBRZTexScalingLevel, 0); + IniFile::Section *sound = iniFile.GetOrCreateSection("Sound"); sound->Get("Enable", &bEnableSound, true); @@ -192,6 +194,7 @@ void Config::Save() graphics->Set("StretchToDisplay", bStretchToDisplay); graphics->Set("TrueColor", bTrueColor); graphics->Set("MipMap", bMipMap); + graphics->Set("XBRZTexScalingLevel", iXBRZTexScalingLevel); IniFile::Section *sound = iniFile.GetOrCreateSection("Sound"); sound->Set("Enable", bEnableSound); diff --git a/Core/Config.h b/Core/Config.h index ba59ed41cd..f004956e20 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -64,13 +64,14 @@ public: int iWindowX; int iWindowY; int iWindowZoom; // for Windows - bool SSAntiAliasing; //for Windows, too + bool SSAntiAliasing; // for Windows, too bool bVertexCache; bool bFullScreen; int iAnisotropyLevel; bool bTrueColor; bool bMipMap; bool bAnisotropicFiltering; + int iXBRZTexScalingLevel; // 0 = 1 = off, 2 = 2xBRZ, 3 = 3xBRZ, ..., 5 = 5xBRZ // Sound bool bEnableSound; From 30f3d8dbee98364b741b7e53b5bbbfb8623b2f84 Mon Sep 17 00:00:00 2001 From: Peter Thoman Date: Tue, 30 Apr 2013 03:47:33 +0200 Subject: [PATCH 3/5] Added Windows UI for xBR texture scaling option --- Windows/WndMainWindow.cpp | 28 ++++++++++++++++++++++++++++ Windows/ppsspp.rc | Bin 35556 -> 36992 bytes Windows/resource.h | Bin 11276 -> 163888 bytes 3 files changed, 28 insertions(+) diff --git a/Windows/WndMainWindow.cpp b/Windows/WndMainWindow.cpp index 6574f98623..6d13f9c89e 100644 --- a/Windows/WndMainWindow.cpp +++ b/Windows/WndMainWindow.cpp @@ -175,6 +175,11 @@ namespace MainWindow ResizeDisplay(); } + void toggleXbrzTexScaling(int num) { + g_Config.iXBRZTexScalingLevel = (g_Config.iXBRZTexScalingLevel == num) ? 0 : num; + if(gpu) gpu->InvalidateCache(0,0); + } + BOOL Show(HINSTANCE hInstance, int nCmdShow) { hInst = hInstance; // Store instance handle in our global variable @@ -495,6 +500,19 @@ namespace MainWindow g_Config.bMipMap = !g_Config.bMipMap; break; + case ID_TEXTURESCALING_2XBRZ: + toggleXbrzTexScaling(2); + break; + case ID_TEXTURESCALING_3XBRZ: + toggleXbrzTexScaling(3); + break; + case ID_TEXTURESCALING_4XBRZ: + toggleXbrzTexScaling(4); + break; + case ID_TEXTURESCALING_5XBRZ: + toggleXbrzTexScaling(5); + break; + case ID_OPTIONS_BUFFEREDRENDERING: g_Config.bBufferedRendering = !g_Config.bBufferedRendering; if (gpu) @@ -785,6 +803,16 @@ namespace MainWindow for (int i = 0; i < 4; i++) { CheckMenuItem(menu, zoomitems[i], MF_BYCOMMAND | ((i == g_Config.iWindowZoom - 1) ? MF_CHECKED : MF_UNCHECKED)); } + + static const int texscalingitems[4] = { + ID_TEXTURESCALING_2XBRZ, + ID_TEXTURESCALING_3XBRZ, + ID_TEXTURESCALING_4XBRZ, + ID_TEXTURESCALING_5XBRZ, + }; + for (int i = 0; i < 4; i++) { + CheckMenuItem(menu, texscalingitems[i], MF_BYCOMMAND | ((i == g_Config.iXBRZTexScalingLevel-2) ? MF_CHECKED : MF_UNCHECKED)); + } } diff --git a/Windows/ppsspp.rc b/Windows/ppsspp.rc index 1e7f8d07a9b4c85c563e38a2a75a8c22d64a69c9..c003dd1ba716c23e7836aa7e3bff1cdfad7f05a3 100644 GIT binary patch delta 497 zcmaDdm8oGN(}bdlW&bwc%V%QTEL51rGf*7JEZ}hfQ4`Fa+h+qg| z2nC9`0(CkwI5PM!cry4gxHH5v7y;!VY6aV6!Mcq}&~1#N+k^z&CK$R+NziQy*KKGc zI{8$x(BuMVsmTsLHk)@uDu@a5GH@}#ure^9(irk4OBdc$b_WI^Fo=NxnFtKfRE88_ zaHauy`3$*0RtZA}F!(?nya&b G%m)CFjC#QU delta 253 zcmZoz$n<0?(}beQ6Z%;u7X91YCo9CLtISZqP{fePkj{_`Bo!Ev8S)uY7*ZMHf#M+1 zR0a(OLk1%TQy|uyT*$t4bAWt;mAfy4KZ7%aH$x~x0D}%tn<11mU@!o)kVz#5J%$*d zam7HK3pHL3$j)O(pIj&=zIj9B9Vte&$=xN2lLdsuCfk*;O+HnkHTgoe-)5Bx1+mRp dj2tAob8{|JHq+!P_BES3Soesr@G@{Q000o!IadGx diff --git a/Windows/resource.h b/Windows/resource.h index 7a9c57097fbf69b13511d0c488e806cf65e85c4a..927c4ee9838352c56790693778fb6b78a8f8ec42 100644 GIT binary patch literal 163888 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

S1STiFPv=jjKk?PjZ0alO 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%0A*`uph6^`et3=BP!nLRq1@qUrYh~D~| z(0jWBOJ?e5K%I4P9=>QLH=T)b7nq@ES}a1BwyEiijtxEYQX_OKnXJUfbePRms=9q0 z8KJ8tHCka~v}d#FLE-VcQ#3~XlI#ckwpdD$aNGlC=$T!0>#^c(Fb;;E&DE`*p`+TL z;cc;lM40639&BXP(6iaJ*E8&68_CG_nxSX2$sHfb@Qm-Q{$ndhb)v#OH*wU;NW!W#Y8Cm#V#U-vp0dZxv;=*F8_kK@}C znfGO;hi=c`|1{b&Ew)9kIa81L{w{`|X|XLj{=549bNy?2=GzjPuXD!vjz=SZkn#0r zfOf5H?4$4PDpniYsu|^I+ap8IWX5&*O+_JIswA+Vt1hImVEb@-s!-lZ( zFypta0^#hJK?QO#tgMaK_#TX=&PHi~YHGnp1swiFJpt`{)$ z%<|Rf?0(AXvlkP7o@NuA$t>U2dWcizOJbcqoZ?K41y=Z~{KG#seD2VnPi|RFv$NV9 z?r-PM_FTMSH`f((I!iB4UdD{X_tvn)k&`Mo_`ojNea=)>sE5e=D7L(k^wmhZN& zw%bJY_!4(>WYQA*HHMze)d<}p<2m$fu5Kk#c{j3!#D5lpIk){twC9z+NE0ZB!pakS3di&# zL(k@_&q?z*u1}-NTI+m98T!gxB`07jMbYo9p}SxHIn8!t=qqzod7t;ExZZH$PeWgs ztL9lMSM#{!j@Xrq^<3L~7+Lj6pRS0IkY%4uA zc<|&7e+ZsqjE=lpltF#+QHdt$735xIKaI}l)6g?bkkd|lFrvMW9@8#sP0c2v*{&Jw znV!jM|7wEvK6;OK?|Cyy_n$_4UyCt($Ujdsa7((Xk)e zSA#5W?B|(n%IfAZ`h$Y~x=`OoW)zI}zK-mn-I41IO3wzT)g!O-Km ziiIZkP6tI#Q`rbcpY(cP6)&>4o1Nhk#}ivqS%G|v_1HWfw zg}=IvWlw)kGYFp@Y*$w~=pM$OUSEpvkF26-@q3ke;UoVZoso@q4&+Bhf3G4feEHhM zX)D;=S@_JZOn%S|%0#lGT~2ZPt>NeMSFwmR&oTUb{^Cgz&s)vDr7=4*`t$iK=~M;B z@`xF#e`oml{H0HoP8T97|J#`(!_Vh0c{#GftklS3o$WmhKcBzIKl)iCd|l3{r`0v| zEv}s#em;LK2P!RUwL!zkJ`KM&e{C(-+roMJ)QAW_o_u>^^zqJ3;QvzI9@w8>*Pm8a zVdLQvd5{@oJr($}?q8M>l%af>E zLfA_XsifN|!-&4?;gSI&-~X!Q$aoh6JJ_g?WR5G67dRdJJE}rk)kZ!QT6uO>8QqNG zZ_2+f%HRH6#`xEA^iw(drR?nZw~IfV&i}I<|Dl}yT;7`ZMfv7WBB|7Vi*%Qm2V`sSv6n>AjFETXY)6{aVzJ*GAv5aqIN+DF0M%Ka_Ac27TbyJpa$NX>rL{PS&iJ1$7&&pjJ^%kpSzQtU literal 11276 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 Date: Tue, 30 Apr 2013 03:49:12 +0200 Subject: [PATCH 4/5] Added xBR texture scaling to TextureCache --- GPU/GLES/TextureCache.cpp | 74 ++++++++++++++++++++++++++++++++++++++- GPU/GLES/TextureCache.h | 4 +++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/GPU/GLES/TextureCache.cpp b/GPU/GLES/TextureCache.cpp index f03a4820cb..631c167573 100644 --- a/GPU/GLES/TextureCache.cpp +++ b/GPU/GLES/TextureCache.cpp @@ -27,6 +27,7 @@ #include "Core/Config.h" #include "native/ext/cityhash/city.h" +#include "ext/xbrz/xbrz.h" // If a texture hasn't been seen for this many frames, get rid of it. #define TEXTURE_KILL_AGE 200 @@ -1194,8 +1195,79 @@ void TextureCache::LoadTextureLevel(TexCacheEntry &entry, int level) { // INFO_LOG(G3D, "Creating texture level %i/%i from %08x: %i x %i (stride: %i). fmt: %i", level, entry.maxLevel, texaddr, w, h, bufw, entry.format); + u32* pixelData = (u32*)finalBuf; + ScaleTexture(pixelData, dstFmt, w, h); + GLuint components = dstFmt == GL_UNSIGNED_SHORT_5_6_5 ? GL_RGB : GL_RGBA; - glTexImage2D(GL_TEXTURE_2D, level, components, w, h, 0, components, dstFmt, finalBuf); + glTexImage2D(GL_TEXTURE_2D, level, components, w, h, 0, components, dstFmt, pixelData); +} + +void TextureCache::ScaleTexture(u32* &data, GLenum &dstFmt, int &width, int &height) { + if(g_Config.iXBRZTexScalingLevel > 1) { + int factor = g_Config.iXBRZTexScalingLevel; + + // I tried to reuse the existing tmpTexBuf initially, but that's not a good idea as data could already be stored in it + // depending on the factor and texture sizes, these can be pretty large (25 MB for a 512 by 512 texture with scaling factor 5) + tmpTexBufScalingInput.resize(width*height); // used to store the input image image if it needs to be reformatted + tmpTexBufScalingOutput.resize(width*height*factor*factor); // used to store the upscaled image + u32 *xbrzInputBuf = tmpTexBufScalingInput.data(); + u32 *xbrzBuf = tmpTexBufScalingOutput.data(); + + // convert texture to correct format for xBRZ + switch(dstFmt) { + case GL_UNSIGNED_BYTE: + xbrzInputBuf = data; // already fine + break; + + case GL_UNSIGNED_SHORT_4_4_4_4: + for(int y = 0; y < height; ++y) { + for(int x = 0; x < width; ++x) { + u32 val = ((u16*)data)[y*width + x]; + u32 r = ((val>>12) & 0xF) * 17; + u32 g = ((val>> 8) & 0xF) * 17; + u32 b = ((val>> 4) & 0xF) * 17; + u32 a = ((val>> 0) & 0xF) * 17; + xbrzInputBuf[y*width + x] = (a << 24) | (b << 16) | (g << 8) | r; + } + } + break; + + case GL_UNSIGNED_SHORT_5_6_5: + for(int y = 0; y < height; ++y) { + for(int x = 0; x < width; ++x) { + u32 val = ((u16*)data)[y*width + x]; + u32 r = ((val>>11) & 0x1F) * 8; + u32 g = ((val>> 5) & 0x3F) * 4; + u32 b = ((val ) & 0x1F) * 8; + xbrzInputBuf[y*width + x] = (0xFF << 24) | (b << 16) | (g << 8) | r; + } + } + break; + + case GL_UNSIGNED_SHORT_5_5_5_1: + for(int y = 0; y < height; ++y) { + for(int x = 0; x < width; ++x) { + u32 val = ((u16*)data)[y*width + x]; + u32 r = ((val>>11) & 0x1F) * 8; + u32 g = ((val>> 6) & 0x1F) * 8; + u32 b = ((val>> 1) & 0x1F) * 8; + u32 a = (val & 0x1) * 255; + xbrzInputBuf[y*width + x] = (a << 24) | (b << 16) | (g << 8) | r; + } + } + break; + + default: + ERROR_LOG(G3D, "iXBRZTexScaling: unsupported texture format"); + } + + // scale and update values accordingly + xbrz::scale(factor, xbrzInputBuf, xbrzBuf, width, height); + data = xbrzBuf; + dstFmt = GL_UNSIGNED_BYTE; + width *= factor; + height *= factor; + } } bool TextureCache::DecodeTexture(u8* output, GPUgstate state) diff --git a/GPU/GLES/TextureCache.h b/GPU/GLES/TextureCache.h index f4f2ec1d5b..4e8949725a 100644 --- a/GPU/GLES/TextureCache.h +++ b/GPU/GLES/TextureCache.h @@ -93,6 +93,7 @@ private: void UpdateSamplingParams(TexCacheEntry &entry, bool force); void LoadTextureLevel(TexCacheEntry &entry, int level); void *DecodeTextureLevel(u8 format, u8 clutformat, int level, u32 &texByteAlign, GLenum &dstFmt); + void ScaleTexture(u32* &data, GLenum &dstfmt, int &width, int &height); TexCacheEntry *GetEntryAt(u32 texaddr); @@ -148,6 +149,9 @@ private: SimpleBuf tmpTexBufRearrange; + SimpleBuf tmpTexBufScalingInput; + SimpleBuf tmpTexBufScalingOutput; + u32 *clutBuf32; u16 *clutBuf16; From 6950cfef357942e057c948714c6b7c1e2f0f2a80 Mon Sep 17 00:00:00 2001 From: Peter Thoman Date: Tue, 30 Apr 2013 12:14:34 +0200 Subject: [PATCH 5/5] Changed xBRZ UI to have separate off option --- Core/Config.cpp | 2 +- Core/Config.h | 2 +- Windows/WndMainWindow.cpp | 22 +++++++++++++--------- Windows/ppsspp.rc | Bin 36992 -> 37142 bytes Windows/resource.h | Bin 163888 -> 163982 bytes 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Core/Config.cpp b/Core/Config.cpp index fb4f91981d..b38dcfd3c9 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -113,7 +113,7 @@ void Config::Load(const char *iniFileName) #else graphics->Get("MipMap", &bMipMap, false); #endif - graphics->Get("XBRZTexScalingLevel", &iXBRZTexScalingLevel, 0); + graphics->Get("XBRZTexScalingLevel", &iXBRZTexScalingLevel, 1); IniFile::Section *sound = iniFile.GetOrCreateSection("Sound"); sound->Get("Enable", &bEnableSound, true); diff --git a/Core/Config.h b/Core/Config.h index f004956e20..8b6aec96ca 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -71,7 +71,7 @@ public: bool bTrueColor; bool bMipMap; bool bAnisotropicFiltering; - int iXBRZTexScalingLevel; // 0 = 1 = off, 2 = 2xBRZ, 3 = 3xBRZ, ..., 5 = 5xBRZ + int iXBRZTexScalingLevel; // 1 = off, 2 = 2xBRZ, ..., 5 = 5xBRZ // Sound bool bEnableSound; diff --git a/Windows/WndMainWindow.cpp b/Windows/WndMainWindow.cpp index 6d13f9c89e..788c1da8fe 100644 --- a/Windows/WndMainWindow.cpp +++ b/Windows/WndMainWindow.cpp @@ -175,8 +175,8 @@ namespace MainWindow ResizeDisplay(); } - void toggleXbrzTexScaling(int num) { - g_Config.iXBRZTexScalingLevel = (g_Config.iXBRZTexScalingLevel == num) ? 0 : num; + void setXbrzTexScaling(int num) { + g_Config.iXBRZTexScalingLevel = num; if(gpu) gpu->InvalidateCache(0,0); } @@ -500,17 +500,20 @@ namespace MainWindow g_Config.bMipMap = !g_Config.bMipMap; break; + case ID_TEXTURESCALING_OFF: + setXbrzTexScaling(1); + break; case ID_TEXTURESCALING_2XBRZ: - toggleXbrzTexScaling(2); + setXbrzTexScaling(2); break; case ID_TEXTURESCALING_3XBRZ: - toggleXbrzTexScaling(3); + setXbrzTexScaling(3); break; case ID_TEXTURESCALING_4XBRZ: - toggleXbrzTexScaling(4); + setXbrzTexScaling(4); break; case ID_TEXTURESCALING_5XBRZ: - toggleXbrzTexScaling(5); + setXbrzTexScaling(5); break; case ID_OPTIONS_BUFFEREDRENDERING: @@ -804,14 +807,15 @@ namespace MainWindow CheckMenuItem(menu, zoomitems[i], MF_BYCOMMAND | ((i == g_Config.iWindowZoom - 1) ? MF_CHECKED : MF_UNCHECKED)); } - static const int texscalingitems[4] = { + static const int texscalingitems[] = { + ID_TEXTURESCALING_OFF, ID_TEXTURESCALING_2XBRZ, ID_TEXTURESCALING_3XBRZ, ID_TEXTURESCALING_4XBRZ, ID_TEXTURESCALING_5XBRZ, }; - for (int i = 0; i < 4; i++) { - CheckMenuItem(menu, texscalingitems[i], MF_BYCOMMAND | ((i == g_Config.iXBRZTexScalingLevel-2) ? MF_CHECKED : MF_UNCHECKED)); + for (int i = 0; i < 5; i++) { + CheckMenuItem(menu, texscalingitems[i], MF_BYCOMMAND | ((i == g_Config.iXBRZTexScalingLevel-1) ? MF_CHECKED : MF_UNCHECKED)); } } diff --git a/Windows/ppsspp.rc b/Windows/ppsspp.rc index c003dd1ba716c23e7836aa7e3bff1cdfad7f05a3..172aeaeab6a310b0d6b70dec1ed792d2c7e291eb 100644 GIT binary patch delta 46 zcmZoz$TV#c(}sdlR)2;xhP26zX{wV2*o7uPC{qE7xG}g*o+u_eIUq%3b3v(qGyrWe B4$uGq delta 14 VcmbQXh^b*A(}sf5&1_`?(f};-1x)|| diff --git a/Windows/resource.h b/Windows/resource.h index 927c4ee9838352c56790693778fb6b78a8f8ec42..444cd777181de5cbd014f5f71a982df72014ce63 100644 GIT binary patch delta 63 zcmV-F0KosSfC`R*3V?(Gv;q=um)>jwAOlYTMw5|-8Mp9m0^ALkBz*#4m&|+uShs|I V0?+}MR&N3pmkfUb7?*;70vN{nI?f?J)