From c4a49eb1ddc1da32e4f915fd7421161cedee2e8f Mon Sep 17 00:00:00 2001
From: Wollnashorn <Wollnashorn@users.noreply.github.com>
Date: Sun, 1 Jan 2023 19:22:46 +0100
Subject: [PATCH] video_core/opengl: Add FSR upscaling filter to the OpenGL
 renderer

---
 src/video_core/CMakeLists.txt                 |   4 +
 src/video_core/fsr.cpp                        | 148 ++++++++++++++++++
 src/video_core/fsr.h                          |  19 +++
 src/video_core/host_shaders/CMakeLists.txt    |  30 +++-
 .../host_shaders/opengl_fidelityfx_fsr.frag   | 108 +++++++++++++
 .../opengl_fidelityfx_fsr_easu.frag           |   9 ++
 .../opengl_fidelityfx_fsr_rcas.frag           |   9 ++
 src/video_core/renderer_opengl/gl_fsr.cpp     | 101 ++++++++++++
 src/video_core/renderer_opengl/gl_fsr.h       |  43 +++++
 .../renderer_opengl/renderer_opengl.cpp       |  90 ++++++++---
 .../renderer_opengl/renderer_opengl.h         |   3 +
 src/video_core/renderer_vulkan/vk_fsr.cpp     | 144 +----------------
 src/yuzu/configuration/configure_graphics.ui  |   2 +-
 src/yuzu/main.cpp                             |   9 --
 14 files changed, 547 insertions(+), 172 deletions(-)
 create mode 100644 src/video_core/fsr.cpp
 create mode 100644 src/video_core/fsr.h
 create mode 100644 src/video_core/host_shaders/opengl_fidelityfx_fsr.frag
 create mode 100644 src/video_core/host_shaders/opengl_fidelityfx_fsr_easu.frag
 create mode 100644 src/video_core/host_shaders/opengl_fidelityfx_fsr_rcas.frag
 create mode 100644 src/video_core/renderer_opengl/gl_fsr.cpp
 create mode 100644 src/video_core/renderer_opengl/gl_fsr.h

diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index b474eb3630..4742bcbe9d 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -52,6 +52,8 @@ add_library(video_core STATIC
     engines/puller.cpp
     engines/puller.h
     framebuffer_config.h
+    fsr.cpp
+    fsr.h
     host1x/codecs/codec.cpp
     host1x/codecs/codec.h
     host1x/codecs/h264.cpp
@@ -110,6 +112,8 @@ add_library(video_core STATIC
     renderer_opengl/gl_device.h
     renderer_opengl/gl_fence_manager.cpp
     renderer_opengl/gl_fence_manager.h
+    renderer_opengl/gl_fsr.cpp
+    renderer_opengl/gl_fsr.h
     renderer_opengl/gl_graphics_pipeline.cpp
     renderer_opengl/gl_graphics_pipeline.h
     renderer_opengl/gl_rasterizer.cpp
diff --git a/src/video_core/fsr.cpp b/src/video_core/fsr.cpp
new file mode 100644
index 0000000000..5653c64fcc
--- /dev/null
+++ b/src/video_core/fsr.cpp
@@ -0,0 +1,148 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <cmath>
+#include "video_core/fsr.h"
+
+namespace FSR {
+namespace {
+// Reimplementations of the constant generating functions in ffx_fsr1.h
+// GCC generated a lot of warnings when using the official header.
+u32 AU1_AH1_AF1(f32 f) {
+    static constexpr u32 base[512]{
+        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
+        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
+        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
+        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
+        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
+        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
+        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
+        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
+        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
+        0x0000, 0x0000, 0x0000, 0x0000, 0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040,
+        0x0080, 0x0100, 0x0200, 0x0400, 0x0800, 0x0c00, 0x1000, 0x1400, 0x1800, 0x1c00, 0x2000,
+        0x2400, 0x2800, 0x2c00, 0x3000, 0x3400, 0x3800, 0x3c00, 0x4000, 0x4400, 0x4800, 0x4c00,
+        0x5000, 0x5400, 0x5800, 0x5c00, 0x6000, 0x6400, 0x6800, 0x6c00, 0x7000, 0x7400, 0x7800,
+        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
+        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
+        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
+        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
+        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
+        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
+        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
+        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
+        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
+        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
+        0x7bff, 0x7bff, 0x7bff, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
+        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
+        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
+        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
+        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
+        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
+        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
+        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
+        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
+        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8001, 0x8002, 0x8004, 0x8008,
+        0x8010, 0x8020, 0x8040, 0x8080, 0x8100, 0x8200, 0x8400, 0x8800, 0x8c00, 0x9000, 0x9400,
+        0x9800, 0x9c00, 0xa000, 0xa400, 0xa800, 0xac00, 0xb000, 0xb400, 0xb800, 0xbc00, 0xc000,
+        0xc400, 0xc800, 0xcc00, 0xd000, 0xd400, 0xd800, 0xdc00, 0xe000, 0xe400, 0xe800, 0xec00,
+        0xf000, 0xf400, 0xf800, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
+        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
+        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
+        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
+        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
+        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
+        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
+        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
+        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
+        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
+        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
+    };
+    static constexpr s8 shift[512]{
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x17, 0x16,
+        0x15, 0x14, 0x13, 0x12, 0x11, 0x10, 0x0f, 0x0e, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+        0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+        0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x17,
+        0x16, 0x15, 0x14, 0x13, 0x12, 0x11, 0x10, 0x0f, 0x0e, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+        0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+        0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+        0x18, 0x18,
+    };
+    const u32 u = Common::BitCast<u32>(f);
+    const u32 i = u >> 23;
+    return base[i] + ((u & 0x7fffff) >> shift[i]);
+}
+
+u32 AU1_AH2_AF2(f32 a[2]) {
+    return AU1_AH1_AF1(a[0]) + (AU1_AH1_AF1(a[1]) << 16);
+}
+
+void FsrEasuCon(u32 con0[4], u32 con1[4], u32 con2[4], u32 con3[4], f32 inputViewportInPixelsX,
+                f32 inputViewportInPixelsY, f32 inputSizeInPixelsX, f32 inputSizeInPixelsY,
+                f32 outputSizeInPixelsX, f32 outputSizeInPixelsY) {
+    con0[0] = Common::BitCast<u32>(inputViewportInPixelsX / outputSizeInPixelsX);
+    con0[1] = Common::BitCast<u32>(inputViewportInPixelsY / outputSizeInPixelsY);
+    con0[2] = Common::BitCast<u32>(0.5f * inputViewportInPixelsX / outputSizeInPixelsX - 0.5f);
+    con0[3] = Common::BitCast<u32>(0.5f * inputViewportInPixelsY / outputSizeInPixelsY - 0.5f);
+    con1[0] = Common::BitCast<u32>(1.0f / inputSizeInPixelsX);
+    con1[1] = Common::BitCast<u32>(1.0f / inputSizeInPixelsY);
+    con1[2] = Common::BitCast<u32>(1.0f / inputSizeInPixelsX);
+    con1[3] = Common::BitCast<u32>(-1.0f / inputSizeInPixelsY);
+    con2[0] = Common::BitCast<u32>(-1.0f / inputSizeInPixelsX);
+    con2[1] = Common::BitCast<u32>(2.0f / inputSizeInPixelsY);
+    con2[2] = Common::BitCast<u32>(1.0f / inputSizeInPixelsX);
+    con2[3] = Common::BitCast<u32>(2.0f / inputSizeInPixelsY);
+    con3[0] = Common::BitCast<u32>(0.0f / inputSizeInPixelsX);
+    con3[1] = Common::BitCast<u32>(4.0f / inputSizeInPixelsY);
+    con3[2] = con3[3] = 0;
+}
+} // Anonymous namespace
+
+void FsrEasuConOffset(u32 con0[4], u32 con1[4], u32 con2[4], u32 con3[4],
+                      f32 inputViewportInPixelsX, f32 inputViewportInPixelsY,
+                      f32 inputSizeInPixelsX, f32 inputSizeInPixelsY, f32 outputSizeInPixelsX,
+                      f32 outputSizeInPixelsY, f32 inputOffsetInPixelsX, f32 inputOffsetInPixelsY) {
+    FsrEasuCon(con0, con1, con2, con3, inputViewportInPixelsX, inputViewportInPixelsY,
+               inputSizeInPixelsX, inputSizeInPixelsY, outputSizeInPixelsX, outputSizeInPixelsY);
+    con0[2] = Common::BitCast<u32>(0.5f * inputViewportInPixelsX / outputSizeInPixelsX - 0.5f +
+                                   inputOffsetInPixelsX);
+    con0[3] = Common::BitCast<u32>(0.5f * inputViewportInPixelsY / outputSizeInPixelsY - 0.5f +
+                                   inputOffsetInPixelsY);
+}
+
+void FsrRcasCon(u32* con, f32 sharpness) {
+    sharpness = std::exp2f(-sharpness);
+    f32 hSharp[2]{sharpness, sharpness};
+    con[0] = Common::BitCast<u32>(sharpness);
+    con[1] = AU1_AH2_AF2(hSharp);
+    con[2] = 0;
+    con[3] = 0;
+}
+} // namespace FSR
diff --git a/src/video_core/fsr.h b/src/video_core/fsr.h
new file mode 100644
index 0000000000..db0d4ec6fb
--- /dev/null
+++ b/src/video_core/fsr.h
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "common/bit_cast.h"
+#include "common/common_types.h"
+
+namespace FSR {
+// Reimplementations of the constant generating functions in ffx_fsr1.h
+// GCC generated a lot of warnings when using the official header.
+void FsrEasuConOffset(u32 con0[4], u32 con1[4], u32 con2[4], u32 con3[4],
+                      f32 inputViewportInPixelsX, f32 inputViewportInPixelsY,
+                      f32 inputSizeInPixelsX, f32 inputSizeInPixelsY, f32 outputSizeInPixelsX,
+                      f32 outputSizeInPixelsY, f32 inputOffsetInPixelsX, f32 inputOffsetInPixelsY);
+
+void FsrRcasCon(u32* con, f32 sharpness);
+
+} // namespace FSR
diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt
index e968ae2203..dad7b07d4a 100644
--- a/src/video_core/host_shaders/CMakeLists.txt
+++ b/src/video_core/host_shaders/CMakeLists.txt
@@ -3,12 +3,16 @@
 
 set(FIDELITYFX_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/externals/FidelityFX-FSR/ffx-fsr)
 
-set(GLSL_INCLUDES
-    fidelityfx_fsr.comp
+set(FIDELITYFX_FILES
     ${FIDELITYFX_INCLUDE_DIR}/ffx_a.h
     ${FIDELITYFX_INCLUDE_DIR}/ffx_fsr1.h
 )
 
+set(GLSL_INCLUDES
+    fidelityfx_fsr.comp
+    ${FIDELITYFX_FILES}
+)
+
 set(SHADER_FILES
     astc_decoder.comp
     blit_color_float.frag
@@ -24,6 +28,9 @@ set(SHADER_FILES
     fxaa.vert
     opengl_convert_s8d24.comp
     opengl_copy_bc4.comp
+    opengl_fidelityfx_fsr.frag
+    opengl_fidelityfx_fsr_easu.frag
+    opengl_fidelityfx_fsr_rcas.frag
     opengl_present.frag
     opengl_present.vert
     opengl_present_scaleforce.frag
@@ -118,6 +125,25 @@ foreach(FILENAME IN ITEMS ${SHADER_FILES})
     endif()
 endforeach()
 
+foreach(FILEPATH IN ITEMS ${FIDELITYFX_FILES})
+    get_filename_component(FILENAME ${FILEPATH} NAME)
+    string(REPLACE "." "_" HEADER_NAME ${FILENAME})
+    set(SOURCE_FILE ${FILEPATH})
+    set(SOURCE_HEADER_FILE ${SHADER_DIR}/${HEADER_NAME}.h)
+    add_custom_command(
+        OUTPUT
+            ${SOURCE_HEADER_FILE}
+        COMMAND
+            ${CMAKE_COMMAND} -P ${HEADER_GENERATOR} ${SOURCE_FILE} ${SOURCE_HEADER_FILE} ${INPUT_FILE}
+        MAIN_DEPENDENCY
+            ${SOURCE_FILE}
+        DEPENDS
+            ${INPUT_FILE}
+            # HEADER_GENERATOR should be included here but msbuild seems to assume it's always modified
+    )
+    set(SHADER_HEADERS ${SHADER_HEADERS} ${SOURCE_HEADER_FILE})
+endforeach()
+
 set(SHADER_SOURCES ${SHADER_FILES})
 list(APPEND SHADER_SOURCES ${GLSL_INCLUDES})
 
diff --git a/src/video_core/host_shaders/opengl_fidelityfx_fsr.frag b/src/video_core/host_shaders/opengl_fidelityfx_fsr.frag
new file mode 100644
index 0000000000..16d22f58e5
--- /dev/null
+++ b/src/video_core/host_shaders/opengl_fidelityfx_fsr.frag
@@ -0,0 +1,108 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+//!#version 460 core
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_ARB_shading_language_420pack : enable
+
+#extension GL_AMD_gpu_shader_half_float : enable
+#extension GL_NV_gpu_shader5 : enable
+
+// FidelityFX Super Resolution Sample
+//
+// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved.
+// 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.
+
+layout (location = 0) uniform uvec4 constants[4];
+
+#define A_GPU 1
+#define A_GLSL 1
+
+#ifdef YUZU_USE_FP16
+    #define A_HALF
+#endif
+#include "ffx_a.h"
+
+#ifndef YUZU_USE_FP16
+    layout (binding=0) uniform sampler2D InputTexture;
+    #if USE_EASU
+        #define FSR_EASU_F 1
+        AF4 FsrEasuRF(AF2 p) { AF4 res = textureGather(InputTexture, p, 0); return res; }
+        AF4 FsrEasuGF(AF2 p) { AF4 res = textureGather(InputTexture, p, 1); return res; }
+        AF4 FsrEasuBF(AF2 p) { AF4 res = textureGather(InputTexture, p, 2); return res; }
+    #endif
+    #if USE_RCAS
+        #define FSR_RCAS_F
+        AF4 FsrRcasLoadF(ASU2 p) { return texelFetch(InputTexture, ASU2(p), 0); }
+        void FsrRcasInputF(inout AF1 r, inout AF1 g, inout AF1 b) {}
+    #endif
+#else
+    layout (binding=0) uniform sampler2D InputTexture;
+    #if USE_EASU
+        #define FSR_EASU_H 1
+        AH4 FsrEasuRH(AF2 p) { AH4 res = AH4(textureGather(InputTexture, p, 0)); return res; }
+        AH4 FsrEasuGH(AF2 p) { AH4 res = AH4(textureGather(InputTexture, p, 1)); return res; }
+        AH4 FsrEasuBH(AF2 p) { AH4 res = AH4(textureGather(InputTexture, p, 2)); return res; }
+    #endif
+    #if USE_RCAS
+        #define FSR_RCAS_H
+        AH4 FsrRcasLoadH(ASW2 p) { return AH4(texelFetch(InputTexture, ASU2(p), 0)); }
+        void FsrRcasInputH(inout AH1 r,inout AH1 g,inout AH1 b){}
+    #endif
+#endif
+
+#include "ffx_fsr1.h"
+
+#if USE_RCAS
+    layout(location = 0) in vec2 frag_texcoord;
+#endif
+layout (location = 0) out vec4 frag_color;
+
+void CurrFilter(AU2 pos)
+{
+#if USE_EASU
+    #ifndef YUZU_USE_FP16
+        AF3 c;
+        FsrEasuF(c, pos, constants[0], constants[1], constants[2], constants[3]);
+        frag_color = AF4(c, 1.0);
+    #else
+        AH3 c;
+        FsrEasuH(c, pos, constants[0], constants[1], constants[2], constants[3]);
+        frag_color = AH4(c, 1.0);
+    #endif
+#endif
+#if USE_RCAS
+    #ifndef YUZU_USE_FP16
+        AF3 c;
+        FsrRcasF(c.r, c.g, c.b, pos, constants[0]);
+        frag_color = AF4(c, 1.0);
+    #else
+        AH3 c;
+        FsrRcasH(c.r, c.g, c.b, pos, constants[0]);
+        frag_color = AH4(c, 1.0);
+    #endif
+#endif
+}
+
+void main()
+{
+#if USE_RCAS
+    CurrFilter(AU2(frag_texcoord * vec2(textureSize(InputTexture, 0))));
+#else
+    CurrFilter(AU2(gl_FragCoord.xy));
+#endif
+}
diff --git a/src/video_core/host_shaders/opengl_fidelityfx_fsr_easu.frag b/src/video_core/host_shaders/opengl_fidelityfx_fsr_easu.frag
new file mode 100644
index 0000000000..d39f80ac19
--- /dev/null
+++ b/src/video_core/host_shaders/opengl_fidelityfx_fsr_easu.frag
@@ -0,0 +1,9 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#version 460 core
+#extension GL_GOOGLE_include_directive : enable
+
+#define USE_EASU 1
+
+#include "opengl_fidelityfx_fsr.frag"
diff --git a/src/video_core/host_shaders/opengl_fidelityfx_fsr_rcas.frag b/src/video_core/host_shaders/opengl_fidelityfx_fsr_rcas.frag
new file mode 100644
index 0000000000..cfa78ddc7a
--- /dev/null
+++ b/src/video_core/host_shaders/opengl_fidelityfx_fsr_rcas.frag
@@ -0,0 +1,9 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#version 460 core
+#extension GL_GOOGLE_include_directive : enable
+
+#define USE_RCAS 1
+
+#include "opengl_fidelityfx_fsr.frag"
diff --git a/src/video_core/renderer_opengl/gl_fsr.cpp b/src/video_core/renderer_opengl/gl_fsr.cpp
new file mode 100644
index 0000000000..77262dcf16
--- /dev/null
+++ b/src/video_core/renderer_opengl/gl_fsr.cpp
@@ -0,0 +1,101 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "common/settings.h"
+#include "video_core/fsr.h"
+#include "video_core/renderer_opengl/gl_fsr.h"
+#include "video_core/renderer_opengl/gl_shader_manager.h"
+#include "video_core/renderer_opengl/gl_shader_util.h"
+
+namespace OpenGL {
+using namespace FSR;
+
+using FsrConstants = std::array<u32, 4 * 4>;
+
+FSR::FSR(std::string_view fsr_vertex_source, std::string_view fsr_easu_source,
+         std::string_view fsr_rcas_source)
+    : fsr_vertex{CreateProgram(fsr_vertex_source, GL_VERTEX_SHADER)},
+      fsr_easu_frag{CreateProgram(fsr_easu_source, GL_FRAGMENT_SHADER)},
+      fsr_rcas_frag{CreateProgram(fsr_rcas_source, GL_FRAGMENT_SHADER)} {
+    glProgramUniform2f(fsr_vertex.handle, 0, 1.0f, 1.0f);
+    glProgramUniform2f(fsr_vertex.handle, 1, 0.0f, 0.0f);
+}
+
+FSR::~FSR() = default;
+
+void FSR::Draw(ProgramManager& program_manager, const Common::Rectangle<u32>& screen,
+               u32 input_image_width, u32 input_image_height,
+               const Common::Rectangle<int>& crop_rect) {
+
+    const auto output_image_width = screen.GetWidth();
+    const auto output_image_height = screen.GetHeight();
+
+    if (fsr_intermediate_tex.handle) {
+        GLint fsr_tex_width, fsr_tex_height;
+        glGetTextureLevelParameteriv(fsr_intermediate_tex.handle, 0, GL_TEXTURE_WIDTH,
+                                     &fsr_tex_width);
+        glGetTextureLevelParameteriv(fsr_intermediate_tex.handle, 0, GL_TEXTURE_HEIGHT,
+                                     &fsr_tex_height);
+        if (static_cast<u32>(fsr_tex_width) != output_image_width ||
+            static_cast<u32>(fsr_tex_height) != output_image_height) {
+            fsr_intermediate_tex.Release();
+        }
+    }
+    if (!fsr_intermediate_tex.handle) {
+        fsr_intermediate_tex.Create(GL_TEXTURE_2D);
+        glTextureStorage2D(fsr_intermediate_tex.handle, 1, GL_RGB16F, output_image_width,
+                           output_image_height);
+        glNamedFramebufferTexture(fsr_framebuffer.handle, GL_COLOR_ATTACHMENT0,
+                                  fsr_intermediate_tex.handle, 0);
+    }
+
+    GLint old_draw_fb;
+    glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &old_draw_fb);
+
+    glFrontFace(GL_CW);
+    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fsr_framebuffer.handle);
+    glViewportIndexedf(0, 0.0f, 0.0f, static_cast<GLfloat>(output_image_width),
+                       static_cast<GLfloat>(output_image_height));
+
+    FsrConstants constants;
+    FsrEasuConOffset(
+        constants.data() + 0, constants.data() + 4, constants.data() + 8, constants.data() + 12,
+
+        static_cast<f32>(crop_rect.GetWidth()), static_cast<f32>(crop_rect.GetHeight()),
+        static_cast<f32>(input_image_width), static_cast<f32>(input_image_height),
+        static_cast<f32>(output_image_width), static_cast<f32>(output_image_height),
+        static_cast<f32>(crop_rect.left), static_cast<f32>(crop_rect.top));
+
+    glProgramUniform4uiv(fsr_easu_frag.handle, 0, sizeof(constants), std::data(constants));
+
+    program_manager.BindPresentPrograms(fsr_vertex.handle, fsr_easu_frag.handle);
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+
+    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, old_draw_fb);
+    glBindTextureUnit(0, fsr_intermediate_tex.handle);
+
+    const float sharpening =
+        static_cast<float>(Settings::values.fsr_sharpening_slider.GetValue()) / 100.0f;
+
+    FsrRcasCon(constants.data(), sharpening);
+    glProgramUniform4uiv(fsr_rcas_frag.handle, 0, sizeof(constants), std::data(constants));
+}
+
+void FSR::InitBuffers() {
+    fsr_framebuffer.Create();
+}
+
+void FSR::ReleaseBuffers() {
+    fsr_framebuffer.Release();
+    fsr_intermediate_tex.Release();
+}
+
+const OGLProgram& FSR::GetPresentFragmentProgram() const noexcept {
+    return fsr_rcas_frag;
+}
+
+bool FSR::AreBuffersInitialized() const noexcept {
+    return fsr_framebuffer.handle;
+}
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/gl_fsr.h b/src/video_core/renderer_opengl/gl_fsr.h
new file mode 100644
index 0000000000..1f6ae3115f
--- /dev/null
+++ b/src/video_core/renderer_opengl/gl_fsr.h
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <string_view>
+
+#include "common/common_types.h"
+#include "common/math_util.h"
+#include "video_core/fsr.h"
+#include "video_core/renderer_opengl/gl_resource_manager.h"
+
+namespace OpenGL {
+
+class ProgramManager;
+
+class FSR {
+public:
+    explicit FSR(std::string_view fsr_vertex_source, std::string_view fsr_easu_source,
+                 std::string_view fsr_rcas_source);
+    ~FSR();
+
+    void Draw(ProgramManager& program_manager, const Common::Rectangle<u32>& screen,
+              u32 input_image_width, u32 input_image_height,
+              const Common::Rectangle<int>& crop_rect);
+
+    void InitBuffers();
+
+    void ReleaseBuffers();
+
+    [[nodiscard]] const OGLProgram& GetPresentFragmentProgram() const noexcept;
+
+    [[nodiscard]] bool AreBuffersInitialized() const noexcept;
+
+private:
+    OGLFramebuffer fsr_framebuffer;
+    OGLProgram fsr_vertex;
+    OGLProgram fsr_easu_frag;
+    OGLProgram fsr_rcas_frag;
+    OGLTexture fsr_intermediate_tex;
+};
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp
index de95f26340..2a74c1d05b 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.cpp
+++ b/src/video_core/renderer_opengl/renderer_opengl.cpp
@@ -17,8 +17,14 @@
 #include "core/frontend/emu_window.h"
 #include "core/memory.h"
 #include "core/telemetry_session.h"
+#include "video_core/host_shaders/ffx_a_h.h"
+#include "video_core/host_shaders/ffx_fsr1_h.h"
+#include "video_core/host_shaders/full_screen_triangle_vert.h"
 #include "video_core/host_shaders/fxaa_frag.h"
 #include "video_core/host_shaders/fxaa_vert.h"
+#include "video_core/host_shaders/opengl_fidelityfx_fsr_easu_frag.h"
+#include "video_core/host_shaders/opengl_fidelityfx_fsr_frag.h"
+#include "video_core/host_shaders/opengl_fidelityfx_fsr_rcas_frag.h"
 #include "video_core/host_shaders/opengl_present_frag.h"
 #include "video_core/host_shaders/opengl_present_scaleforce_frag.h"
 #include "video_core/host_shaders/opengl_present_vert.h"
@@ -31,6 +37,7 @@
 #include "video_core/host_shaders/smaa_edge_detection_vert.h"
 #include "video_core/host_shaders/smaa_neighborhood_blending_frag.h"
 #include "video_core/host_shaders/smaa_neighborhood_blending_vert.h"
+#include "video_core/renderer_opengl/gl_fsr.h"
 #include "video_core/renderer_opengl/gl_rasterizer.h"
 #include "video_core/renderer_opengl/gl_shader_manager.h"
 #include "video_core/renderer_opengl/gl_shader_util.h"
@@ -268,12 +275,17 @@ void RendererOpenGL::InitOpenGLObjects() {
     fxaa_vertex = CreateProgram(HostShaders::FXAA_VERT, GL_VERTEX_SHADER);
     fxaa_fragment = CreateProgram(HostShaders::FXAA_FRAG, GL_FRAGMENT_SHADER);
 
-    const auto SmaaShader = [](std::string_view specialized_source, GLenum stage) {
-        std::string shader_source{specialized_source};
-        constexpr std::string_view include_string = "#include \"opengl_smaa.glsl\"";
+    const auto replace_include = [](std::string& shader_source, std::string_view include_name,
+                                    std::string_view include_content) {
+        const std::string include_string = fmt::format("#include \"{}\"", include_name);
         const std::size_t pos = shader_source.find(include_string);
         ASSERT(pos != std::string::npos);
-        shader_source.replace(pos, include_string.size(), HostShaders::OPENGL_SMAA_GLSL);
+        shader_source.replace(pos, include_string.size(), include_content);
+    };
+
+    const auto SmaaShader = [&](std::string_view specialized_source, GLenum stage) {
+        std::string shader_source{specialized_source};
+        replace_include(shader_source, "opengl_smaa.glsl", HostShaders::OPENGL_SMAA_GLSL);
         return CreateProgram(shader_source, stage);
     };
 
@@ -298,14 +310,32 @@ void RendererOpenGL::InitOpenGLObjects() {
         CreateProgram(fmt::format("#version 460\n{}", HostShaders::OPENGL_PRESENT_SCALEFORCE_FRAG),
                       GL_FRAGMENT_SHADER);
 
+    std::string fsr_source{HostShaders::OPENGL_FIDELITYFX_FSR_FRAG};
+    replace_include(fsr_source, "ffx_a.h", HostShaders::FFX_A_H);
+    replace_include(fsr_source, "ffx_fsr1.h", HostShaders::FFX_FSR1_H);
+
+    std::string fsr_easu_frag_source{HostShaders::OPENGL_FIDELITYFX_FSR_EASU_FRAG};
+    std::string fsr_rcas_frag_source{HostShaders::OPENGL_FIDELITYFX_FSR_RCAS_FRAG};
+    replace_include(fsr_easu_frag_source, "opengl_fidelityfx_fsr.frag", fsr_source);
+    replace_include(fsr_rcas_frag_source, "opengl_fidelityfx_fsr.frag", fsr_source);
+
+    fsr = std::make_unique<FSR>(HostShaders::FULL_SCREEN_TRIANGLE_VERT, fsr_easu_frag_source,
+                                fsr_rcas_frag_source);
+
     // Generate presentation sampler
     present_sampler.Create();
     glSamplerParameteri(present_sampler.handle, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
     glSamplerParameteri(present_sampler.handle, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glSamplerParameteri(present_sampler.handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glSamplerParameteri(present_sampler.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+    glSamplerParameteri(present_sampler.handle, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
 
     present_sampler_nn.Create();
     glSamplerParameteri(present_sampler_nn.handle, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
     glSamplerParameteri(present_sampler_nn.handle, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glSamplerParameteri(present_sampler_nn.handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glSamplerParameteri(present_sampler_nn.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+    glSamplerParameteri(present_sampler_nn.handle, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
 
     // Generate VBO handle for drawing
     vertex_buffer.Create();
@@ -525,6 +555,31 @@ void RendererOpenGL::DrawScreen(const Layout::FramebufferLayout& layout) {
 
         glBindTextureUnit(0, aa_texture.handle);
     }
+    glDisablei(GL_SCISSOR_TEST, 0);
+
+    if (Settings::values.scaling_filter.GetValue() == Settings::ScalingFilter::Fsr) {
+        if (!fsr->AreBuffersInitialized()) {
+            fsr->InitBuffers();
+        }
+
+        auto crop_rect = framebuffer_crop_rect;
+        if (crop_rect.GetWidth() == 0) {
+            crop_rect.right = framebuffer_width;
+        }
+        if (crop_rect.GetHeight() == 0) {
+            crop_rect.bottom = framebuffer_height;
+        }
+        crop_rect = crop_rect.Scale(Settings::values.resolution_info.up_factor);
+        const auto fsr_input_width = Settings::values.resolution_info.ScaleUp(framebuffer_width);
+        const auto fsr_input_height = Settings::values.resolution_info.ScaleUp(framebuffer_height);
+        glBindSampler(0, present_sampler.handle);
+        fsr->Draw(program_manager, layout.screen, fsr_input_width, fsr_input_height, crop_rect);
+    } else {
+        if (fsr->AreBuffersInitialized()) {
+            fsr->ReleaseBuffers();
+        }
+    }
+
     const std::array ortho_matrix =
         MakeOrthographicMatrix(static_cast<float>(layout.width), static_cast<float>(layout.height));
 
@@ -540,10 +595,7 @@ void RendererOpenGL::DrawScreen(const Layout::FramebufferLayout& layout) {
         case Settings::ScalingFilter::ScaleForce:
             return present_scaleforce_fragment.handle;
         case Settings::ScalingFilter::Fsr:
-            LOG_WARNING(
-                Render_OpenGL,
-                "FidelityFX Super Resolution is not supported in OpenGL, changing to ScaleForce");
-            return present_scaleforce_fragment.handle;
+            return fsr->GetPresentFragmentProgram().handle;
         default:
             return present_bilinear_fragment.handle;
         }
@@ -578,15 +630,18 @@ void RendererOpenGL::DrawScreen(const Layout::FramebufferLayout& layout) {
     f32 scale_u = static_cast<f32>(framebuffer_width) / static_cast<f32>(screen_info.texture.width);
     f32 scale_v =
         static_cast<f32>(framebuffer_height) / static_cast<f32>(screen_info.texture.height);
-    // Scale the output by the crop width/height. This is commonly used with 1280x720 rendering
-    // (e.g. handheld mode) on a 1920x1080 framebuffer.
-    if (framebuffer_crop_rect.GetWidth() > 0) {
-        scale_u = static_cast<f32>(framebuffer_crop_rect.GetWidth()) /
-                  static_cast<f32>(screen_info.texture.width);
-    }
-    if (framebuffer_crop_rect.GetHeight() > 0) {
-        scale_v = static_cast<f32>(framebuffer_crop_rect.GetHeight()) /
-                  static_cast<f32>(screen_info.texture.height);
+
+    if (Settings::values.scaling_filter.GetValue() != Settings::ScalingFilter::Fsr) {
+        // Scale the output by the crop width/height. This is commonly used with 1280x720 rendering
+        // (e.g. handheld mode) on a 1920x1080 framebuffer.
+        if (framebuffer_crop_rect.GetWidth() > 0) {
+            scale_u = static_cast<f32>(framebuffer_crop_rect.GetWidth()) /
+                      static_cast<f32>(screen_info.texture.width);
+        }
+        if (framebuffer_crop_rect.GetHeight() > 0) {
+            scale_v = static_cast<f32>(framebuffer_crop_rect.GetHeight()) /
+                      static_cast<f32>(screen_info.texture.height);
+        }
     }
     if (Settings::values.anti_aliasing.GetValue() == Settings::AntiAliasing::Fxaa &&
         !screen_info.was_accelerated) {
@@ -612,7 +667,6 @@ void RendererOpenGL::DrawScreen(const Layout::FramebufferLayout& layout) {
     } else {
         glDisable(GL_FRAMEBUFFER_SRGB);
     }
-    glDisablei(GL_SCISSOR_TEST, 0);
     glViewportIndexedf(0, 0.0f, 0.0f, static_cast<GLfloat>(layout.width),
                        static_cast<GLfloat>(layout.height));
 
diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h
index cc97d7b263..f1d5fd9542 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.h
+++ b/src/video_core/renderer_opengl/renderer_opengl.h
@@ -10,6 +10,7 @@
 
 #include "video_core/renderer_base.h"
 #include "video_core/renderer_opengl/gl_device.h"
+#include "video_core/renderer_opengl/gl_fsr.h"
 #include "video_core/renderer_opengl/gl_rasterizer.h"
 #include "video_core/renderer_opengl/gl_resource_manager.h"
 #include "video_core/renderer_opengl/gl_shader_manager.h"
@@ -141,6 +142,8 @@ private:
     OGLTexture smaa_edges_tex;
     OGLTexture smaa_blend_tex;
 
+    std::unique_ptr<FSR> fsr;
+
     /// OpenGL framebuffer data
     std::vector<u8> gl_framebuffer_data;
 
diff --git a/src/video_core/renderer_vulkan/vk_fsr.cpp b/src/video_core/renderer_vulkan/vk_fsr.cpp
index 33daa8c1ce..df972cd545 100644
--- a/src/video_core/renderer_vulkan/vk_fsr.cpp
+++ b/src/video_core/renderer_vulkan/vk_fsr.cpp
@@ -1,12 +1,11 @@
 // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
 // SPDX-License-Identifier: GPL-2.0-or-later
 
-#include <cmath>
-#include "common/bit_cast.h"
 #include "common/common_types.h"
 #include "common/div_ceil.h"
 #include "common/settings.h"
 
+#include "video_core/fsr.h"
 #include "video_core/host_shaders/vulkan_fidelityfx_fsr_easu_fp16_comp_spv.h"
 #include "video_core/host_shaders/vulkan_fidelityfx_fsr_easu_fp32_comp_spv.h"
 #include "video_core/host_shaders/vulkan_fidelityfx_fsr_rcas_fp16_comp_spv.h"
@@ -17,146 +16,7 @@
 #include "video_core/vulkan_common/vulkan_device.h"
 
 namespace Vulkan {
-namespace {
-// Reimplementations of the constant generating functions in ffx_fsr1.h
-// GCC generated a lot of warnings when using the official header.
-u32 AU1_AH1_AF1(f32 f) {
-    static constexpr u32 base[512]{
-        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
-        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
-        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
-        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
-        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
-        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
-        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
-        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
-        0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
-        0x0000, 0x0000, 0x0000, 0x0000, 0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040,
-        0x0080, 0x0100, 0x0200, 0x0400, 0x0800, 0x0c00, 0x1000, 0x1400, 0x1800, 0x1c00, 0x2000,
-        0x2400, 0x2800, 0x2c00, 0x3000, 0x3400, 0x3800, 0x3c00, 0x4000, 0x4400, 0x4800, 0x4c00,
-        0x5000, 0x5400, 0x5800, 0x5c00, 0x6000, 0x6400, 0x6800, 0x6c00, 0x7000, 0x7400, 0x7800,
-        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
-        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
-        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
-        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
-        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
-        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
-        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
-        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
-        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
-        0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff, 0x7bff,
-        0x7bff, 0x7bff, 0x7bff, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
-        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
-        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
-        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
-        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
-        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
-        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
-        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
-        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
-        0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8001, 0x8002, 0x8004, 0x8008,
-        0x8010, 0x8020, 0x8040, 0x8080, 0x8100, 0x8200, 0x8400, 0x8800, 0x8c00, 0x9000, 0x9400,
-        0x9800, 0x9c00, 0xa000, 0xa400, 0xa800, 0xac00, 0xb000, 0xb400, 0xb800, 0xbc00, 0xc000,
-        0xc400, 0xc800, 0xcc00, 0xd000, 0xd400, 0xd800, 0xdc00, 0xe000, 0xe400, 0xe800, 0xec00,
-        0xf000, 0xf400, 0xf800, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
-        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
-        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
-        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
-        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
-        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
-        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
-        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
-        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
-        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
-        0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff, 0xfbff,
-    };
-    static constexpr s8 shift[512]{
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x17, 0x16,
-        0x15, 0x14, 0x13, 0x12, 0x11, 0x10, 0x0f, 0x0e, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
-        0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
-        0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x17,
-        0x16, 0x15, 0x14, 0x13, 0x12, 0x11, 0x10, 0x0f, 0x0e, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
-        0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
-        0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
-        0x18, 0x18,
-    };
-    const u32 u = Common::BitCast<u32>(f);
-    const u32 i = u >> 23;
-    return base[i] + ((u & 0x7fffff) >> shift[i]);
-}
-
-u32 AU1_AH2_AF2(f32 a[2]) {
-    return AU1_AH1_AF1(a[0]) + (AU1_AH1_AF1(a[1]) << 16);
-}
-
-void FsrEasuCon(u32 con0[4], u32 con1[4], u32 con2[4], u32 con3[4], f32 inputViewportInPixelsX,
-                f32 inputViewportInPixelsY, f32 inputSizeInPixelsX, f32 inputSizeInPixelsY,
-                f32 outputSizeInPixelsX, f32 outputSizeInPixelsY) {
-    con0[0] = Common::BitCast<u32>(inputViewportInPixelsX / outputSizeInPixelsX);
-    con0[1] = Common::BitCast<u32>(inputViewportInPixelsY / outputSizeInPixelsY);
-    con0[2] = Common::BitCast<u32>(0.5f * inputViewportInPixelsX / outputSizeInPixelsX - 0.5f);
-    con0[3] = Common::BitCast<u32>(0.5f * inputViewportInPixelsY / outputSizeInPixelsY - 0.5f);
-    con1[0] = Common::BitCast<u32>(1.0f / inputSizeInPixelsX);
-    con1[1] = Common::BitCast<u32>(1.0f / inputSizeInPixelsY);
-    con1[2] = Common::BitCast<u32>(1.0f / inputSizeInPixelsX);
-    con1[3] = Common::BitCast<u32>(-1.0f / inputSizeInPixelsY);
-    con2[0] = Common::BitCast<u32>(-1.0f / inputSizeInPixelsX);
-    con2[1] = Common::BitCast<u32>(2.0f / inputSizeInPixelsY);
-    con2[2] = Common::BitCast<u32>(1.0f / inputSizeInPixelsX);
-    con2[3] = Common::BitCast<u32>(2.0f / inputSizeInPixelsY);
-    con3[0] = Common::BitCast<u32>(0.0f / inputSizeInPixelsX);
-    con3[1] = Common::BitCast<u32>(4.0f / inputSizeInPixelsY);
-    con3[2] = con3[3] = 0;
-}
-
-void FsrEasuConOffset(u32 con0[4], u32 con1[4], u32 con2[4], u32 con3[4],
-                      f32 inputViewportInPixelsX, f32 inputViewportInPixelsY,
-                      f32 inputSizeInPixelsX, f32 inputSizeInPixelsY, f32 outputSizeInPixelsX,
-                      f32 outputSizeInPixelsY, f32 inputOffsetInPixelsX, f32 inputOffsetInPixelsY) {
-    FsrEasuCon(con0, con1, con2, con3, inputViewportInPixelsX, inputViewportInPixelsY,
-               inputSizeInPixelsX, inputSizeInPixelsY, outputSizeInPixelsX, outputSizeInPixelsY);
-    con0[2] = Common::BitCast<u32>(0.5f * inputViewportInPixelsX / outputSizeInPixelsX - 0.5f +
-                                   inputOffsetInPixelsX);
-    con0[3] = Common::BitCast<u32>(0.5f * inputViewportInPixelsY / outputSizeInPixelsY - 0.5f +
-                                   inputOffsetInPixelsY);
-}
-
-void FsrRcasCon(u32* con, f32 sharpness) {
-    sharpness = std::exp2f(-sharpness);
-    f32 hSharp[2]{sharpness, sharpness};
-    con[0] = Common::BitCast<u32>(sharpness);
-    con[1] = AU1_AH2_AF2(hSharp);
-    con[2] = 0;
-    con[3] = 0;
-}
-} // Anonymous namespace
+using namespace FSR;
 
 FSR::FSR(const Device& device_, MemoryAllocator& memory_allocator_, size_t image_count_,
          VkExtent2D output_size_)
diff --git a/src/yuzu/configuration/configure_graphics.ui b/src/yuzu/configuration/configure_graphics.ui
index bb9910a530..a45ec69ecf 100644
--- a/src/yuzu/configuration/configure_graphics.ui
+++ b/src/yuzu/configuration/configure_graphics.ui
@@ -460,7 +460,7 @@
              </item>
              <item>
               <property name="text">
-               <string>AMD FidelityFX™️ Super Resolution (Vulkan Only)</string>
+               <string>AMD FidelityFX™️ Super Resolution</string>
               </property>
              </item>
             </widget>
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 571eacf9f4..06ae122025 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -983,11 +983,6 @@ void GMainWindow::InitializeWidgets() {
     filter_status_button->setFocusPolicy(Qt::NoFocus);
     connect(filter_status_button, &QPushButton::clicked, this,
             &GMainWindow::OnToggleAdaptingFilter);
-    auto filter = Settings::values.scaling_filter.GetValue();
-    if (Settings::values.renderer_backend.GetValue() == Settings::RendererBackend::OpenGL &&
-        filter == Settings::ScalingFilter::Fsr) {
-        Settings::values.scaling_filter.SetValue(Settings::ScalingFilter::NearestNeighbor);
-    }
     UpdateFilterText();
     filter_status_button->setCheckable(true);
     filter_status_button->setChecked(true);
@@ -3469,10 +3464,6 @@ void GMainWindow::OnToggleAdaptingFilter() {
     } else {
         filter = static_cast<Settings::ScalingFilter>(static_cast<u32>(filter) + 1);
     }
-    if (Settings::values.renderer_backend.GetValue() == Settings::RendererBackend::OpenGL &&
-        filter == Settings::ScalingFilter::Fsr) {
-        filter = Settings::ScalingFilter::NearestNeighbor;
-    }
     Settings::values.scaling_filter.SetValue(filter);
     filter_status_button->setChecked(true);
     UpdateFilterText();