From d2fb92518f50c6154f33f05be39128e48d6b83be Mon Sep 17 00:00:00 2001 From: Rodolfo Bogado Date: Fri, 28 Aug 2015 18:23:16 -0300 Subject: [PATCH] Experimental: Add Support for texture scaling using PPSSPP implementation code. Thanks to PPSSPP team for the great work. --- CMakeLists.txt | 7 + Externals/xbrz/CMakeLists.txt | 8 + Externals/xbrz/config.h | 40 + Externals/xbrz/xbrz.cpp | 1293 +++++++++++++++++ Externals/xbrz/xbrz.h | 104 ++ Externals/xbrz/xbrz.vcxproj | 53 + Source/Core/Common/MemoryUtil.h | 45 + Source/Core/DolphinWX/VideoConfigDiag.cpp | 31 + Source/Core/DolphinWX/VideoConfigDiag.h | 1 + .../Core/VideoBackends/DX11/TextureCache.cpp | 59 +- .../Core/VideoBackends/DX9/TextureCache.cpp | 32 +- .../Core/VideoBackends/OGL/TextureCache.cpp | 28 +- Source/Core/VideoCommon/TextureCacheBase.cpp | 12 + Source/Core/VideoCommon/TextureCacheBase.h | 3 + .../Core/VideoCommon/TextureScalerCommon.cpp | 627 ++++++++ Source/Core/VideoCommon/TextureScalerCommon.h | 50 + Source/Core/VideoCommon/VideoCommon.vcxproj | 5 + .../VideoCommon/VideoCommon.vcxproj.filters | 6 + Source/Core/VideoCommon/VideoConfig.cpp | 28 + Source/Core/VideoCommon/VideoConfig.h | 3 + Source/Dolphin.sln | 11 +- 21 files changed, 2428 insertions(+), 18 deletions(-) create mode 100644 Externals/xbrz/CMakeLists.txt create mode 100644 Externals/xbrz/config.h create mode 100644 Externals/xbrz/xbrz.cpp create mode 100644 Externals/xbrz/xbrz.h create mode 100644 Externals/xbrz/xbrz.vcxproj create mode 100644 Source/Core/VideoCommon/TextureScalerCommon.cpp create mode 100644 Source/Core/VideoCommon/TextureScalerCommon.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e32b9268aa..ee7365e808 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -594,6 +594,13 @@ if(NOT XXHASH_FOUND) endif() LIST(APPEND LIBS xxhash) +if(NOT XBRZ_FOUND) + message("Using static xbrz from Externals") + add_subdirectory(Externals/xbrz) + include_directories(Externals/xbrz) +endif() +LIST(APPEND LIBS xbrz) + # If zlib has already been found on a previous run of cmake don't check again # as the check seems to take a long time. if(NOT ZLIB_FOUND) diff --git a/Externals/xbrz/CMakeLists.txt b/Externals/xbrz/CMakeLists.txt new file mode 100644 index 0000000000..36228bc4e2 --- /dev/null +++ b/Externals/xbrz/CMakeLists.txt @@ -0,0 +1,8 @@ + + +set(SRCS + xbrz.cpp +) + + +add_library(xbrz STATIC ${SRCS}) diff --git a/Externals/xbrz/config.h b/Externals/xbrz/config.h new file mode 100644 index 0000000000..ae11f7a0f5 --- /dev/null +++ b/Externals/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/Externals/xbrz/xbrz.cpp b/Externals/xbrz/xbrz.cpp new file mode 100644 index 0000000000..30b35652fc --- /dev/null +++ b/Externals/xbrz/xbrz.cpp @@ -0,0 +1,1293 @@ +// **************************************************************************** +// * 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 +#include +#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<0>(val); } +inline unsigned char getGreen(uint32_t val) { return getByte<1>(val); } +inline unsigned char getBlue (uint32_t val) { return getByte<2>(val); } +inline unsigned char getAlpha(uint32_t val) { return getByte<3>(val); } + + +template inline +T abs(T value) +{ + static_assert(std::numeric_limits::is_signed, "abs() requires signed types"); + 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); +} + + +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_b = 0.0593; //ITU-R BT.2020 conversion + const double k_r = 0.2627; // + 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)); +} + + +struct DistYCbCrBuffer //30% perf boost compared to distYCbCr()! +{ +public: + DistYCbCrBuffer() : buffer(256 * 256 * 256) + { + for (uint32_t i = 0; i < 256 * 256 * 256; ++i) //startup time: 114 ms on Intel Core i5 (four cores) + { + const int r_diff = getByte<2>(i) * 2 - 255; + const int g_diff = getByte<1>(i) * 2 - 255; + const int b_diff = getByte<0>(i) * 2 - 255; + + const double k_b = 0.0593; //ITU-R BT.2020 conversion + const double k_r = 0.2627; // + 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); + + buffer[i] = static_cast(std::sqrt(square(y) + square(c_b) + square(c_r))); + } + } + + double dist(uint32_t pix1, uint32_t pix2) const + { + //if (pix1 == pix2) -> 8% perf degradation! + // return 0; + //if (pix1 > pix2) + // std::swap(pix1, pix2); -> 30% perf degradation!!! + + const int r_diff = static_cast(getRed (pix1)) - getRed (pix2); + const int g_diff = static_cast(getGreen(pix1)) - getGreen(pix2); + const int b_diff = static_cast(getBlue (pix1)) - getBlue (pix2); + + return buffer[(((r_diff + 255) / 2) << 16) | //slightly reduce precision (division by 2) to squeeze value into single byte + (((g_diff + 255) / 2) << 8) | + (( b_diff + 255) / 2)]; + } + +private: + std::vector buffer; //consumes 64 MB memory; using double is 2% faster, but takes 128 MB +}; +DistYCbCrBuffer *distYCbCrBuffer = nullptr; + + +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] + +#ifdef _DEBUG + const double eps = 0.5; + assert(abs(y) <= 255 + eps); + assert(abs(u) <= 255 * 2 * u_max + eps); + assert(abs(v) <= 255 * 2 * v_max + eps); +#endif + + return std::sqrt(square(luminanceWeight * y) + square(u) + square(v)); +} + + +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 | //evaluate the four corners between F, G, J, K +----|---|---|---| //input pixel is at position F +| I | J | K | L | +----|---|---|---| +| M | N | O | P | +----------------- +*/ +template +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 pix1, uint32_t pix2) { return ColorDistance::dist(pix1, pix2, 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; } + + +#ifdef _DEBUG +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) + + const unsigned char blend = rotateBlendInfo(blendInfo); + + if (getBottomR(blend) >= BLEND_NORMAL) + { + auto eq = [&](uint32_t pix1, uint32_t pix2) { return ColorDistance::dist(pix1, pix2, cfg.luminanceWeight_) < cfg.equalColorTolerance_; }; + auto dist = [&](uint32_t pix1, uint32_t pix2) { return ColorDistance::dist(pix1, pix2, 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(e, i) && eq(g, h) && eq(h , i) && eq(i, f) && eq(f, c)) + 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 negligible + 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 < bufferSize) + 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) + { +#ifdef _DEBUG + 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); + + Kernel_4x4 ker4 = {}; //perf: initialization is negligible + + ker4.a = s_m1[x_m1]; //read sequentially from memory as far as possible + ker4.b = s_m1[x]; + ker4.c = s_m1[x_p1]; + ker4.d = s_m1[x_p2]; + + ker4.e = s_0[x_m1]; + ker4.f = s_0[x]; + ker4.g = s_0[x_p1]; + ker4.h = s_0[x_p2]; + + ker4.i = s_p1[x_m1]; + ker4.j = s_p1[x]; + ker4.k = s_p1[x_p1]; + ker4.l = s_p1[x_p2]; + + ker4.m = s_p2[x_m1]; + ker4.n = s_p2[x]; + ker4.o = s_p2[x_p1]; + ker4.p = s_p2[x_p2]; + + //evaluate the four corners on bottom-right of current pixel + unsigned char blend_xy = 0; //for current (x, y) position + { + const BlendResult res = preProcessCorners(ker4, 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 < bufferSize) //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), ker4.f, 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 ker3 = {}; //perf: initialization is negligible + + ker3.a = ker4.a; + ker3.b = ker4.b; + ker3.c = ker4.c; + + ker3.d = ker4.e; + ker3.e = ker4.f; + ker3.f = ker4.g; + + ker3.g = ker4.i; + ker3.h = ker4.j; + ker3.i = ker4.k; + + scalePixel(ker3, out, trgWidth, blend_xy, cfg); + scalePixel(ker3, out, trgWidth, blend_xy, cfg); + scalePixel(ker3, out, trgWidth, blend_xy, cfg); + scalePixel(ker3, 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 -> negligible + //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 -> negligible + //alphaBlend<8, 1000>(out.template ref<2, 4>(), col); //0.008384061834 + } +}; + +//------------------------------------------------------------------------------------ + +struct ColorDistanceRGB +{ + static double dist(uint32_t pix1, uint32_t pix2, double luminanceWeight) + { + return distYCbCrBuffer->dist(pix1, pix2); + + //if (pix1 == pix2) //about 4% perf boost + // return 0; + //return distYCbCr(pix1, pix2, luminanceWeight); + } +}; + +struct ColorDistanceARGB +{ + static double dist(uint32_t pix1, uint32_t pix2, double luminanceWeight) + { + const double a1 = getAlpha(pix1) / 255.0 ; + const double a2 = getAlpha(pix2) / 255.0 ; + /* + Requirements for a color distance handling alpha channel: with a1, a2 in [0, 1] + + 1. if a1 = a2, distance should be: a1 * distYCbCr() + 2. if a1 = 0, distance should be: a2 * distYCbCr(black, white) = a2 * 255 + 3. if a1 = 1, distance should be: 255 * (1 - a2) + a2 * distYCbCr() + */ + + const double d = distYCbCrBuffer->dist(pix1, pix2); + if (a1 > a2) + return a2 * d + 255 * (a1 - a2); + else + return a1 * d + 255 * (a2 - a1); + + //if (pix1 == pix2) + // return 0; + //return std::min(a1, a2) * distYCbCr(pix1, pix2, luminanceWeight) + 255 * abs(a1 - a2); + } +}; +} + + +void xbrz::scale(size_t factor, const uint32_t* src, uint32_t* trg, int srcWidth, int srcHeight, ColorFormat colFmt, const xbrz::ScalerCfg& cfg, int yFirst, int yLast) +{ + switch (colFmt) + { + case ColorFormat::ARGB: + 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); + } + case ColorFormat::RGB: + 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); +} + + +void xbrz::init() +{ + if (distYCbCrBuffer == nullptr) + distYCbCrBuffer = new DistYCbCrBuffer(); +} + + +void xbrz::shutdown() +{ + delete distYCbCrBuffer; + distYCbCrBuffer = nullptr; +} + + +bool xbrz::equalColorTest(uint32_t col1, uint32_t col2, ColorFormat colFmt, double luminanceWeight, double equalColorTolerance) +{ + switch (colFmt) + { + case ColorFormat::ARGB: + return ColorDistanceARGB::dist(col1, col2, luminanceWeight) < equalColorTolerance; + + case ColorFormat::RGB: + return ColorDistanceRGB::dist(col1, col2, luminanceWeight) < equalColorTolerance; + } + assert(false); + return false; +} + + +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/Externals/xbrz/xbrz.h b/Externals/xbrz/xbrz.h new file mode 100644 index 0000000000..364e7c5d22 --- /dev/null +++ b/Externals/xbrz/xbrz.h @@ -0,0 +1,104 @@ +// **************************************************************************** +// * 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 + +#undef min +#undef max + +#include //size_t +#if defined(IOS) +#include +#else +#include //uint32_t +#endif +#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 +- highly optimized for performance +- support alpha channel +- support multithreading +- support 64-bit architectures +- support processing image slices +*/ + +enum class ColorFormat //from high bits -> low bits, 8 bit per channel +{ + ARGB, //including alpha channel, BGRA byte order on little-endian machines + RGB, //8 bit for each red, green, blue, upper 8 bits unused +}; + +/* +-> map source (srcWidth * srcHeight) to target (scale * width x scale * height) image, optionally processing a half-open slice of rows [yFirst, yLast) only +-> support for source/target pitch in bytes! +-> if your emulator changes only a few image slices during each cycle (e.g. DOSBox) then there's no need to run xBRZ on the complete image: + Just make sure you enlarge the source image slice by 2 rows on top and 2 on bottom (this is the additional range the xBRZ algorithm is using during analysis) + Caveat: If there are multiple changed slices, make sure they do not overlap after adding these additional rows in order to avoid a memory race condition + in the target image data if you are using multiple threads for processing each enlarged slice! + +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; suggestion: process 6 rows at least +*/ +void scale(size_t factor, //valid range: 2 - 5 + const uint32_t* src, uint32_t* trg, int srcWidth, int srcHeight, + ColorFormat colFmt, + const ScalerCfg& cfg = ScalerCfg(), + int yFirst = 0, int yLast = std::numeric_limits::max()); //slice of source image + +void init(); + +void shutdown(); + +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 equalColorTest(uint32_t col1, uint32_t col2, ColorFormat colFmt, 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 diff --git a/Externals/xbrz/xbrz.vcxproj b/Externals/xbrz/xbrz.vcxproj new file mode 100644 index 0000000000..73169ffead --- /dev/null +++ b/Externals/xbrz/xbrz.vcxproj @@ -0,0 +1,53 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {869EA016-4458-2A45-25AF-DC44D6E98C0A} + + + + StaticLibrary + v120 + Unicode + + + true + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/Core/Common/MemoryUtil.h b/Source/Core/Common/MemoryUtil.h index e986069d24..fb71acc7b1 100644 --- a/Source/Core/Common/MemoryUtil.h +++ b/Source/Core/Common/MemoryUtil.h @@ -22,3 +22,48 @@ void GuardMemoryMake(void* ptr, size_t size); void GuardMemoryUnmake(void* ptr, size_t size); inline int GetPageSize() { return 4096; } + +template +class SimpleBuf { +public: + SimpleBuf() : buf_(0), size_(0) { + } + + SimpleBuf(size_t size) : buf_(0) { + resize(size); + } + + ~SimpleBuf() { + if (buf_ != 0) { + FreeMemoryPages(buf_, size_ * sizeof(T)); + } + } + + inline T &operator[](size_t index) { + return buf_[index]; + } + + // Doesn't preserve contents. + void resize(size_t size) { + if (size_ < size) { + if (buf_ != 0) { + FreeMemoryPages(buf_, size_ * sizeof(T)); + } + buf_ = (T *)AllocateMemoryPages(size * sizeof(T)); + size_ = size; + } + } + + T *data() { + return buf_; + } + + size_t size() const { + return size_; + } + +private: + T *buf_; + size_t size_; +}; + diff --git a/Source/Core/DolphinWX/VideoConfigDiag.cpp b/Source/Core/DolphinWX/VideoConfigDiag.cpp index f5f33d5597..1446cf1252 100644 --- a/Source/Core/DolphinWX/VideoConfigDiag.cpp +++ b/Source/Core/DolphinWX/VideoConfigDiag.cpp @@ -154,6 +154,9 @@ static wxString stereo_separation_desc = _("Control the separation distance, thi static wxString stereo_convergence_desc = _("Control the convergence distance, this controls the apparant distance of virtual objects.\nA higher value creates stronger out-of-screen effects while a lower value is more comfortable."); static wxString stereo_swap_desc = _("Swap the left and right eye, mostly useful if you want to view side-by-side cross-eyed.\n\nIf unsure, leave this unchecked."); static const char *s_bbox_mode_text[] = { "Disabled", "CPU", "GPU" }; +static wxString texture_scaling_desc = _("Apply the selected scaling algorithm to improve texture quality."); +static wxString scaling_factor_desc = _("Multiplier applied to the texture size."); +static wxString texture_deposterize_desc = _("Decrease some gradient's artifacts caused by scaling."); // Search for available resolutions - TODO: Move to Common? static wxArrayString GetListOfResolutions() @@ -467,6 +470,27 @@ VideoConfigDiag::VideoConfigDiag(wxWindow* parent, const std::string &title, con szr_enh_main->Add(group_stereo, 0, wxEXPAND | wxALL, 5); } + wxFlexGridSizer* const szr_texturescaling = new wxFlexGridSizer(2, 5, 5); + + szr_texturescaling->Add(new wxStaticText(page_enh, wxID_ANY, _("Texture Scaling Mode:")), 1, wxALIGN_CENTER_VERTICAL, 0); + + const wxString scaling_choices[] = { "Off", "XBRZ", "Hybrid", "Bicubic", "Hybrid-Bicubic" }; + wxChoice* scaling_choice = CreateChoice(page_enh, vconfig.iTexScalingType, (texture_scaling_desc), ArraySize(scaling_choices), scaling_choices); + szr_texturescaling->Add(scaling_choice); + + wxSlider* const factor_slider = new wxSlider(page_enh, wxID_ANY, vconfig.iTexScalingFactor, 2, 5, wxDefaultPosition, wxDefaultSize); + factor_slider->Bind(wxEVT_SLIDER, &VideoConfigDiag::Event_ScalingFactor, this); + RegisterControl(factor_slider, (scaling_factor_desc)); + + szr_texturescaling->Add(new wxStaticText(page_enh, wxID_ANY, _("Scaling factor:")), 1, wxALIGN_CENTER_VERTICAL, 0); + szr_texturescaling->Add(factor_slider, 0, wxEXPAND | wxRIGHT); + + szr_texturescaling->Add(CreateCheckBox(page_enh, _("DePosterize"), (stereo_swap_desc), vconfig.bTexDeposterize)); + + wxStaticBoxSizer* const group_scaling = new wxStaticBoxSizer(wxVERTICAL, page_enh, _("Texture Scaling")); + group_scaling->Add(szr_texturescaling, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 5); + szr_enh_main->Add(group_scaling, 0, wxEXPAND | wxALL, 5); + szr_enh_main->AddStretchSpacer(); CreateDescriptionArea(page_enh, szr_enh_main); page_enh->SetSizerAndFit(szr_enh_main); @@ -843,6 +867,13 @@ void VideoConfigDiag::Event_StereoDepth(wxCommandEvent &ev) ev.Skip(); } +void VideoConfigDiag::Event_ScalingFactor(wxCommandEvent &ev) +{ + vconfig.iTexScalingFactor = ev.GetInt(); + + ev.Skip(); +} + void VideoConfigDiag::Event_StereoConvergence(wxCommandEvent &ev) { vconfig.iStereoConvergence = ev.GetInt(); diff --git a/Source/Core/DolphinWX/VideoConfigDiag.h b/Source/Core/DolphinWX/VideoConfigDiag.h index 3a3aae0d51..d75ab31f48 100644 --- a/Source/Core/DolphinWX/VideoConfigDiag.h +++ b/Source/Core/DolphinWX/VideoConfigDiag.h @@ -91,6 +91,7 @@ class VideoConfigDiag : public wxDialog void Event_StereoDepth(wxCommandEvent &ev); void Event_StereoConvergence(wxCommandEvent &ev); void Event_StereoMode(wxCommandEvent &ev); + void Event_ScalingFactor(wxCommandEvent &ev); void Event_ClickClose(wxCommandEvent&); void Event_Close(wxCloseEvent&); diff --git a/Source/Core/VideoBackends/DX11/TextureCache.cpp b/Source/Core/VideoBackends/DX11/TextureCache.cpp index 182fdcc82c..13de431871 100644 --- a/Source/Core/VideoBackends/DX11/TextureCache.cpp +++ b/Source/Core/VideoBackends/DX11/TextureCache.cpp @@ -18,12 +18,14 @@ #include "VideoCommon/ImageWrite.h" #include "VideoCommon/RenderBase.h" #include "VideoCommon/VideoConfig.h" +#include "VideoCommon/TextureScalerCommon.h" namespace DX11 { static TextureEncoder* s_encoder = nullptr; static TextureDecoder* s_decoder = nullptr; +static TextureScaler* s_scaler = nullptr; const size_t MAX_COPY_BUFFERS = 33; D3D::BufferPtr efbcopycbuf[MAX_COPY_BUFFERS]; @@ -167,14 +169,20 @@ void TextureCache::TCacheEntry::Load(const u8* src, u32 width, u32 height, void TextureCache::TCacheEntry::Load(const u8* src, u32 width, u32 height, u32 expandedWidth, u32 expandedHeight, const s32 texformat, const u32 tlutaddr, const TlutFormat tlutfmt, u32 level) { - if (!s_decoder->Decode( - src, - TexDecoder_GetTextureSizeInBytes(expandedWidth, expandedHeight, texformat), - texformat, - width, - height, - level, - *texture)) + bool need_cpu_decode = g_ActiveConfig.iTexScalingType > 0; + + if (!need_cpu_decode) + { + need_cpu_decode = !s_decoder->Decode( + src, + TexDecoder_GetTextureSizeInBytes(expandedWidth, expandedHeight, texformat), + texformat, + width, + height, + level, + *texture); + } + if (need_cpu_decode) { TexDecoder_Decode( TextureCache::temp, @@ -186,9 +194,17 @@ void TextureCache::TCacheEntry::Load(const u8* src, u32 width, u32 height, u32 e tlutfmt, PC_TEX_FMT_RGBA32 == config.pcformat, compressed); + u8* data = TextureCache::temp; + if (g_ActiveConfig.iTexScalingType) + { + data = (u8*)s_scaler->Scale((u32*)data, expandedWidth, height); + width *= g_ActiveConfig.iTexScalingFactor; + height *= g_ActiveConfig.iTexScalingFactor; + expandedWidth *= g_ActiveConfig.iTexScalingFactor; + } D3D::ReplaceTexture2D( texture->GetTex(), - TextureCache::temp, + data, width, height, expandedWidth, @@ -202,7 +218,13 @@ void TextureCache::TCacheEntry::Load(const u8* src, u32 width, u32 height, u32 e void TextureCache::TCacheEntry::LoadFromTmem(const u8* ar_src, const u8* gb_src, u32 width, u32 height, u32 expanded_width, u32 expanded_Height, u32 level) { - if (!s_decoder->DecodeRGBAFromTMEM(ar_src, gb_src,width, height,*texture)) + bool need_cpu_decode = g_ActiveConfig.iTexScalingType > 0; + + if (!need_cpu_decode) + { + need_cpu_decode = !s_decoder->DecodeRGBAFromTMEM(ar_src, gb_src, width, height, *texture); + } + if (need_cpu_decode) { TexDecoder_DecodeRGBA8FromTmem( (u32*)TextureCache::temp, @@ -210,9 +232,17 @@ void TextureCache::TCacheEntry::LoadFromTmem(const u8* ar_src, const u8* gb_src, gb_src, expanded_width, expanded_Height); + u8* data = TextureCache::temp; + if (g_ActiveConfig.iTexScalingType) + { + data = (u8*)s_scaler->Scale((u32*)data, expanded_width, height); + width *= g_ActiveConfig.iTexScalingFactor; + height *= g_ActiveConfig.iTexScalingFactor; + expanded_width *= g_ActiveConfig.iTexScalingFactor; + } D3D::ReplaceTexture2D( texture->GetTex(), - TextureCache::temp, + data, width, height, expanded_width, @@ -222,7 +252,6 @@ void TextureCache::TCacheEntry::LoadFromTmem(const u8* ar_src, const u8* gb_src, swap_rg, convertrgb565); } - } PC_TexFormat TextureCache::GetNativeTextureFormat(const s32 texformat, const TlutFormat tlutfmt, u32 width, u32 height) @@ -397,6 +426,7 @@ TextureCache::TextureCache() s_encoder->Init(); s_decoder = new CSTextureDecoder; s_decoder->Init(); + s_scaler = new TextureScaler(); } TextureCache::~TextureCache() @@ -411,6 +441,11 @@ TextureCache::~TextureCache() s_decoder->Shutdown(); delete s_decoder; s_decoder = nullptr; + if (s_scaler) + { + delete s_scaler; + s_scaler = nullptr; + } } } diff --git a/Source/Core/VideoBackends/DX9/TextureCache.cpp b/Source/Core/VideoBackends/DX9/TextureCache.cpp index c45c7a94db..247abd6bc0 100644 --- a/Source/Core/VideoBackends/DX9/TextureCache.cpp +++ b/Source/Core/VideoBackends/DX9/TextureCache.cpp @@ -28,6 +28,7 @@ #include "VideoCommon/PixelShaderManager.h" #include "VideoCommon/Statistics.h" #include "VideoCommon/TextureDecoder.h" +#include "VideoCommon/TextureScalerCommon.h" #include "VideoCommon/VertexShaderManager.h" @@ -56,6 +57,7 @@ static u32 s_memPoolTextureW[MEM_TEXTURE_POOL_SIZE]; static u32 s_memPoolTextureH[MEM_TEXTURE_POOL_SIZE]; static Depalettizer *s_depaletizer = nullptr; +static TextureScaler* s_scaler = nullptr; TextureCache::TCacheEntry::~TCacheEntry() { @@ -170,14 +172,32 @@ void TextureCache::TCacheEntry::Load(const u8* src, u32 width, u32 height, void TextureCache::TCacheEntry::Load(const u8* src, u32 width, u32 height, u32 expandedWidth, u32 expandedHeight, const s32 texformat, const u32 tlutaddr, const TlutFormat tlutfmt, u32 level) { - config.pcformat = TexDecoder_Decode(TextureCache::temp, src, expandedWidth, expandedHeight, texformat, tlutaddr, tlutfmt, false, compressed); - ReplaceTexture(TextureCache::temp, width, height, expandedWidth, level); + config.pcformat = TexDecoder_Decode(TextureCache::temp, src, expandedWidth, expandedHeight, texformat, tlutaddr, tlutfmt, g_ActiveConfig.iTexScalingType > 0, compressed); + u8* data = TextureCache::temp; + if (g_ActiveConfig.iTexScalingType) + { + data = (u8*)s_scaler->Scale((u32*)data, expandedWidth, height); + width *= g_ActiveConfig.iTexScalingFactor; + height *= g_ActiveConfig.iTexScalingFactor; + expandedWidth *= g_ActiveConfig.iTexScalingFactor; + } + ReplaceTexture(data, width, height, expandedWidth, level); } void TextureCache::TCacheEntry::LoadFromTmem(const u8* ar_src, const u8* gb_src, u32 width, u32 height, u32 expanded_width, u32 expanded_Height, u32 level) { + config.pcformat = PC_TEX_FMT_BGRA32; + swap_r_b = false; TexDecoder_DecodeBGRA8FromTmem((u32*)TextureCache::temp, ar_src, gb_src, expanded_width, expanded_Height); - ReplaceTexture(TextureCache::temp, width, height, expanded_width, level); + u8* data = TextureCache::temp; + if (g_ActiveConfig.iTexScalingType) + { + data = (u8*)s_scaler->Scale((u32*)data, expanded_width, height); + width *= g_ActiveConfig.iTexScalingFactor; + height *= g_ActiveConfig.iTexScalingFactor; + expanded_width *= g_ActiveConfig.iTexScalingFactor; + } + ReplaceTexture(data, width, height, expanded_width, level); } void TextureCache::TCacheEntry::FromRenderTarget( @@ -336,6 +356,7 @@ TextureCache::TextureCache() s_memPoolTextureH[i] = 1024u; } s_depaletizer = new Depalettizer(); + s_scaler = new TextureScaler(); } TextureCache::~TextureCache() @@ -353,6 +374,11 @@ TextureCache::~TextureCache() delete s_depaletizer; s_depaletizer = nullptr; } + if (s_scaler) + { + delete s_scaler; + s_scaler = nullptr; + } } diff --git a/Source/Core/VideoBackends/OGL/TextureCache.cpp b/Source/Core/VideoBackends/OGL/TextureCache.cpp index d3eb058ad3..a82f9a02be 100644 --- a/Source/Core/VideoBackends/OGL/TextureCache.cpp +++ b/Source/Core/VideoBackends/OGL/TextureCache.cpp @@ -33,6 +33,7 @@ #include "VideoCommon/ImageWrite.h" #include "VideoCommon/Statistics.h" #include "VideoCommon/TextureDecoder.h" +#include "VideoCommon/TextureScalerCommon.h" #include "VideoCommon/VideoConfig.h" namespace OGL @@ -58,6 +59,7 @@ static GLuint s_palette_buffer_offset_uniform[3]; static GLuint s_palette_multiplier_uniform[3]; static GLuint s_palette_copy_position_uniform[3]; static Depalettizer* s_depaletizer = nullptr; +static TextureScaler* s_scaler = nullptr; static std::pair s_last_pallet_Buffer; static TlutFormat s_last_TlutFormat = TlutFormat::GX_TL_IA8; bool SaveTexture(const std::string& filename, u32 textarget, u32 tex, int virtual_width, int virtual_height, u32 level) @@ -327,8 +329,16 @@ void TextureCache::TCacheEntry::Load(const u8* src, u32 width, u32 height, u32 e u32 expandedHeight, const s32 texformat, const u32 tlutaddr, const TlutFormat tlutfmt, u32 level) { config.pcformat = TexDecoder_Decode(TextureCache::temp, src, expandedWidth, expandedHeight, texformat, tlutaddr, tlutfmt, PC_TEX_FMT_RGBA32 == config.pcformat, compressed); + u8* data = TextureCache::temp; + if (g_ActiveConfig.iTexScalingType) + { + data = (u8*)s_scaler->Scale((u32*)data, expandedWidth, height); + width *= g_ActiveConfig.iTexScalingFactor; + height *= g_ActiveConfig.iTexScalingFactor; + expandedWidth *= g_ActiveConfig.iTexScalingFactor; + } SetFormat(); - Load(TextureCache::temp, width, height, expandedWidth, level); + Load(data, width, height, expandedWidth, level); } void TextureCache::TCacheEntry::LoadFromTmem(const u8* ar_src, const u8* gb_src, u32 width, u32 height, u32 expanded_width, u32 expanded_Height, u32 level) @@ -338,7 +348,15 @@ void TextureCache::TCacheEntry::LoadFromTmem(const u8* ar_src, const u8* gb_src, gl_iformat = GL_RGBA; gl_type = GL_UNSIGNED_BYTE; TexDecoder_DecodeRGBA8FromTmem((u32*)TextureCache::temp, ar_src, gb_src, expanded_width, expanded_Height); - Load(TextureCache::temp, width, height, expanded_width, level); + u8* data = TextureCache::temp; + if (g_ActiveConfig.iTexScalingType) + { + data = (u8*)s_scaler->Scale((u32*)data, expanded_width, height); + width *= g_ActiveConfig.iTexScalingFactor; + height *= g_ActiveConfig.iTexScalingFactor; + expanded_width *= g_ActiveConfig.iTexScalingFactor; + } + Load(data, width, height, expanded_width, level); } void TextureCache::TCacheEntry::FromRenderTarget( @@ -478,6 +496,7 @@ TextureCache::TextureCache() { s_depaletizer = new Depalettizer(); } + s_scaler = new TextureScaler(); } void TextureCache::CompileShaders() @@ -712,6 +731,11 @@ TextureCache::~TextureCache() s_depaletizer = nullptr; } } + if (s_scaler) + { + delete s_scaler; + s_scaler = nullptr; + } } void TextureCache::LoadLut(u32 lutFmt, void* addr, u32 size) diff --git a/Source/Core/VideoCommon/TextureCacheBase.cpp b/Source/Core/VideoCommon/TextureCacheBase.cpp index 54f15766c2..9f9f1e949f 100644 --- a/Source/Core/VideoCommon/TextureCacheBase.cpp +++ b/Source/Core/VideoCommon/TextureCacheBase.cpp @@ -122,6 +122,9 @@ void TextureCache::OnConfigChanged(VideoConfig& config) config.bTexFmtOverlayEnable != backup_config.s_texfmt_overlay || config.bTexFmtOverlayCenter != backup_config.s_texfmt_overlay_center || config.bHiresTextures != backup_config.s_hires_textures || + config.iTexScalingFactor != backup_config.s_scaling_factor || + config.iTexScalingType != backup_config.s_scaling_mode || + config.bTexDeposterize != backup_config.s_scaling_deposterize || invalidate_texture_cache_requested) { g_texture_cache->Invalidate(); @@ -155,6 +158,9 @@ void TextureCache::OnConfigChanged(VideoConfig& config) backup_config.s_cache_hires_textures = config.bCacheHiresTextures; backup_config.s_stereo_3d = config.iStereoMode > 0; backup_config.s_efb_mono_depth = config.bStereoEFBMonoDepth; + backup_config.s_scaling_factor = config.iTexScalingFactor; + backup_config.s_scaling_mode = config.iTexScalingType; + backup_config.s_scaling_deposterize = config.bTexDeposterize; } void TextureCache::Cleanup(int _frameCount) @@ -688,6 +694,12 @@ TextureCache::TCacheEntryBase* TextureCache::Load(const u32 stage) config.height = height; config.levels = texLevels; config.pcformat = pcfmt; + if (g_ActiveConfig.iTexScalingType && !hires_tex) + { + config.width *= g_ActiveConfig.iTexScalingFactor; + config.height *= g_ActiveConfig.iTexScalingFactor; + config.pcformat = PC_TEX_FMT_RGBA32; + } TCacheEntryBase* entry = AllocateTexture(config); GFX_DEBUGGER_PAUSE_AT(NEXT_NEW_TEXTURE, true); diff --git a/Source/Core/VideoCommon/TextureCacheBase.h b/Source/Core/VideoCommon/TextureCacheBase.h index 9603c1d2c2..ea94177e1a 100644 --- a/Source/Core/VideoCommon/TextureCacheBase.h +++ b/Source/Core/VideoCommon/TextureCacheBase.h @@ -241,6 +241,9 @@ class TextureCache bool s_cache_hires_textures; bool s_stereo_3d; bool s_efb_mono_depth; + s32 s_scaling_mode; + s32 s_scaling_factor; + bool s_scaling_deposterize; } backup_config; }; diff --git a/Source/Core/VideoCommon/TextureScalerCommon.cpp b/Source/Core/VideoCommon/TextureScalerCommon.cpp new file mode 100644 index 0000000000..559503fceb --- /dev/null +++ b/Source/Core/VideoCommon/TextureScalerCommon.cpp @@ -0,0 +1,627 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#if _MSC_VER == 1700 +// Has to be included before TextureScaler.h, else we get those std::bind errors in VS2012.. +#include "../native/base/basictypes.h" +#endif + +#include +#include +#include + + + +#include "Common/Common.h" +#include "Common/Logging/Log.h" +#include "Common/MsgHandler.h" +#include "Common/CommonFuncs.h" +#include "Common/ThreadPool.h" +#include "Common/CPUDetect.h" +#include "Common/Intrinsics.h" +#include "VideoCommon/VideoConfig.h" +#include "VideoCommon/TextureScalerCommon.h" +#include "xbrz/xbrz.h" + +#if _M_SSE >= 0x401 +#include +#endif + +// Report the time and throughput for each larger scaling operation in the log +//#define SCALING_MEASURE_TIME + +#ifdef SCALING_MEASURE_TIME +#include "native/base/timeutil.h" +#endif + +/////////////////////////////////////// Helper Functions (mostly math for parallelization) + +namespace { +//////////////////////////////////////////////////////////////////// Various image processing + +#define R(_col) ((_col>> 0)&0xFF) +#define G(_col) ((_col>> 8)&0xFF) +#define B(_col) ((_col>>16)&0xFF) +#define A(_col) ((_col>>24)&0xFF) + +#define DISTANCE(_p1,_p2) ( abs(static_cast(static_cast(R(_p1))-R(_p2))) + abs(static_cast(static_cast(G(_p1))-G(_p2))) \ + + abs(static_cast(static_cast(B(_p1))-B(_p2))) + abs(static_cast(static_cast(A(_p1))-A(_p2))) ) + +// this is sadly much faster than an inline function with a loop, at least in VC10 +#define MIX_PIXELS(_p0, _p1, _factors) \ + ( (R(_p0)*(_factors)[0] + R(_p1)*(_factors)[1])/255 << 0 ) | \ + ( (G(_p0)*(_factors)[0] + G(_p1)*(_factors)[1])/255 << 8 ) | \ + ( (B(_p0)*(_factors)[0] + B(_p1)*(_factors)[1])/255 << 16 ) | \ + ( (A(_p0)*(_factors)[0] + A(_p1)*(_factors)[1])/255 << 24 ) + +#define BLOCK_SIZE 32 + +// 3x3 convolution with Neumann boundary conditions, parallelizable +// quite slow, could be sped up a lot +// especially handling of separable kernels +void convolve3x3(u32* data, u32* out, const int kernel[3][3], int width, int height, int l, int u) { + for (int yb = 0; yb < (u - l) / BLOCK_SIZE + 1; ++yb) { + for (int xb = 0; xb < width / BLOCK_SIZE + 1; ++xb) { + for (int y = l + yb*BLOCK_SIZE; y < l + (yb + 1)*BLOCK_SIZE && y < u; ++y) { + for (int x = xb*BLOCK_SIZE; x < (xb + 1)*BLOCK_SIZE && x < width; ++x) { + int val = 0; + for (int yoff = -1; yoff <= 1; ++yoff) { + int yy = std::max(std::min(y + yoff, height - 1), 0); + for (int xoff = -1; xoff <= 1; ++xoff) { + int xx = std::max(std::min(x + xoff, width - 1), 0); + val += data[yy*width + xx] * kernel[yoff + 1][xoff + 1]; + } + } + out[y*width + x] = abs(val); + } + } + } + } +} + +// deposterization: smoothes posterized gradients from low-color-depth (e.g. 444, 565, compressed) sources +void deposterizeH(u32* data, u32* out, int w, int l, int u) { + static const int T = 8; + for (int y = l; y < u; ++y) { + for (int x = 0; x < w; ++x) { + int inpos = y*w + x; + u32 center = data[inpos]; + if (x == 0 || x == w - 1) { + out[y*w + x] = center; + continue; + } + u32 left = data[inpos - 1]; + u32 right = data[inpos + 1]; + out[y*w + x] = 0; + for (int c = 0; c < 4; ++c) { + u8 lc = ((left >> c * 8) & 0xFF); + u8 cc = ((center >> c * 8) & 0xFF); + u8 rc = ((right >> c * 8) & 0xFF); + if ((lc != rc) && ((lc == cc && abs((int)((int)rc) - cc) <= T) || (rc == cc && abs((int)((int)lc) - cc) <= T))) { + // blend this component + out[y*w + x] |= ((rc + lc) / 2) << (c * 8); + } else { + // no change for this component + out[y*w + x] |= cc << (c * 8); + } + } + } + } +} +void deposterizeV(u32* data, u32* out, int w, int h, int l, int u) { + static const int T = 8; + for (int xb = 0; xb < w / BLOCK_SIZE + 1; ++xb) { + for (int y = l; y < u; ++y) { + for (int x = xb*BLOCK_SIZE; x < (xb + 1)*BLOCK_SIZE && x < w; ++x) { + u32 center = data[y * w + x]; + if (y == 0 || y == h - 1) { + out[y*w + x] = center; + continue; + } + u32 upper = data[(y - 1) * w + x]; + u32 lower = data[(y + 1) * w + x]; + out[y*w + x] = 0; + for (int c = 0; c < 4; ++c) { + u8 uc = ((upper >> c * 8) & 0xFF); + u8 cc = ((center >> c * 8) & 0xFF); + u8 lc = ((lower >> c * 8) & 0xFF); + if ((uc != lc) && ((uc == cc && abs((int)((int)lc) - cc) <= T) || (lc == cc && abs((int)((int)uc) - cc) <= T))) { + // blend this component + out[y*w + x] |= ((lc + uc) / 2) << (c * 8); + } else { + // no change for this component + out[y*w + x] |= cc << (c * 8); + } + } + } + } + } +} + +// generates a distance mask value for each pixel in data +// higher values -> larger distance to the surrounding pixels +void generateDistanceMask(u32* data, u32* out, int width, int height, int l, int u) { + for (int yb = 0; yb < (u - l) / BLOCK_SIZE + 1; ++yb) { + for (int xb = 0; xb < width / BLOCK_SIZE + 1; ++xb) { + for (int y = l + yb*BLOCK_SIZE; y < l + (yb + 1)*BLOCK_SIZE && y < u; ++y) { + for (int x = xb*BLOCK_SIZE; x < (xb + 1)*BLOCK_SIZE && x < width; ++x) { + const u32 center = data[y*width + x]; + u32 dist = 0; + for (int yoff = -1; yoff <= 1; ++yoff) { + int yy = y + yoff; + if (yy == height || yy == -1) { + dist += 1200; // assume distance at borders, usually makes for better result + continue; + } + for (int xoff = -1; xoff <= 1; ++xoff) { + if (yoff == 0 && xoff == 0) continue; + int xx = x + xoff; + if (xx == width || xx == -1) { + dist += 400; // assume distance at borders, usually makes for better result + continue; + } + dist += DISTANCE(data[yy*width + xx], center); + } + } + out[y*width + x] = dist; + } + } + } + } +} + +// mix two images based on a mask +void mix(u32* data, u32* source, u32* mask, u32 maskmax, int width, int l, int u) { + for (int y = l; y < u; ++y) { + for (int x = 0; x < width; ++x) { + int pos = y*width + x; + u8 mixFactors[2] = { 0, static_cast((std::min(mask[pos], maskmax) * 255) / maskmax) }; + mixFactors[0] = 255 - mixFactors[1]; + data[pos] = MIX_PIXELS(data[pos], source[pos], mixFactors); + if (A(source[pos]) == 0) data[pos] = data[pos] & 0x00FFFFFF; // xBRZ always does a better job with hard alpha + } + } +} + +//////////////////////////////////////////////////////////////////// Bicubic scaling + +// generate the value of a Mitchell-Netravali scaling spline at distance d, with parameters A and B +// B=1 C=0 : cubic B spline (very smooth) +// B=C=1/3 : recommended for general upscaling +// B=0 C=1/2 : Catmull-Rom spline (sharp, ringing) +// see Mitchell & Netravali, "Reconstruction Filters in Computer Graphics" +inline float mitchell(float x, float B, float C) { + float ax = fabs(x); + if (ax >= 2.0f) return 0.0f; + if (ax >= 1.0f) return ((-B - 6 * C)*(x*x*x) + (6 * B + 30 * C)*(x*x) + (-12 * B - 48 * C)*x + (8 * B + 24 * C)) / 6.0f; + return ((12 - 9 * B - 6 * C)*(x*x*x) + (-18 + 12 * B + 6 * C)*(x*x) + (6 - 2 * B)) / 6.0f; +} + +// arrays for pre-calculating weights and sums (~20KB) +// Dimensions: +// 0: 0 = BSpline, 1 = mitchell +// 2: 2-5x scaling +// 2,3: 5x5 generated pixels +// 4,5: 5x5 pixels sampled from +float bicubicWeights[2][4][5][5][5][5]; +float bicubicInvSums[2][4][5][5]; + +// initialize pre-computed weights array +void initBicubicWeights() { + float B[2] = { 1.0f, 0.334f }; + float C[2] = { 0.0f, 0.334f }; + for (int type = 0; type < 2; ++type) { + for (int factor = 2; factor <= 5; ++factor) { + for (int x = 0; x < factor; ++x) { + for (int y = 0; y < factor; ++y) { + float sum = 0.0f; + for (int sx = -2; sx <= 2; ++sx) { + for (int sy = -2; sy <= 2; ++sy) { + float dx = (x + 0.5f) / factor - (sx + 0.5f); + float dy = (y + 0.5f) / factor - (sy + 0.5f); + float dist = sqrt(dx*dx + dy*dy); + float weight = mitchell(dist, B[type], C[type]); + bicubicWeights[type][factor - 2][x][y][sx + 2][sy + 2] = weight; + sum += weight; + } + } + bicubicInvSums[type][factor - 2][x][y] = 1.0f / sum; + } + } + } + } +} + +// perform bicubic scaling by factor f, with precomputed spline type T +template +void scaleBicubicT(u32* data, u32* out, int w, int h, int l, int u) { + int outw = w*f; + for (int yb = 0; yb < (u - l)*f / BLOCK_SIZE + 1; ++yb) { + for (int xb = 0; xb < w*f / BLOCK_SIZE + 1; ++xb) { + for (int y = l*f + yb*BLOCK_SIZE; y < l*f + (yb + 1)*BLOCK_SIZE && y < u*f; ++y) { + for (int x = xb*BLOCK_SIZE; x < (xb + 1)*BLOCK_SIZE && x < w*f; ++x) { + float r = 0.0f, g = 0.0f, b = 0.0f, a = 0.0f; + int cx = x / f, cy = y / f; + // sample supporting pixels in original image + for (int sx = -2; sx <= 2; ++sx) { + for (int sy = -2; sy <= 2; ++sy) { + float weight = bicubicWeights[T][f - 2][x%f][y%f][sx + 2][sy + 2]; + if (weight != 0.0f) { + // clamp pixel locations + int csy = std::max(std::min(sy + cy, h - 1), 0); + int csx = std::max(std::min(sx + cx, w - 1), 0); + // sample & add weighted components + u32 sample = data[csy*w + csx]; + r += weight*R(sample); + g += weight*G(sample); + b += weight*B(sample); + a += weight*A(sample); + } + } + } + // generate and write result + float invSum = bicubicInvSums[T][f - 2][x%f][y%f]; + int ri = std::min(std::max(static_cast(ceilf(r*invSum)), 0), 255); + int gi = std::min(std::max(static_cast(ceilf(g*invSum)), 0), 255); + int bi = std::min(std::max(static_cast(ceilf(b*invSum)), 0), 255); + int ai = std::min(std::max(static_cast(ceilf(a*invSum)), 0), 255); + out[y*outw + x] = (ai << 24) | (bi << 16) | (gi << 8) | ri; + } + } + } + } +} +#if _M_SSE >= 0x401 +template +void scaleBicubicTSSE41(u32* data, u32* out, int w, int h, int l, int u) { + int outw = w*f; + for (int yb = 0; yb < (u - l)*f / BLOCK_SIZE + 1; ++yb) { + for (int xb = 0; xb < w*f / BLOCK_SIZE + 1; ++xb) { + for (int y = l*f + yb*BLOCK_SIZE; y < l*f + (yb + 1)*BLOCK_SIZE && y < u*f; ++y) { + for (int x = xb*BLOCK_SIZE; x < (xb + 1)*BLOCK_SIZE && x < w*f; ++x) { + __m128 result = _mm_set1_ps(0.0f); + int cx = x / f, cy = y / f; + // sample supporting pixels in original image + for (int sx = -2; sx <= 2; ++sx) { + for (int sy = -2; sy <= 2; ++sy) { + float weight = bicubicWeights[T][f - 2][x%f][y%f][sx + 2][sy + 2]; + if (weight != 0.0f) { + // clamp pixel locations + int csy = std::max(std::min(sy + cy, h - 1), 0); + int csx = std::max(std::min(sx + cx, w - 1), 0); + // sample & add weighted components + __m128i sample = _mm_cvtsi32_si128(data[csy*w + csx]); + sample = _mm_cvtepu8_epi32(sample); + __m128 col = _mm_cvtepi32_ps(sample); + col = _mm_mul_ps(col, _mm_set1_ps(weight)); + result = _mm_add_ps(result, col); + } + } + } + // generate and write result + __m128i pixel = _mm_cvtps_epi32(_mm_mul_ps(result, _mm_set1_ps(bicubicInvSums[T][f - 2][x%f][y%f]))); + pixel = _mm_packs_epi32(pixel, pixel); + pixel = _mm_packus_epi16(pixel, pixel); + out[y*outw + x] = _mm_cvtsi128_si32(pixel); + } + } + } + } +} +#endif + +void scaleBicubicBSpline(int factor, u32* data, u32* out, int w, int h, int l, int u) { +#if _M_SSE >= 0x401 + if (cpu_info.bSSE4_1) { + switch (factor) { + case 2: scaleBicubicTSSE41<2, 0>(data, out, w, h, l, u); break; // when I first tested this, + case 3: scaleBicubicTSSE41<3, 0>(data, out, w, h, l, u); break; // it was even slower than I had expected + case 4: scaleBicubicTSSE41<4, 0>(data, out, w, h, l, u); break; // turns out I had not included + case 5: scaleBicubicTSSE41<5, 0>(data, out, w, h, l, u); break; // any of these break statements + default: ERROR_LOG(VIDEO, "Bicubic upsampling only implemented for factors 2 to 5"); + } + } else { +#endif + switch (factor) { + case 2: scaleBicubicT<2, 0>(data, out, w, h, l, u); break; // when I first tested this, + case 3: scaleBicubicT<3, 0>(data, out, w, h, l, u); break; // it was even slower than I had expected + case 4: scaleBicubicT<4, 0>(data, out, w, h, l, u); break; // turns out I had not included + case 5: scaleBicubicT<5, 0>(data, out, w, h, l, u); break; // any of these break statements + default: ERROR_LOG(VIDEO, "Bicubic upsampling only implemented for factors 2 to 5"); + } +#if _M_SSE >= 0x401 + } +#endif +} + +void scaleBicubicMitchell(int factor, u32* data, u32* out, int w, int h, int l, int u) { +#if _M_SSE >= 0x401 + if (cpu_info.bSSE4_1) { + switch (factor) { + case 2: scaleBicubicTSSE41<2, 1>(data, out, w, h, l, u); break; + case 3: scaleBicubicTSSE41<3, 1>(data, out, w, h, l, u); break; + case 4: scaleBicubicTSSE41<4, 1>(data, out, w, h, l, u); break; + case 5: scaleBicubicTSSE41<5, 1>(data, out, w, h, l, u); break; + default: ERROR_LOG(VIDEO, "Bicubic upsampling only implemented for factors 2 to 5"); + } + } else { +#endif + switch (factor) { + case 2: scaleBicubicT<2, 1>(data, out, w, h, l, u); break; + case 3: scaleBicubicT<3, 1>(data, out, w, h, l, u); break; + case 4: scaleBicubicT<4, 1>(data, out, w, h, l, u); break; + case 5: scaleBicubicT<5, 1>(data, out, w, h, l, u); break; + default: ERROR_LOG(VIDEO, "Bicubic upsampling only implemented for factors 2 to 5"); + } +#if _M_SSE >= 0x401 + } +#endif +} + +//////////////////////////////////////////////////////////////////// Bilinear scaling + +const static u8 BILINEAR_FACTORS[4][3][2] = { + { { 44, 211 }, { 0, 0 }, { 0, 0 } }, // x2 + { { 64, 191 }, { 0, 255 }, { 0, 0 } }, // x3 + { { 77, 178 }, { 26, 229 }, { 0, 0 } }, // x4 + { { 102, 153 }, { 51, 204 }, { 0, 255 } }, // x5 +}; +// integral bilinear upscaling by factor f, horizontal part +template +void bilinearHt(u32* data, u32* out, int w, int l, int u) { + static_assert(f > 1 && f <= 5, "Bilinear scaling only implemented for factors 2 to 5"); + int outw = w*f; + for (int y = l; y < u; ++y) { + for (int x = 0; x < w; ++x) { + int inpos = y*w + x; + u32 left = data[inpos - (x == 0 ? 0 : 1)]; + u32 center = data[inpos]; + u32 right = data[inpos + (x == w - 1 ? 0 : 1)]; + int i = 0; + for (; i < f / 2 + f % 2; ++i) { // first half of the new pixels + center, hope the compiler unrolls this + out[y*outw + x*f + i] = MIX_PIXELS(left, center, BILINEAR_FACTORS[f - 2][i]); + } + for (; i < f; ++i) { // second half of the new pixels, hope the compiler unrolls this + out[y*outw + x*f + i] = MIX_PIXELS(right, center, BILINEAR_FACTORS[f - 2][f - 1 - i]); + } + } + } +} +void bilinearH(int factor, u32* data, u32* out, int w, int l, int u) { + switch (factor) { + case 2: bilinearHt<2>(data, out, w, l, u); break; + case 3: bilinearHt<3>(data, out, w, l, u); break; + case 4: bilinearHt<4>(data, out, w, l, u); break; + case 5: bilinearHt<5>(data, out, w, l, u); break; + default: ERROR_LOG(VIDEO, "Bilinear upsampling only implemented for factors 2 to 5"); + } +} +// integral bilinear upscaling by factor f, vertical part +// gl/gu == global lower and upper bound +template +void bilinearVt(u32* data, u32* out, int w, int gl, int gu, int l, int u) { + static_assert(f>1 && f <= 5, "Bilinear scaling only implemented for 2x, 3x, 4x, and 5x"); + int outw = w*f; + for (int xb = 0; xb < outw / BLOCK_SIZE + 1; ++xb) { + for (int y = l; y < u; ++y) { + u32 uy = y - (y == gl ? 0 : 1); + u32 ly = y + (y == gu - 1 ? 0 : 1); + for (int x = xb*BLOCK_SIZE; x < (xb + 1)*BLOCK_SIZE && x < outw; ++x) { + u32 upper = data[uy * outw + x]; + u32 center = data[y * outw + x]; + u32 lower = data[ly * outw + x]; + int i = 0; + for (; i < f / 2 + f % 2; ++i) { // first half of the new pixels + center, hope the compiler unrolls this + out[(y*f + i)*outw + x] = MIX_PIXELS(upper, center, BILINEAR_FACTORS[f - 2][i]); + } + for (; i < f; ++i) { // second half of the new pixels, hope the compiler unrolls this + out[(y*f + i)*outw + x] = MIX_PIXELS(lower, center, BILINEAR_FACTORS[f - 2][f - 1 - i]); + } + } + } + } +} +void bilinearV(int factor, u32* data, u32* out, int w, int gl, int gu, int l, int u) { + switch (factor) { + case 2: bilinearVt<2>(data, out, w, gl, gu, l, u); break; + case 3: bilinearVt<3>(data, out, w, gl, gu, l, u); break; + case 4: bilinearVt<4>(data, out, w, gl, gu, l, u); break; + case 5: bilinearVt<5>(data, out, w, gl, gu, l, u); break; + default: ERROR_LOG(VIDEO, "Bilinear upsampling only implemented for factors 2 to 5"); + } +} + +#undef BLOCK_SIZE +#undef MIX_PIXELS +#undef DISTANCE +#undef R +#undef G +#undef B +#undef A + +// used for debugging texture scaling (writing textures to files) +static int g_imgCount = 0; +void dbgPPM(int w, int h, u8* pixels, const char* prefix = "dbg") { // 3 component RGB + char fn[32]; + snprintf(fn, 32, "%s%04d.ppm", prefix, g_imgCount++); + FILE *fp = fopen(fn, "wb"); + fprintf(fp, "P6\n%d %d\n255\n", w, h); + for (int j = 0; j < h; ++j) { + for (int i = 0; i < w; ++i) { + static unsigned char color[3]; + color[0] = pixels[(j*w + i) * 4 + 0]; /* red */ + color[1] = pixels[(j*w + i) * 4 + 1]; /* green */ + color[2] = pixels[(j*w + i) * 4 + 2]; /* blue */ + fwrite(color, 1, 3, fp); + } + } + fclose(fp); +} +void dbgPGM(int w, int h, u32* pixels, const char* prefix = "dbg") { // 1 component + char fn[32]; + snprintf(fn, 32, "%s%04d.pgm", prefix, g_imgCount++); + FILE *fp = fopen(fn, "wb"); + fprintf(fp, "P5\n%d %d\n65536\n", w, h); + for (int j = 0; j < h; ++j) { + for (int i = 0; i < w; ++i) { + fwrite((pixels + (j*w + i)), 1, 2, fp); + } + } + fclose(fp); +} +} + +/////////////////////////////////////// Texture Scaler + +TextureScaler::TextureScaler() { + initBicubicWeights(); +} + +TextureScaler::~TextureScaler() { + xbrz::shutdown(); +} + +bool TextureScaler::IsEmptyOrFlat(u32* data, int pixels) { + u32 ref = data[0]; + for (int i = 0; i < pixels; ++i) { + if (data[i] != ref) return false; + } + return true; +} + +u32* TextureScaler::Scale(u32* data, int width, int height) { + // prevent processing empty or flat textures (this happens a lot in some games) + // doesn't hurt the standard case, will be very quick for textures with actual texture + /*if (IsEmptyOrFlat(data, width*height)) { + INFO_LOG(VIDEO, "TextureScaler: early exit -- empty/flat texture"); + return nullptr; + }*/ + +#ifdef SCALING_MEASURE_TIME + double t_start = real_time_now(); +#endif + int factor = g_ActiveConfig.iTexScalingFactor; + //bufInput.resize(width*height); // used to store the input image image if it needs to be reformatted + bufOutput.resize(width*height*factor*factor); // used to store the upscaled image + u32 *inputBuf = data; + u32 *outputBuf = bufOutput.data(); + + // deposterize + if (g_ActiveConfig.bTexDeposterize) { + bufDeposter.resize(width*height); + DePosterize(inputBuf, bufDeposter.data(), width, height); + inputBuf = bufDeposter.data(); + } + + // scale + switch (g_ActiveConfig.iTexScalingType) { + case XBRZ: + ScaleXBRZ(factor, inputBuf, outputBuf, width, height); + break; + case HYBRID: + ScaleHybrid(factor, inputBuf, outputBuf, width, height); + break; + case BICUBIC: + ScaleBicubicMitchell(factor, inputBuf, outputBuf, width, height); + break; + case HYBRID_BICUBIC: + ScaleHybrid(factor, inputBuf, outputBuf, width, height, true); + break; + default: + ERROR_LOG(VIDEO, "Unknown scaling type: %d", g_ActiveConfig.iTexScalingType); + } +#ifdef SCALING_MEASURE_TIME + if (width*height > 64 * 64 * factor*factor) { + double t = real_time_now() - t_start; + NOTICE_LOG(MASTER_LOG, "TextureScaler: processed %9d pixels in %6.5lf seconds. (%9.2lf Mpixels/second)", + width*height, t, (width*height) / (t * 1000 * 1000)); + } +#endif + return outputBuf; +} + +void TextureScaler::ScaleXBRZ(int factor, u32* source, u32* dest, int width, int height) { + xbrz::ScalerCfg cfg; + xbrz::init(); + //GlobalThreadPool::Loop(std::bind(&xbrz::scale, factor, source, dest, width, height, xbrz::ColorFormat::ARGB, cfg, placeholder::_1, placeholder::_2), 0, height); + xbrz::scale(factor, source, dest, width, height, xbrz::ColorFormat::ARGB, cfg, 0, height); +} + +void TextureScaler::ScaleBilinear(int factor, u32* source, u32* dest, int width, int height) { + bufTmp1.resize(width*height*factor); + u32 *tmpBuf = bufTmp1.data(); + //GlobalThreadPool::Loop(std::bind(&bilinearH, factor, source, tmpBuf, width, placeholder::_1, placeholder::_2), 0, height); + //GlobalThreadPool::Loop(std::bind(&bilinearV, factor, tmpBuf, dest, width, 0, height, placeholder::_1, placeholder::_2), 0, height); + bilinearH(factor, source, tmpBuf, width, 0, height); + bilinearV(factor, tmpBuf, dest, width, 0, height, 0, height); +} + +void TextureScaler::ScaleBicubicBSpline(int factor, u32* source, u32* dest, int width, int height) { + //GlobalThreadPool::Loop(std::bind(&scaleBicubicBSpline, factor, source, dest, width, height, placeholder::_1, placeholder::_2), 0, height); + scaleBicubicBSpline( factor, source, dest, width, height, 0, height); +} + +void TextureScaler::ScaleBicubicMitchell(int factor, u32* source, u32* dest, int width, int height) { + //GlobalThreadPool::Loop(std::bind(&scaleBicubicMitchell, factor, source, dest, width, height, placeholder::_1, placeholder::_2), 0, height); + scaleBicubicMitchell( factor, source, dest, width, height, 0, height); +} + +void TextureScaler::ScaleHybrid(int factor, u32* source, u32* dest, int width, int height, bool bicubic) { + // Basic algorithm: + // 1) determine a feature mask C based on a sobel-ish filter + splatting, and upscale that mask bilinearly + // 2) generate 2 scaled images: A - using Bilinear filtering, B - using xBRZ + // 3) output = A*C + B*(1-C) + + const static int KERNEL_SPLAT[3][3] = { + { 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 } + }; + + bufTmp1.resize(width*height); + bufTmp2.resize(width*height*factor*factor); + bufTmp3.resize(width*height*factor*factor); + //GlobalThreadPool::Loop(std::bind(&generateDistanceMask, source, bufTmp1.data(), width, height, placeholder::_1, placeholder::_2), 0, height); + //GlobalThreadPool::Loop(std::bind(&convolve3x3, bufTmp1.data(), bufTmp2.data(), KERNEL_SPLAT, width, height, placeholder::_1, placeholder::_2), 0, height); + generateDistanceMask(source, bufTmp1.data(), width, height, 0, height); + convolve3x3( bufTmp1.data(), bufTmp2.data(), KERNEL_SPLAT, width, height, 0, height); + + ScaleBilinear(factor, bufTmp2.data(), bufTmp3.data(), width, height); + // mask C is now in bufTmp3 + + ScaleXBRZ(factor, source, bufTmp2.data(), width, height); + // xBRZ upscaled source is in bufTmp2 + + if (bicubic) ScaleBicubicBSpline(factor, source, dest, width, height); + else ScaleBilinear(factor, source, dest, width, height); + // Upscaled source is in dest + + // Now we can mix it all together + // The factor 8192 was found through practical testing on a variety of textures + //GlobalThreadPool::Loop(std::bind(&mix, dest, bufTmp2.data(), bufTmp3.data(), 8192, width*factor, placeholder::_1, placeholder::_2), 0, height*factor); + mix(dest, bufTmp2.data(), bufTmp3.data(), 8192, width*factor, 0, height*factor); +} + +void TextureScaler::DePosterize(u32* source, u32* dest, int width, int height) { + bufTmp3.resize(width*height); + //GlobalThreadPool::Loop(std::bind(&deposterizeH, source, bufTmp3.data(), width, placeholder::_1, placeholder::_2), 0, height); + //GlobalThreadPool::Loop(std::bind(&deposterizeV, bufTmp3.data(), dest, width, height, placeholder::_1, placeholder::_2), 0, height); + //GlobalThreadPool::Loop(std::bind(&deposterizeH, dest, bufTmp3.data(), width, placeholder::_1, placeholder::_2), 0, height); + //GlobalThreadPool::Loop(std::bind(&deposterizeV, bufTmp3.data(), dest, width, height, placeholder::_1, placeholder::_2), 0, height); + deposterizeH(source, bufTmp3.data(), width, 0, height); + deposterizeV(bufTmp3.data(), dest, width, height, 0, height); + deposterizeH(dest, bufTmp3.data(), width, 0, height); + deposterizeV(bufTmp3.data(), dest, width, height, 0, height); +} diff --git a/Source/Core/VideoCommon/TextureScalerCommon.h b/Source/Core/VideoCommon/TextureScalerCommon.h new file mode 100644 index 0000000000..6b5d289093 --- /dev/null +++ b/Source/Core/VideoCommon/TextureScalerCommon.h @@ -0,0 +1,50 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include "Common/CommonTypes.h" +#include "Common/MemoryUtil.h" + +#include + +class TextureScaler { +public: + TextureScaler(); + ~TextureScaler(); + + u32* Scale(u32* data, int width, int height); + + enum { NONE = 0, XBRZ = 1, HYBRID = 2, BICUBIC = 3, HYBRID_BICUBIC = 4 }; + +private: + + void ScaleXBRZ(int factor, u32* source, u32* dest, int width, int height); + void ScaleBilinear(int factor, u32* source, u32* dest, int width, int height); + void ScaleBicubicBSpline(int factor, u32* source, u32* dest, int width, int height); + void ScaleBicubicMitchell(int factor, u32* source, u32* dest, int width, int height); + void ScaleHybrid(int factor, u32* source, u32* dest, int width, int height, bool bicubic = false); + + void DePosterize(u32* source, u32* dest, int width, int height); + + bool IsEmptyOrFlat(u32* data, int pixels); + + // depending on the factor and texture sizes, these can get pretty large + // maximum is (100 MB total for a 512 by 512 texture with scaling factor 5 and hybrid scaling) + // of course, scaling factor 5 is totally silly anyway + SimpleBuf bufInput, bufDeposter, bufOutput, bufTmp1, bufTmp2, bufTmp3; +}; diff --git a/Source/Core/VideoCommon/VideoCommon.vcxproj b/Source/Core/VideoCommon/VideoCommon.vcxproj index 1e825509dc..2d7c4b12ea 100644 --- a/Source/Core/VideoCommon/VideoCommon.vcxproj +++ b/Source/Core/VideoCommon/VideoCommon.vcxproj @@ -97,6 +97,7 @@ + @@ -179,6 +180,7 @@ + @@ -220,6 +222,9 @@ {677EA016-1182-440C-9345-DC88D1E98C0C} + + {869ea016-4458-2a45-25af-dc44d6e98c0a} + diff --git a/Source/Core/VideoCommon/VideoCommon.vcxproj.filters b/Source/Core/VideoCommon/VideoCommon.vcxproj.filters index 528e9ed16c..0aab95883a 100644 --- a/Source/Core/VideoCommon/VideoCommon.vcxproj.filters +++ b/Source/Core/VideoCommon/VideoCommon.vcxproj.filters @@ -217,6 +217,9 @@ Shader Managers + + Util + @@ -463,6 +466,9 @@ Shader Managers + + Util + diff --git a/Source/Core/VideoCommon/VideoConfig.cpp b/Source/Core/VideoCommon/VideoConfig.cpp index f790bd0cb9..93578697cb 100644 --- a/Source/Core/VideoCommon/VideoConfig.cpp +++ b/Source/Core/VideoCommon/VideoConfig.cpp @@ -50,6 +50,9 @@ VideoConfig::VideoConfig() iStereoDepthPercentage = 100; iStereoConvergenceMinimum = 0; bUseScalingFilter = false; + bTexDeposterize = false; + iTexScalingType = 0; + iTexScalingFactor = 2; } void VideoConfig::Load(const std::string& ini_file) @@ -105,6 +108,9 @@ void VideoConfig::Load(const std::string& ini_file) enhancements->Get("StereoConvergence", &iStereoConvergence, 20); enhancements->Get("StereoSwapEyes", &bStereoSwapEyes, false); enhancements->Get("UseScalingFilter", &bUseScalingFilter, false); + enhancements->Get("TextureScalingType", &iTexScalingType, 0); + enhancements->Get("TextureScalingFactor", &iTexScalingFactor, 2); + enhancements->Get("UseDePosterize", &bTexDeposterize, false); IniFile::Section* hacks = iniFile.GetOrCreateSection("Hacks"); hacks->Get("EFBAccessEnable", &bEFBAccessEnable, true); @@ -214,6 +220,9 @@ void VideoConfig::GameIniLoad() CHECK_SETTING("Video_Enhancements", "StereoConvergence", iStereoConvergence); CHECK_SETTING("Video_Enhancements", "StereoSwapEyes", bStereoSwapEyes); CHECK_SETTING("Video_Enhancements", "UseScalingFilter", bUseScalingFilter); + CHECK_SETTING("Video_Enhancements", "TextureScalingType", iTexScalingType); + CHECK_SETTING("Video_Enhancements", "TextureScalingFactor", iTexScalingFactor); + CHECK_SETTING("Video_Enhancements", "UseDePosterize", bTexDeposterize); CHECK_SETTING("Video_Stereoscopy", "StereoEFBMonoDepth", bStereoEFBMonoDepth); CHECK_SETTING("Video_Stereoscopy", "StereoDepthPercentage", iStereoDepthPercentage); @@ -278,6 +287,22 @@ void VideoConfig::VerifyValidity() { iBBoxMode = BBoxCPU; } + if (iTexScalingFactor < 2) + { + iTexScalingFactor = 2; + } + else if (iTexScalingFactor > 5) + { + iTexScalingFactor = 5; + } + if (iTexScalingType < 0) + { + iTexScalingType = 0; + } + else if(iTexScalingType > 4) + { + iTexScalingType = 4; + } } @@ -334,6 +359,9 @@ void VideoConfig::Save(const std::string& ini_file) enhancements->Set("StereoConvergence", iStereoConvergence); enhancements->Set("StereoSwapEyes", bStereoSwapEyes); enhancements->Set("UseScalingFilter", bUseScalingFilter); + enhancements->Set("TextureScalingType", iTexScalingType); + enhancements->Set("TextureScalingFactor", iTexScalingFactor); + enhancements->Set("UseDePosterize", bTexDeposterize); IniFile::Section* hacks = iniFile.GetOrCreateSection("Hacks"); diff --git a/Source/Core/VideoCommon/VideoConfig.h b/Source/Core/VideoCommon/VideoConfig.h index e106e608d8..db42642713 100644 --- a/Source/Core/VideoCommon/VideoConfig.h +++ b/Source/Core/VideoCommon/VideoConfig.h @@ -106,6 +106,9 @@ struct VideoConfig final int iStereoConvergence; bool bStereoSwapEyes; bool bUseScalingFilter; + bool bTexDeposterize; + int iTexScalingType; + int iTexScalingFactor; // Information bool bShowFPS; diff --git a/Source/Dolphin.sln b/Source/Dolphin.sln index b5e2de0cfe..e2ab29822e 100644 --- a/Source/Dolphin.sln +++ b/Source/Dolphin.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2013 -VisualStudioVersion = 12.0.31101.0 +VisualStudioVersion = 12.0.40629.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dolphin", "Core\DolphinWX\Dolphin.vcxproj", "{1B099EF8-6F87-47A2-A3E7-898A24584F49}" ProjectSection(ProjectDependencies) = postProject @@ -138,6 +138,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UICommon", "Core\UICommon\U EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "enet", "..\Externals\enet\enet.vcxproj", "{CBC76802-C128-4B17-BF6C-23B08C313E5E}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "xbrz", "..\Externals\xbrz\xbrz.vcxproj", "{869EA016-4458-2A45-25AF-DC44D6E98C0A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Win32 = Debug|Win32 @@ -356,6 +358,12 @@ Global {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Release|Win32.ActiveCfg = Release|x64 {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Release|x64.ActiveCfg = Release|x64 {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Release|x64.Build.0 = Release|x64 + {869EA016-4458-2A45-25AF-DC44D6E98C0A}.Debug|Win32.ActiveCfg = Debug|x64 + {869EA016-4458-2A45-25AF-DC44D6E98C0A}.Debug|x64.ActiveCfg = Debug|x64 + {869EA016-4458-2A45-25AF-DC44D6E98C0A}.Debug|x64.Build.0 = Debug|x64 + {869EA016-4458-2A45-25AF-DC44D6E98C0A}.Release|Win32.ActiveCfg = Release|x64 + {869EA016-4458-2A45-25AF-DC44D6E98C0A}.Release|x64.ActiveCfg = Release|x64 + {869EA016-4458-2A45-25AF-DC44D6E98C0A}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -387,5 +395,6 @@ Global {3ECEBBE7-1A0B-4056-99F4-0C0848DA8494} = {701E4F10-DED0-4ECB-A486-91188247EDBD} {604C8368-F34A-4D55-82C8-CC92A0C13254} = {701E4F10-DED0-4ECB-A486-91188247EDBD} {CBC76802-C128-4B17-BF6C-23B08C313E5E} = {39DB5AF5-003D-412B-8FF1-FB195541DB7A} + {869EA016-4458-2A45-25AF-DC44D6E98C0A} = {39DB5AF5-003D-412B-8FF1-FB195541DB7A} EndGlobalSection EndGlobal