From a7d3489dc98760509e6258c055f9a017cfcfc592 Mon Sep 17 00:00:00 2001
From: Marshall Mohror <mohror64@gmail.com>
Date: Mon, 16 Mar 2020 09:42:05 -0500
Subject: [PATCH] video_core: add texture filtering (#5017)

video_core: add texture filtering
---
 src/citra/config.cpp                          |   4 +
 src/citra/default_ini.h                       |   4 +
 src/citra_qt/configuration/config.cpp         |  13 +
 .../configuration/configure_enhancements.cpp  |  30 +++
 .../configuration/configure_enhancements.h    |   1 +
 .../configuration/configure_enhancements.ui   |  50 ++++
 src/core/settings.cpp                         |   6 +
 src/core/settings.h                           |   2 +
 src/video_core/CMakeLists.txt                 |  38 +++
 src/video_core/generate_shaders.cmake         |  16 ++
 .../renderer_opengl/gl_rasterizer_cache.cpp   | 118 ++++-----
 .../renderer_opengl/gl_rasterizer_cache.h     |  19 +-
 src/video_core/renderer_opengl/gl_state.h     |  14 +-
 .../renderer_opengl/renderer_opengl.cpp       |   7 +-
 .../anime4k/anime4k_ultrafast.cpp             | 138 ++++++++++
 .../anime4k/anime4k_ultrafast.h               |  45 ++++
 .../texture_filters/anime4k/refine.frag       | 117 +++++++++
 .../texture_filters/anime4k/refine.vert       |  14 +
 .../texture_filters/anime4k/x_gradient.frag   |  18 ++
 .../texture_filters/anime4k/y_gradient.frag   |  16 ++
 .../texture_filters/anime4k/y_gradient.vert   |  12 +
 .../texture_filters/bicubic/bicubic.cpp       |  56 ++++
 .../texture_filters/bicubic/bicubic.frag      |  52 ++++
 .../texture_filters/bicubic/bicubic.h         |  32 +++
 .../texture_filters/tex_coord.vert            |  10 +
 .../texture_filter_interface.h                |  38 +++
 .../texture_filter_manager.cpp                |  89 +++++++
 .../texture_filters/texture_filter_manager.h  |  55 ++++
 .../texture_filters/xbrz/xbrz_freescale.cpp   | 100 ++++++++
 .../texture_filters/xbrz/xbrz_freescale.frag  | 242 ++++++++++++++++++
 .../texture_filters/xbrz/xbrz_freescale.h     |  33 +++
 .../texture_filters/xbrz/xbrz_freescale.vert  |  17 ++
 src/video_core/video_core.cpp                 |   1 +
 33 files changed, 1337 insertions(+), 70 deletions(-)
 create mode 100644 src/video_core/generate_shaders.cmake
 create mode 100644 src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.cpp
 create mode 100644 src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h
 create mode 100644 src/video_core/renderer_opengl/texture_filters/anime4k/refine.frag
 create mode 100644 src/video_core/renderer_opengl/texture_filters/anime4k/refine.vert
 create mode 100644 src/video_core/renderer_opengl/texture_filters/anime4k/x_gradient.frag
 create mode 100644 src/video_core/renderer_opengl/texture_filters/anime4k/y_gradient.frag
 create mode 100644 src/video_core/renderer_opengl/texture_filters/anime4k/y_gradient.vert
 create mode 100644 src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.cpp
 create mode 100644 src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.frag
 create mode 100644 src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.h
 create mode 100644 src/video_core/renderer_opengl/texture_filters/tex_coord.vert
 create mode 100644 src/video_core/renderer_opengl/texture_filters/texture_filter_interface.h
 create mode 100644 src/video_core/renderer_opengl/texture_filters/texture_filter_manager.cpp
 create mode 100644 src/video_core/renderer_opengl/texture_filters/texture_filter_manager.h
 create mode 100644 src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp
 create mode 100644 src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.frag
 create mode 100644 src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h
 create mode 100644 src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.vert

diff --git a/src/citra/config.cpp b/src/citra/config.cpp
index b2c878ddf..e47820e18 100644
--- a/src/citra/config.cpp
+++ b/src/citra/config.cpp
@@ -128,6 +128,10 @@ void Config::ReadValues() {
         static_cast<u16>(sdl2_config->GetInteger("Renderer", "frame_limit", 100));
     Settings::values.use_vsync_new =
         static_cast<u16>(sdl2_config->GetInteger("Renderer", "use_vsync_new", 1));
+    Settings::values.texture_filter_name =
+        sdl2_config->GetString("Renderer", "texture_filter_name", "none");
+    Settings::values.texture_filter_factor =
+        sdl2_config->GetInteger("Renderer", "texture_filter_factor", 1);
 
     Settings::values.render_3d = static_cast<Settings::StereoRenderOption>(
         sdl2_config->GetInteger("Renderer", "render_3d", 0));
diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h
index 9c441e354..c3ffb37d3 100644
--- a/src/citra/default_ini.h
+++ b/src/citra/default_ini.h
@@ -126,6 +126,10 @@ use_disk_shader_cache =
 # factor for the 3DS resolution
 resolution_factor =
 
+# Texture filter name and scale factor
+texture_filter_name =
+texture_filter_factor =
+
 # Turns on the frame limiter, which will limit frames output to the target game speed
 # 0: Off, 1: On (default)
 use_frame_limit =
diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp
index 82274bff0..1c0de30ba 100644
--- a/src/citra_qt/configuration/config.cpp
+++ b/src/citra_qt/configuration/config.cpp
@@ -448,6 +448,13 @@ void Config::ReadRendererValues() {
     Settings::values.bg_green = ReadSetting(QStringLiteral("bg_green"), 0.0).toFloat();
     Settings::values.bg_blue = ReadSetting(QStringLiteral("bg_blue"), 0.0).toFloat();
 
+    Settings::values.texture_filter_name =
+        ReadSetting(QStringLiteral("texture_filter_name"), QStringLiteral("none"))
+            .toString()
+            .toStdString();
+    Settings::values.texture_filter_factor =
+        ReadSetting(QStringLiteral("texture_filter_factor"), 1).toInt();
+
     qt_config->endGroup();
 }
 
@@ -879,6 +886,12 @@ void Config::SaveRendererValues() {
     WriteSetting(QStringLiteral("bg_green"), (double)Settings::values.bg_green, 0.0);
     WriteSetting(QStringLiteral("bg_blue"), (double)Settings::values.bg_blue, 0.0);
 
+    WriteSetting(QStringLiteral("texture_filter_name"),
+                 QString::fromStdString(Settings::values.texture_filter_name),
+                 QStringLiteral("none"));
+    WriteSetting(QStringLiteral("texture_filter_factor"), Settings::values.texture_filter_factor,
+                 1);
+
     qt_config->endGroup();
 }
 
diff --git a/src/citra_qt/configuration/configure_enhancements.cpp b/src/citra_qt/configuration/configure_enhancements.cpp
index 6c83018a3..e1c6b6bdd 100644
--- a/src/citra_qt/configuration/configure_enhancements.cpp
+++ b/src/citra_qt/configuration/configure_enhancements.cpp
@@ -8,10 +8,18 @@
 #include "core/settings.h"
 #include "ui_configure_enhancements.h"
 #include "video_core/renderer_opengl/post_processing_opengl.h"
+#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h"
 
 ConfigureEnhancements::ConfigureEnhancements(QWidget* parent)
     : QWidget(parent), ui(new Ui::ConfigureEnhancements) {
     ui->setupUi(this);
+
+    for (const auto& filter : OpenGL::TextureFilterManager::TextureFilterMap())
+        ui->texture_filter_combobox->addItem(QString::fromStdString(filter.first.data()));
+
+    connect(ui->texture_filter_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
+            &ConfigureEnhancements::updateTextureFilter);
+
     SetConfiguration();
 
     ui->layoutBox->setEnabled(!Settings::values.custom_layout);
@@ -52,6 +60,15 @@ void ConfigureEnhancements::SetConfiguration() {
     ui->factor_3d->setValue(Settings::values.factor_3d);
     updateShaders(Settings::values.render_3d);
     ui->toggle_linear_filter->setChecked(Settings::values.filter_mode);
+    ui->texture_scale_spinbox->setValue(Settings::values.texture_filter_factor);
+    int tex_filter_idx = ui->texture_filter_combobox->findText(
+        QString::fromStdString(Settings::values.texture_filter_name));
+    if (tex_filter_idx == -1) {
+        ui->texture_filter_combobox->setCurrentIndex(0);
+    } else {
+        ui->texture_filter_combobox->setCurrentIndex(tex_filter_idx);
+    }
+    updateTextureFilter(tex_filter_idx);
     ui->layout_combobox->setCurrentIndex(static_cast<int>(Settings::values.layout_option));
     ui->swap_screen->setChecked(Settings::values.swap_screen);
     ui->toggle_disk_shader_cache->setChecked(Settings::values.use_hw_shader &&
@@ -88,6 +105,17 @@ void ConfigureEnhancements::updateShaders(Settings::StereoRenderOption stereo_op
     }
 }
 
+void ConfigureEnhancements::updateTextureFilter(int index) {
+    if (index == -1)
+        return;
+    ui->texture_filter_group->setEnabled(index != 0);
+    const auto& clamp = OpenGL::TextureFilterManager::TextureFilterMap()
+                            .at(ui->texture_filter_combobox->currentText().toStdString())
+                            .clamp_scale;
+    ui->texture_scale_spinbox->setMinimum(clamp.min);
+    ui->texture_scale_spinbox->setMaximum(clamp.max);
+}
+
 void ConfigureEnhancements::RetranslateUI() {
     ui->retranslateUi(this);
 }
@@ -101,6 +129,8 @@ void ConfigureEnhancements::ApplyConfiguration() {
     Settings::values.pp_shader_name =
         ui->shader_combobox->itemText(ui->shader_combobox->currentIndex()).toStdString();
     Settings::values.filter_mode = ui->toggle_linear_filter->isChecked();
+    Settings::values.texture_filter_name = ui->texture_filter_combobox->currentText().toStdString();
+    Settings::values.texture_filter_factor = ui->texture_scale_spinbox->value();
     Settings::values.layout_option =
         static_cast<Settings::LayoutOption>(ui->layout_combobox->currentIndex());
     Settings::values.swap_screen = ui->swap_screen->isChecked();
diff --git a/src/citra_qt/configuration/configure_enhancements.h b/src/citra_qt/configuration/configure_enhancements.h
index 7155140bd..9526d056f 100644
--- a/src/citra_qt/configuration/configure_enhancements.h
+++ b/src/citra_qt/configuration/configure_enhancements.h
@@ -27,6 +27,7 @@ public:
 
 private:
     void updateShaders(Settings::StereoRenderOption stereo_option);
+    void updateTextureFilter(int index);
 
     Ui::ConfigureEnhancements* ui;
     QColor bg_color;
diff --git a/src/citra_qt/configuration/configure_enhancements.ui b/src/citra_qt/configuration/configure_enhancements.ui
index 289c1178e..d3193e9eb 100644
--- a/src/citra_qt/configuration/configure_enhancements.ui
+++ b/src/citra_qt/configuration/configure_enhancements.ui
@@ -117,6 +117,56 @@
         </item>
        </layout>
       </item>
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_3">
+        <item>
+         <widget class="QLabel" name="label_5">
+          <property name="text">
+           <string>Texture Filter</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="texture_filter_combobox"/>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <widget class="QWidget" name="texture_filter_group" native="true">
+        <layout class="QVBoxLayout" name="verticalLayout_7">
+         <property name="leftMargin">
+          <number>16</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item>
+          <layout class="QHBoxLayout" name="horizontalLayout_8">
+           <item>
+            <widget class="QLabel" name="label_6">
+             <property name="text">
+              <string>Texture Scale Factor</string>
+             </property>
+            </widget>
+           </item>
+           <item>
+            <widget class="QSpinBox" name="texture_scale_spinbox">
+             <property name="minimum">
+              <number>1</number>
+             </property>
+            </widget>
+           </item>
+          </layout>
+         </item>
+        </layout>
+       </widget>
+      </item>
      </layout>
     </widget>
    </item>
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index ec3a36115..f6792604b 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -13,6 +13,7 @@
 #include "core/hle/service/mic_u.h"
 #include "core/settings.h"
 #include "video_core/renderer_base.h"
+#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h"
 #include "video_core/video_core.h"
 
 namespace Settings {
@@ -38,6 +39,9 @@ void Apply() {
     VideoCore::g_renderer_sampler_update_requested = true;
     VideoCore::g_renderer_shader_update_requested = true;
 
+    OpenGL::TextureFilterManager::GetInstance().SetTextureFilter(values.texture_filter_name,
+                                                                 values.texture_filter_factor);
+
     auto& system = Core::System::GetInstance();
     if (system.IsPoweredOn()) {
         Core::DSP().SetSink(values.sink_id, values.audio_device_id);
@@ -83,6 +87,8 @@ void LogSettings() {
     LogSetting("Renderer_FrameLimit", Settings::values.frame_limit);
     LogSetting("Renderer_PostProcessingShader", Settings::values.pp_shader_name);
     LogSetting("Renderer_FilterMode", Settings::values.filter_mode);
+    LogSetting("Renderer_TextureFilterFactor", Settings::values.texture_filter_factor);
+    LogSetting("Renderer_TextureFilterName", Settings::values.texture_filter_name);
     LogSetting("Stereoscopy_Render3d", static_cast<int>(Settings::values.render_3d));
     LogSetting("Stereoscopy_Factor3d", Settings::values.factor_3d);
     LogSetting("Layout_LayoutOption", static_cast<int>(Settings::values.layout_option));
diff --git a/src/core/settings.h b/src/core/settings.h
index 78b11912c..bb04d9d9f 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -147,6 +147,8 @@ struct Values {
     u16 resolution_factor;
     bool use_frame_limit;
     u16 frame_limit;
+    u16 texture_filter_factor;
+    std::string texture_filter_name;
 
     LayoutOption layout_option;
     bool swap_screen;
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index 4cb976354..a7dbc42de 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -50,6 +50,15 @@ add_library(video_core STATIC
     renderer_opengl/post_processing_opengl.h
     renderer_opengl/renderer_opengl.cpp
     renderer_opengl/renderer_opengl.h
+    renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.cpp
+    renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h
+    renderer_opengl/texture_filters/bicubic/bicubic.cpp
+    renderer_opengl/texture_filters/bicubic/bicubic.h
+    renderer_opengl/texture_filters/texture_filter_interface.h
+    renderer_opengl/texture_filters/texture_filter_manager.cpp
+    renderer_opengl/texture_filters/texture_filter_manager.h
+    renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp
+    renderer_opengl/texture_filters/xbrz/xbrz_freescale.h
     shader/debug_data.h
     shader/shader.cpp
     shader/shader.h
@@ -80,6 +89,35 @@ add_library(video_core STATIC
     video_core.h
 )
 
+set(SHADER_FILES
+    renderer_opengl/texture_filters/anime4k/refine.frag
+    renderer_opengl/texture_filters/anime4k/refine.vert
+    renderer_opengl/texture_filters/anime4k/x_gradient.frag
+    renderer_opengl/texture_filters/anime4k/y_gradient.frag
+    renderer_opengl/texture_filters/anime4k/y_gradient.vert
+    renderer_opengl/texture_filters/bicubic/bicubic.frag
+    renderer_opengl/texture_filters/tex_coord.vert
+    renderer_opengl/texture_filters/xbrz/xbrz_freescale.frag
+    renderer_opengl/texture_filters/xbrz/xbrz_freescale.vert
+)
+
+include(${CMAKE_CURRENT_SOURCE_DIR}/generate_shaders.cmake)
+
+foreach(shader_file ${SHADER_FILES})
+    get_filename_component(shader_file_name ${shader_file} NAME)
+    GetShaderHeaderFile(${shader_file_name})
+    list(APPEND SHADER_HEADERS ${shader_header_file})
+endforeach()
+
+add_custom_target(shaders
+    BYPRODUCTS ${SHADER_HEADERS}
+    COMMAND cmake -P ${CMAKE_CURRENT_SOURCE_DIR}/generate_shaders.cmake
+    SOURCES ${SHADER_FILES}
+)
+add_dependencies(video_core shaders)
+
+target_include_directories(video_core PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+
 if(ARCHITECTURE_x86_64)
     target_sources(video_core
         PRIVATE
diff --git a/src/video_core/generate_shaders.cmake b/src/video_core/generate_shaders.cmake
new file mode 100644
index 000000000..e0adca65f
--- /dev/null
+++ b/src/video_core/generate_shaders.cmake
@@ -0,0 +1,16 @@
+function(GetShaderHeaderFile shader_file_name)
+    set(shader_header_file ${CMAKE_CURRENT_BINARY_DIR}/shaders/${shader_file_name} PARENT_SCOPE)
+endfunction()
+
+foreach(shader_file ${SHADER_FILES})
+    file(READ ${shader_file} shader)
+    get_filename_component(shader_file_name ${shader_file} NAME)
+    string(REPLACE . _ shader_name ${shader_file_name})
+    GetShaderHeaderFile(${shader_file_name})
+    file(WRITE ${shader_header_file}
+        "#pragma once\n"
+        "constexpr std::string_view ${shader_name} = R\"(\n"
+        "${shader}"
+        ")\";\n"
+    )
+endforeach()
diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
index af2a918a6..cbf7d4926 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
@@ -34,6 +34,7 @@
 #include "video_core/renderer_opengl/gl_rasterizer_cache.h"
 #include "video_core/renderer_opengl/gl_state.h"
 #include "video_core/renderer_opengl/gl_vars.h"
+#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h"
 #include "video_core/utils.h"
 #include "video_core/video_core.h"
 
@@ -42,12 +43,6 @@ namespace OpenGL {
 using SurfaceType = SurfaceParams::SurfaceType;
 using PixelFormat = SurfaceParams::PixelFormat;
 
-struct FormatTuple {
-    GLint internal_format;
-    GLenum format;
-    GLenum type;
-};
-
 static constexpr std::array<FormatTuple, 5> fb_format_tuples = {{
     {GL_RGBA8, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8},     // RGBA8
     {GL_RGB8, GL_BGR, GL_UNSIGNED_BYTE},              // RGB8
@@ -74,9 +69,7 @@ static constexpr std::array<FormatTuple, 4> depth_format_tuples = {{
     {GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8}, // D24S8
 }};
 
-static constexpr FormatTuple tex_tuple = {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE};
-
-static const FormatTuple& GetFormatTuple(PixelFormat pixel_format) {
+const FormatTuple& GetFormatTuple(PixelFormat pixel_format) {
     const SurfaceType type = SurfaceParams::GetFormatType(pixel_format);
     if (type == SurfaceType::Color) {
         ASSERT(static_cast<std::size_t>(pixel_format) < fb_format_tuples.size());
@@ -745,9 +738,8 @@ void CachedSurface::LoadGLBuffer(PAddr load_start, PAddr load_end) {
     if (texture_src_data == nullptr)
         return;
 
-    if (gl_buffer == nullptr) {
-        gl_buffer_size = width * height * GetGLBytesPerPixel(pixel_format);
-        gl_buffer.reset(new u8[gl_buffer_size]);
+    if (gl_buffer.empty()) {
+        gl_buffer.resize(width * height * GetGLBytesPerPixel(pixel_format));
     }
 
     // TODO: Should probably be done in ::Memory:: and check for other regions too
@@ -819,7 +811,7 @@ void CachedSurface::FlushGLBuffer(PAddr flush_start, PAddr flush_end) {
     if (dst_buffer == nullptr)
         return;
 
-    ASSERT(gl_buffer_size == width * height * GetGLBytesPerPixel(pixel_format));
+    ASSERT(gl_buffer.size() == width * height * GetGLBytesPerPixel(pixel_format));
 
     // TODO: Should probably be done in ::Memory:: and check for other regions too
     // same as loadglbuffer()
@@ -858,8 +850,7 @@ void CachedSurface::FlushGLBuffer(PAddr flush_start, PAddr flush_end) {
     }
 }
 
-bool CachedSurface::LoadCustomTexture(u64 tex_hash, Core::CustomTexInfo& tex_info,
-                                      Common::Rectangle<u32>& custom_rect) {
+bool CachedSurface::LoadCustomTexture(u64 tex_hash, Core::CustomTexInfo& tex_info) {
     bool result = false;
     auto& custom_tex_cache = Core::System::GetInstance().CustomTexCache();
     const auto& image_interface = Core::System::GetInstance().GetImageInterface();
@@ -889,13 +880,6 @@ bool CachedSurface::LoadCustomTexture(u64 tex_hash, Core::CustomTexInfo& tex_inf
         }
     }
 
-    if (result) {
-        custom_rect.left = (custom_rect.left / width) * tex_info.width;
-        custom_rect.top = (custom_rect.top / height) * tex_info.height;
-        custom_rect.right = (custom_rect.right / width) * tex_info.width;
-        custom_rect.bottom = (custom_rect.bottom / height) * tex_info.height;
-    }
-
     return result;
 }
 
@@ -943,31 +927,31 @@ void CachedSurface::DumpTexture(GLuint target_tex, u64 tex_hash) {
 }
 
 MICROPROFILE_DEFINE(OpenGL_TextureUL, "OpenGL", "Texture Upload", MP_RGB(128, 192, 64));
-void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint read_fb_handle,
+void CachedSurface::UploadGLTexture(Common::Rectangle<u32> rect, GLuint read_fb_handle,
                                     GLuint draw_fb_handle) {
     if (type == SurfaceType::Fill)
         return;
 
     MICROPROFILE_SCOPE(OpenGL_TextureUL);
 
-    ASSERT(gl_buffer_size == width * height * GetGLBytesPerPixel(pixel_format));
+    ASSERT(gl_buffer.size() == width * height * GetGLBytesPerPixel(pixel_format));
 
-    // Read custom texture
-    auto& custom_tex_cache = Core::System::GetInstance().CustomTexCache();
     std::string dump_path; // Has to be declared here for logging later
     u64 tex_hash = 0;
-    // Required for rect to function properly with custom textures
-    Common::Rectangle custom_rect = rect;
 
     if (Settings::values.dump_textures || Settings::values.custom_textures)
-        tex_hash = Common::ComputeHash64(gl_buffer.get(), gl_buffer_size);
+        tex_hash = Common::ComputeHash64(gl_buffer.data(), gl_buffer.size());
 
     if (Settings::values.custom_textures)
-        is_custom = LoadCustomTexture(tex_hash, custom_tex_info, custom_rect);
+        is_custom = LoadCustomTexture(tex_hash, custom_tex_info);
+
+    TextureFilterInterface* const texture_filter =
+        is_custom ? nullptr : TextureFilterManager::GetInstance().GetTextureFilter();
+    const u16 default_scale = texture_filter ? texture_filter->scale_factor : 1;
 
     // Load data from memory to the surface
-    GLint x0 = static_cast<GLint>(custom_rect.left);
-    GLint y0 = static_cast<GLint>(custom_rect.bottom);
+    GLint x0 = static_cast<GLint>(rect.left);
+    GLint y0 = static_cast<GLint>(rect.bottom);
     std::size_t buffer_offset = (y0 * stride + x0) * GetGLBytesPerPixel(pixel_format);
 
     const FormatTuple& tuple = GetFormatTuple(pixel_format);
@@ -976,7 +960,7 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint r
     // If not 1x scale, create 1x texture that we will blit from to replace texture subrect in
     // surface
     OGLTexture unscaled_tex;
-    if (res_scale != 1) {
+    if (res_scale != default_scale) {
         x0 = 0;
         y0 = 0;
 
@@ -985,8 +969,8 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint r
             AllocateSurfaceTexture(unscaled_tex.handle, GetFormatTuple(PixelFormat::RGBA8),
                                    custom_tex_info.width, custom_tex_info.height);
         } else {
-            AllocateSurfaceTexture(unscaled_tex.handle, tuple, custom_rect.GetWidth(),
-                                   custom_rect.GetHeight());
+            AllocateSurfaceTexture(unscaled_tex.handle, tuple, rect.GetWidth() * default_scale,
+                                   rect.GetHeight() * default_scale);
         }
         target_tex = unscaled_tex.handle;
     }
@@ -1012,6 +996,16 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint r
         glActiveTexture(GL_TEXTURE0);
         glTexSubImage2D(GL_TEXTURE_2D, 0, x0, y0, custom_tex_info.width, custom_tex_info.height,
                         GL_RGBA, GL_UNSIGNED_BYTE, custom_tex_info.tex.data());
+    } else if (texture_filter) {
+        if (res_scale == default_scale) {
+            AllocateSurfaceTexture(texture.handle, GetFormatTuple(pixel_format),
+                                   rect.GetWidth() * default_scale,
+                                   rect.GetHeight() * default_scale);
+            cur_state.texture_units[0].texture_2d = texture.handle;
+            cur_state.Apply();
+        }
+        texture_filter->scale(*this, {(u32)x0, (u32)y0, rect.GetWidth(), rect.GetHeight()},
+                              buffer_offset);
     } else {
         glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(stride));
 
@@ -1022,21 +1016,23 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint r
     }
 
     glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
-    if (Settings::values.dump_textures && !is_custom)
+    if (Settings::values.dump_textures && !is_custom && !texture_filter)
         DumpTexture(target_tex, tex_hash);
 
     cur_state.texture_units[0].texture_2d = old_tex;
     cur_state.Apply();
 
-    if (res_scale != 1) {
-        auto scaled_rect = custom_rect;
+    if (res_scale != default_scale) {
+        auto scaled_rect = rect;
         scaled_rect.left *= res_scale;
         scaled_rect.top *= res_scale;
         scaled_rect.right *= res_scale;
         scaled_rect.bottom *= res_scale;
-
-        BlitTextures(unscaled_tex.handle, {0, custom_rect.GetHeight(), custom_rect.GetWidth(), 0},
-                     texture.handle, scaled_rect, type, read_fb_handle, draw_fb_handle);
+        auto from_rect =
+            is_custom ? Common::Rectangle<u32>{0, custom_tex_info.height, custom_tex_info.width, 0}
+                      : Common::Rectangle<u32>{0, rect.GetHeight(), rect.GetWidth(), 0};
+        BlitTextures(unscaled_tex.handle, from_rect, texture.handle, scaled_rect, type,
+                     read_fb_handle, draw_fb_handle);
     }
 
     InvalidateAllWatcher();
@@ -1050,9 +1046,8 @@ void CachedSurface::DownloadGLTexture(const Common::Rectangle<u32>& rect, GLuint
 
     MICROPROFILE_SCOPE(OpenGL_TextureDL);
 
-    if (gl_buffer == nullptr) {
-        gl_buffer_size = width * height * GetGLBytesPerPixel(pixel_format);
-        gl_buffer.reset(new u8[gl_buffer_size]);
+    if (gl_buffer.empty()) {
+        gl_buffer.resize(width * height * GetGLBytesPerPixel(pixel_format));
     }
 
     OpenGLState state = OpenGLState::GetCurState();
@@ -1090,7 +1085,7 @@ void CachedSurface::DownloadGLTexture(const Common::Rectangle<u32>& rect, GLuint
         if (GLES) {
             GetTexImageOES(GL_TEXTURE_2D, 0, tuple.format, tuple.type, rect.GetHeight(),
                            rect.GetWidth(), 0, &gl_buffer[buffer_offset],
-                           gl_buffer_size - buffer_offset);
+                           gl_buffer.size() - buffer_offset);
         } else {
             glGetTexImage(GL_TEXTURE_2D, 0, tuple.format, tuple.type, &gl_buffer[buffer_offset]);
         }
@@ -1373,8 +1368,8 @@ Surface RasterizerCacheOpenGL::GetSurface(const SurfaceParams& params, ScaleMatc
     if (surface == nullptr) {
         u16 target_res_scale = params.res_scale;
         if (match_res_scale != ScaleMatch::Exact) {
-            // This surface may have a subrect of another surface with a higher res_scale, find it
-            // to adjust our params
+            // This surface may have a subrect of another surface with a higher res_scale, find
+            // it to adjust our params
             SurfaceParams find_params = params;
             Surface expandable = FindMatch<MatchFlags::Expand | MatchFlags::Invalid>(
                 surface_cache, find_params, match_res_scale);
@@ -1423,7 +1418,6 @@ SurfaceRect_Tuple RasterizerCacheOpenGL::GetSurfaceSubRect(const SurfaceParams&
         surface = FindMatch<MatchFlags::SubRect | MatchFlags::Invalid>(surface_cache, params,
                                                                        ScaleMatch::Ignore);
         if (surface != nullptr) {
-            ASSERT(surface->res_scale < params.res_scale);
             SurfaceParams new_params = *surface;
             new_params.res_scale = params.res_scale;
 
@@ -1503,6 +1497,11 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf
     params.height = info.height;
     params.is_tiled = true;
     params.pixel_format = SurfaceParams::PixelFormatFromTextureFormat(info.format);
+    TextureFilterInterface* filter{};
+
+    params.res_scale = (filter = TextureFilterManager::GetInstance().GetTextureFilter())
+                           ? filter->scale_factor
+                           : 1;
     params.UpdateParams();
 
     u32 min_width = info.width >> max_level;
@@ -1520,6 +1519,8 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf
     }
 
     auto surface = GetSurface(params, ScaleMatch::Ignore, true);
+    if (!surface)
+        return nullptr;
 
     // Update mipmap if necessary
     if (max_level != 0) {
@@ -1546,8 +1547,8 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf
                 width = surface->custom_tex_info.width;
                 height = surface->custom_tex_info.height;
             } else {
-                width = surface->width * surface->res_scale;
-                height = surface->height * surface->res_scale;
+                width = surface->GetScaledWidth();
+                height = surface->GetScaledHeight();
             }
             for (u32 level = surface->max_level + 1; level <= max_level; ++level) {
                 glTexImage2D(GL_TEXTURE_2D, level, format_tuple.internal_format, width >> level,
@@ -1645,9 +1646,10 @@ const CachedTextureCube& RasterizerCacheOpenGL::GetTextureCube(const TextureCube
             if (surface) {
                 face.watcher = surface->CreateWatcher();
             } else {
-                // Can occur when texture address is invalid. We mark the watcher with nullptr in
-                // this case and the content of the face wouldn't get updated. These are usually
-                // leftover setup in the texture unit and games are not supposed to draw using them.
+                // Can occur when texture address is invalid. We mark the watcher with nullptr
+                // in this case and the content of the face wouldn't get updated. These are
+                // usually leftover setup in the texture unit and games are not supposed to draw
+                // using them.
                 face.watcher = nullptr;
             }
         }
@@ -1713,7 +1715,9 @@ SurfaceSurfaceRect_Tuple RasterizerCacheOpenGL::GetFramebufferSurfaces(
 
     // update resolution_scale_factor and reset cache if changed
     static u16 resolution_scale_factor = VideoCore::GetResolutionScaleFactor();
-    if (resolution_scale_factor != VideoCore::GetResolutionScaleFactor()) {
+    if (resolution_scale_factor != VideoCore::GetResolutionScaleFactor() ||
+        TextureFilterManager::GetInstance().IsUpdated()) {
+        TextureFilterManager::GetInstance().Reset();
         resolution_scale_factor = VideoCore::GetResolutionScaleFactor();
         FlushAll();
         while (!surface_cache.empty())
@@ -1936,8 +1940,8 @@ void RasterizerCacheOpenGL::FlushRegion(PAddr addr, u32 size, Surface flush_surf
 
     for (auto& pair : RangeFromInterval(dirty_regions, flush_interval)) {
         // small sizes imply that this most likely comes from the cpu, flush the entire region
-        // the point is to avoid thousands of small writes every frame if the cpu decides to access
-        // that region, anything higher than 8 you're guaranteed it comes from a service
+        // the point is to avoid thousands of small writes every frame if the cpu decides to
+        // access that region, anything higher than 8 you're guaranteed it comes from a service
         const auto interval = size <= 8 ? pair.first : pair.first & flush_interval;
         auto& surface = pair.second;
 
@@ -2031,7 +2035,7 @@ Surface RasterizerCacheOpenGL::CreateSurface(const SurfaceParams& params) {
 
     surface->texture.Create();
 
-    surface->gl_buffer_size = 0;
+    surface->gl_buffer.resize(0);
     surface->invalid_regions.insert(surface->GetInterval());
     AllocateSurfaceTexture(surface->texture.handle, GetFormatTuple(surface->pixel_format),
                            surface->GetScaledWidth(), surface->GetScaledHeight());
diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.h b/src/video_core/renderer_opengl/gl_rasterizer_cache.h
index f484de213..466ebb8d0 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer_cache.h
+++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.h
@@ -382,21 +382,18 @@ struct CachedSurface : SurfaceParams, std::enable_shared_from_this<CachedSurface
                          : SurfaceParams::GetFormatBpp(format) / 8;
     }
 
-    std::unique_ptr<u8[]> gl_buffer;
-    std::size_t gl_buffer_size = 0;
+    std::vector<u8> gl_buffer;
 
     // Read/Write data in 3DS memory to/from gl_buffer
     void LoadGLBuffer(PAddr load_start, PAddr load_end);
     void FlushGLBuffer(PAddr flush_start, PAddr flush_end);
 
     // Custom texture loading and dumping
-    bool LoadCustomTexture(u64 tex_hash, Core::CustomTexInfo& tex_info,
-                           Common::Rectangle<u32>& custom_rect);
+    bool LoadCustomTexture(u64 tex_hash, Core::CustomTexInfo& tex_info);
     void DumpTexture(GLuint target_tex, u64 tex_hash);
 
     // Upload/Download data in gl_buffer in/to this surface's texture
-    void UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint read_fb_handle,
-                         GLuint draw_fb_handle);
+    void UploadGLTexture(Common::Rectangle<u32> rect, GLuint read_fb_handle, GLuint draw_fb_handle);
     void DownloadGLTexture(const Common::Rectangle<u32>& rect, GLuint read_fb_handle,
                            GLuint draw_fb_handle);
 
@@ -525,4 +522,14 @@ private:
 
     std::unordered_map<TextureCubeConfig, CachedTextureCube> texture_cube_cache;
 };
+
+struct FormatTuple {
+    GLint internal_format;
+    GLenum format;
+    GLenum type;
+};
+
+constexpr FormatTuple tex_tuple = {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE};
+
+const FormatTuple& GetFormatTuple(SurfaceParams::PixelFormat pixel_format);
 } // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/gl_state.h b/src/video_core/renderer_opengl/gl_state.h
index cc7a864a2..5d655a127 100644
--- a/src/video_core/renderer_opengl/gl_state.h
+++ b/src/video_core/renderer_opengl/gl_state.h
@@ -38,6 +38,13 @@ constexpr GLuint ShadowTexturePZ = 5;
 constexpr GLuint ShadowTextureNZ = 6;
 } // namespace ImageUnits
 
+struct Viewport {
+    GLint x;
+    GLint y;
+    GLsizei width;
+    GLsizei height;
+};
+
 class OpenGLState {
 public:
     struct {
@@ -135,12 +142,7 @@ public:
         GLsizei height;
     } scissor;
 
-    struct {
-        GLint x;
-        GLint y;
-        GLsizei width;
-        GLsizei height;
-    } viewport;
+    Viewport viewport;
 
     std::array<bool, 2> clip_distance; // GL_CLIP_DISTANCE
 
diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp
index 5046895e0..e7e0bb72f 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.cpp
+++ b/src/video_core/renderer_opengl/renderer_opengl.cpp
@@ -32,6 +32,7 @@
 #include "video_core/renderer_opengl/gl_vars.h"
 #include "video_core/renderer_opengl/post_processing_opengl.h"
 #include "video_core/renderer_opengl/renderer_opengl.h"
+#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h"
 #include "video_core/video_core.h"
 
 namespace Frontend {
@@ -1178,10 +1179,14 @@ VideoCore::ResultStatus RendererOpenGL::Init() {
 
     RefreshRasterizerSetting();
 
+    TextureFilterManager::GetInstance().Reset();
+
     return VideoCore::ResultStatus::Success;
 }
 
 /// Shutdown the renderer
-void RendererOpenGL::ShutDown() {}
+void RendererOpenGL::ShutDown() {
+    TextureFilterManager::GetInstance().Destroy();
+}
 
 } // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.cpp b/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.cpp
new file mode 100644
index 000000000..86b4a85b6
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.cpp
@@ -0,0 +1,138 @@
+// Copyright 2019 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+// modified from
+// https://github.com/bloc97/Anime4K/blob/533cee5f7018d0e57ad2a26d76d43f13b9d8782a/glsl/Anime4K_Adaptive_v1.0RC2_UltraFast.glsl
+
+// MIT License
+//
+// Copyright(c) 2019 bloc97
+//
+// Permission is hereby granted,
+// free of charge,
+// to any person obtaining a copy of this software and associated documentation
+// files(the "Software"),
+// to deal in the Software without restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software,
+// and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions :
+//
+// The above copyright notice and this permission notice shall be included in all copies
+// or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS",
+// WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#include "video_core/renderer_opengl/gl_rasterizer_cache.h"
+#include "video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h"
+
+#include "shaders/refine.frag"
+#include "shaders/refine.vert"
+#include "shaders/tex_coord.vert"
+#include "shaders/x_gradient.frag"
+#include "shaders/y_gradient.frag"
+#include "shaders/y_gradient.vert"
+
+namespace OpenGL {
+
+Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterInterface(scale_factor) {
+    const OpenGLState cur_state = OpenGLState::GetCurState();
+    const auto setup_temp_tex = [this, scale_factor](TempTex& texture, GLint internal_format,
+                                                     GLint format) {
+        texture.fbo.Create();
+        texture.tex.Create();
+        state.draw.draw_framebuffer = texture.fbo.handle;
+        state.Apply();
+        glActiveTexture(GL_TEXTURE0);
+        glBindTexture(GL_TEXTURE_RECTANGLE, texture.tex.handle);
+        glTexImage2D(GL_TEXTURE_RECTANGLE, 0, internal_format, 1024 * scale_factor,
+                     1024 * scale_factor, 0, format, GL_HALF_FLOAT, nullptr);
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE,
+                               texture.tex.handle, 0);
+    };
+    setup_temp_tex(LUMAD, GL_R16F, GL_RED);
+    setup_temp_tex(XY, GL_RG16F, GL_RG);
+
+    vao.Create();
+    out_fbo.Create();
+
+    for (std::size_t idx = 0; idx < samplers.size(); ++idx) {
+        samplers[idx].Create();
+        state.texture_units[idx].sampler = samplers[idx].handle;
+        glSamplerParameteri(samplers[idx].handle, GL_TEXTURE_MIN_FILTER,
+                            idx == 0 ? GL_LINEAR : GL_NEAREST);
+        glSamplerParameteri(samplers[idx].handle, GL_TEXTURE_MAG_FILTER,
+                            idx == 0 ? GL_LINEAR : GL_NEAREST);
+        glSamplerParameteri(samplers[idx].handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+        glSamplerParameteri(samplers[idx].handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+    }
+    state.draw.vertex_array = vao.handle;
+
+    gradient_x_program.Create(tex_coord_vert.data(), x_gradient_frag.data());
+    gradient_y_program.Create(y_gradient_vert.data(), y_gradient_frag.data());
+    refine_program.Create(refine_vert.data(), refine_frag.data());
+
+    state.draw.shader_program = gradient_y_program.handle;
+    state.Apply();
+    glUniform1i(glGetUniformLocation(gradient_y_program.handle, "tex_input"), 2);
+
+    state.draw.shader_program = refine_program.handle;
+    state.Apply();
+    glUniform1i(glGetUniformLocation(refine_program.handle, "LUMAD"), 1);
+
+    cur_state.Apply();
+}
+
+void Anime4kUltrafast::scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
+                             std::size_t buffer_offset) {
+    const OpenGLState cur_state = OpenGLState::GetCurState();
+
+    OGLTexture src_tex;
+    src_tex.Create();
+
+    state.viewport = RectToViewport(rect);
+
+    state.texture_units[0].texture_2d = src_tex.handle;
+    state.draw.draw_framebuffer = XY.fbo.handle;
+    state.draw.shader_program = gradient_x_program.handle;
+    state.Apply();
+
+    const FormatTuple tuple = GetFormatTuple(surface.pixel_format);
+    glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(surface.stride));
+    glActiveTexture(GL_TEXTURE0);
+    glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, rect.GetWidth(), rect.GetHeight(), 0,
+                 tuple.format, tuple.type, &surface.gl_buffer[buffer_offset]);
+
+    glActiveTexture(GL_TEXTURE1);
+    glBindTexture(GL_TEXTURE_RECTANGLE, LUMAD.tex.handle);
+    glActiveTexture(GL_TEXTURE2);
+    glBindTexture(GL_TEXTURE_RECTANGLE, XY.tex.handle);
+
+    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+
+    // gradient y pass
+    state.draw.draw_framebuffer = LUMAD.fbo.handle;
+    state.draw.shader_program = gradient_y_program.handle;
+    state.Apply();
+    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+
+    // refine pass
+    state.draw.draw_framebuffer = out_fbo.handle;
+    state.draw.shader_program = refine_program.handle;
+    state.Apply();
+    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                           cur_state.texture_units[0].texture_2d, 0);
+    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+
+    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
+    cur_state.Apply();
+}
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h b/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h
new file mode 100644
index 000000000..79a058fd5
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h
@@ -0,0 +1,45 @@
+// Copyright 2019 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include "video_core/renderer_opengl/gl_resource_manager.h"
+#include "video_core/renderer_opengl/gl_state.h"
+#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h"
+
+namespace OpenGL {
+
+class Anime4kUltrafast : public TextureFilterInterface {
+public:
+    static TextureFilterInfo GetInfo() {
+        TextureFilterInfo info;
+        info.name = "Anime4K Ultrafast";
+        info.clamp_scale = {2, 2};
+        info.constructor = std::make_unique<Anime4kUltrafast, u16>;
+        return info;
+    }
+
+    Anime4kUltrafast(u16 scale_factor);
+    void scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
+               std::size_t buffer_offset) override;
+
+private:
+    OpenGLState state{};
+
+    OGLVertexArray vao;
+    OGLFramebuffer out_fbo;
+
+    struct TempTex {
+        OGLTexture tex;
+        OGLFramebuffer fbo;
+    };
+    TempTex LUMAD;
+    TempTex XY;
+
+    std::array<OGLSampler, 3> samplers;
+
+    OGLProgram gradient_x_program, gradient_y_program, refine_program;
+};
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/texture_filters/anime4k/refine.frag b/src/video_core/renderer_opengl/texture_filters/anime4k/refine.frag
new file mode 100644
index 000000000..26f2526ba
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/anime4k/refine.frag
@@ -0,0 +1,117 @@
+//? #version 330
+in vec2 tex_coord;
+in vec2 input_max;
+
+out vec4 frag_color;
+
+uniform sampler2D HOOKED;
+uniform sampler2DRect LUMAD;
+uniform sampler2DRect LUMAG;
+
+const float LINE_DETECT_THRESHOLD = 0.4;
+const float STRENGTH = 0.6;
+
+// the original shader used the alpha channel for luminance,
+// which doesn't work for our use case
+struct RGBAL {
+    vec4 c;
+    float l;
+};
+
+vec4 getAverage(vec4 cc, vec4 a, vec4 b, vec4 c) {
+    return cc * (1 - STRENGTH) + ((a + b + c) / 3) * STRENGTH;
+}
+
+#define GetRGBAL(offset)                                                                           \
+    RGBAL(textureOffset(HOOKED, tex_coord, offset),                                                \
+          texture(LUMAD, clamp(gl_FragCoord.xy + offset, vec2(0.0), input_max)).x)
+
+float min3v(float a, float b, float c) {
+    return min(min(a, b), c);
+}
+
+float max3v(float a, float b, float c) {
+    return max(max(a, b), c);
+}
+
+vec4 Compute() {
+    RGBAL cc = GetRGBAL(ivec2(0));
+
+    if (cc.l > LINE_DETECT_THRESHOLD) {
+        return cc.c;
+    }
+
+    RGBAL tl = GetRGBAL(ivec2(-1, -1));
+    RGBAL t = GetRGBAL(ivec2(0, -1));
+    RGBAL tr = GetRGBAL(ivec2(1, -1));
+
+    RGBAL l = GetRGBAL(ivec2(-1, 0));
+
+    RGBAL r = GetRGBAL(ivec2(1, 0));
+
+    RGBAL bl = GetRGBAL(ivec2(-1, 1));
+    RGBAL b = GetRGBAL(ivec2(0, 1));
+    RGBAL br = GetRGBAL(ivec2(1, 1));
+
+    // Kernel 0 and 4
+    float maxDark = max3v(br.l, b.l, bl.l);
+    float minLight = min3v(tl.l, t.l, tr.l);
+
+    if (minLight > cc.l && minLight > maxDark) {
+        return getAverage(cc.c, tl.c, t.c, tr.c);
+    } else {
+        maxDark = max3v(tl.l, t.l, tr.l);
+        minLight = min3v(br.l, b.l, bl.l);
+        if (minLight > cc.l && minLight > maxDark) {
+            return getAverage(cc.c, br.c, b.c, bl.c);
+        }
+    }
+
+    // Kernel 1 and 5
+    maxDark = max3v(cc.l, l.l, b.l);
+    minLight = min3v(r.l, t.l, tr.l);
+
+    if (minLight > maxDark) {
+        return getAverage(cc.c, r.c, t.c, tr.c);
+    } else {
+        maxDark = max3v(cc.l, r.l, t.l);
+        minLight = min3v(bl.l, l.l, b.l);
+        if (minLight > maxDark) {
+            return getAverage(cc.c, bl.c, l.c, b.c);
+        }
+    }
+
+    // Kernel 2 and 6
+    maxDark = max3v(l.l, tl.l, bl.l);
+    minLight = min3v(r.l, br.l, tr.l);
+
+    if (minLight > cc.l && minLight > maxDark) {
+        return getAverage(cc.c, r.c, br.c, tr.c);
+    } else {
+        maxDark = max3v(r.l, br.l, tr.l);
+        minLight = min3v(l.l, tl.l, bl.l);
+        if (minLight > cc.l && minLight > maxDark) {
+            return getAverage(cc.c, l.c, tl.c, bl.c);
+        }
+    }
+
+    // Kernel 3 and 7
+    maxDark = max3v(cc.l, l.l, t.l);
+    minLight = min3v(r.l, br.l, b.l);
+
+    if (minLight > maxDark) {
+        return getAverage(cc.c, r.c, br.c, b.c);
+    } else {
+        maxDark = max3v(cc.l, r.l, b.l);
+        minLight = min3v(t.l, l.l, tl.l);
+        if (minLight > maxDark) {
+            return getAverage(cc.c, t.c, l.c, tl.c);
+        }
+    }
+
+    return cc.c;
+}
+
+void main() {
+    frag_color = Compute();
+}
diff --git a/src/video_core/renderer_opengl/texture_filters/anime4k/refine.vert b/src/video_core/renderer_opengl/texture_filters/anime4k/refine.vert
new file mode 100644
index 000000000..552a218fb
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/anime4k/refine.vert
@@ -0,0 +1,14 @@
+//? #version 330
+out vec2 tex_coord;
+out vec2 input_max;
+
+uniform sampler2D HOOKED;
+
+const vec2 vertices[4] =
+    vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));
+
+void main() {
+    gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
+    tex_coord = (vertices[gl_VertexID] + 1.0) / 2.0;
+    input_max = textureSize(HOOKED, 0) * 2.0 - 1.0;
+}
diff --git a/src/video_core/renderer_opengl/texture_filters/anime4k/x_gradient.frag b/src/video_core/renderer_opengl/texture_filters/anime4k/x_gradient.frag
new file mode 100644
index 000000000..49502fac7
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/anime4k/x_gradient.frag
@@ -0,0 +1,18 @@
+//? #version 330
+in vec2 tex_coord;
+
+out vec2 frag_color;
+
+uniform sampler2D tex_input;
+
+const vec3 K = vec3(0.2627, 0.6780, 0.0593);
+// TODO: improve handling of alpha channel
+#define GetLum(xoffset) dot(K, textureOffset(tex_input, tex_coord, ivec2(xoffset, 0)).rgb)
+
+void main() {
+    float l = GetLum(-1);
+    float c = GetLum(0);
+    float r = GetLum(1);
+
+    frag_color = vec2(r - l, l + 2.0 * c + r);
+}
diff --git a/src/video_core/renderer_opengl/texture_filters/anime4k/y_gradient.frag b/src/video_core/renderer_opengl/texture_filters/anime4k/y_gradient.frag
new file mode 100644
index 000000000..a0e820001
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/anime4k/y_gradient.frag
@@ -0,0 +1,16 @@
+//? #version 330
+in vec2 input_max;
+
+out float frag_color;
+
+uniform sampler2DRect tex_input;
+
+void main() {
+    vec2 t = texture(tex_input, min(gl_FragCoord.xy + vec2(0.0, 1.0), input_max)).xy;
+    vec2 c = texture(tex_input, gl_FragCoord.xy).xy;
+    vec2 b = texture(tex_input, max(gl_FragCoord.xy - vec2(0.0, 1.0), vec2(0.0))).xy;
+
+    vec2 grad = vec2(t.x + 2 * c.x + b.x, b.y - t.y);
+
+    frag_color = 1 - length(grad);
+}
diff --git a/src/video_core/renderer_opengl/texture_filters/anime4k/y_gradient.vert b/src/video_core/renderer_opengl/texture_filters/anime4k/y_gradient.vert
new file mode 100644
index 000000000..376a67b79
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/anime4k/y_gradient.vert
@@ -0,0 +1,12 @@
+//? #version 330
+out vec2 input_max;
+
+uniform sampler2D tex_size;
+
+const vec2 vertices[4] =
+    vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));
+
+void main() {
+    gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
+    input_max = textureSize(tex_size, 0) * 2 - 1;
+}
diff --git a/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.cpp b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.cpp
new file mode 100644
index 000000000..16f5bd2c5
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.cpp
@@ -0,0 +1,56 @@
+// Copyright 2020 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "video_core/renderer_opengl/gl_rasterizer_cache.h"
+#include "video_core/renderer_opengl/texture_filters/bicubic/bicubic.h"
+
+#include "shaders/bicubic.frag"
+#include "shaders/tex_coord.vert"
+
+namespace OpenGL {
+
+Bicubic::Bicubic(u16 scale_factor) : TextureFilterInterface(scale_factor) {
+    program.Create(tex_coord_vert.data(), bicubic_frag.data());
+    vao.Create();
+    draw_fbo.Create();
+    src_sampler.Create();
+
+    state.draw.shader_program = program.handle;
+    state.draw.vertex_array = vao.handle;
+    state.draw.shader_program = program.handle;
+    state.draw.draw_framebuffer = draw_fbo.handle;
+    state.texture_units[0].sampler = src_sampler.handle;
+
+    glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+    glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+}
+
+void Bicubic::scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
+                    std::size_t buffer_offset) {
+    const OpenGLState cur_state = OpenGLState::GetCurState();
+
+    OGLTexture src_tex;
+    src_tex.Create();
+    state.texture_units[0].texture_2d = src_tex.handle;
+
+    state.viewport = RectToViewport(rect);
+    state.Apply();
+
+    const FormatTuple tuple = GetFormatTuple(surface.pixel_format);
+    glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(surface.stride));
+    glActiveTexture(GL_TEXTURE0);
+    glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, rect.GetWidth(), rect.GetHeight(), 0,
+                 tuple.format, tuple.type, &surface.gl_buffer[buffer_offset]);
+
+    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                           cur_state.texture_units[0].texture_2d, 0);
+    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, NULL, 0);
+
+    cur_state.Apply();
+}
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.frag b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.frag
new file mode 100644
index 000000000..2bdab3cf6
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.frag
@@ -0,0 +1,52 @@
+//? #version 330
+in vec2 tex_coord;
+
+out vec4 frag_color;
+
+uniform sampler2D input_texture;
+
+// from http://www.java-gaming.org/index.php?topic=35123.0
+vec4 cubic(float v) {
+    vec4 n = vec4(1.0, 2.0, 3.0, 4.0) - v;
+    vec4 s = n * n * n;
+    float x = s.x;
+    float y = s.y - 4.0 * s.x;
+    float z = s.z - 4.0 * s.y + 6.0 * s.x;
+    float w = 6.0 - x - y - z;
+    return vec4(x, y, z, w) * (1.0 / 6.0);
+}
+
+vec4 textureBicubic(sampler2D sampler, vec2 texCoords) {
+
+    vec2 texSize = textureSize(sampler, 0);
+    vec2 invTexSize = 1.0 / texSize;
+
+    texCoords = texCoords * texSize - 0.5;
+
+    vec2 fxy = fract(texCoords);
+    texCoords -= fxy;
+
+    vec4 xcubic = cubic(fxy.x);
+    vec4 ycubic = cubic(fxy.y);
+
+    vec4 c = texCoords.xxyy + vec2(-0.5, +1.5).xyxy;
+
+    vec4 s = vec4(xcubic.xz + xcubic.yw, ycubic.xz + ycubic.yw);
+    vec4 offset = c + vec4(xcubic.yw, ycubic.yw) / s;
+
+    offset *= invTexSize.xxyy;
+
+    vec4 sample0 = texture(sampler, offset.xz);
+    vec4 sample1 = texture(sampler, offset.yz);
+    vec4 sample2 = texture(sampler, offset.xw);
+    vec4 sample3 = texture(sampler, offset.yw);
+
+    float sx = s.x / (s.x + s.y);
+    float sy = s.z / (s.z + s.w);
+
+    return mix(mix(sample3, sample2, sx), mix(sample1, sample0, sx), sy);
+}
+
+void main() {
+    frag_color = textureBicubic(input_texture, tex_coord);
+}
diff --git a/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.h b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.h
new file mode 100644
index 000000000..a16bdafcf
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.h
@@ -0,0 +1,32 @@
+// Copyright 2020 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include "video_core/renderer_opengl/gl_resource_manager.h"
+#include "video_core/renderer_opengl/gl_state.h"
+#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h"
+
+namespace OpenGL {
+class Bicubic : public TextureFilterInterface {
+public:
+    static TextureFilterInfo GetInfo() {
+        TextureFilterInfo info;
+        info.name = "Bicubic";
+        info.constructor = std::make_unique<Bicubic, u16>;
+        return info;
+    }
+
+    Bicubic(u16 scale_factor);
+    void scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
+               std::size_t buffer_offset) override;
+
+private:
+    OpenGLState state{};
+    OGLProgram program{};
+    OGLVertexArray vao{};
+    OGLFramebuffer draw_fbo{};
+    OGLSampler src_sampler{};
+};
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/texture_filters/tex_coord.vert b/src/video_core/renderer_opengl/texture_filters/tex_coord.vert
new file mode 100644
index 000000000..e5e153330
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/tex_coord.vert
@@ -0,0 +1,10 @@
+//? #version 330
+out vec2 tex_coord;
+
+const vec2 vertices[4] =
+    vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));
+
+void main() {
+    gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
+    tex_coord = (vertices[gl_VertexID] + 1.0) / 2.0;
+}
diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filter_interface.h b/src/video_core/renderer_opengl/texture_filters/texture_filter_interface.h
new file mode 100644
index 000000000..c2bf2a686
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/texture_filter_interface.h
@@ -0,0 +1,38 @@
+// Copyright 2019 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <functional>
+#include <string_view>
+#include "common/common_types.h"
+#include "common/math_util.h"
+
+namespace OpenGL {
+
+struct CachedSurface;
+struct Viewport;
+
+class TextureFilterInterface {
+public:
+    const u16 scale_factor{};
+    TextureFilterInterface(u16 scale_factor) : scale_factor{scale_factor} {}
+    virtual void scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
+                       std::size_t buffer_offset) = 0;
+    virtual ~TextureFilterInterface() = default;
+
+protected:
+    Viewport RectToViewport(const Common::Rectangle<u32>& rect);
+};
+
+// every texture filter should have a static GetInfo function
+struct TextureFilterInfo {
+    std::string_view name;
+    struct {
+        u16 min, max;
+    } clamp_scale{1, 10};
+    std::function<std::unique_ptr<TextureFilterInterface>(u16 scale_factor)> constructor;
+};
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.cpp b/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.cpp
new file mode 100644
index 000000000..77d07111e
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.cpp
@@ -0,0 +1,89 @@
+// Copyright 2019 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/logging/log.h"
+#include "video_core/renderer_opengl/gl_state.h"
+#include "video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h"
+#include "video_core/renderer_opengl/texture_filters/bicubic/bicubic.h"
+#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h"
+#include "video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h"
+
+namespace OpenGL {
+
+Viewport TextureFilterInterface::RectToViewport(const Common::Rectangle<u32>& rect) {
+    return {
+        static_cast<GLint>(rect.left) * scale_factor,
+        static_cast<GLint>(rect.top) * scale_factor,
+        static_cast<GLsizei>(rect.GetWidth()) * scale_factor,
+        static_cast<GLsizei>(rect.GetHeight()) * scale_factor,
+    };
+}
+
+namespace {
+template <typename T>
+std::pair<std::string_view, TextureFilterInfo> FilterMapPair() {
+    return {T::GetInfo().name, T::GetInfo()};
+};
+
+struct NoFilter {
+    static TextureFilterInfo GetInfo() {
+        TextureFilterInfo info;
+        info.name = TextureFilterManager::NONE;
+        info.clamp_scale = {1, 1};
+        info.constructor = [](u16) { return nullptr; };
+        return info;
+    }
+};
+} // namespace
+
+const std::map<std::string_view, TextureFilterInfo, TextureFilterManager::FilterNameComp>&
+TextureFilterManager::TextureFilterMap() {
+    static const std::map<std::string_view, TextureFilterInfo, FilterNameComp> filter_map{
+        FilterMapPair<NoFilter>(),
+        FilterMapPair<Anime4kUltrafast>(),
+        FilterMapPair<Bicubic>(),
+        FilterMapPair<XbrzFreescale>(),
+    };
+    return filter_map;
+}
+
+void TextureFilterManager::SetTextureFilter(std::string filter_name, u16 new_scale_factor) {
+    if (name == filter_name && scale_factor == new_scale_factor)
+        return;
+    std::lock_guard<std::mutex> lock{mutex};
+    name = std::move(filter_name);
+    scale_factor = new_scale_factor;
+    updated = true;
+}
+
+TextureFilterInterface* TextureFilterManager::GetTextureFilter() const {
+    return filter.get();
+}
+
+bool TextureFilterManager::IsUpdated() const {
+    return updated;
+}
+
+void TextureFilterManager::Reset() {
+    std::lock_guard<std::mutex> lock{mutex};
+    updated = false;
+    auto iter = TextureFilterMap().find(name);
+    if (iter == TextureFilterMap().end()) {
+        LOG_ERROR(Render_OpenGL, "Invalid texture filter: {}", name);
+        filter = nullptr;
+        return;
+    }
+
+    const auto& filter_info = iter->second;
+
+    u16 clamped_scale =
+        std::clamp(scale_factor, filter_info.clamp_scale.min, filter_info.clamp_scale.max);
+    if (clamped_scale != scale_factor)
+        LOG_ERROR(Render_OpenGL, "Invalid scale factor {} for texture filter {}, clamped to {}",
+                  scale_factor, filter_info.name, clamped_scale);
+
+    filter = filter_info.constructor(clamped_scale);
+}
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.h b/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.h
new file mode 100644
index 000000000..1299a9151
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.h
@@ -0,0 +1,55 @@
+// Copyright 2019 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <atomic>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <string_view>
+#include <tuple>
+#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h"
+
+namespace OpenGL {
+
+class TextureFilterManager {
+public:
+    static constexpr std::string_view NONE = "none";
+    struct FilterNameComp {
+        bool operator()(const std::string_view a, const std::string_view b) const {
+            bool na = a == NONE;
+            bool nb = b == NONE;
+            if (na | nb)
+                return na & !nb;
+            return a < b;
+        }
+    };
+    // function ensures map is initialized before use
+    static const std::map<std::string_view, TextureFilterInfo, FilterNameComp>& TextureFilterMap();
+
+    static TextureFilterManager& GetInstance() {
+        static TextureFilterManager singleton;
+        return singleton;
+    }
+
+    void Destroy() {
+        filter.reset();
+    }
+    void SetTextureFilter(std::string filter_name, u16 new_scale_factor);
+    TextureFilterInterface* GetTextureFilter() const;
+    // returns true if filter has been changed and a cache reset is needed
+    bool IsUpdated() const;
+    void Reset();
+
+private:
+    std::atomic<bool> updated{false};
+    std::mutex mutex;
+    std::string name{"none"};
+    u16 scale_factor{1};
+
+    std::unique_ptr<TextureFilterInterface> filter;
+};
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp
new file mode 100644
index 000000000..091666627
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp
@@ -0,0 +1,100 @@
+// Copyright 2019 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+// adapted from
+// https://github.com/libretro/glsl-shaders/blob/d7a8b8eb2a61a5732da4cbe2e0f9ad30600c3f17/xbrz/shaders/xbrz-freescale.glsl
+
+// xBRZ freescale
+// based on :
+// 4xBRZ shader - Copyright (C) 2014-2016 DeSmuME team
+//
+// This file 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, either version 2 of the License, or
+// (at your option) any later version.
+//
+// This file 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 for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with the this software.  If not, see <http://www.gnu.org/licenses/>.
+
+// Hyllian's xBR-vertex code and texel mapping
+// Copyright (C) 2011/2016 Hyllian - sergiogdb@gmail.com
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+#include "video_core/renderer_opengl/gl_rasterizer_cache.h"
+#include "video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h"
+
+#include "shaders/xbrz_freescale.frag"
+#include "shaders/xbrz_freescale.vert"
+
+namespace OpenGL {
+
+XbrzFreescale::XbrzFreescale(u16 scale_factor) : TextureFilterInterface(scale_factor) {
+    const OpenGLState cur_state = OpenGLState::GetCurState();
+
+    program.Create(xbrz_freescale_vert.data(), xbrz_freescale_frag.data());
+    vao.Create();
+    draw_fbo.Create();
+    src_sampler.Create();
+
+    state.draw.shader_program = program.handle;
+    state.Apply();
+
+    glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+    glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+    glUniform1f(glGetUniformLocation(program.handle, "scale"), static_cast<GLfloat>(scale_factor));
+
+    cur_state.Apply();
+    state.draw.vertex_array = vao.handle;
+    state.draw.shader_program = program.handle;
+    state.draw.draw_framebuffer = draw_fbo.handle;
+    state.texture_units[0].sampler = src_sampler.handle;
+}
+
+void XbrzFreescale::scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
+                          std::size_t buffer_offset) {
+    const OpenGLState cur_state = OpenGLState::GetCurState();
+
+    OGLTexture src_tex;
+    src_tex.Create();
+    state.texture_units[0].texture_2d = src_tex.handle;
+
+    state.viewport = RectToViewport(rect);
+    state.Apply();
+
+    const FormatTuple tuple = GetFormatTuple(surface.pixel_format);
+    glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(surface.stride));
+    glActiveTexture(GL_TEXTURE0);
+    glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, rect.GetWidth(), rect.GetHeight(), 0,
+                 tuple.format, tuple.type, &surface.gl_buffer[buffer_offset]);
+
+    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                           cur_state.texture_units[0].texture_2d, 0);
+    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, NULL, 0);
+
+    cur_state.Apply();
+}
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.frag b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.frag
new file mode 100644
index 000000000..4868d18f7
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.frag
@@ -0,0 +1,242 @@
+//? #version 330
+in vec2 tex_coord;
+in vec2 source_size;
+in vec2 output_size;
+
+out vec4 frag_color;
+
+uniform sampler2D tex;
+uniform float scale;
+
+const int BLEND_NONE = 0;
+const int BLEND_NORMAL = 1;
+const int BLEND_DOMINANT = 2;
+const float LUMINANCE_WEIGHT = 1.0;
+const float EQUAL_COLOR_TOLERANCE = 30.0 / 255.0;
+const float STEEP_DIRECTION_THRESHOLD = 2.2;
+const float DOMINANT_DIRECTION_THRESHOLD = 3.6;
+
+float ColorDist(vec4 a, vec4 b) {
+    // https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.2020_conversion
+    const vec3 K = vec3(0.2627, 0.6780, 0.0593);
+    const mat3 MATRIX = mat3(K, -.5 * K.r / (1.0 - K.b), -.5 * K.g / (1.0 - K.b), .5, .5,
+                             -.5 * K.g / (1.0 - K.r), -.5 * K.b / (1.0 - K.r));
+    vec4 diff = a - b;
+    vec3 YCbCr = diff.rgb * MATRIX;
+    // LUMINANCE_WEIGHT is currently 1, otherwise y would be multiplied by it
+    float d = length(YCbCr);
+    return sqrt(a.a * b.a * d * d + diff.a * diff.a);
+}
+
+bool IsPixEqual(const vec4 pixA, const vec4 pixB) {
+    return ColorDist(pixA, pixB) < EQUAL_COLOR_TOLERANCE;
+}
+
+float GetLeftRatio(vec2 center, vec2 origin, vec2 direction) {
+    vec2 P0 = center - origin;
+    vec2 proj = direction * (dot(P0, direction) / dot(direction, direction));
+    vec2 distv = P0 - proj;
+    vec2 orth = vec2(-direction.y, direction.x);
+    float side = sign(dot(P0, orth));
+    float v = side * length(distv * scale);
+    return smoothstep(-sqrt(2.0) / 2.0, sqrt(2.0) / 2.0, v);
+}
+
+vec2 pos = fract(tex_coord * source_size) - vec2(0.5, 0.5);
+vec2 coord = tex_coord - pos / source_size;
+
+#define P(x, y) textureOffset(tex, coord, ivec2(x, y))
+
+void main() {
+    //---------------------------------------
+    // Input Pixel Mapping:  -|x|x|x|-
+    //                       x|A|B|C|x
+    //                       x|D|E|F|x
+    //                       x|G|H|I|x
+    //                       -|x|x|x|-
+    vec4 A = P(-1, -1);
+    vec4 B = P(0, -1);
+    vec4 C = P(1, -1);
+    vec4 D = P(-1, 0);
+    vec4 E = P(0, 0);
+    vec4 F = P(1, 0);
+    vec4 G = P(-1, 1);
+    vec4 H = P(0, 1);
+    vec4 I = P(1, 1);
+    // blendResult Mapping: x|y|
+    //                      w|z|
+    ivec4 blendResult = ivec4(BLEND_NONE, BLEND_NONE, BLEND_NONE, BLEND_NONE);
+    // Preprocess corners
+    // Pixel Tap Mapping: -|-|-|-|-
+    //                    -|-|B|C|-
+    //                    -|D|E|F|x
+    //                    -|G|H|I|x
+    //                    -|-|x|x|-
+    if (!((E == F && H == I) || (E == H && F == I))) {
+        float dist_H_F = ColorDist(G, E) + ColorDist(E, C) + ColorDist(P(0, 2), I) +
+                         ColorDist(I, P(2, 0)) + (4.0 * ColorDist(H, F));
+        float dist_E_I = ColorDist(D, H) + ColorDist(H, P(1, 2)) + ColorDist(B, F) +
+                         ColorDist(F, P(2, 1)) + (4.0 * ColorDist(E, I));
+        bool dominantGradient = (DOMINANT_DIRECTION_THRESHOLD * dist_H_F) < dist_E_I;
+        blendResult.z = ((dist_H_F < dist_E_I) && E != F && E != H)
+                            ? ((dominantGradient) ? BLEND_DOMINANT : BLEND_NORMAL)
+                            : BLEND_NONE;
+    }
+    // Pixel Tap Mapping: -|-|-|-|-
+    //                    -|A|B|-|-
+    //                    x|D|E|F|-
+    //                    x|G|H|I|-
+    //                    -|x|x|-|-
+    if (!((D == E && G == H) || (D == G && E == H))) {
+        float dist_G_E = ColorDist(P(-2, 1), D) + ColorDist(D, B) + ColorDist(P(-1, 2), H) +
+                         ColorDist(H, F) + (4.0 * ColorDist(G, E));
+        float dist_D_H = ColorDist(P(-2, 0), G) + ColorDist(G, P(0, 2)) + ColorDist(A, E) +
+                         ColorDist(E, I) + (4.0 * ColorDist(D, H));
+        bool dominantGradient = (DOMINANT_DIRECTION_THRESHOLD * dist_D_H) < dist_G_E;
+        blendResult.w = ((dist_G_E > dist_D_H) && E != D && E != H)
+                            ? ((dominantGradient) ? BLEND_DOMINANT : BLEND_NORMAL)
+                            : BLEND_NONE;
+    }
+    // Pixel Tap Mapping: -|-|x|x|-
+    //                    -|A|B|C|x
+    //                    -|D|E|F|x
+    //                    -|-|H|I|-
+    //                    -|-|-|-|-
+    if (!((B == C && E == F) || (B == E && C == F))) {
+        float dist_E_C = ColorDist(D, B) + ColorDist(B, P(1, -2)) + ColorDist(H, F) +
+                         ColorDist(F, P(2, -1)) + (4.0 * ColorDist(E, C));
+        float dist_B_F = ColorDist(A, E) + ColorDist(E, I) + ColorDist(P(0, -2), C) +
+                         ColorDist(C, P(2, 0)) + (4.0 * ColorDist(B, F));
+        bool dominantGradient = (DOMINANT_DIRECTION_THRESHOLD * dist_B_F) < dist_E_C;
+        blendResult.y = ((dist_E_C > dist_B_F) && E != B && E != F)
+                            ? ((dominantGradient) ? BLEND_DOMINANT : BLEND_NORMAL)
+                            : BLEND_NONE;
+    }
+    // Pixel Tap Mapping: -|x|x|-|-
+    //                    x|A|B|C|-
+    //                    x|D|E|F|-
+    //                    -|G|H|-|-
+    //                    -|-|-|-|-
+    if (!((A == B && D == E) || (A == D && B == E))) {
+        float dist_D_B = ColorDist(P(-2, 0), A) + ColorDist(A, P(0, -2)) + ColorDist(G, E) +
+                         ColorDist(E, C) + (4.0 * ColorDist(D, B));
+        float dist_A_E = ColorDist(P(-2, -1), D) + ColorDist(D, H) + ColorDist(P(-1, -2), B) +
+                         ColorDist(B, F) + (4.0 * ColorDist(A, E));
+        bool dominantGradient = (DOMINANT_DIRECTION_THRESHOLD * dist_D_B) < dist_A_E;
+        blendResult.x = ((dist_D_B < dist_A_E) && E != D && E != B)
+                            ? ((dominantGradient) ? BLEND_DOMINANT : BLEND_NORMAL)
+                            : BLEND_NONE;
+    }
+    vec4 res = E;
+    // Pixel Tap Mapping: -|-|-|-|-
+    //                    -|-|B|C|-
+    //                    -|D|E|F|x
+    //                    -|G|H|I|x
+    //                    -|-|x|x|-
+    if (blendResult.z != BLEND_NONE) {
+        float dist_F_G = ColorDist(F, G);
+        float dist_H_C = ColorDist(H, C);
+        bool doLineBlend = (blendResult.z == BLEND_DOMINANT ||
+                            !((blendResult.y != BLEND_NONE && !IsPixEqual(E, G)) ||
+                              (blendResult.w != BLEND_NONE && !IsPixEqual(E, C)) ||
+                              (IsPixEqual(G, H) && IsPixEqual(H, I) && IsPixEqual(I, F) &&
+                               IsPixEqual(F, C) && !IsPixEqual(E, I))));
+        vec2 origin = vec2(0.0, 1.0 / sqrt(2.0));
+        ivec2 direction = ivec2(1, -1);
+        if (doLineBlend) {
+            bool haveShallowLine =
+                (STEEP_DIRECTION_THRESHOLD * dist_F_G <= dist_H_C) && E != G && D != G;
+            bool haveSteepLine =
+                (STEEP_DIRECTION_THRESHOLD * dist_H_C <= dist_F_G) && E != C && B != C;
+            origin = haveShallowLine ? vec2(0.0, 0.25) : vec2(0.0, 0.5);
+            direction.x += haveShallowLine ? 1 : 0;
+            direction.y -= haveSteepLine ? 1 : 0;
+        }
+        vec4 blendPix = mix(H, F, step(ColorDist(E, F), ColorDist(E, H)));
+        res = mix(res, blendPix, GetLeftRatio(pos, origin, direction));
+    }
+    // Pixel Tap Mapping: -|-|-|-|-
+    //                    -|A|B|-|-
+    //                    x|D|E|F|-
+    //                    x|G|H|I|-
+    //                    -|x|x|-|-
+    if (blendResult.w != BLEND_NONE) {
+        float dist_H_A = ColorDist(H, A);
+        float dist_D_I = ColorDist(D, I);
+        bool doLineBlend = (blendResult.w == BLEND_DOMINANT ||
+                            !((blendResult.z != BLEND_NONE && !IsPixEqual(E, A)) ||
+                              (blendResult.x != BLEND_NONE && !IsPixEqual(E, I)) ||
+                              (IsPixEqual(A, D) && IsPixEqual(D, G) && IsPixEqual(G, H) &&
+                               IsPixEqual(H, I) && !IsPixEqual(E, G))));
+        vec2 origin = vec2(-1.0 / sqrt(2.0), 0.0);
+        ivec2 direction = ivec2(1, 1);
+        if (doLineBlend) {
+            bool haveShallowLine =
+                (STEEP_DIRECTION_THRESHOLD * dist_H_A <= dist_D_I) && E != A && B != A;
+            bool haveSteepLine =
+                (STEEP_DIRECTION_THRESHOLD * dist_D_I <= dist_H_A) && E != I && F != I;
+            origin = haveShallowLine ? vec2(-0.25, 0.0) : vec2(-0.5, 0.0);
+            direction.y += haveShallowLine ? 1 : 0;
+            direction.x += haveSteepLine ? 1 : 0;
+        }
+        origin = origin;
+        direction = direction;
+        vec4 blendPix = mix(H, D, step(ColorDist(E, D), ColorDist(E, H)));
+        res = mix(res, blendPix, GetLeftRatio(pos, origin, direction));
+    }
+    // Pixel Tap Mapping: -|-|x|x|-
+    //                    -|A|B|C|x
+    //                    -|D|E|F|x
+    //                    -|-|H|I|-
+    //                    -|-|-|-|-
+    if (blendResult.y != BLEND_NONE) {
+        float dist_B_I = ColorDist(B, I);
+        float dist_F_A = ColorDist(F, A);
+        bool doLineBlend = (blendResult.y == BLEND_DOMINANT ||
+                            !((blendResult.x != BLEND_NONE && !IsPixEqual(E, I)) ||
+                              (blendResult.z != BLEND_NONE && !IsPixEqual(E, A)) ||
+                              (IsPixEqual(I, F) && IsPixEqual(F, C) && IsPixEqual(C, B) &&
+                               IsPixEqual(B, A) && !IsPixEqual(E, C))));
+        vec2 origin = vec2(1.0 / sqrt(2.0), 0.0);
+        ivec2 direction = ivec2(-1, -1);
+        if (doLineBlend) {
+            bool haveShallowLine =
+                (STEEP_DIRECTION_THRESHOLD * dist_B_I <= dist_F_A) && E != I && H != I;
+            bool haveSteepLine =
+                (STEEP_DIRECTION_THRESHOLD * dist_F_A <= dist_B_I) && E != A && D != A;
+            origin = haveShallowLine ? vec2(0.25, 0.0) : vec2(0.5, 0.0);
+            direction.y -= haveShallowLine ? 1 : 0;
+            direction.x -= haveSteepLine ? 1 : 0;
+        }
+        vec4 blendPix = mix(F, B, step(ColorDist(E, B), ColorDist(E, F)));
+        res = mix(res, blendPix, GetLeftRatio(pos, origin, direction));
+    }
+    // Pixel Tap Mapping: -|x|x|-|-
+    //                    x|A|B|C|-
+    //                    x|D|E|F|-
+    //                    -|G|H|-|-
+    //                    -|-|-|-|-
+    if (blendResult.x != BLEND_NONE) {
+        float dist_D_C = ColorDist(D, C);
+        float dist_B_G = ColorDist(B, G);
+        bool doLineBlend = (blendResult.x == BLEND_DOMINANT ||
+                            !((blendResult.w != BLEND_NONE && !IsPixEqual(E, C)) ||
+                              (blendResult.y != BLEND_NONE && !IsPixEqual(E, G)) ||
+                              (IsPixEqual(C, B) && IsPixEqual(B, A) && IsPixEqual(A, D) &&
+                               IsPixEqual(D, G) && !IsPixEqual(E, A))));
+        vec2 origin = vec2(0.0, -1.0 / sqrt(2.0));
+        ivec2 direction = ivec2(-1, 1);
+        if (doLineBlend) {
+            bool haveShallowLine =
+                (STEEP_DIRECTION_THRESHOLD * dist_D_C <= dist_B_G) && E != C && F != C;
+            bool haveSteepLine =
+                (STEEP_DIRECTION_THRESHOLD * dist_B_G <= dist_D_C) && E != G && H != G;
+            origin = haveShallowLine ? vec2(0.0, -0.25) : vec2(0.0, -0.5);
+            direction.x -= haveShallowLine ? 1 : 0;
+            direction.y += haveSteepLine ? 1 : 0;
+        }
+        vec4 blendPix = mix(D, B, step(ColorDist(E, B), ColorDist(E, D)));
+        res = mix(res, blendPix, GetLeftRatio(pos, origin, direction));
+    }
+    frag_color = res;
+}
diff --git a/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h
new file mode 100644
index 000000000..aad10f308
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h
@@ -0,0 +1,33 @@
+// Copyright 2019 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include "video_core/renderer_opengl/gl_resource_manager.h"
+#include "video_core/renderer_opengl/gl_state.h"
+#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h"
+
+namespace OpenGL {
+
+class XbrzFreescale : public TextureFilterInterface {
+public:
+    static TextureFilterInfo GetInfo() {
+        TextureFilterInfo info;
+        info.name = "xBRZ freescale";
+        info.constructor = std::make_unique<XbrzFreescale, u16>;
+        return info;
+    }
+
+    XbrzFreescale(u16 scale_factor);
+    void scale(CachedSurface& surface, const Common::Rectangle<u32>& rect,
+               std::size_t buffer_offset) override;
+
+private:
+    OpenGLState state{};
+    OGLProgram program{};
+    OGLVertexArray vao{};
+    OGLFramebuffer draw_fbo{};
+    OGLSampler src_sampler{};
+};
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.vert b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.vert
new file mode 100644
index 000000000..adf45d564
--- /dev/null
+++ b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.vert
@@ -0,0 +1,17 @@
+//? #version 330
+out vec2 tex_coord;
+out vec2 source_size;
+out vec2 output_size;
+
+uniform sampler2D tex;
+uniform float scale;
+
+const vec2 vertices[4] =
+    vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));
+
+void main() {
+    gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
+    tex_coord = (vertices[gl_VertexID] + 1.0) / 2.0;
+    source_size = textureSize(tex, 0);
+    output_size = source_size * scale;
+}
diff --git a/src/video_core/video_core.cpp b/src/video_core/video_core.cpp
index b7e1d4885..a7ed5fb9e 100644
--- a/src/video_core/video_core.cpp
+++ b/src/video_core/video_core.cpp
@@ -57,6 +57,7 @@ ResultStatus Init(Frontend::EmuWindow& emu_window, Memory::MemorySystem& memory)
 void Shutdown() {
     Pica::Shutdown();
 
+    g_renderer->ShutDown();
     g_renderer.reset();
 
     LOG_DEBUG(Render, "shutdown OK");