From 22f3a7e94ce0e325a8c3dfeb0814c4b82a42649d Mon Sep 17 00:00:00 2001
From: tfarley <tfarleygithub@gmail.com>
Date: Sat, 16 Apr 2016 18:57:57 -0400
Subject: [PATCH] HWRasterizer: Texture forwarding

---
 src/core/hle/service/fs/archive.cpp           |   1 +
 src/core/hle/service/gsp_gpu.cpp              |  31 +-
 src/core/hle/service/y2r_u.cpp                |  10 +-
 src/core/hw/gpu.cpp                           | 351 ++++----
 src/core/hw/gpu.h                             |   4 +-
 src/core/memory.cpp                           | 140 +++
 src/core/memory.h                             |  16 +
 src/video_core/debug_utils/debug_utils.cpp    |   4 +-
 src/video_core/pica.h                         |   2 +-
 src/video_core/rasterizer_interface.h         |  31 +-
 src/video_core/renderer_base.cpp              |   2 -
 .../renderer_opengl/gl_rasterizer.cpp         | 851 +++++++-----------
 .../renderer_opengl/gl_rasterizer.h           |  76 +-
 .../renderer_opengl/gl_rasterizer_cache.cpp   | 719 ++++++++++++++-
 .../renderer_opengl/gl_rasterizer_cache.h     | 215 ++++-
 src/video_core/renderer_opengl/gl_state.cpp   |  63 +-
 src/video_core/renderer_opengl/gl_state.h     |  25 +-
 .../renderer_opengl/renderer_opengl.cpp       | 128 +--
 .../renderer_opengl/renderer_opengl.h         |  44 +-
 src/video_core/swrasterizer.h                 |   6 +-
 20 files changed, 1749 insertions(+), 970 deletions(-)

diff --git a/src/core/hle/service/fs/archive.cpp b/src/core/hle/service/fs/archive.cpp
index e9588cb72..cc51ede0c 100644
--- a/src/core/hle/service/fs/archive.cpp
+++ b/src/core/hle/service/fs/archive.cpp
@@ -114,6 +114,7 @@ ResultVal<bool> File::SyncRequest() {
                 return read.Code();
             }
             cmd_buff[2] = static_cast<u32>(*read);
+            Memory::RasterizerFlushAndInvalidateRegion(Memory::VirtualToPhysicalAddress(address), length);
             break;
         }
 
diff --git a/src/core/hle/service/gsp_gpu.cpp b/src/core/hle/service/gsp_gpu.cpp
index 0c655395e..211fcf599 100644
--- a/src/core/hle/service/gsp_gpu.cpp
+++ b/src/core/hle/service/gsp_gpu.cpp
@@ -4,6 +4,7 @@
 
 #include "common/bit_field.h"
 #include "common/microprofile.h"
+#include "common/profiler.h"
 
 #include "core/memory.h"
 #include "core/hle/kernel/event.h"
@@ -15,8 +16,6 @@
 
 #include "video_core/gpu_debugger.h"
 #include "video_core/debug_utils/debug_utils.h"
-#include "video_core/renderer_base.h"
-#include "video_core/video_core.h"
 
 #include "gsp_gpu.h"
 
@@ -291,8 +290,6 @@ static void FlushDataCache(Service::Interface* self) {
     u32 size    = cmd_buff[2];
     u32 process = cmd_buff[4];
 
-    VideoCore::g_renderer->Rasterizer()->InvalidateRegion(Memory::VirtualToPhysicalAddress(address), size);
-
     // TODO(purpasmart96): Verify return header on HW
 
     cmd_buff[1] = RESULT_SUCCESS.raw; // No error
@@ -408,6 +405,8 @@ void SignalInterrupt(InterruptId interrupt_id) {
     g_interrupt_event->Signal();
 }
 
+MICROPROFILE_DEFINE(GPU_GSP_DMA, "GPU", "GSP DMA", MP_RGB(100, 0, 255));
+
 /// Executes the next GSP command
 static void ExecuteCommand(const Command& command, u32 thread_id) {
     // Utility function to convert register ID to address
@@ -419,18 +418,21 @@ static void ExecuteCommand(const Command& command, u32 thread_id) {
 
     // GX request DMA - typically used for copying memory from GSP heap to VRAM
     case CommandId::REQUEST_DMA:
-        VideoCore::g_renderer->Rasterizer()->FlushRegion(Memory::VirtualToPhysicalAddress(command.dma_request.source_address),
-                                                            command.dma_request.size);
+    {
+        MICROPROFILE_SCOPE(GPU_GSP_DMA);
+
+        // TODO: Consider attempting rasterizer-accelerated surface blit if that usage is ever possible/likely
+        Memory::RasterizerFlushRegion(Memory::VirtualToPhysicalAddress(command.dma_request.source_address),
+                            command.dma_request.size);
+        Memory::RasterizerFlushAndInvalidateRegion(Memory::VirtualToPhysicalAddress(command.dma_request.dest_address),
+                            command.dma_request.size);
 
         memcpy(Memory::GetPointer(command.dma_request.dest_address),
                Memory::GetPointer(command.dma_request.source_address),
                command.dma_request.size);
         SignalInterrupt(InterruptId::DMA);
-
-        VideoCore::g_renderer->Rasterizer()->InvalidateRegion(Memory::VirtualToPhysicalAddress(command.dma_request.dest_address),
-                                                          command.dma_request.size);
         break;
-
+    }
     // TODO: This will need some rework in the future. (why?)
     case CommandId::SUBMIT_GPU_CMDLIST:
     {
@@ -517,13 +519,8 @@ static void ExecuteCommand(const Command& command, u32 thread_id) {
 
     case CommandId::CACHE_FLUSH:
     {
-        for (auto& region : command.cache_flush.regions) {
-            if (region.size == 0)
-                break;
-
-            VideoCore::g_renderer->Rasterizer()->InvalidateRegion(
-                Memory::VirtualToPhysicalAddress(region.address), region.size);
-        }
+        // NOTE: Rasterizer flushing handled elsewhere in CPU read/write and other GPU handlers
+        // Use command.cache_flush.regions to implement this handler
         break;
     }
 
diff --git a/src/core/hle/service/y2r_u.cpp b/src/core/hle/service/y2r_u.cpp
index 22f373adf..1672ad775 100644
--- a/src/core/hle/service/y2r_u.cpp
+++ b/src/core/hle/service/y2r_u.cpp
@@ -12,9 +12,6 @@
 #include "core/hle/service/y2r_u.h"
 #include "core/hw/y2r.h"
 
-#include "video_core/renderer_base.h"
-#include "video_core/video_core.h"
-
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 // Namespace Y2R_U
 
@@ -262,13 +259,12 @@ static void SetAlpha(Service::Interface* self) {
 static void StartConversion(Service::Interface* self) {
     u32* cmd_buff = Kernel::GetCommandBuffer();
 
-    HW::Y2R::PerformConversion(conversion);
-
     // dst_image_size would seem to be perfect for this, but it doesn't include the gap :(
     u32 total_output_size = conversion.input_lines *
         (conversion.dst.transfer_unit + conversion.dst.gap);
-    VideoCore::g_renderer->Rasterizer()->InvalidateRegion(
-        Memory::VirtualToPhysicalAddress(conversion.dst.address), total_output_size);
+    Memory::RasterizerFlushAndInvalidateRegion(Memory::VirtualToPhysicalAddress(conversion.dst.address), total_output_size);
+
+    HW::Y2R::PerformConversion(conversion);
 
     LOG_DEBUG(Service_Y2R, "called");
     completion_event->Signal();
diff --git a/src/core/hw/gpu.cpp b/src/core/hw/gpu.cpp
index 7e2f9cdfa..2fe856293 100644
--- a/src/core/hw/gpu.cpp
+++ b/src/core/hw/gpu.cpp
@@ -115,21 +115,39 @@ inline void Write(u32 addr, const T data) {
                 u8* start = Memory::GetPhysicalPointer(config.GetStartAddress());
                 u8* end = Memory::GetPhysicalPointer(config.GetEndAddress());
 
-                if (config.fill_24bit) {
-                    // fill with 24-bit values
-                    for (u8* ptr = start; ptr < end; ptr += 3) {
-                        ptr[0] = config.value_24bit_r;
-                        ptr[1] = config.value_24bit_g;
-                        ptr[2] = config.value_24bit_b;
+                // TODO: Consider always accelerating and returning vector of
+                //       regions that the accelerated fill did not cover to
+                //       reduce/eliminate the fill that the cpu has to do.
+                //       This would also mean that the flush below is not needed.
+                //       Fill should first flush all surfaces that touch but are
+                //       not completely within the fill range.
+                //       Then fill all completely covered surfaces, and return the
+                //       regions that were between surfaces or within the touching
+                //       ones for cpu to manually fill here.
+                if (!VideoCore::g_renderer->Rasterizer()->AccelerateFill(config)) {
+                    Memory::RasterizerFlushAndInvalidateRegion(config.GetStartAddress(), config.GetEndAddress() - config.GetStartAddress());
+
+                    if (config.fill_24bit) {
+                        // fill with 24-bit values
+                        for (u8* ptr = start; ptr < end; ptr += 3) {
+                            ptr[0] = config.value_24bit_r;
+                            ptr[1] = config.value_24bit_g;
+                            ptr[2] = config.value_24bit_b;
+                        }
+                    } else if (config.fill_32bit) {
+                        // fill with 32-bit values
+                        if (end > start) {
+                            u32 value = config.value_32bit;
+                            size_t len = (end - start) / sizeof(u32);
+                            for (size_t i = 0; i < len; ++i)
+                                memcpy(&start[i * sizeof(u32)], &value, sizeof(u32));
+                        }
+                    } else {
+                        // fill with 16-bit values
+                        u16 value_16bit = config.value_16bit.Value();
+                        for (u8* ptr = start; ptr < end; ptr += sizeof(u16))
+                            memcpy(ptr, &value_16bit, sizeof(u16));
                     }
-                } else if (config.fill_32bit) {
-                    // fill with 32-bit values
-                    for (u32* ptr = (u32*)start; ptr < (u32*)end; ++ptr)
-                        *ptr = config.value_32bit;
-                } else {
-                    // fill with 16-bit values
-                    for (u16* ptr = (u16*)start; ptr < (u16*)end; ++ptr)
-                        *ptr = config.value_16bit;
                 }
 
                 LOG_TRACE(HW_GPU, "MemoryFill from 0x%08x to 0x%08x", config.GetStartAddress(), config.GetEndAddress());
@@ -139,8 +157,6 @@ inline void Write(u32 addr, const T data) {
                 } else {
                     GSP_GPU::SignalInterrupt(GSP_GPU::InterruptId::PSC1);
                 }
-
-                VideoCore::g_renderer->Rasterizer()->InvalidateRegion(config.GetStartAddress(), config.GetEndAddress() - config.GetStartAddress());
             }
 
             // Reset "trigger" flag and set the "finish" flag
@@ -161,184 +177,185 @@ inline void Write(u32 addr, const T data) {
             if (Pica::g_debug_context)
                 Pica::g_debug_context->OnEvent(Pica::DebugContext::Event::IncomingDisplayTransfer, nullptr);
 
-            u8* src_pointer = Memory::GetPhysicalPointer(config.GetPhysicalInputAddress());
-            u8* dst_pointer = Memory::GetPhysicalPointer(config.GetPhysicalOutputAddress());
+            if (!VideoCore::g_renderer->Rasterizer()->AccelerateDisplayTransfer(config)) {
+                u8* src_pointer = Memory::GetPhysicalPointer(config.GetPhysicalInputAddress());
+                u8* dst_pointer = Memory::GetPhysicalPointer(config.GetPhysicalOutputAddress());
 
-            if (config.is_texture_copy) {
-                u32 input_width = config.texture_copy.input_width * 16;
-                u32 input_gap = config.texture_copy.input_gap * 16;
-                u32 output_width = config.texture_copy.output_width * 16;
-                u32 output_gap = config.texture_copy.output_gap * 16;
+                if (config.is_texture_copy) {
+                    u32 input_width = config.texture_copy.input_width * 16;
+                    u32 input_gap = config.texture_copy.input_gap * 16;
+                    u32 output_width = config.texture_copy.output_width * 16;
+                    u32 output_gap = config.texture_copy.output_gap * 16;
 
-                size_t contiguous_input_size = config.texture_copy.size / input_width * (input_width + input_gap);
-                VideoCore::g_renderer->Rasterizer()->FlushRegion(config.GetPhysicalInputAddress(), contiguous_input_size);
+                    size_t contiguous_input_size = config.texture_copy.size / input_width * (input_width + input_gap);
+                    Memory::RasterizerFlushRegion(config.GetPhysicalInputAddress(), contiguous_input_size);
 
-                u32 remaining_size = config.texture_copy.size;
-                u32 remaining_input = input_width;
-                u32 remaining_output = output_width;
-                while (remaining_size > 0) {
-                    u32 copy_size = std::min({ remaining_input, remaining_output, remaining_size });
+                    size_t contiguous_output_size = config.texture_copy.size / output_width * (output_width + output_gap);
+                    Memory::RasterizerFlushAndInvalidateRegion(config.GetPhysicalOutputAddress(), contiguous_output_size);
 
-                    std::memcpy(dst_pointer, src_pointer, copy_size);
-                    src_pointer += copy_size;
-                    dst_pointer += copy_size;
+                    u32 remaining_size = config.texture_copy.size;
+                    u32 remaining_input = input_width;
+                    u32 remaining_output = output_width;
+                    while (remaining_size > 0) {
+                        u32 copy_size = std::min({ remaining_input, remaining_output, remaining_size });
 
-                    remaining_input -= copy_size;
-                    remaining_output -= copy_size;
-                    remaining_size -= copy_size;
+                        std::memcpy(dst_pointer, src_pointer, copy_size);
+                        src_pointer += copy_size;
+                        dst_pointer += copy_size;
 
-                    if (remaining_input == 0) {
-                        remaining_input = input_width;
-                        src_pointer += input_gap;
-                    }
-                    if (remaining_output == 0) {
-                        remaining_output = output_width;
-                        dst_pointer += output_gap;
-                    }
-                }
+                        remaining_input -= copy_size;
+                        remaining_output -= copy_size;
+                        remaining_size -= copy_size;
 
-                LOG_TRACE(HW_GPU, "TextureCopy: 0x%X bytes from 0x%08X(%u+%u)-> 0x%08X(%u+%u), flags 0x%08X",
-                    config.texture_copy.size,
-                    config.GetPhysicalInputAddress(), input_width, input_gap,
-                    config.GetPhysicalOutputAddress(), output_width, output_gap,
-                    config.flags);
-
-                size_t contiguous_output_size = config.texture_copy.size / output_width * (output_width + output_gap);
-                VideoCore::g_renderer->Rasterizer()->InvalidateRegion(config.GetPhysicalOutputAddress(), contiguous_output_size);
-
-                GSP_GPU::SignalInterrupt(GSP_GPU::InterruptId::PPF);
-                break;
-            }
-
-            if (config.scaling > config.ScaleXY) {
-                LOG_CRITICAL(HW_GPU, "Unimplemented display transfer scaling mode %u", config.scaling.Value());
-                UNIMPLEMENTED();
-                break;
-            }
-
-            if (config.input_linear && config.scaling != config.NoScale) {
-                LOG_CRITICAL(HW_GPU, "Scaling is only implemented on tiled input");
-                UNIMPLEMENTED();
-                break;
-            }
-
-            bool horizontal_scale = config.scaling != config.NoScale;
-            bool vertical_scale = config.scaling == config.ScaleXY;
-
-            u32 output_width = config.output_width >> horizontal_scale;
-            u32 output_height = config.output_height >> vertical_scale;
-
-            u32 input_size = config.input_width * config.input_height * GPU::Regs::BytesPerPixel(config.input_format);
-            u32 output_size = output_width * output_height * GPU::Regs::BytesPerPixel(config.output_format);
-
-            VideoCore::g_renderer->Rasterizer()->FlushRegion(config.GetPhysicalInputAddress(), input_size);
-
-            for (u32 y = 0; y < output_height; ++y) {
-                for (u32 x = 0; x < output_width; ++x) {
-                    Math::Vec4<u8> src_color;
-
-                    // Calculate the [x,y] position of the input image
-                    // based on the current output position and the scale
-                    u32 input_x = x << horizontal_scale;
-                    u32 input_y = y << vertical_scale;
-
-                    if (config.flip_vertically) {
-                        // Flip the y value of the output data,
-                        // we do this after calculating the [x,y] position of the input image
-                        // to account for the scaling options.
-                        y = output_height - y - 1;
-                    }
-
-                    u32 dst_bytes_per_pixel = GPU::Regs::BytesPerPixel(config.output_format);
-                    u32 src_bytes_per_pixel = GPU::Regs::BytesPerPixel(config.input_format);
-                    u32 src_offset;
-                    u32 dst_offset;
-
-                    if (config.input_linear) {
-                        if (!config.dont_swizzle) {
-                            // Interpret the input as linear and the output as tiled
-                            u32 coarse_y = y & ~7;
-                            u32 stride = output_width * dst_bytes_per_pixel;
-
-                            src_offset = (input_x + input_y * config.input_width) * src_bytes_per_pixel;
-                            dst_offset = VideoCore::GetMortonOffset(x, y, dst_bytes_per_pixel) + coarse_y * stride;
-                        } else {
-                           // Both input and output are linear
-                            src_offset = (input_x + input_y * config.input_width) * src_bytes_per_pixel;
-                            dst_offset = (x + y * output_width) * dst_bytes_per_pixel;
+                        if (remaining_input == 0) {
+                            remaining_input = input_width;
+                            src_pointer += input_gap;
                         }
-                    } else {
-                        if (!config.dont_swizzle) {
-                            // Interpret the input as tiled and the output as linear
-                            u32 coarse_y = input_y & ~7;
-                            u32 stride = config.input_width * src_bytes_per_pixel;
-
-                            src_offset = VideoCore::GetMortonOffset(input_x, input_y, src_bytes_per_pixel) + coarse_y * stride;
-                            dst_offset = (x + y * output_width) * dst_bytes_per_pixel;
-                        } else {
-                            // Both input and output are tiled
-                            u32 out_coarse_y = y & ~7;
-                            u32 out_stride = output_width * dst_bytes_per_pixel;
-
-                            u32 in_coarse_y = input_y & ~7;
-                            u32 in_stride = config.input_width * src_bytes_per_pixel;
-
-                            src_offset = VideoCore::GetMortonOffset(input_x, input_y, src_bytes_per_pixel) + in_coarse_y * in_stride;
-                            dst_offset = VideoCore::GetMortonOffset(x, y, dst_bytes_per_pixel) + out_coarse_y * out_stride;
+                        if (remaining_output == 0) {
+                            remaining_output = output_width;
+                            dst_pointer += output_gap;
                         }
                     }
 
-                    const u8* src_pixel = src_pointer + src_offset;
-                    src_color = DecodePixel(config.input_format, src_pixel);
-                    if (config.scaling == config.ScaleX) {
-                        Math::Vec4<u8> pixel = DecodePixel(config.input_format, src_pixel + src_bytes_per_pixel);
-                        src_color = ((src_color + pixel) / 2).Cast<u8>();
-                    } else if (config.scaling == config.ScaleXY) {
-                        Math::Vec4<u8> pixel1 = DecodePixel(config.input_format, src_pixel + 1 * src_bytes_per_pixel);
-                        Math::Vec4<u8> pixel2 = DecodePixel(config.input_format, src_pixel + 2 * src_bytes_per_pixel);
-                        Math::Vec4<u8> pixel3 = DecodePixel(config.input_format, src_pixel + 3 * src_bytes_per_pixel);
-                        src_color = (((src_color + pixel1) + (pixel2 + pixel3)) / 4).Cast<u8>();
-                    }
+                    LOG_TRACE(HW_GPU, "TextureCopy: 0x%X bytes from 0x%08X(%u+%u)-> 0x%08X(%u+%u), flags 0x%08X",
+                        config.texture_copy.size,
+                        config.GetPhysicalInputAddress(), input_width, input_gap,
+                        config.GetPhysicalOutputAddress(), output_width, output_gap,
+                        config.flags);
 
-                    u8* dst_pixel = dst_pointer + dst_offset;
-                    switch (config.output_format) {
-                    case Regs::PixelFormat::RGBA8:
-                        Color::EncodeRGBA8(src_color, dst_pixel);
-                        break;
+                    GSP_GPU::SignalInterrupt(GSP_GPU::InterruptId::PPF);
+                    break;
+                }
 
-                    case Regs::PixelFormat::RGB8:
-                        Color::EncodeRGB8(src_color, dst_pixel);
-                        break;
+                if (config.scaling > config.ScaleXY) {
+                    LOG_CRITICAL(HW_GPU, "Unimplemented display transfer scaling mode %u", config.scaling.Value());
+                    UNIMPLEMENTED();
+                    break;
+                }
 
-                    case Regs::PixelFormat::RGB565:
-                        Color::EncodeRGB565(src_color, dst_pixel);
-                        break;
+                if (config.input_linear && config.scaling != config.NoScale) {
+                    LOG_CRITICAL(HW_GPU, "Scaling is only implemented on tiled input");
+                    UNIMPLEMENTED();
+                    break;
+                }
 
-                    case Regs::PixelFormat::RGB5A1:
-                        Color::EncodeRGB5A1(src_color, dst_pixel);
-                        break;
+                int horizontal_scale = config.scaling != config.NoScale ? 1 : 0;
+                int vertical_scale = config.scaling == config.ScaleXY ? 1 : 0;
 
-                    case Regs::PixelFormat::RGBA4:
-                        Color::EncodeRGBA4(src_color, dst_pixel);
-                        break;
+                u32 output_width = config.output_width >> horizontal_scale;
+                u32 output_height = config.output_height >> vertical_scale;
 
-                    default:
-                        LOG_ERROR(HW_GPU, "Unknown destination framebuffer format %x", config.output_format.Value());
-                        break;
+                u32 input_size = config.input_width * config.input_height * GPU::Regs::BytesPerPixel(config.input_format);
+                u32 output_size = output_width * output_height * GPU::Regs::BytesPerPixel(config.output_format);
+
+                Memory::RasterizerFlushRegion(config.GetPhysicalInputAddress(), input_size);
+                Memory::RasterizerFlushAndInvalidateRegion(config.GetPhysicalOutputAddress(), output_size);
+
+                for (u32 y = 0; y < output_height; ++y) {
+                    for (u32 x = 0; x < output_width; ++x) {
+                        Math::Vec4<u8> src_color;
+
+                        // Calculate the [x,y] position of the input image
+                        // based on the current output position and the scale
+                        u32 input_x = x << horizontal_scale;
+                        u32 input_y = y << vertical_scale;
+
+                        if (config.flip_vertically) {
+                            // Flip the y value of the output data,
+                            // we do this after calculating the [x,y] position of the input image
+                            // to account for the scaling options.
+                            y = output_height - y - 1;
+                        }
+
+                        u32 dst_bytes_per_pixel = GPU::Regs::BytesPerPixel(config.output_format);
+                        u32 src_bytes_per_pixel = GPU::Regs::BytesPerPixel(config.input_format);
+                        u32 src_offset;
+                        u32 dst_offset;
+
+                        if (config.input_linear) {
+                            if (!config.dont_swizzle) {
+                                // Interpret the input as linear and the output as tiled
+                                u32 coarse_y = y & ~7;
+                                u32 stride = output_width * dst_bytes_per_pixel;
+
+                                src_offset = (input_x + input_y * config.input_width) * src_bytes_per_pixel;
+                                dst_offset = VideoCore::GetMortonOffset(x, y, dst_bytes_per_pixel) + coarse_y * stride;
+                            } else {
+                                // Both input and output are linear
+                                src_offset = (input_x + input_y * config.input_width) * src_bytes_per_pixel;
+                                dst_offset = (x + y * output_width) * dst_bytes_per_pixel;
+                            }
+                        } else {
+                            if (!config.dont_swizzle) {
+                                // Interpret the input as tiled and the output as linear
+                                u32 coarse_y = input_y & ~7;
+                                u32 stride = config.input_width * src_bytes_per_pixel;
+
+                                src_offset = VideoCore::GetMortonOffset(input_x, input_y, src_bytes_per_pixel) + coarse_y * stride;
+                                dst_offset = (x + y * output_width) * dst_bytes_per_pixel;
+                            } else {
+                                // Both input and output are tiled
+                                u32 out_coarse_y = y & ~7;
+                                u32 out_stride = output_width * dst_bytes_per_pixel;
+
+                                u32 in_coarse_y = input_y & ~7;
+                                u32 in_stride = config.input_width * src_bytes_per_pixel;
+
+                                src_offset = VideoCore::GetMortonOffset(input_x, input_y, src_bytes_per_pixel) + in_coarse_y * in_stride;
+                                dst_offset = VideoCore::GetMortonOffset(x, y, dst_bytes_per_pixel) + out_coarse_y * out_stride;
+                            }
+                        }
+
+                        const u8* src_pixel = src_pointer + src_offset;
+                        src_color = DecodePixel(config.input_format, src_pixel);
+                        if (config.scaling == config.ScaleX) {
+                            Math::Vec4<u8> pixel = DecodePixel(config.input_format, src_pixel + src_bytes_per_pixel);
+                            src_color = ((src_color + pixel) / 2).Cast<u8>();
+                        } else if (config.scaling == config.ScaleXY) {
+                            Math::Vec4<u8> pixel1 = DecodePixel(config.input_format, src_pixel + 1 * src_bytes_per_pixel);
+                            Math::Vec4<u8> pixel2 = DecodePixel(config.input_format, src_pixel + 2 * src_bytes_per_pixel);
+                            Math::Vec4<u8> pixel3 = DecodePixel(config.input_format, src_pixel + 3 * src_bytes_per_pixel);
+                            src_color = (((src_color + pixel1) + (pixel2 + pixel3)) / 4).Cast<u8>();
+                        }
+
+                        u8* dst_pixel = dst_pointer + dst_offset;
+                        switch (config.output_format) {
+                        case Regs::PixelFormat::RGBA8:
+                            Color::EncodeRGBA8(src_color, dst_pixel);
+                            break;
+
+                        case Regs::PixelFormat::RGB8:
+                            Color::EncodeRGB8(src_color, dst_pixel);
+                            break;
+
+                        case Regs::PixelFormat::RGB565:
+                            Color::EncodeRGB565(src_color, dst_pixel);
+                            break;
+
+                        case Regs::PixelFormat::RGB5A1:
+                            Color::EncodeRGB5A1(src_color, dst_pixel);
+                            break;
+
+                        case Regs::PixelFormat::RGBA4:
+                            Color::EncodeRGBA4(src_color, dst_pixel);
+                            break;
+
+                        default:
+                            LOG_ERROR(HW_GPU, "Unknown destination framebuffer format %x", config.output_format.Value());
+                            break;
+                        }
                     }
                 }
-            }
 
-            LOG_TRACE(HW_GPU, "DisplayTriggerTransfer: 0x%08x bytes from 0x%08x(%ux%u)-> 0x%08x(%ux%u), dst format %x, flags 0x%08X",
+                LOG_TRACE(HW_GPU, "DisplayTriggerTransfer: 0x%08x bytes from 0x%08x(%ux%u)-> 0x%08x(%ux%u), dst format %x, flags 0x%08X",
                       config.output_height * output_width * GPU::Regs::BytesPerPixel(config.output_format),
                       config.GetPhysicalInputAddress(), config.input_width.Value(), config.input_height.Value(),
                       config.GetPhysicalOutputAddress(), output_width, output_height,
                       config.output_format.Value(), config.flags);
+            }
 
             g_regs.display_transfer_config.trigger = 0;
             GSP_GPU::SignalInterrupt(GSP_GPU::InterruptId::PPF);
-
-            VideoCore::g_renderer->Rasterizer()->InvalidateRegion(config.GetPhysicalOutputAddress(), output_size);
         }
         break;
     }
diff --git a/src/core/hw/gpu.h b/src/core/hw/gpu.h
index a00adbf53..da4c345b4 100644
--- a/src/core/hw/gpu.h
+++ b/src/core/hw/gpu.h
@@ -78,7 +78,7 @@ struct Regs {
 
     INSERT_PADDING_WORDS(0x4);
 
-    struct {
+    struct MemoryFillConfig {
         u32 address_start;
         u32 address_end;
 
@@ -165,7 +165,7 @@ struct Regs {
 
     INSERT_PADDING_WORDS(0x169);
 
-    struct {
+    struct DisplayTransferConfig {
         u32 input_address;
         u32 output_address;
 
diff --git a/src/core/memory.cpp b/src/core/memory.cpp
index 7de5bd15d..ee9b69f81 100644
--- a/src/core/memory.cpp
+++ b/src/core/memory.cpp
@@ -15,6 +15,9 @@
 #include "core/memory_setup.h"
 #include "core/mmio.h"
 
+#include "video_core/renderer_base.h"
+#include "video_core/video_core.h"
+
 namespace Memory {
 
 enum class PageType {
@@ -22,8 +25,12 @@ enum class PageType {
     Unmapped,
     /// Page is mapped to regular memory. This is the only type you can get pointers to.
     Memory,
+    /// Page is mapped to regular memory, but also needs to check for rasterizer cache flushing and invalidation
+    RasterizerCachedMemory,
     /// Page is mapped to a I/O region. Writing and reading to this page is handled by functions.
     Special,
+    /// Page is mapped to a I/O region, but also needs to check for rasterizer cache flushing and invalidation
+    RasterizerCachedSpecial,
 };
 
 struct SpecialRegion {
@@ -57,6 +64,12 @@ struct PageTable {
      * the corresponding entry in `pointers` MUST be set to null.
      */
     std::array<PageType, NUM_ENTRIES> attributes;
+
+    /**
+     * Indicates the number of externally cached resources touching a page that should be
+     * flushed before the memory is accessed
+     */
+    std::array<u8, NUM_ENTRIES> cached_res_count;
 };
 
 /// Singular page table used for the singleton process
@@ -72,8 +85,15 @@ static void MapPages(u32 base, u32 size, u8* memory, PageType type) {
     while (base != end) {
         ASSERT_MSG(base < PageTable::NUM_ENTRIES, "out of range mapping at %08X", base);
 
+        // Since pages are unmapped on shutdown after video core is shutdown, the renderer may be null here
+        if (current_page_table->attributes[base] == PageType::RasterizerCachedMemory ||
+            current_page_table->attributes[base] == PageType::RasterizerCachedSpecial) {
+            RasterizerFlushAndInvalidateRegion(VirtualToPhysicalAddress(base << PAGE_BITS), PAGE_SIZE);
+        }
+
         current_page_table->attributes[base] = type;
         current_page_table->pointers[base] = memory;
+        current_page_table->cached_res_count[base] = 0;
 
         base += 1;
         if (memory != nullptr)
@@ -84,6 +104,7 @@ static void MapPages(u32 base, u32 size, u8* memory, PageType type) {
 void InitMemoryMap() {
     main_page_table.pointers.fill(nullptr);
     main_page_table.attributes.fill(PageType::Unmapped);
+    main_page_table.cached_res_count.fill(0);
 }
 
 void MapMemoryRegion(VAddr base, u32 size, u8* target) {
@@ -106,6 +127,28 @@ void UnmapRegion(VAddr base, u32 size) {
     MapPages(base / PAGE_SIZE, size / PAGE_SIZE, nullptr, PageType::Unmapped);
 }
 
+/**
+ * Gets a pointer to the exact memory at the virtual address (i.e. not page aligned)
+ * using a VMA from the current process
+ */
+static u8* GetPointerFromVMA(VAddr vaddr) {
+    u8* direct_pointer = nullptr;
+
+    auto& vma = Kernel::g_current_process->vm_manager.FindVMA(vaddr)->second;
+    switch (vma.type) {
+    case Kernel::VMAType::AllocatedMemoryBlock:
+        direct_pointer = vma.backing_block->data() + vma.offset;
+        break;
+    case Kernel::VMAType::BackingMemory:
+        direct_pointer = vma.backing_memory;
+        break;
+    default:
+        UNREACHABLE();
+    }
+
+    return direct_pointer + (vaddr - vma.base);
+}
+
 /**
  * This function should only be called for virtual addreses with attribute `PageType::Special`.
  */
@@ -126,6 +169,7 @@ template <typename T>
 T Read(const VAddr vaddr) {
     const u8* page_pointer = current_page_table->pointers[vaddr >> PAGE_BITS];
     if (page_pointer) {
+        // NOTE: Avoid adding any extra logic to this fast-path block
         T value;
         std::memcpy(&value, &page_pointer[vaddr & PAGE_MASK], sizeof(T));
         return value;
@@ -139,8 +183,22 @@ T Read(const VAddr vaddr) {
     case PageType::Memory:
         ASSERT_MSG(false, "Mapped memory page without a pointer @ %08X", vaddr);
         break;
+    case PageType::RasterizerCachedMemory:
+    {
+        RasterizerFlushRegion(VirtualToPhysicalAddress(vaddr), sizeof(T));
+
+        T value;
+        std::memcpy(&value, GetPointerFromVMA(vaddr), sizeof(T));
+        return value;
+    }
     case PageType::Special:
         return ReadMMIO<T>(GetMMIOHandler(vaddr), vaddr);
+    case PageType::RasterizerCachedSpecial:
+    {
+        RasterizerFlushRegion(VirtualToPhysicalAddress(vaddr), sizeof(T));
+
+        return ReadMMIO<T>(GetMMIOHandler(vaddr), vaddr);
+    }
     default:
         UNREACHABLE();
     }
@@ -153,6 +211,7 @@ template <typename T>
 void Write(const VAddr vaddr, const T data) {
     u8* page_pointer = current_page_table->pointers[vaddr >> PAGE_BITS];
     if (page_pointer) {
+        // NOTE: Avoid adding any extra logic to this fast-path block
         std::memcpy(&page_pointer[vaddr & PAGE_MASK], &data, sizeof(T));
         return;
     }
@@ -165,9 +224,23 @@ void Write(const VAddr vaddr, const T data) {
     case PageType::Memory:
         ASSERT_MSG(false, "Mapped memory page without a pointer @ %08X", vaddr);
         break;
+    case PageType::RasterizerCachedMemory:
+    {
+        RasterizerFlushAndInvalidateRegion(VirtualToPhysicalAddress(vaddr), sizeof(T));
+
+        std::memcpy(GetPointerFromVMA(vaddr), &data, sizeof(T));
+        break;
+    }
     case PageType::Special:
         WriteMMIO<T>(GetMMIOHandler(vaddr), vaddr, data);
         break;
+    case PageType::RasterizerCachedSpecial:
+    {
+        RasterizerFlushAndInvalidateRegion(VirtualToPhysicalAddress(vaddr), sizeof(T));
+
+        WriteMMIO<T>(GetMMIOHandler(vaddr), vaddr, data);
+        break;
+    }
     default:
         UNREACHABLE();
     }
@@ -179,6 +252,10 @@ u8* GetPointer(const VAddr vaddr) {
         return page_pointer + (vaddr & PAGE_MASK);
     }
 
+    if (current_page_table->attributes[vaddr >> PAGE_BITS] == PageType::RasterizerCachedMemory) {
+        return GetPointerFromVMA(vaddr);
+    }
+
     LOG_ERROR(HW_Memory, "unknown GetPointer @ 0x%08x", vaddr);
     return nullptr;
 }
@@ -187,6 +264,69 @@ u8* GetPhysicalPointer(PAddr address) {
     return GetPointer(PhysicalToVirtualAddress(address));
 }
 
+void RasterizerMarkRegionCached(PAddr start, u32 size, int count_delta) {
+    if (start == 0) {
+        return;
+    }
+
+    u32 num_pages = ((start + size - 1) >> PAGE_BITS) - (start >> PAGE_BITS) + 1;
+    PAddr paddr = start;
+
+    for (unsigned i = 0; i < num_pages; ++i) {
+        VAddr vaddr = PhysicalToVirtualAddress(paddr);
+        u8& res_count = current_page_table->cached_res_count[vaddr >> PAGE_BITS];
+        ASSERT_MSG(count_delta <= UINT8_MAX - res_count, "Rasterizer resource cache counter overflow!");
+        ASSERT_MSG(count_delta >= -res_count, "Rasterizer resource cache counter underflow!");
+
+        // Switch page type to cached if now cached
+        if (res_count == 0) {
+            PageType& page_type = current_page_table->attributes[vaddr >> PAGE_BITS];
+            switch (page_type) {
+            case PageType::Memory:
+                page_type = PageType::RasterizerCachedMemory;
+                current_page_table->pointers[vaddr >> PAGE_BITS] = nullptr;
+                break;
+            case PageType::Special:
+                page_type = PageType::RasterizerCachedSpecial;
+                break;
+            default:
+                UNREACHABLE();
+            }
+        }
+
+        res_count += count_delta;
+
+        // Switch page type to uncached if now uncached
+        if (res_count == 0) {
+            PageType& page_type = current_page_table->attributes[vaddr >> PAGE_BITS];
+            switch (page_type) {
+            case PageType::RasterizerCachedMemory:
+                page_type = PageType::Memory;
+                current_page_table->pointers[vaddr >> PAGE_BITS] = GetPointerFromVMA(vaddr & ~PAGE_MASK);
+                break;
+            case PageType::RasterizerCachedSpecial:
+                page_type = PageType::Special;
+                break;
+            default:
+                UNREACHABLE();
+            }
+        }
+        paddr += PAGE_SIZE;
+    }
+}
+
+void RasterizerFlushRegion(PAddr start, u32 size) {
+    if (VideoCore::g_renderer != nullptr) {
+        VideoCore::g_renderer->Rasterizer()->FlushRegion(start, size);
+    }
+}
+
+void RasterizerFlushAndInvalidateRegion(PAddr start, u32 size) {
+    if (VideoCore::g_renderer != nullptr) {
+        VideoCore::g_renderer->Rasterizer()->FlushAndInvalidateRegion(start, size);
+    }
+}
+
 u8 Read8(const VAddr addr) {
     return Read<u8>(addr);
 }
diff --git a/src/core/memory.h b/src/core/memory.h
index 5af72b7a7..9caa3c3f5 100644
--- a/src/core/memory.h
+++ b/src/core/memory.h
@@ -148,4 +148,20 @@ VAddr PhysicalToVirtualAddress(PAddr addr);
  */
 u8* GetPhysicalPointer(PAddr address);
 
+/**
+ * Adds the supplied value to the rasterizer resource cache counter of each
+ * page touching the region.
+ */
+void RasterizerMarkRegionCached(PAddr start, u32 size, int count_delta);
+
+/**
+ * Flushes any externally cached rasterizer resources touching the given region.
+ */
+void RasterizerFlushRegion(PAddr start, u32 size);
+
+/**
+ * Flushes and invalidates any externally cached rasterizer resources touching the given region.
+ */
+void RasterizerFlushAndInvalidateRegion(PAddr start, u32 size);
+
 }
diff --git a/src/video_core/debug_utils/debug_utils.cpp b/src/video_core/debug_utils/debug_utils.cpp
index c3a9c9598..1f058c4e2 100644
--- a/src/video_core/debug_utils/debug_utils.cpp
+++ b/src/video_core/debug_utils/debug_utils.cpp
@@ -47,8 +47,8 @@ void DebugContext::OnEvent(Event event, void* data) {
     {
         std::unique_lock<std::mutex> lock(breakpoint_mutex);
 
-        // Commit the hardware renderer's framebuffer so it will show on debug widgets
-        VideoCore::g_renderer->Rasterizer()->FlushFramebuffer();
+        // Commit the rasterizer's caches so framebuffers, render targets, etc. will show on debug widgets
+        VideoCore::g_renderer->Rasterizer()->FlushAll();
 
         // TODO: Should stop the CPU thread here once we multithread emulation.
 
diff --git a/src/video_core/pica.h b/src/video_core/pica.h
index 4552ff81c..1810eca98 100644
--- a/src/video_core/pica.h
+++ b/src/video_core/pica.h
@@ -577,7 +577,7 @@ struct Regs {
         }
     }
 
-    struct {
+    struct FramebufferConfig {
         INSERT_PADDING_WORDS(0x3);
 
         union {
diff --git a/src/video_core/rasterizer_interface.h b/src/video_core/rasterizer_interface.h
index 008c5827b..bf7101665 100644
--- a/src/video_core/rasterizer_interface.h
+++ b/src/video_core/rasterizer_interface.h
@@ -6,6 +6,10 @@
 
 #include "common/common_types.h"
 
+#include "core/hw/gpu.h"
+
+struct ScreenInfo;
+
 namespace Pica {
 namespace Shader {
 struct OutputVertex;
@@ -18,12 +22,6 @@ class RasterizerInterface {
 public:
     virtual ~RasterizerInterface() {}
 
-    /// Initialize API-specific GPU objects
-    virtual void InitObjects() = 0;
-
-    /// Reset the rasterizer, such as flushing all caches and updating all state
-    virtual void Reset() = 0;
-
     /// Queues the primitive formed by the given vertices for rendering
     virtual void AddTriangle(const Pica::Shader::OutputVertex& v0,
                              const Pica::Shader::OutputVertex& v1,
@@ -32,17 +30,26 @@ public:
     /// Draw the current batch of triangles
     virtual void DrawTriangles() = 0;
 
-    /// Commit the rasterizer's framebuffer contents immediately to the current 3DS memory framebuffer
-    virtual void FlushFramebuffer() = 0;
-
     /// Notify rasterizer that the specified PICA register has been changed
     virtual void NotifyPicaRegisterChanged(u32 id) = 0;
 
-    /// Notify rasterizer that any caches of the specified region should be flushed to 3DS memory.
+    /// Notify rasterizer that all caches should be flushed to 3DS memory
+    virtual void FlushAll() = 0;
+
+    /// Notify rasterizer that any caches of the specified region should be flushed to 3DS memory
     virtual void FlushRegion(PAddr addr, u32 size) = 0;
 
-    /// Notify rasterizer that any caches of the specified region should be discraded and reloaded from 3DS memory.
-    virtual void InvalidateRegion(PAddr addr, u32 size) = 0;
+    /// Notify rasterizer that any caches of the specified region should be flushed to 3DS memory and invalidated
+    virtual void FlushAndInvalidateRegion(PAddr addr, u32 size) = 0;
+
+    /// Attempt to use a faster method to perform a display transfer
+    virtual bool AccelerateDisplayTransfer(const GPU::Regs::DisplayTransferConfig& config) { return false; }
+
+    /// Attempt to use a faster method to fill a region
+    virtual bool AccelerateFill(const GPU::Regs::MemoryFillConfig& config) { return false; }
+
+    /// Attempt to use a faster method to display the framebuffer to screen
+    virtual bool AccelerateDisplay(const GPU::Regs::FramebufferConfig& config, PAddr framebuffer_addr, u32 pixel_stride, ScreenInfo& screen_info) { return false; }
 };
 
 }
diff --git a/src/video_core/renderer_base.cpp b/src/video_core/renderer_base.cpp
index 101f84eb9..ccd497de0 100644
--- a/src/video_core/renderer_base.cpp
+++ b/src/video_core/renderer_base.cpp
@@ -21,7 +21,5 @@ void RendererBase::RefreshRasterizerSetting() {
         } else {
             rasterizer = std::make_unique<VideoCore::SWRasterizer>();
         }
-        rasterizer->InitObjects();
-        rasterizer->Reset();
     }
 }
diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp
index 6ca9f45e2..da4121c35 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer.cpp
+++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp
@@ -36,10 +36,7 @@ static bool IsPassThroughTevStage(const Pica::Regs::TevStageConfig& stage) {
             stage.GetAlphaMultiplier() == 1);
 }
 
-RasterizerOpenGL::RasterizerOpenGL() : cached_fb_color_addr(0), cached_fb_depth_addr(0) { }
-RasterizerOpenGL::~RasterizerOpenGL() { }
-
-void RasterizerOpenGL::InitObjects() {
+RasterizerOpenGL::RasterizerOpenGL() : shader_dirty(true) {
     // Create sampler objects
     for (size_t i = 0; i < texture_samplers.size(); ++i) {
         texture_samplers[i].Create();
@@ -61,6 +58,10 @@ void RasterizerOpenGL::InitObjects() {
 
     uniform_block_data.dirty = true;
 
+    for (unsigned index = 0; index < lighting_luts.size(); index++) {
+        uniform_block_data.lut_dirty[index] = true;
+    }
+
     // Set vertex attributes
     glVertexAttribPointer(GLShader::ATTRIBUTE_POSITION, 4, GL_FLOAT, GL_FALSE, sizeof(HardwareVertex), (GLvoid*)offsetof(HardwareVertex, position));
     glEnableVertexAttribArray(GLShader::ATTRIBUTE_POSITION);
@@ -81,70 +82,24 @@ void RasterizerOpenGL::InitObjects() {
     glVertexAttribPointer(GLShader::ATTRIBUTE_VIEW, 3, GL_FLOAT, GL_FALSE, sizeof(HardwareVertex), (GLvoid*)offsetof(HardwareVertex, view));
     glEnableVertexAttribArray(GLShader::ATTRIBUTE_VIEW);
 
-    SetShader();
-
-    // Create textures for OGL framebuffer that will be rendered to, initially 1x1 to succeed in framebuffer creation
-    fb_color_texture.texture.Create();
-    ReconfigureColorTexture(fb_color_texture, Pica::Regs::ColorFormat::RGBA8, 1, 1);
-
-    state.texture_units[0].texture_2d = fb_color_texture.texture.handle;
-    state.Apply();
-
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
-
-    state.texture_units[0].texture_2d = 0;
-    state.Apply();
-
-    fb_depth_texture.texture.Create();
-    ReconfigureDepthTexture(fb_depth_texture, Pica::Regs::DepthFormat::D16, 1, 1);
-
-    state.texture_units[0].texture_2d = fb_depth_texture.texture.handle;
-    state.Apply();
-
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE);
-
-    state.texture_units[0].texture_2d = 0;
-    state.Apply();
-
-    // Configure OpenGL framebuffer
+    // Create render framebuffer
     framebuffer.Create();
 
-    state.draw.framebuffer = framebuffer.handle;
+    // Allocate and bind lighting lut textures
+    for (size_t i = 0; i < lighting_luts.size(); ++i) {
+        lighting_luts[i].Create();
+        state.lighting_luts[i].texture_1d = lighting_luts[i].handle;
+    }
     state.Apply();
 
-    glActiveTexture(GL_TEXTURE0);
-    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fb_color_texture.texture.handle, 0);
-    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, fb_depth_texture.texture.handle, 0);
-
-    for (size_t i = 0; i < lighting_lut.size(); ++i) {
-        lighting_lut[i].Create();
-        state.lighting_lut[i].texture_1d = lighting_lut[i].handle;
-
+    for (size_t i = 0; i < lighting_luts.size(); ++i) {
         glActiveTexture(GL_TEXTURE3 + i);
-        glBindTexture(GL_TEXTURE_1D, state.lighting_lut[i].texture_1d);
-
         glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA32F, 256, 0, GL_RGBA, GL_FLOAT, nullptr);
         glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
         glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
     }
-    state.Apply();
 
-    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
-    ASSERT_MSG(status == GL_FRAMEBUFFER_COMPLETE,
-               "OpenGL rasterizer framebuffer setup failed, status %X", status);
-}
-
-void RasterizerOpenGL::Reset() {
+    // Sync fixed function OpenGL state
     SyncCullMode();
     SyncDepthModifiers();
     SyncBlendEnabled();
@@ -156,10 +111,10 @@ void RasterizerOpenGL::Reset() {
     SyncColorWriteMask();
     SyncStencilWriteMask();
     SyncDepthWriteMask();
+}
 
-    SetShader();
+RasterizerOpenGL::~RasterizerOpenGL() {
 
-    res_cache.InvalidateAll();
 }
 
 /**
@@ -196,47 +151,98 @@ void RasterizerOpenGL::DrawTriangles() {
     if (vertex_batch.empty())
         return;
 
-    SyncFramebuffer();
-    SyncDrawState();
+    const auto& regs = Pica::g_state.regs;
 
-    if (state.draw.shader_dirty) {
-        SetShader();
-        state.draw.shader_dirty = false;
+    // Sync and bind the framebuffer surfaces
+    CachedSurface* color_surface;
+    CachedSurface* depth_surface;
+    MathUtil::Rectangle<int> rect;
+    std::tie(color_surface, depth_surface, rect) = res_cache.GetFramebufferSurfaces(regs.framebuffer);
+
+    state.draw.draw_framebuffer = framebuffer.handle;
+    state.Apply();
+
+    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color_surface != nullptr ? color_surface->texture.handle : 0, 0);
+    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth_surface != nullptr ? depth_surface->texture.handle : 0, 0);
+    bool has_stencil = regs.framebuffer.depth_format == Pica::Regs::DepthFormat::D24S8;
+    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, (has_stencil && depth_surface != nullptr) ? depth_surface->texture.handle : 0, 0);
+
+    if (OpenGLState::CheckFBStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+        return;
     }
 
-    for (unsigned index = 0; index < lighting_lut.size(); index++) {
+    // Sync the viewport
+    // These registers hold half-width and half-height, so must be multiplied by 2
+    GLsizei viewport_width = (GLsizei)Pica::float24::FromRaw(regs.viewport_size_x).ToFloat32() * 2;
+    GLsizei viewport_height = (GLsizei)Pica::float24::FromRaw(regs.viewport_size_y).ToFloat32() * 2;
+
+    glViewport((GLint)(rect.left + regs.viewport_corner.x * color_surface->res_scale_width),
+               (GLint)(rect.bottom + regs.viewport_corner.y * color_surface->res_scale_height),
+               (GLsizei)(viewport_width * color_surface->res_scale_width), (GLsizei)(viewport_height * color_surface->res_scale_height));
+
+    // Sync and bind the texture surfaces
+    const auto pica_textures = regs.GetTextures();
+    for (unsigned texture_index = 0; texture_index < pica_textures.size(); ++texture_index) {
+        const auto& texture = pica_textures[texture_index];
+
+        if (texture.enabled) {
+            texture_samplers[texture_index].SyncWithConfig(texture.config);
+            CachedSurface* surface = res_cache.GetTextureSurface(texture);
+            if (surface != nullptr) {
+                state.texture_units[texture_index].texture_2d = surface->texture.handle;
+            } else {
+                // Can occur when texture addr is null or its memory is unmapped/invalid
+                state.texture_units[texture_index].texture_2d = 0;
+            }
+        } else {
+            state.texture_units[texture_index].texture_2d = 0;
+        }
+    }
+
+    // Sync and bind the shader
+    if (shader_dirty) {
+        SetShader();
+        shader_dirty = false;
+    }
+
+    // Sync the lighting luts
+    for (unsigned index = 0; index < lighting_luts.size(); index++) {
         if (uniform_block_data.lut_dirty[index]) {
             SyncLightingLUT(index);
             uniform_block_data.lut_dirty[index] = false;
         }
     }
 
+    // Sync the uniform data
     if (uniform_block_data.dirty) {
         glBufferData(GL_UNIFORM_BUFFER, sizeof(UniformData), &uniform_block_data.data, GL_STATIC_DRAW);
         uniform_block_data.dirty = false;
     }
 
+    state.Apply();
+
+    // Draw the vertex batch
     glBufferData(GL_ARRAY_BUFFER, vertex_batch.size() * sizeof(HardwareVertex), vertex_batch.data(), GL_STREAM_DRAW);
     glDrawArrays(GL_TRIANGLES, 0, (GLsizei)vertex_batch.size());
 
+    // Mark framebuffer surfaces as dirty
+    // TODO: Restrict invalidation area to the viewport
+    if (color_surface != nullptr) {
+        color_surface->dirty = true;
+        res_cache.FlushRegion(color_surface->addr, color_surface->size, color_surface, true);
+    }
+    if (depth_surface != nullptr) {
+        depth_surface->dirty = true;
+        res_cache.FlushRegion(depth_surface->addr, depth_surface->size, depth_surface, true);
+    }
+
     vertex_batch.clear();
 
-    // Flush the resource cache at the current depth and color framebuffer addresses for render-to-texture
-    const auto& regs = Pica::g_state.regs;
-
-    u32 cached_fb_color_size = Pica::Regs::BytesPerColorPixel(fb_color_texture.format)
-                               * fb_color_texture.width * fb_color_texture.height;
-
-    u32 cached_fb_depth_size = Pica::Regs::BytesPerDepthPixel(fb_depth_texture.format)
-                               * fb_depth_texture.width * fb_depth_texture.height;
-
-    res_cache.InvalidateInRange(cached_fb_color_addr, cached_fb_color_size, true);
-    res_cache.InvalidateInRange(cached_fb_depth_addr, cached_fb_depth_size, true);
-}
-
-void RasterizerOpenGL::FlushFramebuffer() {
-    CommitColorBuffer();
-    CommitDepthBuffer();
+    // Unbind textures for potential future use as framebuffer attachments
+    for (unsigned texture_index = 0; texture_index < pica_textures.size(); ++texture_index) {
+        state.texture_units[texture_index].texture_2d = 0;
+    }
+    state.Apply();
 }
 
 void RasterizerOpenGL::NotifyPicaRegisterChanged(u32 id) {
@@ -268,7 +274,7 @@ void RasterizerOpenGL::NotifyPicaRegisterChanged(u32 id) {
     // Alpha test
     case PICA_REG_INDEX(output_merger.alpha_test):
         SyncAlphaTest();
-        state.draw.shader_dirty = true;
+        shader_dirty = true;
         break;
 
     // Sync GL stencil test + stencil write mask
@@ -334,7 +340,7 @@ void RasterizerOpenGL::NotifyPicaRegisterChanged(u32 id) {
     case PICA_REG_INDEX(tev_stage5.color_op):
     case PICA_REG_INDEX(tev_stage5.color_scale):
     case PICA_REG_INDEX(tev_combiner_buffer_input):
-        state.draw.shader_dirty = true;
+        shader_dirty = true;
         break;
     case PICA_REG_INDEX(tev_stage0.const_r):
         SyncTevConstColor(0, regs.tev_stage0);
@@ -521,41 +527,257 @@ void RasterizerOpenGL::NotifyPicaRegisterChanged(u32 id) {
     }
 }
 
-void RasterizerOpenGL::FlushRegion(PAddr addr, u32 size) {
-    const auto& regs = Pica::g_state.regs;
-
-    u32 cached_fb_color_size = Pica::Regs::BytesPerColorPixel(fb_color_texture.format)
-                               * fb_color_texture.width * fb_color_texture.height;
-
-    u32 cached_fb_depth_size = Pica::Regs::BytesPerDepthPixel(fb_depth_texture.format)
-                               * fb_depth_texture.width * fb_depth_texture.height;
-
-    // If source memory region overlaps 3DS framebuffers, commit them before the copy happens
-    if (MathUtil::IntervalsIntersect(addr, size, cached_fb_color_addr, cached_fb_color_size))
-        CommitColorBuffer();
-
-    if (MathUtil::IntervalsIntersect(addr, size, cached_fb_depth_addr, cached_fb_depth_size))
-        CommitDepthBuffer();
+void RasterizerOpenGL::FlushAll() {
+    res_cache.FlushAll();
 }
 
-void RasterizerOpenGL::InvalidateRegion(PAddr addr, u32 size) {
-    const auto& regs = Pica::g_state.regs;
+void RasterizerOpenGL::FlushRegion(PAddr addr, u32 size) {
+    res_cache.FlushRegion(addr, size, nullptr, false);
+}
 
-    u32 cached_fb_color_size = Pica::Regs::BytesPerColorPixel(fb_color_texture.format)
-                               * fb_color_texture.width * fb_color_texture.height;
+void RasterizerOpenGL::FlushAndInvalidateRegion(PAddr addr, u32 size) {
+    res_cache.FlushRegion(addr, size, nullptr, true);
+}
 
-    u32 cached_fb_depth_size = Pica::Regs::BytesPerDepthPixel(fb_depth_texture.format)
-                               * fb_depth_texture.width * fb_depth_texture.height;
+bool RasterizerOpenGL::AccelerateDisplayTransfer(const GPU::Regs::DisplayTransferConfig& config) {
+    using PixelFormat = CachedSurface::PixelFormat;
+    using SurfaceType = CachedSurface::SurfaceType;
 
-    // If modified memory region overlaps 3DS framebuffers, reload their contents into OpenGL
-    if (MathUtil::IntervalsIntersect(addr, size, cached_fb_color_addr, cached_fb_color_size))
-        ReloadColorBuffer();
+    if (config.is_texture_copy) {
+        // TODO(tfarley): Try to hardware accelerate this
+        return false;
+    }
 
-    if (MathUtil::IntervalsIntersect(addr, size, cached_fb_depth_addr, cached_fb_depth_size))
-        ReloadDepthBuffer();
+    CachedSurface src_params;
+    src_params.addr = config.GetPhysicalInputAddress();
+    src_params.width = config.output_width;
+    src_params.height = config.output_height;
+    src_params.is_tiled = !config.input_linear;
+    src_params.pixel_format = CachedSurface::PixelFormatFromGPUPixelFormat(config.input_format);
 
-    // Notify cache of flush in case the region touches a cached resource
-    res_cache.InvalidateInRange(addr, size);
+    CachedSurface dst_params;
+    dst_params.addr = config.GetPhysicalOutputAddress();
+    dst_params.width = config.scaling != config.NoScale ? config.output_width / 2 : config.output_width.Value();
+    dst_params.height = config.scaling == config.ScaleXY ? config.output_height / 2 : config.output_height.Value();
+    dst_params.is_tiled = config.input_linear != config.dont_swizzle;
+    dst_params.pixel_format = CachedSurface::PixelFormatFromGPUPixelFormat(config.output_format);
+
+    MathUtil::Rectangle<int> src_rect;
+    CachedSurface* src_surface = res_cache.GetSurfaceRect(src_params, false, true, src_rect);
+
+    if (src_surface == nullptr) {
+        return false;
+    }
+
+    // Require destination surface to have same resolution scale as source to preserve scaling
+    dst_params.res_scale_width = src_surface->res_scale_width;
+    dst_params.res_scale_height = src_surface->res_scale_height;
+
+    MathUtil::Rectangle<int> dst_rect;
+    CachedSurface* dst_surface = res_cache.GetSurfaceRect(dst_params, true, false, dst_rect);
+
+    if (dst_surface == nullptr) {
+        return false;
+    }
+
+    // Don't accelerate if the src and dst surfaces are the same
+    if (src_surface == dst_surface) {
+        return false;
+    }
+
+    if (config.flip_vertically) {
+        std::swap(dst_rect.top, dst_rect.bottom);
+    }
+
+    if (!res_cache.TryBlitSurfaces(src_surface, src_rect, dst_surface, dst_rect)) {
+        return false;
+    }
+
+    u32 dst_size = dst_params.width * dst_params.height * CachedSurface::GetFormatBpp(dst_params.pixel_format) / 8;
+    dst_surface->dirty = true;
+    res_cache.FlushRegion(config.GetPhysicalOutputAddress(), dst_size, dst_surface, true);
+    return true;
+}
+
+bool RasterizerOpenGL::AccelerateFill(const GPU::Regs::MemoryFillConfig& config) {
+    using PixelFormat = CachedSurface::PixelFormat;
+    using SurfaceType = CachedSurface::SurfaceType;
+
+    CachedSurface* dst_surface = res_cache.TryGetFillSurface(config);
+
+    if (dst_surface == nullptr) {
+        return false;
+    }
+
+    OpenGLState cur_state = OpenGLState::GetCurState();
+
+    SurfaceType dst_type = CachedSurface::GetFormatType(dst_surface->pixel_format);
+
+    GLuint old_fb = cur_state.draw.draw_framebuffer;
+    cur_state.draw.draw_framebuffer = framebuffer.handle;
+    // TODO: When scissor test is implemented, need to disable scissor test in cur_state here so Clear call isn't affected
+    cur_state.Apply();
+
+    if (dst_type == SurfaceType::Color || dst_type == SurfaceType::Texture) {
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_surface->texture.handle, 0);
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
+
+        if (OpenGLState::CheckFBStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+            return false;
+        }
+
+        GLfloat color_values[4] = {0.0f, 0.0f, 0.0f, 0.0f};
+
+        // TODO: Handle additional pixel format and fill value size combinations to accelerate more cases
+        //       For instance, checking if fill value's bytes/bits repeat to allow filling I8/A8/I4/A4/...
+        //       Currently only handles formats that are multiples of the fill value size
+
+        if (config.fill_24bit) {
+            switch (dst_surface->pixel_format) {
+            case PixelFormat::RGB8:
+                color_values[0] = config.value_24bit_r / 255.0f;
+                color_values[1] = config.value_24bit_g / 255.0f;
+                color_values[2] = config.value_24bit_b / 255.0f;
+                break;
+            default:
+                return false;
+            }
+        } else if (config.fill_32bit) {
+            u32 value = config.value_32bit;
+
+            switch (dst_surface->pixel_format) {
+            case PixelFormat::RGBA8:
+                color_values[0] = (value >> 24) / 255.0f;
+                color_values[1] = ((value >> 16) & 0xFF) / 255.0f;
+                color_values[2] = ((value >> 8) & 0xFF) / 255.0f;
+                color_values[3] = (value & 0xFF) / 255.0f;
+                break;
+            default:
+                return false;
+            }
+        } else {
+            u16 value_16bit = config.value_16bit.Value();
+            Math::Vec4<u8> color;
+
+            switch (dst_surface->pixel_format) {
+            case PixelFormat::RGBA8:
+                color_values[0] = (value_16bit >> 8) / 255.0f;
+                color_values[1] = (value_16bit & 0xFF) / 255.0f;
+                color_values[2] = color_values[0];
+                color_values[3] = color_values[1];
+                break;
+            case PixelFormat::RGB5A1:
+                color = Color::DecodeRGB5A1((const u8*)&value_16bit);
+                color_values[0] = color[0] / 31.0f;
+                color_values[1] = color[1] / 31.0f;
+                color_values[2] = color[2] / 31.0f;
+                color_values[3] = color[3];
+                break;
+            case PixelFormat::RGB565:
+                color = Color::DecodeRGB565((const u8*)&value_16bit);
+                color_values[0] = color[0] / 31.0f;
+                color_values[1] = color[1] / 63.0f;
+                color_values[2] = color[2] / 31.0f;
+                break;
+            case PixelFormat::RGBA4:
+                color = Color::DecodeRGBA4((const u8*)&value_16bit);
+                color_values[0] = color[0] / 15.0f;
+                color_values[1] = color[1] / 15.0f;
+                color_values[2] = color[2] / 15.0f;
+                color_values[3] = color[3] / 15.0f;
+                break;
+            case PixelFormat::IA8:
+            case PixelFormat::RG8:
+                color_values[0] = (value_16bit >> 8) / 255.0f;
+                color_values[1] = (value_16bit & 0xFF) / 255.0f;
+                break;
+            default:
+                return false;
+            }
+        }
+
+        cur_state.color_mask.red_enabled = true;
+        cur_state.color_mask.green_enabled = true;
+        cur_state.color_mask.blue_enabled = true;
+        cur_state.color_mask.alpha_enabled = true;
+        cur_state.Apply();
+        glClearBufferfv(GL_COLOR, 0, color_values);
+    } else if (dst_type == SurfaceType::Depth) {
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, dst_surface->texture.handle, 0);
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
+
+        if (OpenGLState::CheckFBStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+            return false;
+        }
+
+        GLfloat value_float;
+        if (dst_surface->pixel_format == CachedSurface::PixelFormat::D16) {
+            value_float = config.value_32bit / 65535.0f; // 2^16 - 1
+        } else if (dst_surface->pixel_format == CachedSurface::PixelFormat::D24) {
+            value_float = config.value_32bit / 16777215.0f; // 2^24 - 1
+        }
+
+        cur_state.depth.write_mask = true;
+        cur_state.Apply();
+        glClearBufferfv(GL_DEPTH, 0, &value_float);
+    } else if (dst_type == SurfaceType::DepthStencil) {
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, dst_surface->texture.handle, 0);
+
+        if (OpenGLState::CheckFBStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+            return false;
+        }
+
+        GLfloat value_float = (config.value_32bit & 0xFFFFFF) / 16777215.0f; // 2^24 - 1
+        GLint value_int = (config.value_32bit >> 24);
+
+        cur_state.depth.write_mask = true;
+        cur_state.stencil.write_mask = true;
+        cur_state.Apply();
+        glClearBufferfi(GL_DEPTH_STENCIL, 0, value_float, value_int);
+    }
+
+    cur_state.draw.draw_framebuffer = old_fb;
+    // TODO: Return scissor test to previous value when scissor test is implemented
+    cur_state.Apply();
+
+    dst_surface->dirty = true;
+    res_cache.FlushRegion(dst_surface->addr, dst_surface->size, dst_surface, true);
+    return true;
+}
+
+bool RasterizerOpenGL::AccelerateDisplay(const GPU::Regs::FramebufferConfig& config, PAddr framebuffer_addr, u32 pixel_stride, ScreenInfo& screen_info) {
+    if (framebuffer_addr == 0) {
+        return false;
+    }
+
+    CachedSurface src_params;
+    src_params.addr = framebuffer_addr;
+    src_params.width = config.width;
+    src_params.height = config.height;
+    src_params.stride = pixel_stride;
+    src_params.is_tiled = false;
+    src_params.pixel_format = CachedSurface::PixelFormatFromGPUPixelFormat(config.color_format);
+
+    MathUtil::Rectangle<int> src_rect;
+    CachedSurface* src_surface = res_cache.GetSurfaceRect(src_params, false, true, src_rect);
+
+    if (src_surface == nullptr) {
+        return false;
+    }
+
+    u32 scaled_width = src_surface->GetScaledWidth();
+    u32 scaled_height = src_surface->GetScaledHeight();
+
+    screen_info.display_texcoords = MathUtil::Rectangle<float>((float)src_rect.top / (float)scaled_height,
+                                                               (float)src_rect.left / (float)scaled_width,
+                                                               (float)src_rect.bottom / (float)scaled_height,
+                                                               (float)src_rect.right / (float)scaled_width);
+
+    screen_info.display_texture = src_surface->texture.handle;
+
+    return true;
 }
 
 void RasterizerOpenGL::SamplerInfo::Create() {
@@ -597,108 +819,6 @@ void RasterizerOpenGL::SamplerInfo::SyncWithConfig(const Pica::Regs::TextureConf
     }
 }
 
-void RasterizerOpenGL::ReconfigureColorTexture(TextureInfo& texture, Pica::Regs::ColorFormat format, u32 width, u32 height) {
-    GLint internal_format;
-
-    texture.format = format;
-    texture.width = width;
-    texture.height = height;
-
-    switch (format) {
-    case Pica::Regs::ColorFormat::RGBA8:
-        internal_format = GL_RGBA;
-        texture.gl_format = GL_RGBA;
-        texture.gl_type = GL_UNSIGNED_INT_8_8_8_8;
-        break;
-
-    case Pica::Regs::ColorFormat::RGB8:
-        // This pixel format uses BGR since GL_UNSIGNED_BYTE specifies byte-order, unlike every
-        // specific OpenGL type used in this function using native-endian (that is, little-endian
-        // mostly everywhere) for words or half-words.
-        // TODO: check how those behave on big-endian processors.
-        internal_format = GL_RGB;
-        texture.gl_format = GL_BGR;
-        texture.gl_type = GL_UNSIGNED_BYTE;
-        break;
-
-    case Pica::Regs::ColorFormat::RGB5A1:
-        internal_format = GL_RGBA;
-        texture.gl_format = GL_RGBA;
-        texture.gl_type = GL_UNSIGNED_SHORT_5_5_5_1;
-        break;
-
-    case Pica::Regs::ColorFormat::RGB565:
-        internal_format = GL_RGB;
-        texture.gl_format = GL_RGB;
-        texture.gl_type = GL_UNSIGNED_SHORT_5_6_5;
-        break;
-
-    case Pica::Regs::ColorFormat::RGBA4:
-        internal_format = GL_RGBA;
-        texture.gl_format = GL_RGBA;
-        texture.gl_type = GL_UNSIGNED_SHORT_4_4_4_4;
-        break;
-
-    default:
-        LOG_CRITICAL(Render_OpenGL, "Unknown framebuffer texture color format %x", format);
-        UNIMPLEMENTED();
-        break;
-    }
-
-    state.texture_units[0].texture_2d = texture.texture.handle;
-    state.Apply();
-
-    glActiveTexture(GL_TEXTURE0);
-    glTexImage2D(GL_TEXTURE_2D, 0, internal_format, texture.width, texture.height, 0,
-                 texture.gl_format, texture.gl_type, nullptr);
-
-    state.texture_units[0].texture_2d = 0;
-    state.Apply();
-}
-
-void RasterizerOpenGL::ReconfigureDepthTexture(DepthTextureInfo& texture, Pica::Regs::DepthFormat format, u32 width, u32 height) {
-    GLint internal_format;
-
-    texture.format = format;
-    texture.width = width;
-    texture.height = height;
-
-    switch (format) {
-    case Pica::Regs::DepthFormat::D16:
-        internal_format = GL_DEPTH_COMPONENT16;
-        texture.gl_format = GL_DEPTH_COMPONENT;
-        texture.gl_type = GL_UNSIGNED_SHORT;
-        break;
-
-    case Pica::Regs::DepthFormat::D24:
-        internal_format = GL_DEPTH_COMPONENT24;
-        texture.gl_format = GL_DEPTH_COMPONENT;
-        texture.gl_type = GL_UNSIGNED_INT;
-        break;
-
-    case Pica::Regs::DepthFormat::D24S8:
-        internal_format = GL_DEPTH24_STENCIL8;
-        texture.gl_format = GL_DEPTH_STENCIL;
-        texture.gl_type = GL_UNSIGNED_INT_24_8;
-        break;
-
-    default:
-        LOG_CRITICAL(Render_OpenGL, "Unknown framebuffer texture depth format %x", format);
-        UNIMPLEMENTED();
-        break;
-    }
-
-    state.texture_units[0].texture_2d = texture.texture.handle;
-    state.Apply();
-
-    glActiveTexture(GL_TEXTURE0);
-    glTexImage2D(GL_TEXTURE_2D, 0, internal_format, texture.width, texture.height, 0,
-                 texture.gl_format, texture.gl_type, nullptr);
-
-    state.texture_units[0].texture_2d = 0;
-    state.Apply();
-}
-
 void RasterizerOpenGL::SetShader() {
     PicaShaderConfig config = PicaShaderConfig::CurrentConfig();
     std::unique_ptr<PicaShader> shader = std::make_unique<PicaShader>();
@@ -761,83 +881,6 @@ void RasterizerOpenGL::SetShader() {
     }
 }
 
-void RasterizerOpenGL::SyncFramebuffer() {
-    const auto& regs = Pica::g_state.regs;
-
-    PAddr new_fb_color_addr = regs.framebuffer.GetColorBufferPhysicalAddress();
-    Pica::Regs::ColorFormat new_fb_color_format = regs.framebuffer.color_format;
-
-    PAddr new_fb_depth_addr = regs.framebuffer.GetDepthBufferPhysicalAddress();
-    Pica::Regs::DepthFormat new_fb_depth_format = regs.framebuffer.depth_format;
-
-    bool fb_size_changed = fb_color_texture.width != static_cast<GLsizei>(regs.framebuffer.GetWidth()) ||
-                           fb_color_texture.height != static_cast<GLsizei>(regs.framebuffer.GetHeight());
-
-    bool color_fb_prop_changed = fb_color_texture.format != new_fb_color_format ||
-                                 fb_size_changed;
-
-    bool depth_fb_prop_changed = fb_depth_texture.format != new_fb_depth_format ||
-                                 fb_size_changed;
-
-    bool color_fb_modified = cached_fb_color_addr != new_fb_color_addr ||
-                             color_fb_prop_changed;
-
-    bool depth_fb_modified = cached_fb_depth_addr != new_fb_depth_addr ||
-                             depth_fb_prop_changed;
-
-    // Commit if framebuffer modified in any way
-    if (color_fb_modified)
-        CommitColorBuffer();
-
-    if (depth_fb_modified)
-        CommitDepthBuffer();
-
-    // Reconfigure framebuffer textures if any property has changed
-    if (color_fb_prop_changed) {
-        ReconfigureColorTexture(fb_color_texture, new_fb_color_format,
-                                regs.framebuffer.GetWidth(), regs.framebuffer.GetHeight());
-    }
-
-    if (depth_fb_prop_changed) {
-        ReconfigureDepthTexture(fb_depth_texture, new_fb_depth_format,
-                                regs.framebuffer.GetWidth(), regs.framebuffer.GetHeight());
-
-        // Only attach depth buffer as stencil if it supports stencil
-        switch (new_fb_depth_format) {
-        case Pica::Regs::DepthFormat::D16:
-        case Pica::Regs::DepthFormat::D24:
-            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
-            break;
-
-        case Pica::Regs::DepthFormat::D24S8:
-            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, fb_depth_texture.texture.handle, 0);
-            break;
-
-        default:
-            LOG_CRITICAL(Render_OpenGL, "Unknown framebuffer depth format %x", new_fb_depth_format);
-            UNIMPLEMENTED();
-            break;
-        }
-    }
-
-    // Load buffer data again if fb modified in any way
-    if (color_fb_modified) {
-        cached_fb_color_addr = new_fb_color_addr;
-
-        ReloadColorBuffer();
-    }
-
-    if (depth_fb_modified) {
-        cached_fb_depth_addr = new_fb_depth_addr;
-
-        ReloadDepthBuffer();
-    }
-
-    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
-    ASSERT_MSG(status == GL_FRAMEBUFFER_COMPLETE,
-               "OpenGL rasterizer framebuffer setup failed, status %X", status);
-}
-
 void RasterizerOpenGL::SyncCullMode() {
     const auto& regs = Pica::g_state.regs;
 
@@ -1034,229 +1077,3 @@ void RasterizerOpenGL::SyncLightPosition(int light_index) {
         uniform_block_data.dirty = true;
     }
 }
-
-void RasterizerOpenGL::SyncDrawState() {
-    const auto& regs = Pica::g_state.regs;
-
-    // Sync the viewport
-    GLsizei viewport_width = (GLsizei)Pica::float24::FromRaw(regs.viewport_size_x).ToFloat32() * 2;
-    GLsizei viewport_height = (GLsizei)Pica::float24::FromRaw(regs.viewport_size_y).ToFloat32() * 2;
-
-    // OpenGL uses different y coordinates, so negate corner offset and flip origin
-    // TODO: Ensure viewport_corner.x should not be negated or origin flipped
-    // TODO: Use floating-point viewports for accuracy if supported
-    glViewport((GLsizei)regs.viewport_corner.x,
-               (GLsizei)regs.viewport_corner.y,
-                viewport_width, viewport_height);
-
-    // Sync bound texture(s), upload if not cached
-    const auto pica_textures = regs.GetTextures();
-    for (unsigned texture_index = 0; texture_index < pica_textures.size(); ++texture_index) {
-        const auto& texture = pica_textures[texture_index];
-
-        if (texture.enabled) {
-            texture_samplers[texture_index].SyncWithConfig(texture.config);
-            res_cache.LoadAndBindTexture(state, texture_index, texture);
-        } else {
-            state.texture_units[texture_index].texture_2d = 0;
-        }
-    }
-
-    state.draw.uniform_buffer = uniform_buffer.handle;
-    state.Apply();
-}
-
-MICROPROFILE_DEFINE(OpenGL_FramebufferReload, "OpenGL", "FB Reload", MP_RGB(70, 70, 200));
-
-void RasterizerOpenGL::ReloadColorBuffer() {
-    u8* color_buffer = Memory::GetPhysicalPointer(cached_fb_color_addr);
-
-    if (color_buffer == nullptr)
-        return;
-
-    MICROPROFILE_SCOPE(OpenGL_FramebufferReload);
-
-    u32 bytes_per_pixel = Pica::Regs::BytesPerColorPixel(fb_color_texture.format);
-
-    std::unique_ptr<u8[]> temp_fb_color_buffer(new u8[fb_color_texture.width * fb_color_texture.height * bytes_per_pixel]);
-
-    // Directly copy pixels. Internal OpenGL color formats are consistent so no conversion is necessary.
-    for (int y = 0; y < fb_color_texture.height; ++y) {
-        for (int x = 0; x < fb_color_texture.width; ++x) {
-            const u32 coarse_y = y & ~7;
-            u32 dst_offset = VideoCore::GetMortonOffset(x, y, bytes_per_pixel) + coarse_y * fb_color_texture.width * bytes_per_pixel;
-            u32 gl_pixel_index = (x + (fb_color_texture.height - 1 - y) * fb_color_texture.width) * bytes_per_pixel;
-
-            u8* pixel = color_buffer + dst_offset;
-            memcpy(&temp_fb_color_buffer[gl_pixel_index], pixel, bytes_per_pixel);
-        }
-    }
-
-    state.texture_units[0].texture_2d = fb_color_texture.texture.handle;
-    state.Apply();
-
-    glActiveTexture(GL_TEXTURE0);
-    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, fb_color_texture.width, fb_color_texture.height,
-                    fb_color_texture.gl_format, fb_color_texture.gl_type, temp_fb_color_buffer.get());
-
-    state.texture_units[0].texture_2d = 0;
-    state.Apply();
-}
-
-void RasterizerOpenGL::ReloadDepthBuffer() {
-    if (cached_fb_depth_addr == 0)
-        return;
-
-    // TODO: Appears to work, but double-check endianness of depth values and order of depth-stencil
-    u8* depth_buffer = Memory::GetPhysicalPointer(cached_fb_depth_addr);
-
-    if (depth_buffer == nullptr)
-        return;
-
-    MICROPROFILE_SCOPE(OpenGL_FramebufferReload);
-
-    u32 bytes_per_pixel = Pica::Regs::BytesPerDepthPixel(fb_depth_texture.format);
-
-    // OpenGL needs 4 bpp alignment for D24
-    u32 gl_bpp = bytes_per_pixel == 3 ? 4 : bytes_per_pixel;
-
-    std::unique_ptr<u8[]> temp_fb_depth_buffer(new u8[fb_depth_texture.width * fb_depth_texture.height * gl_bpp]);
-
-    u8* temp_fb_depth_data = bytes_per_pixel == 3 ? (temp_fb_depth_buffer.get() + 1) : temp_fb_depth_buffer.get();
-
-    if (fb_depth_texture.format == Pica::Regs::DepthFormat::D24S8) {
-        for (int y = 0; y < fb_depth_texture.height; ++y) {
-            for (int x = 0; x < fb_depth_texture.width; ++x) {
-                const u32 coarse_y = y & ~7;
-                u32 dst_offset = VideoCore::GetMortonOffset(x, y, bytes_per_pixel) + coarse_y * fb_depth_texture.width * bytes_per_pixel;
-                u32 gl_pixel_index = (x + (fb_depth_texture.height - 1 - y) * fb_depth_texture.width);
-
-                u8* pixel = depth_buffer + dst_offset;
-                u32 depth_stencil = *(u32*)pixel;
-                ((u32*)temp_fb_depth_data)[gl_pixel_index] = (depth_stencil << 8) | (depth_stencil >> 24);
-            }
-        }
-    } else {
-        for (int y = 0; y < fb_depth_texture.height; ++y) {
-            for (int x = 0; x < fb_depth_texture.width; ++x) {
-                const u32 coarse_y = y & ~7;
-                u32 dst_offset = VideoCore::GetMortonOffset(x, y, bytes_per_pixel) + coarse_y * fb_depth_texture.width * bytes_per_pixel;
-                u32 gl_pixel_index = (x + (fb_depth_texture.height - 1 - y) * fb_depth_texture.width) * gl_bpp;
-
-                u8* pixel = depth_buffer + dst_offset;
-                memcpy(&temp_fb_depth_data[gl_pixel_index], pixel, bytes_per_pixel);
-            }
-        }
-    }
-
-    state.texture_units[0].texture_2d = fb_depth_texture.texture.handle;
-    state.Apply();
-
-    glActiveTexture(GL_TEXTURE0);
-    if (fb_depth_texture.format == Pica::Regs::DepthFormat::D24S8) {
-        // TODO(Subv): There is a bug with Intel Windows drivers that makes glTexSubImage2D not change the stencil buffer.
-        // The bug has been reported to Intel (https://communities.intel.com/message/324464)
-        glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, fb_depth_texture.width, fb_depth_texture.height, 0,
-            GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, temp_fb_depth_buffer.get());
-    } else {
-        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, fb_depth_texture.width, fb_depth_texture.height,
-            fb_depth_texture.gl_format, fb_depth_texture.gl_type, temp_fb_depth_buffer.get());
-    }
-
-    state.texture_units[0].texture_2d = 0;
-    state.Apply();
-}
-
-Common::Profiling::TimingCategory buffer_commit_category("Framebuffer Commit");
-MICROPROFILE_DEFINE(OpenGL_FramebufferCommit, "OpenGL", "FB Commit", MP_RGB(70, 70, 200));
-
-void RasterizerOpenGL::CommitColorBuffer() {
-    if (cached_fb_color_addr != 0) {
-        u8* color_buffer = Memory::GetPhysicalPointer(cached_fb_color_addr);
-
-        if (color_buffer != nullptr) {
-            Common::Profiling::ScopeTimer timer(buffer_commit_category);
-            MICROPROFILE_SCOPE(OpenGL_FramebufferCommit);
-
-            u32 bytes_per_pixel = Pica::Regs::BytesPerColorPixel(fb_color_texture.format);
-
-            std::unique_ptr<u8[]> temp_gl_color_buffer(new u8[fb_color_texture.width * fb_color_texture.height * bytes_per_pixel]);
-
-            state.texture_units[0].texture_2d = fb_color_texture.texture.handle;
-            state.Apply();
-
-            glActiveTexture(GL_TEXTURE0);
-            glGetTexImage(GL_TEXTURE_2D, 0, fb_color_texture.gl_format, fb_color_texture.gl_type, temp_gl_color_buffer.get());
-
-            state.texture_units[0].texture_2d = 0;
-            state.Apply();
-
-            // Directly copy pixels. Internal OpenGL color formats are consistent so no conversion is necessary.
-            for (int y = 0; y < fb_color_texture.height; ++y) {
-                for (int x = 0; x < fb_color_texture.width; ++x) {
-                    const u32 coarse_y = y & ~7;
-                    u32 dst_offset = VideoCore::GetMortonOffset(x, y, bytes_per_pixel) + coarse_y * fb_color_texture.width * bytes_per_pixel;
-                    u32 gl_pixel_index = x * bytes_per_pixel + (fb_color_texture.height - 1 - y) * fb_color_texture.width * bytes_per_pixel;
-
-                    u8* pixel = color_buffer + dst_offset;
-                    memcpy(pixel, &temp_gl_color_buffer[gl_pixel_index], bytes_per_pixel);
-                }
-            }
-        }
-    }
-}
-
-void RasterizerOpenGL::CommitDepthBuffer() {
-    if (cached_fb_depth_addr != 0) {
-        // TODO: Output seems correct visually, but doesn't quite match sw renderer output. One of them is wrong.
-        u8* depth_buffer = Memory::GetPhysicalPointer(cached_fb_depth_addr);
-
-        if (depth_buffer != nullptr) {
-            Common::Profiling::ScopeTimer timer(buffer_commit_category);
-            MICROPROFILE_SCOPE(OpenGL_FramebufferCommit);
-
-            u32 bytes_per_pixel = Pica::Regs::BytesPerDepthPixel(fb_depth_texture.format);
-
-            // OpenGL needs 4 bpp alignment for D24
-            u32 gl_bpp = bytes_per_pixel == 3 ? 4 : bytes_per_pixel;
-
-            std::unique_ptr<u8[]> temp_gl_depth_buffer(new u8[fb_depth_texture.width * fb_depth_texture.height * gl_bpp]);
-
-            state.texture_units[0].texture_2d = fb_depth_texture.texture.handle;
-            state.Apply();
-
-            glActiveTexture(GL_TEXTURE0);
-            glGetTexImage(GL_TEXTURE_2D, 0, fb_depth_texture.gl_format, fb_depth_texture.gl_type, temp_gl_depth_buffer.get());
-
-            state.texture_units[0].texture_2d = 0;
-            state.Apply();
-
-            u8* temp_gl_depth_data = bytes_per_pixel == 3 ? (temp_gl_depth_buffer.get() + 1) : temp_gl_depth_buffer.get();
-
-            if (fb_depth_texture.format == Pica::Regs::DepthFormat::D24S8) {
-                for (int y = 0; y < fb_depth_texture.height; ++y) {
-                    for (int x = 0; x < fb_depth_texture.width; ++x) {
-                        const u32 coarse_y = y & ~7;
-                        u32 dst_offset = VideoCore::GetMortonOffset(x, y, bytes_per_pixel) + coarse_y * fb_depth_texture.width * bytes_per_pixel;
-                        u32 gl_pixel_index = (x + (fb_depth_texture.height - 1 - y) * fb_depth_texture.width);
-
-                        u8* pixel = depth_buffer + dst_offset;
-                        u32 depth_stencil = ((u32*)temp_gl_depth_data)[gl_pixel_index];
-                        *(u32*)pixel = (depth_stencil >> 8) | (depth_stencil << 24);
-                    }
-                }
-            } else {
-                for (int y = 0; y < fb_depth_texture.height; ++y) {
-                    for (int x = 0; x < fb_depth_texture.width; ++x) {
-                        const u32 coarse_y = y & ~7;
-                        u32 dst_offset = VideoCore::GetMortonOffset(x, y, bytes_per_pixel) + coarse_y * fb_depth_texture.width * bytes_per_pixel;
-                        u32 gl_pixel_index = (x + (fb_depth_texture.height - 1 - y) * fb_depth_texture.width) * gl_bpp;
-
-                        u8* pixel = depth_buffer + dst_offset;
-                        memcpy(pixel, &temp_gl_depth_data[gl_pixel_index], bytes_per_pixel);
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/src/video_core/renderer_opengl/gl_rasterizer.h b/src/video_core/renderer_opengl/gl_rasterizer.h
index 390349a0c..5aa638985 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer.h
+++ b/src/video_core/renderer_opengl/gl_rasterizer.h
@@ -19,6 +19,7 @@
 #include "video_core/renderer_opengl/gl_rasterizer_cache.h"
 #include "video_core/renderer_opengl/gl_state.h"
 #include "video_core/renderer_opengl/pica_to_gl.h"
+#include "video_core/renderer_opengl/renderer_opengl.h"
 #include "video_core/shader/shader_interpreter.h"
 
 /**
@@ -191,16 +192,17 @@ public:
     RasterizerOpenGL();
     ~RasterizerOpenGL() override;
 
-    void InitObjects() override;
-    void Reset() override;
     void AddTriangle(const Pica::Shader::OutputVertex& v0,
                      const Pica::Shader::OutputVertex& v1,
                      const Pica::Shader::OutputVertex& v2) override;
     void DrawTriangles() override;
-    void FlushFramebuffer() override;
     void NotifyPicaRegisterChanged(u32 id) override;
+    void FlushAll() override;
     void FlushRegion(PAddr addr, u32 size) override;
-    void InvalidateRegion(PAddr addr, u32 size) override;
+    void FlushAndInvalidateRegion(PAddr addr, u32 size) override;
+    bool AccelerateDisplayTransfer(const GPU::Regs::DisplayTransferConfig& config) override;
+    bool AccelerateFill(const GPU::Regs::MemoryFillConfig& config) override;
+    bool AccelerateDisplay(const GPU::Regs::FramebufferConfig& config, PAddr framebuffer_addr, u32 pixel_stride, ScreenInfo& screen_info) override;
 
     /// OpenGL shader generated for a given Pica register state
     struct PicaShader {
@@ -210,26 +212,6 @@ public:
 
 private:
 
-    /// Structure used for storing information about color textures
-    struct TextureInfo {
-        OGLTexture texture;
-        GLsizei width;
-        GLsizei height;
-        Pica::Regs::ColorFormat format;
-        GLenum gl_format;
-        GLenum gl_type;
-    };
-
-    /// Structure used for storing information about depth textures
-    struct DepthTextureInfo {
-        OGLTexture texture;
-        GLsizei width;
-        GLsizei height;
-        Pica::Regs::DepthFormat format;
-        GLenum gl_format;
-        GLenum gl_type;
-    };
-
     struct SamplerInfo {
         using TextureConfig = Pica::Regs::TextureConfig;
 
@@ -311,18 +293,9 @@ private:
     static_assert(sizeof(UniformData) == 0x310, "The size of the UniformData structure has changed, update the structure in the shader");
     static_assert(sizeof(UniformData) < 16384, "UniformData structure must be less than 16kb as per the OpenGL spec");
 
-    /// Reconfigure the OpenGL color texture to use the given format and dimensions
-    void ReconfigureColorTexture(TextureInfo& texture, Pica::Regs::ColorFormat format, u32 width, u32 height);
-
-    /// Reconfigure the OpenGL depth texture to use the given format and dimensions
-    void ReconfigureDepthTexture(DepthTextureInfo& texture, Pica::Regs::DepthFormat format, u32 width, u32 height);
-
     /// Sets the OpenGL shader in accordance with the current PICA register state
     void SetShader();
 
-    /// Syncs the state and contents of the OpenGL framebuffer to match the current PICA framebuffer
-    void SyncFramebuffer();
-
     /// Syncs the cull mode to match the PICA register
     void SyncCullMode();
 
@@ -386,45 +359,15 @@ private:
     /// Syncs the specified light's specular 1 color to match the PICA register
     void SyncLightSpecular1(int light_index);
 
-    /// Syncs the remaining OpenGL drawing state to match the current PICA state
-    void SyncDrawState();
-
-    /// Copies the 3DS color framebuffer into the OpenGL color framebuffer texture
-    void ReloadColorBuffer();
-
-    /// Copies the 3DS depth framebuffer into the OpenGL depth framebuffer texture
-    void ReloadDepthBuffer();
-
-    /**
-     * Save the current OpenGL color framebuffer to the current PICA framebuffer in 3DS memory
-     * Loads the OpenGL framebuffer textures into temporary buffers
-     * Then copies into the 3DS framebuffer using proper Morton order
-     */
-    void CommitColorBuffer();
-
-    /**
-     * Save the current OpenGL depth framebuffer to the current PICA framebuffer in 3DS memory
-     * Loads the OpenGL framebuffer textures into temporary buffers
-     * Then copies into the 3DS framebuffer using proper Morton order
-     */
-    void CommitDepthBuffer();
+    OpenGLState state;
 
     RasterizerCacheOpenGL res_cache;
 
     std::vector<HardwareVertex> vertex_batch;
 
-    OpenGLState state;
-
-    PAddr cached_fb_color_addr;
-    PAddr cached_fb_depth_addr;
-
-    // Hardware rasterizer
-    std::array<SamplerInfo, 3> texture_samplers;
-    TextureInfo fb_color_texture;
-    DepthTextureInfo fb_depth_texture;
-
     std::unordered_map<PicaShaderConfig, std::unique_ptr<PicaShader>> shader_cache;
     const PicaShader* current_shader = nullptr;
+    bool shader_dirty;
 
     struct {
         UniformData data;
@@ -432,11 +375,12 @@ private:
         bool dirty;
     } uniform_block_data;
 
+    std::array<SamplerInfo, 3> texture_samplers;
     OGLVertexArray vertex_array;
     OGLBuffer vertex_buffer;
     OGLBuffer uniform_buffer;
     OGLFramebuffer framebuffer;
 
-    std::array<OGLTexture, 6> lighting_lut;
+    std::array<OGLTexture, 6> lighting_luts;
     std::array<std::array<GLvec4, 256>, 6> lighting_lut_data;
 };
diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
index 1323c12e4..55c2fb283 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
@@ -2,8 +2,9 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
-#include <memory>
+#include <unordered_set>
 
+#include "common/emu_window.h"
 #include "common/hash.h"
 #include "common/math_util.h"
 #include "common/microprofile.h"
@@ -12,71 +13,693 @@
 #include "core/memory.h"
 
 #include "video_core/debug_utils/debug_utils.h"
+#include "video_core/pica_state.h"
 #include "video_core/renderer_opengl/gl_rasterizer_cache.h"
 #include "video_core/renderer_opengl/pica_to_gl.h"
+#include "video_core/utils.h"
+#include "video_core/video_core.h"
+
+struct FormatTuple {
+    GLint internal_format;
+    GLenum format;
+    GLenum type;
+};
+
+static const 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
+    { GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1 }, // RGB5A1
+    { GL_RGB565,  GL_RGB,  GL_UNSIGNED_SHORT_5_6_5 },   // RGB565
+    { GL_RGBA4,   GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4 }, // RGBA4
+}};
+
+static const std::array<FormatTuple, 4> depth_format_tuples = {{
+    { GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT },    // D16
+    {},
+    { GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT },      // D24
+    { GL_DEPTH24_STENCIL8,  GL_DEPTH_STENCIL,   GL_UNSIGNED_INT_24_8 }, // D24S8
+}};
+
+RasterizerCacheOpenGL::RasterizerCacheOpenGL() {
+    transfer_framebuffers[0].Create();
+    transfer_framebuffers[1].Create();
+}
 
 RasterizerCacheOpenGL::~RasterizerCacheOpenGL() {
-    InvalidateAll();
+    FlushAll();
 }
 
-MICROPROFILE_DEFINE(OpenGL_TextureUpload, "OpenGL", "Texture Upload", MP_RGB(128, 64, 192));
+static void MortonCopyPixels(CachedSurface::PixelFormat pixel_format, u32 width, u32 height, u32 bytes_per_pixel, u32 gl_bytes_per_pixel, u8* morton_data, u8* gl_data, bool morton_to_gl) {
+    using PixelFormat = CachedSurface::PixelFormat;
 
-void RasterizerCacheOpenGL::LoadAndBindTexture(OpenGLState &state, unsigned texture_unit, const Pica::DebugUtils::TextureInfo& info) {
-    const auto cached_texture = texture_cache.find(info.physical_address);
+    u8* data_ptrs[2];
+    u32 depth_stencil_shifts[2] = {24, 8};
 
-    if (cached_texture != texture_cache.end()) {
-        state.texture_units[texture_unit].texture_2d = cached_texture->second->texture.handle;
-        state.Apply();
-    } else {
-        MICROPROFILE_SCOPE(OpenGL_TextureUpload);
+    if (morton_to_gl) {
+        std::swap(depth_stencil_shifts[0], depth_stencil_shifts[1]);
+    }
 
-        std::unique_ptr<CachedTexture> new_texture = std::make_unique<CachedTexture>();
+    if (pixel_format == PixelFormat::D24S8) {
+        for (unsigned y = 0; y < height; ++y) {
+            for (unsigned x = 0; x < width; ++x) {
+                const u32 coarse_y = y & ~7;
+                u32 morton_offset = VideoCore::GetMortonOffset(x, y, bytes_per_pixel) + coarse_y * width * bytes_per_pixel;
+                u32 gl_pixel_index = (x + (height - 1 - y) * width) * gl_bytes_per_pixel;
 
-        new_texture->texture.Create();
-        state.texture_units[texture_unit].texture_2d = new_texture->texture.handle;
-        state.Apply();
-        glActiveTexture(GL_TEXTURE0 + texture_unit);
+                data_ptrs[morton_to_gl] = morton_data + morton_offset;
+                data_ptrs[!morton_to_gl] = &gl_data[gl_pixel_index];
 
-        u8* texture_src_data = Memory::GetPhysicalPointer(info.physical_address);
+                // Swap depth and stencil value ordering since 3DS does not match OpenGL
+                u32 depth_stencil;
+                memcpy(&depth_stencil, data_ptrs[1], sizeof(u32));
+                depth_stencil = (depth_stencil << depth_stencil_shifts[0]) | (depth_stencil >> depth_stencil_shifts[1]);
 
-        new_texture->width = info.width;
-        new_texture->height = info.height;
-        new_texture->size = info.stride * info.height;
-        new_texture->addr = info.physical_address;
-        new_texture->hash = Common::ComputeHash64(texture_src_data, new_texture->size);
-
-        std::unique_ptr<Math::Vec4<u8>[]> temp_texture_buffer_rgba(new Math::Vec4<u8>[info.width * info.height]);
-
-        for (int y = 0; y < info.height; ++y) {
-            for (int x = 0; x < info.width; ++x) {
-                temp_texture_buffer_rgba[x + info.width * y] = Pica::DebugUtils::LookupTexture(texture_src_data, x, info.height - 1 - y, info);
+                memcpy(data_ptrs[0], &depth_stencil, sizeof(u32));
             }
         }
+    } else {
+        for (unsigned y = 0; y < height; ++y) {
+            for (unsigned x = 0; x < width; ++x) {
+                const u32 coarse_y = y & ~7;
+                u32 morton_offset = VideoCore::GetMortonOffset(x, y, bytes_per_pixel) + coarse_y * width * bytes_per_pixel;
+                u32 gl_pixel_index = (x + (height - 1 - y) * width) * gl_bytes_per_pixel;
 
-        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, info.width, info.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, temp_texture_buffer_rgba.get());
+                data_ptrs[morton_to_gl] = morton_data + morton_offset;
+                data_ptrs[!morton_to_gl] = &gl_data[gl_pixel_index];
 
-        texture_cache.emplace(info.physical_address, std::move(new_texture));
-    }
-}
-
-void RasterizerCacheOpenGL::InvalidateInRange(PAddr addr, u32 size, bool ignore_hash) {
-    // TODO: Optimize by also inserting upper bound (addr + size) of each texture into the same map and also narrow using lower_bound
-    auto cache_upper_bound = texture_cache.upper_bound(addr + size);
-
-    for (auto it = texture_cache.begin(); it != cache_upper_bound;) {
-        const auto& info = *it->second;
-
-        // Flush the texture only if the memory region intersects and a change is detected
-        if (MathUtil::IntervalsIntersect(addr, size, info.addr, info.size) &&
-            (ignore_hash || info.hash != Common::ComputeHash64(Memory::GetPhysicalPointer(info.addr), info.size))) {
-
-            it = texture_cache.erase(it);
-        } else {
-            ++it;
+                memcpy(data_ptrs[0], data_ptrs[1], bytes_per_pixel);
+            }
         }
     }
 }
 
-void RasterizerCacheOpenGL::InvalidateAll() {
-    texture_cache.clear();
+bool RasterizerCacheOpenGL::BlitTextures(GLuint src_tex, GLuint dst_tex, CachedSurface::SurfaceType type, const MathUtil::Rectangle<int>& src_rect, const MathUtil::Rectangle<int>& dst_rect) {
+    using SurfaceType = CachedSurface::SurfaceType;
+
+    OpenGLState cur_state = OpenGLState::GetCurState();
+
+    // Make sure textures aren't bound to texture units, since going to bind them to framebuffer components
+    OpenGLState::ResetTexture(src_tex);
+    OpenGLState::ResetTexture(dst_tex);
+
+    // Keep track of previous framebuffer bindings
+    GLuint old_fbs[2] = { cur_state.draw.read_framebuffer, cur_state.draw.draw_framebuffer };
+    cur_state.draw.read_framebuffer = transfer_framebuffers[0].handle;
+    cur_state.draw.draw_framebuffer = transfer_framebuffers[1].handle;
+    cur_state.Apply();
+
+    u32 buffers = 0;
+
+    if (type == SurfaceType::Color || type == SurfaceType::Texture) {
+        glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, src_tex, 0);
+        glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
+
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0);
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
+
+        buffers = GL_COLOR_BUFFER_BIT;
+    } else if (type == SurfaceType::Depth) {
+        glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
+        glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, src_tex, 0);
+        glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
+
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, dst_tex, 0);
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
+
+        buffers = GL_DEPTH_BUFFER_BIT;
+    } else if (type == SurfaceType::DepthStencil) {
+        glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
+        glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, src_tex, 0);
+
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, dst_tex, 0);
+
+        buffers = GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
+    }
+
+    if (OpenGLState::CheckFBStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+        return false;
+    }
+
+    if (OpenGLState::CheckFBStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+        return false;
+    }
+
+    glBlitFramebuffer(src_rect.left, src_rect.top, src_rect.right, src_rect.bottom,
+                      dst_rect.left, dst_rect.top, dst_rect.right, dst_rect.bottom,
+                      buffers, buffers == GL_COLOR_BUFFER_BIT ? GL_LINEAR : GL_NEAREST);
+
+    // Restore previous framebuffer bindings
+    cur_state.draw.read_framebuffer = old_fbs[0];
+    cur_state.draw.draw_framebuffer = old_fbs[1];
+    cur_state.Apply();
+
+    return true;
+}
+
+bool RasterizerCacheOpenGL::TryBlitSurfaces(CachedSurface* src_surface, const MathUtil::Rectangle<int>& src_rect, CachedSurface* dst_surface, const MathUtil::Rectangle<int>& dst_rect) {
+    using SurfaceType = CachedSurface::SurfaceType;
+
+    if (!CachedSurface::CheckFormatsBlittable(src_surface->pixel_format, dst_surface->pixel_format)) {
+        return false;
+    }
+
+    return BlitTextures(src_surface->texture.handle, dst_surface->texture.handle, CachedSurface::GetFormatType(src_surface->pixel_format), src_rect, dst_rect);
+}
+
+static void AllocateSurfaceTexture(GLuint texture, CachedSurface::PixelFormat pixel_format, u32 width, u32 height) {
+    // Allocate an uninitialized texture of appropriate size and format for the surface
+    using SurfaceType = CachedSurface::SurfaceType;
+
+    OpenGLState cur_state = OpenGLState::GetCurState();
+
+    // Keep track of previous texture bindings
+    GLuint old_tex = cur_state.texture_units[0].texture_2d;
+    cur_state.texture_units[0].texture_2d = texture;
+    cur_state.Apply();
+    glActiveTexture(GL_TEXTURE0);
+
+    SurfaceType type = CachedSurface::GetFormatType(pixel_format);
+
+    FormatTuple tuple;
+    if (type == SurfaceType::Color) {
+        ASSERT((size_t)pixel_format < fb_format_tuples.size());
+        tuple = fb_format_tuples[(unsigned int)pixel_format];
+    } else if (type == SurfaceType::Depth || type == SurfaceType::DepthStencil) {
+        size_t tuple_idx = (size_t)pixel_format - 14;
+        ASSERT(tuple_idx < depth_format_tuples.size());
+        tuple = depth_format_tuples[tuple_idx];
+    } else {
+        tuple = { GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE };
+    }
+
+    glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, width, height, 0,
+                 tuple.format, tuple.type, nullptr);
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+    // Restore previous texture bindings
+    cur_state.texture_units[0].texture_2d = old_tex;
+    cur_state.Apply();
+}
+
+MICROPROFILE_DEFINE(OpenGL_SurfaceUpload, "OpenGL", "Surface Upload", MP_RGB(128, 64, 192));
+CachedSurface* RasterizerCacheOpenGL::GetSurface(const CachedSurface& params, bool match_res_scale, bool load_if_create) {
+    using PixelFormat = CachedSurface::PixelFormat;
+    using SurfaceType = CachedSurface::SurfaceType;
+
+    if (params.addr == 0) {
+        return nullptr;
+    }
+
+    u32 params_size = params.width * params.height * CachedSurface::GetFormatBpp(params.pixel_format) / 8;
+
+    // Check for an exact match in existing surfaces
+    CachedSurface* best_exact_surface = nullptr;
+    float exact_surface_goodness = -1.f;
+
+    auto surface_interval = boost::icl::interval<PAddr>::right_open(params.addr, params.addr + params_size);
+    auto range = surface_cache.equal_range(surface_interval);
+    for (auto it = range.first; it != range.second; ++it) {
+        for (auto it2 = it->second.begin(); it2 != it->second.end(); ++it2) {
+            CachedSurface* surface = it2->get();
+
+            // Check if the request matches the surface exactly
+            if (params.addr == surface->addr &&
+                params.width == surface->width && params.height == surface->height &&
+                params.pixel_format == surface->pixel_format)
+            {
+                // Make sure optional param-matching criteria are fulfilled
+                bool tiling_match = (params.is_tiled == surface->is_tiled);
+                bool res_scale_match = (params.res_scale_width == surface->res_scale_width && params.res_scale_height == surface->res_scale_height);
+                if (!match_res_scale || res_scale_match) {
+                    // Prioritize same-tiling and highest resolution surfaces
+                    float match_goodness = (float)tiling_match + surface->res_scale_width * surface->res_scale_height;
+                    if (match_goodness > exact_surface_goodness || surface->dirty) {
+                        exact_surface_goodness = match_goodness;
+                        best_exact_surface = surface;
+                    }
+                }
+            }
+        }
+    }
+
+    // Return the best exact surface if found
+    if (best_exact_surface != nullptr) {
+        return best_exact_surface;
+    }
+
+    // No matching surfaces found, so create a new one
+    u8* texture_src_data = Memory::GetPhysicalPointer(params.addr);
+    if (texture_src_data == nullptr) {
+        return nullptr;
+    }
+
+    MICROPROFILE_SCOPE(OpenGL_SurfaceUpload);
+
+    std::shared_ptr<CachedSurface> new_surface = std::make_shared<CachedSurface>();
+
+    new_surface->addr = params.addr;
+    new_surface->size = params_size;
+
+    new_surface->texture.Create();
+    new_surface->width = params.width;
+    new_surface->height = params.height;
+    new_surface->stride = params.stride;
+    new_surface->res_scale_width = params.res_scale_width;
+    new_surface->res_scale_height = params.res_scale_height;
+
+    new_surface->is_tiled = params.is_tiled;
+    new_surface->pixel_format = params.pixel_format;
+    new_surface->dirty = false;
+
+    if (!load_if_create) {
+        // Don't load any data; just allocate the surface's texture
+        AllocateSurfaceTexture(new_surface->texture.handle, new_surface->pixel_format, new_surface->GetScaledWidth(), new_surface->GetScaledHeight());
+    } else {
+        // TODO: Consider attempting subrect match in existing surfaces and direct blit here instead of memory upload below if that's a common scenario in some game
+
+        Memory::RasterizerFlushRegion(params.addr, params_size);
+
+        // Load data from memory to the new surface
+        OpenGLState cur_state = OpenGLState::GetCurState();
+
+        GLuint old_tex = cur_state.texture_units[0].texture_2d;
+        cur_state.texture_units[0].texture_2d = new_surface->texture.handle;
+        cur_state.Apply();
+        glActiveTexture(GL_TEXTURE0);
+
+        glPixelStorei(GL_UNPACK_ROW_LENGTH, (GLint)new_surface->stride);
+        if (!new_surface->is_tiled) {
+            // TODO: Ensure this will always be a color format, not a depth or other format
+            ASSERT((size_t)new_surface->pixel_format < fb_format_tuples.size());
+            const FormatTuple& tuple = fb_format_tuples[(unsigned int)params.pixel_format];
+
+            glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, params.width, params.height, 0,
+                         tuple.format, tuple.type, texture_src_data);
+        } else {
+            SurfaceType type = CachedSurface::GetFormatType(new_surface->pixel_format);
+            if (type != SurfaceType::Depth && type != SurfaceType::DepthStencil) {
+                FormatTuple tuple;
+                if ((size_t)params.pixel_format < fb_format_tuples.size()) {
+                    tuple = fb_format_tuples[(unsigned int)params.pixel_format];
+                } else {
+                    // Texture
+                    tuple = { GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE };
+                }
+
+                std::vector<Math::Vec4<u8>> tex_buffer(params.width * params.height);
+
+                Pica::DebugUtils::TextureInfo tex_info;
+                tex_info.width = params.width;
+                tex_info.height = params.height;
+                tex_info.stride = params.width * CachedSurface::GetFormatBpp(params.pixel_format) / 8;
+                tex_info.format = (Pica::Regs::TextureFormat)params.pixel_format;
+                tex_info.physical_address = params.addr;
+
+                for (unsigned y = 0; y < params.height; ++y) {
+                    for (unsigned x = 0; x < params.width; ++x) {
+                        tex_buffer[x + params.width * y] = Pica::DebugUtils::LookupTexture(texture_src_data, x, params.height - 1 - y, tex_info);
+                    }
+                }
+
+                glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, params.width, params.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, tex_buffer.data());
+            } else {
+                // Depth/Stencil formats need special treatment since they aren't sampleable using LookupTexture and can't use RGBA format
+                size_t tuple_idx = (size_t)params.pixel_format - 14;
+                ASSERT(tuple_idx < depth_format_tuples.size());
+                const FormatTuple& tuple = depth_format_tuples[tuple_idx];
+
+                u32 bytes_per_pixel = CachedSurface::GetFormatBpp(params.pixel_format) / 8;
+
+                // OpenGL needs 4 bpp alignment for D24 since using GL_UNSIGNED_INT as type
+                bool use_4bpp = (params.pixel_format == PixelFormat::D24);
+
+                u32 gl_bytes_per_pixel = use_4bpp ? 4 : bytes_per_pixel;
+
+                std::vector<u8> temp_fb_depth_buffer(params.width * params.height * gl_bytes_per_pixel);
+
+                u8* temp_fb_depth_buffer_ptr = use_4bpp ? temp_fb_depth_buffer.data() + 1 : temp_fb_depth_buffer.data();
+
+                MortonCopyPixels(params.pixel_format, params.width, params.height, bytes_per_pixel, gl_bytes_per_pixel, texture_src_data, temp_fb_depth_buffer_ptr, true);
+
+                glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, params.width, params.height, 0,
+                             tuple.format, tuple.type, temp_fb_depth_buffer.data());
+            }
+        }
+        glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+
+        // If not 1x scale, blit 1x texture to a new scaled texture and replace texture in surface
+        if (new_surface->res_scale_width != 1.f || new_surface->res_scale_height != 1.f) {
+            OGLTexture scaled_texture;
+            scaled_texture.Create();
+
+            AllocateSurfaceTexture(scaled_texture.handle, new_surface->pixel_format, new_surface->GetScaledWidth(), new_surface->GetScaledHeight());
+            BlitTextures(new_surface->texture.handle, scaled_texture.handle, CachedSurface::GetFormatType(new_surface->pixel_format),
+                MathUtil::Rectangle<int>(0, 0, new_surface->width, new_surface->height),
+                MathUtil::Rectangle<int>(0, 0, new_surface->GetScaledWidth(), new_surface->GetScaledHeight()));
+
+            new_surface->texture.Release();
+            new_surface->texture.handle = scaled_texture.handle;
+            scaled_texture.handle = 0;
+            cur_state.texture_units[0].texture_2d = new_surface->texture.handle;
+            cur_state.Apply();
+        }
+
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+        cur_state.texture_units[0].texture_2d = old_tex;
+        cur_state.Apply();
+    }
+
+    Memory::RasterizerMarkRegionCached(new_surface->addr, new_surface->size, 1);
+    surface_cache.add(std::make_pair(boost::icl::interval<PAddr>::right_open(new_surface->addr, new_surface->addr + new_surface->size), std::set<std::shared_ptr<CachedSurface>>({ new_surface })));
+    return new_surface.get();
+}
+
+CachedSurface* RasterizerCacheOpenGL::GetSurfaceRect(const CachedSurface& params, bool match_res_scale, bool load_if_create, MathUtil::Rectangle<int>& out_rect) {
+    if (params.addr == 0) {
+        return nullptr;
+    }
+
+    u32 total_pixels = params.width * params.height;
+    u32 params_size = total_pixels * CachedSurface::GetFormatBpp(params.pixel_format) / 8;
+
+    // Attempt to find encompassing surfaces
+    CachedSurface* best_subrect_surface = nullptr;
+    float subrect_surface_goodness = -1.f;
+
+    auto surface_interval = boost::icl::interval<PAddr>::right_open(params.addr, params.addr + params_size);
+    auto cache_upper_bound = surface_cache.upper_bound(surface_interval);
+    for (auto it = surface_cache.lower_bound(surface_interval); it != cache_upper_bound; ++it) {
+        for (auto it2 = it->second.begin(); it2 != it->second.end(); ++it2) {
+            CachedSurface* surface = it2->get();
+
+            // Check if the request is contained in the surface
+            if (params.addr >= surface->addr &&
+                params.addr + params_size - 1 <= surface->addr + surface->size - 1 &&
+                params.pixel_format == surface->pixel_format)
+            {
+                // Make sure optional param-matching criteria are fulfilled
+                bool tiling_match = (params.is_tiled == surface->is_tiled);
+                bool res_scale_match = (params.res_scale_width == surface->res_scale_width && params.res_scale_height == surface->res_scale_height);
+                if (!match_res_scale || res_scale_match) {
+                    // Prioritize same-tiling and highest resolution surfaces
+                    float match_goodness = (float)tiling_match + surface->res_scale_width * surface->res_scale_height;
+                    if (match_goodness > subrect_surface_goodness || surface->dirty) {
+                        subrect_surface_goodness = match_goodness;
+                        best_subrect_surface = surface;
+                    }
+                }
+            }
+        }
+    }
+
+    // Return the best subrect surface if found
+    if (best_subrect_surface != nullptr) {
+        unsigned int bytes_per_pixel = (CachedSurface::GetFormatBpp(best_subrect_surface->pixel_format) / 8);
+
+        int x0, y0;
+
+        if (!params.is_tiled) {
+            u32 begin_pixel_index = (params.addr - best_subrect_surface->addr) / bytes_per_pixel;
+            x0 = begin_pixel_index % best_subrect_surface->width;
+            y0 = begin_pixel_index / best_subrect_surface->width;
+
+            out_rect = MathUtil::Rectangle<int>(x0, y0, x0 + params.width, y0 + params.height);
+        } else {
+            u32 bytes_per_tile = 8 * 8 * bytes_per_pixel;
+            u32 tiles_per_row = best_subrect_surface->width / 8;
+
+            u32 begin_tile_index = (params.addr - best_subrect_surface->addr) / bytes_per_tile;
+            x0 = begin_tile_index % tiles_per_row * 8;
+            y0 = begin_tile_index / tiles_per_row * 8;
+
+            // Tiled surfaces are flipped vertically in the rasterizer vs. 3DS memory.
+            out_rect = MathUtil::Rectangle<int>(x0, best_subrect_surface->height - y0, x0 + params.width, best_subrect_surface->height - (y0 + params.height));
+        }
+
+        out_rect.left = (int)(out_rect.left * best_subrect_surface->res_scale_width);
+        out_rect.right = (int)(out_rect.right * best_subrect_surface->res_scale_width);
+        out_rect.top = (int)(out_rect.top * best_subrect_surface->res_scale_height);
+        out_rect.bottom = (int)(out_rect.bottom * best_subrect_surface->res_scale_height);
+
+        return best_subrect_surface;
+    }
+
+    // No subrect found - create and return a new surface
+    if (!params.is_tiled) {
+        out_rect = MathUtil::Rectangle<int>(0, 0, (int)(params.width * params.res_scale_width), (int)(params.height * params.res_scale_height));
+    } else {
+        out_rect = MathUtil::Rectangle<int>(0, (int)(params.height * params.res_scale_height), (int)(params.width * params.res_scale_width), 0);
+    }
+
+    return GetSurface(params, match_res_scale, load_if_create);
+}
+
+CachedSurface* RasterizerCacheOpenGL::GetTextureSurface(const Pica::Regs::FullTextureConfig& config) {
+    Pica::DebugUtils::TextureInfo info = Pica::DebugUtils::TextureInfo::FromPicaRegister(config.config, config.format);
+
+    CachedSurface params;
+    params.addr = info.physical_address;
+    params.width = info.width;
+    params.height = info.height;
+    params.is_tiled = true;
+    params.pixel_format = CachedSurface::PixelFormatFromTextureFormat(info.format);
+    return GetSurface(params, false, true);
+}
+
+std::tuple<CachedSurface*, CachedSurface*, MathUtil::Rectangle<int>> RasterizerCacheOpenGL::GetFramebufferSurfaces(const Pica::Regs::FramebufferConfig& config) {
+    const auto& regs = Pica::g_state.regs;
+
+    // Make sur that framebuffers don't overlap if both color and depth are being used
+    u32 fb_area = config.GetWidth() * config.GetHeight();
+    bool framebuffers_overlap = config.GetColorBufferPhysicalAddress() != 0 &&
+                                config.GetDepthBufferPhysicalAddress() != 0 &&
+                                MathUtil::IntervalsIntersect(config.GetColorBufferPhysicalAddress(), fb_area * GPU::Regs::BytesPerPixel(GPU::Regs::PixelFormat(config.color_format.Value())),
+                                                             config.GetDepthBufferPhysicalAddress(), fb_area * Pica::Regs::BytesPerDepthPixel(config.depth_format));
+    bool using_color_fb = config.GetColorBufferPhysicalAddress() != 0;
+    bool using_depth_fb = config.GetDepthBufferPhysicalAddress() != 0 && (regs.output_merger.depth_test_enable || regs.output_merger.depth_write_enable || !framebuffers_overlap);
+
+    if (framebuffers_overlap && using_color_fb && using_depth_fb) {
+        LOG_CRITICAL(Render_OpenGL, "Color and depth framebuffer memory regions overlap; overlapping framebuffers not supported!");
+        using_depth_fb = false;
+    }
+
+    // get color and depth surfaces
+    CachedSurface color_params;
+    CachedSurface depth_params;
+    color_params.width = depth_params.width = config.GetWidth();
+    color_params.height = depth_params.height = config.GetHeight();
+    color_params.is_tiled = depth_params.is_tiled = true;
+    if (VideoCore::g_scaled_resolution_enabled) {
+        auto layout = VideoCore::g_emu_window->GetFramebufferLayout();
+
+        // Assume same scaling factor for top and bottom screens
+        color_params.res_scale_width = depth_params.res_scale_width = (float)layout.top_screen.GetWidth() / VideoCore::kScreenTopWidth;
+        color_params.res_scale_height = depth_params.res_scale_height = (float)layout.top_screen.GetHeight() / VideoCore::kScreenTopHeight;
+    }
+
+    color_params.addr = config.GetColorBufferPhysicalAddress();
+    color_params.pixel_format = CachedSurface::PixelFormatFromColorFormat(config.color_format);
+
+    depth_params.addr = config.GetDepthBufferPhysicalAddress();
+    depth_params.pixel_format = CachedSurface::PixelFormatFromDepthFormat(config.depth_format);
+
+    MathUtil::Rectangle<int> color_rect;
+    CachedSurface* color_surface = using_color_fb ? GetSurfaceRect(color_params, true, true, color_rect) : nullptr;
+
+    MathUtil::Rectangle<int> depth_rect;
+    CachedSurface* depth_surface = using_depth_fb ? GetSurfaceRect(depth_params, true, true, depth_rect) : nullptr;
+
+    // Sanity check to make sure found surfaces aren't the same
+    if (using_depth_fb && using_color_fb && color_surface == depth_surface) {
+        LOG_CRITICAL(Render_OpenGL, "Color and depth framebuffer surfaces overlap; overlapping surfaces not supported!");
+        using_depth_fb = false;
+        depth_surface = nullptr;
+    }
+
+    MathUtil::Rectangle<int> rect;
+
+    if (color_surface != nullptr && depth_surface != nullptr && (depth_rect.left != color_rect.left || depth_rect.top != color_rect.top)) {
+        // Can't specify separate color and depth viewport offsets in OpenGL, so re-zero both if they don't match
+        if (color_rect.left != 0 || color_rect.top != 0) {
+            color_surface = GetSurface(color_params, true, true);
+        }
+
+        if (depth_rect.left != 0 || depth_rect.top != 0) {
+            depth_surface = GetSurface(depth_params, true, true);
+        }
+
+        if (!color_surface->is_tiled) {
+            rect = MathUtil::Rectangle<int>(0, 0, (int)(color_params.width * color_params.res_scale_width), (int)(color_params.height * color_params.res_scale_height));
+        } else {
+            rect = MathUtil::Rectangle<int>(0, (int)(color_params.height * color_params.res_scale_height), (int)(color_params.width * color_params.res_scale_width), 0);
+        }
+    } else if (color_surface != nullptr) {
+        rect = color_rect;
+    } else if (depth_surface != nullptr) {
+        rect = depth_rect;
+    } else {
+        rect = MathUtil::Rectangle<int>(0, 0, 0, 0);
+    }
+
+    return std::make_tuple(color_surface, depth_surface, rect);
+}
+
+CachedSurface* RasterizerCacheOpenGL::TryGetFillSurface(const GPU::Regs::MemoryFillConfig& config) {
+    auto surface_interval = boost::icl::interval<PAddr>::right_open(config.GetStartAddress(), config.GetEndAddress());
+    auto range = surface_cache.equal_range(surface_interval);
+    for (auto it = range.first; it != range.second; ++it) {
+        for (auto it2 = it->second.begin(); it2 != it->second.end(); ++it2) {
+            int bits_per_value = 0;
+            if (config.fill_24bit) {
+                bits_per_value = 24;
+            } else if (config.fill_32bit) {
+                bits_per_value = 32;
+            } else {
+                bits_per_value = 16;
+            }
+
+            CachedSurface* surface = it2->get();
+
+            if (surface->addr == config.GetStartAddress() &&
+                CachedSurface::GetFormatBpp(surface->pixel_format) == bits_per_value &&
+                (surface->width * surface->height * CachedSurface::GetFormatBpp(surface->pixel_format) / 8) == (config.GetEndAddress() - config.GetStartAddress()))
+            {
+                return surface;
+            }
+        }
+    }
+
+    return nullptr;
+}
+
+MICROPROFILE_DEFINE(OpenGL_SurfaceDownload, "OpenGL", "Surface Download", MP_RGB(128, 192, 64));
+void RasterizerCacheOpenGL::FlushSurface(CachedSurface* surface) {
+    using PixelFormat = CachedSurface::PixelFormat;
+    using SurfaceType = CachedSurface::SurfaceType;
+
+    if (!surface->dirty) {
+        return;
+    }
+
+    MICROPROFILE_SCOPE(OpenGL_SurfaceDownload);
+
+    u8* dst_buffer = Memory::GetPhysicalPointer(surface->addr);
+    if (dst_buffer == nullptr) {
+        return;
+    }
+
+    OpenGLState cur_state = OpenGLState::GetCurState();
+    GLuint old_tex = cur_state.texture_units[0].texture_2d;
+
+    OGLTexture unscaled_tex;
+    GLuint texture_to_flush = surface->texture.handle;
+
+    // If not 1x scale, blit scaled texture to a new 1x texture and use that to flush
+    if (surface->res_scale_width != 1.f || surface->res_scale_height != 1.f) {
+        unscaled_tex.Create();
+
+        AllocateSurfaceTexture(unscaled_tex.handle, surface->pixel_format, surface->width, surface->height);
+        BlitTextures(surface->texture.handle, unscaled_tex.handle, CachedSurface::GetFormatType(surface->pixel_format),
+            MathUtil::Rectangle<int>(0, 0, surface->GetScaledWidth(), surface->GetScaledHeight()),
+            MathUtil::Rectangle<int>(0, 0, surface->width, surface->height));
+
+        texture_to_flush = unscaled_tex.handle;
+    }
+
+    cur_state.texture_units[0].texture_2d = texture_to_flush;
+    cur_state.Apply();
+    glActiveTexture(GL_TEXTURE0);
+
+    glPixelStorei(GL_PACK_ROW_LENGTH, (GLint)surface->stride);
+    if (!surface->is_tiled) {
+        // TODO: Ensure this will always be a color format, not a depth or other format
+        ASSERT((size_t)surface->pixel_format < fb_format_tuples.size());
+        const FormatTuple& tuple = fb_format_tuples[(unsigned int)surface->pixel_format];
+
+        glGetTexImage(GL_TEXTURE_2D, 0, tuple.format, tuple.type, dst_buffer);
+    } else {
+        SurfaceType type = CachedSurface::GetFormatType(surface->pixel_format);
+        if (type != SurfaceType::Depth && type != SurfaceType::DepthStencil) {
+            ASSERT((size_t)surface->pixel_format < fb_format_tuples.size());
+            const FormatTuple& tuple = fb_format_tuples[(unsigned int)surface->pixel_format];
+
+            u32 bytes_per_pixel = CachedSurface::GetFormatBpp(surface->pixel_format) / 8;
+
+            std::vector<u8> temp_gl_buffer(surface->width * surface->height * bytes_per_pixel);
+
+            glGetTexImage(GL_TEXTURE_2D, 0, tuple.format, tuple.type, temp_gl_buffer.data());
+
+            // Directly copy pixels. Internal OpenGL color formats are consistent so no conversion is necessary.
+            MortonCopyPixels(surface->pixel_format, surface->width, surface->height, bytes_per_pixel, bytes_per_pixel, dst_buffer, temp_gl_buffer.data(), false);
+        } else {
+            // Depth/Stencil formats need special treatment since they aren't sampleable using LookupTexture and can't use RGBA format
+            size_t tuple_idx = (size_t)surface->pixel_format - 14;
+            ASSERT(tuple_idx < depth_format_tuples.size());
+            const FormatTuple& tuple = depth_format_tuples[tuple_idx];
+
+            u32 bytes_per_pixel = CachedSurface::GetFormatBpp(surface->pixel_format) / 8;
+
+            // OpenGL needs 4 bpp alignment for D24 since using GL_UNSIGNED_INT as type
+            bool use_4bpp = (surface->pixel_format == PixelFormat::D24);
+
+            u32 gl_bytes_per_pixel = use_4bpp ? 4 : bytes_per_pixel;
+
+            std::vector<u8> temp_gl_buffer(surface->width * surface->height * gl_bytes_per_pixel);
+
+            glGetTexImage(GL_TEXTURE_2D, 0, tuple.format, tuple.type, temp_gl_buffer.data());
+
+            u8* temp_gl_buffer_ptr = use_4bpp ? temp_gl_buffer.data() + 1 : temp_gl_buffer.data();
+
+            MortonCopyPixels(surface->pixel_format, surface->width, surface->height, bytes_per_pixel, gl_bytes_per_pixel, dst_buffer, temp_gl_buffer_ptr, false);
+        }
+    }
+    glPixelStorei(GL_PACK_ROW_LENGTH, 0);
+
+    surface->dirty = false;
+
+    cur_state.texture_units[0].texture_2d = old_tex;
+    cur_state.Apply();
+}
+
+void RasterizerCacheOpenGL::FlushRegion(PAddr addr, u32 size, const CachedSurface* skip_surface, bool invalidate) {
+    if (size == 0) {
+        return;
+    }
+
+    // Gather up unique surfaces that touch the region
+    std::unordered_set<std::shared_ptr<CachedSurface>> touching_surfaces;
+
+    auto surface_interval = boost::icl::interval<PAddr>::right_open(addr, addr + size);
+    auto cache_upper_bound = surface_cache.upper_bound(surface_interval);
+    for (auto it = surface_cache.lower_bound(surface_interval); it != cache_upper_bound; ++it) {
+        std::copy_if(it->second.begin(), it->second.end(), std::inserter(touching_surfaces, touching_surfaces.end()),
+            [skip_surface](std::shared_ptr<CachedSurface> surface) { return (surface.get() != skip_surface); });
+    }
+
+    // Flush and invalidate surfaces
+    for (auto surface : touching_surfaces) {
+        FlushSurface(surface.get());
+        if (invalidate) {
+            Memory::RasterizerMarkRegionCached(surface->addr, surface->size, -1);
+            surface_cache.subtract(std::make_pair(boost::icl::interval<PAddr>::right_open(surface->addr, surface->addr + surface->size), std::set<std::shared_ptr<CachedSurface>>({ surface })));
+        }
+    }
+}
+
+void RasterizerCacheOpenGL::FlushAll() {
+    for (auto& surfaces : surface_cache) {
+        for (auto& surface : surfaces.second) {
+            FlushSurface(surface.get());
+        }
+    }
 }
diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.h b/src/video_core/renderer_opengl/gl_rasterizer_cache.h
index b69651427..893d51138 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer_cache.h
+++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.h
@@ -6,38 +6,211 @@
 
 #include <map>
 #include <memory>
+#include <set>
+
+#include <boost/icl/interval_map.hpp>
+
+#include "common/math_util.h"
+
+#include "core/hw/gpu.h"
 
 #include "video_core/pica.h"
 #include "video_core/debug_utils/debug_utils.h"
 #include "video_core/renderer_opengl/gl_resource_manager.h"
 #include "video_core/renderer_opengl/gl_state.h"
 
-class RasterizerCacheOpenGL : NonCopyable {
-public:
-    ~RasterizerCacheOpenGL();
+struct CachedSurface;
 
-    /// Loads a texture from 3DS memory to OpenGL and caches it (if not already cached)
-    void LoadAndBindTexture(OpenGLState &state, unsigned texture_unit, const Pica::DebugUtils::TextureInfo& info);
+using SurfaceCache = boost::icl::interval_map<PAddr, std::set<std::shared_ptr<CachedSurface>>>;
 
-    void LoadAndBindTexture(OpenGLState &state, unsigned texture_unit, const Pica::Regs::FullTextureConfig& config) {
-        LoadAndBindTexture(state, texture_unit, Pica::DebugUtils::TextureInfo::FromPicaRegister(config.config, config.format));
-    }
+struct CachedSurface {
+    enum class PixelFormat {
+        // First 5 formats are shared between textures and color buffers
+        RGBA8        =  0,
+        RGB8         =  1,
+        RGB5A1       =  2,
+        RGB565       =  3,
+        RGBA4        =  4,
 
-    /// Invalidate any cached resource intersecting the specified region.
-    void InvalidateInRange(PAddr addr, u32 size, bool ignore_hash = false);
+        // Texture-only formats
+        IA8          =  5,
+        RG8          =  6,
+        I8           =  7,
+        A8           =  8,
+        IA4          =  9,
+        I4           = 10,
+        A4           = 11,
+        ETC1         = 12,
+        ETC1A4       = 13,
 
-    /// Invalidate all cached OpenGL resources tracked by this cache manager
-    void InvalidateAll();
+        // Depth buffer-only formats
+        D16          = 14,
+        // gap
+        D24          = 16,
+        D24S8        = 17,
 
-private:
-    struct CachedTexture {
-        OGLTexture texture;
-        GLuint width;
-        GLuint height;
-        u32 size;
-        u64 hash;
-        PAddr addr;
+        Invalid      = 255,
     };
 
-    std::map<PAddr, std::unique_ptr<CachedTexture>> texture_cache;
+    enum class SurfaceType {
+        Color        = 0,
+        Texture      = 1,
+        Depth        = 2,
+        DepthStencil = 3,
+        Invalid      = 4,
+    };
+
+    static unsigned int GetFormatBpp(CachedSurface::PixelFormat format) {
+        static const std::array<unsigned int, 18> bpp_table = {
+            32, // RGBA8
+            24, // RGB8
+            16, // RGB5A1
+            16, // RGB565
+            16, // RGBA4
+            16, // IA8
+            16, // RG8
+            8,  // I8
+            8,  // A8
+            8,  // IA4
+            4,  // I4
+            4,  // A4
+            4,  // ETC1
+            8,  // ETC1A4
+            16, // D16
+            0,
+            24, // D24
+            32, // D24S8
+        };
+
+        ASSERT((unsigned int)format < ARRAY_SIZE(bpp_table));
+        return bpp_table[(unsigned int)format];
+    }
+
+    static PixelFormat PixelFormatFromTextureFormat(Pica::Regs::TextureFormat format) {
+        return ((unsigned int)format < 14) ? (PixelFormat)format : PixelFormat::Invalid;
+    }
+
+    static PixelFormat PixelFormatFromColorFormat(Pica::Regs::ColorFormat format) {
+        return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid;
+    }
+
+    static PixelFormat PixelFormatFromDepthFormat(Pica::Regs::DepthFormat format) {
+        return ((unsigned int)format < 4) ? (PixelFormat)((unsigned int)format + 14) : PixelFormat::Invalid;
+    }
+
+    static PixelFormat PixelFormatFromGPUPixelFormat(GPU::Regs::PixelFormat format) {
+        switch (format) {
+        // RGB565 and RGB5A1 are switched in PixelFormat compared to ColorFormat
+        case GPU::Regs::PixelFormat::RGB565:
+            return PixelFormat::RGB565;
+        case GPU::Regs::PixelFormat::RGB5A1:
+            return PixelFormat::RGB5A1;
+        default:
+            return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid;
+        }
+    }
+
+    static bool CheckFormatsBlittable(PixelFormat pixel_format_a, PixelFormat pixel_format_b) {
+        SurfaceType a_type = GetFormatType(pixel_format_a);
+        SurfaceType b_type = GetFormatType(pixel_format_b);
+
+        if ((a_type == SurfaceType::Color || a_type == SurfaceType::Texture) && (b_type == SurfaceType::Color || b_type == SurfaceType::Texture)) {
+            return true;
+        }
+
+        if (a_type == SurfaceType::Depth && b_type == SurfaceType::Depth) {
+            return true;
+        }
+
+        if (a_type == SurfaceType::DepthStencil && b_type == SurfaceType::DepthStencil) {
+            return true;
+        }
+
+        return false;
+    }
+
+    static SurfaceType GetFormatType(PixelFormat pixel_format) {
+        if ((unsigned int)pixel_format < 5) {
+            return SurfaceType::Color;
+        }
+
+        if ((unsigned int)pixel_format < 14) {
+            return SurfaceType::Texture;
+        }
+
+        if (pixel_format == PixelFormat::D16 || pixel_format == PixelFormat::D24) {
+            return SurfaceType::Depth;
+        }
+
+        if (pixel_format == PixelFormat::D24S8) {
+            return SurfaceType::DepthStencil;
+        }
+
+        return SurfaceType::Invalid;
+    }
+
+    u32 GetScaledWidth() const {
+        return (u32)(width * res_scale_width);
+    }
+
+    u32 GetScaledHeight() const {
+        return (u32)(height * res_scale_height);
+    }
+
+    PAddr addr;
+    u32 size;
+
+    PAddr min_valid;
+    PAddr max_valid;
+
+    OGLTexture texture;
+    u32 width;
+    u32 height;
+    u32 stride = 0;
+    float res_scale_width = 1.f;
+    float res_scale_height = 1.f;
+
+    bool is_tiled;
+    PixelFormat pixel_format;
+    bool dirty;
+};
+
+class RasterizerCacheOpenGL : NonCopyable {
+public:
+    RasterizerCacheOpenGL();
+    ~RasterizerCacheOpenGL();
+
+    /// Blits one texture to another
+    bool BlitTextures(GLuint src_tex, GLuint dst_tex, CachedSurface::SurfaceType type, const MathUtil::Rectangle<int>& src_rect, const MathUtil::Rectangle<int>& dst_rect);
+
+    /// Attempt to blit one surface's texture to another
+    bool TryBlitSurfaces(CachedSurface* src_surface, const MathUtil::Rectangle<int>& src_rect, CachedSurface* dst_surface, const MathUtil::Rectangle<int>& dst_rect);
+
+    /// Loads a texture from 3DS memory to OpenGL and caches it (if not already cached)
+    CachedSurface* GetSurface(const CachedSurface& params, bool match_res_scale, bool load_if_create);
+
+    /// Attempt to find a subrect (resolution scaled) of a surface, otherwise loads a texture from 3DS memory to OpenGL and caches it (if not already cached)
+    CachedSurface* GetSurfaceRect(const CachedSurface& params, bool match_res_scale, bool load_if_create, MathUtil::Rectangle<int>& out_rect);
+
+    /// Gets a surface based on the texture configuration
+    CachedSurface* GetTextureSurface(const Pica::Regs::FullTextureConfig& config);
+
+    /// Gets the color and depth surfaces and rect (resolution scaled) based on the framebuffer configuration
+    std::tuple<CachedSurface*, CachedSurface*, MathUtil::Rectangle<int>> GetFramebufferSurfaces(const Pica::Regs::FramebufferConfig& config);
+
+    /// Attempt to get a surface that exactly matches the fill region and format
+    CachedSurface* TryGetFillSurface(const GPU::Regs::MemoryFillConfig& config);
+
+    /// Write the surface back to memory
+    void FlushSurface(CachedSurface* surface);
+
+    /// Write any cached resources overlapping the region back to memory (if dirty) and optionally invalidate them in the cache
+    void FlushRegion(PAddr addr, u32 size, const CachedSurface* skip_surface, bool invalidate);
+
+    /// Flush all cached resources tracked by this cache manager
+    void FlushAll();
+
+private:
+    SurfaceCache surface_cache;
+    OGLFramebuffer transfer_framebuffers[2];
 };
diff --git a/src/video_core/renderer_opengl/gl_state.cpp b/src/video_core/renderer_opengl/gl_state.cpp
index 08e4d0b54..f04bdd8c5 100644
--- a/src/video_core/renderer_opengl/gl_state.cpp
+++ b/src/video_core/renderer_opengl/gl_state.cpp
@@ -3,6 +3,7 @@
 // Refer to the license.txt file included.
 
 #include "video_core/pica.h"
+#include "video_core/renderer_opengl/gl_resource_manager.h"
 #include "video_core/renderer_opengl/gl_state.h"
 
 OpenGLState OpenGLState::cur_state;
@@ -48,17 +49,19 @@ OpenGLState::OpenGLState() {
         texture_unit.sampler = 0;
     }
 
-    for (auto& lut : lighting_lut) {
+    for (auto& lut : lighting_luts) {
         lut.texture_1d = 0;
     }
 
-    draw.framebuffer = 0;
+    draw.read_framebuffer = 0;
+    draw.draw_framebuffer = 0;
     draw.vertex_array = 0;
     draw.vertex_buffer = 0;
+    draw.uniform_buffer = 0;
     draw.shader_program = 0;
 }
 
-void OpenGLState::Apply() {
+void OpenGLState::Apply() const {
     // Culling
     if (cull.enabled != cur_state.cull.enabled) {
         if (cull.enabled) {
@@ -175,16 +178,19 @@ void OpenGLState::Apply() {
     }
 
     // Lighting LUTs
-    for (unsigned i = 0; i < ARRAY_SIZE(lighting_lut); ++i) {
-        if (lighting_lut[i].texture_1d != cur_state.lighting_lut[i].texture_1d) {
+    for (unsigned i = 0; i < ARRAY_SIZE(lighting_luts); ++i) {
+        if (lighting_luts[i].texture_1d != cur_state.lighting_luts[i].texture_1d) {
             glActiveTexture(GL_TEXTURE3 + i);
-            glBindTexture(GL_TEXTURE_1D, lighting_lut[i].texture_1d);
+            glBindTexture(GL_TEXTURE_1D, lighting_luts[i].texture_1d);
         }
     }
 
     // Framebuffer
-    if (draw.framebuffer != cur_state.draw.framebuffer) {
-        glBindFramebuffer(GL_FRAMEBUFFER, draw.framebuffer);
+    if (draw.read_framebuffer != cur_state.draw.read_framebuffer) {
+        glBindFramebuffer(GL_READ_FRAMEBUFFER, draw.read_framebuffer);
+    }
+    if (draw.draw_framebuffer != cur_state.draw.draw_framebuffer) {
+        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, draw.draw_framebuffer);
     }
 
     // Vertex array
@@ -210,45 +216,58 @@ void OpenGLState::Apply() {
     cur_state = *this;
 }
 
-void OpenGLState::ResetTexture(GLuint id) {
+GLenum OpenGLState::CheckFBStatus(GLenum target) {
+    GLenum fb_status = glCheckFramebufferStatus(target);
+    if (fb_status != GL_FRAMEBUFFER_COMPLETE) {
+        const char* fb_description = (target == GL_READ_FRAMEBUFFER ? "READ" : (target == GL_DRAW_FRAMEBUFFER ? "DRAW" : "UNK"));
+        LOG_CRITICAL(Render_OpenGL, "OpenGL %s framebuffer check failed, status %X", fb_description, fb_status);
+    }
+
+    return fb_status;
+}
+
+void OpenGLState::ResetTexture(GLuint handle) {
     for (auto& unit : cur_state.texture_units) {
-        if (unit.texture_2d == id) {
+        if (unit.texture_2d == handle) {
             unit.texture_2d = 0;
         }
     }
 }
 
-void OpenGLState::ResetSampler(GLuint id) {
+void OpenGLState::ResetSampler(GLuint handle) {
     for (auto& unit : cur_state.texture_units) {
-        if (unit.sampler == id) {
+        if (unit.sampler == handle) {
             unit.sampler = 0;
         }
     }
 }
 
-void OpenGLState::ResetProgram(GLuint id) {
-    if (cur_state.draw.shader_program == id) {
+void OpenGLState::ResetProgram(GLuint handle) {
+    if (cur_state.draw.shader_program == handle) {
         cur_state.draw.shader_program = 0;
     }
 }
 
-void OpenGLState::ResetBuffer(GLuint id) {
-    if (cur_state.draw.vertex_buffer == id) {
+void OpenGLState::ResetBuffer(GLuint handle) {
+    if (cur_state.draw.vertex_buffer == handle) {
         cur_state.draw.vertex_buffer = 0;
     }
-    if (cur_state.draw.uniform_buffer == id) {
+    if (cur_state.draw.uniform_buffer == handle) {
         cur_state.draw.uniform_buffer = 0;
     }
 }
 
-void OpenGLState::ResetVertexArray(GLuint id) {
-    if (cur_state.draw.vertex_array == id) {
+void OpenGLState::ResetVertexArray(GLuint handle) {
+    if (cur_state.draw.vertex_array == handle) {
         cur_state.draw.vertex_array = 0;
     }
 }
 
-void OpenGLState::ResetFramebuffer(GLuint id) {
-    if (cur_state.draw.framebuffer == id) {
-        cur_state.draw.framebuffer = 0;
+void OpenGLState::ResetFramebuffer(GLuint handle) {
+    if (cur_state.draw.read_framebuffer == handle) {
+        cur_state.draw.read_framebuffer = 0;
+    }
+    if (cur_state.draw.draw_framebuffer == handle) {
+        cur_state.draw.draw_framebuffer = 0;
     }
 }
diff --git a/src/video_core/renderer_opengl/gl_state.h b/src/video_core/renderer_opengl/gl_state.h
index e848058d7..0f72e9004 100644
--- a/src/video_core/renderer_opengl/gl_state.h
+++ b/src/video_core/renderer_opengl/gl_state.h
@@ -5,6 +5,7 @@
 #pragma once
 
 #include <glad/glad.h>
+#include <memory>
 
 class OpenGLState {
 public:
@@ -63,15 +64,15 @@ public:
 
     struct {
         GLuint texture_1d; // GL_TEXTURE_BINDING_1D
-    } lighting_lut[6];
+    } lighting_luts[6];
 
     struct {
-        GLuint framebuffer; // GL_DRAW_FRAMEBUFFER_BINDING
+        GLuint read_framebuffer; // GL_READ_FRAMEBUFFER_BINDING
+        GLuint draw_framebuffer; // GL_DRAW_FRAMEBUFFER_BINDING
         GLuint vertex_array; // GL_VERTEX_ARRAY_BINDING
         GLuint vertex_buffer; // GL_ARRAY_BUFFER_BINDING
         GLuint uniform_buffer; // GL_UNIFORM_BUFFER_BINDING
         GLuint shader_program; // GL_CURRENT_PROGRAM
-        bool shader_dirty;
     } draw;
 
     OpenGLState();
@@ -82,14 +83,18 @@ public:
     }
 
     /// Apply this state as the current OpenGL state
-    void Apply();
+    void Apply() const;
 
-    static void ResetTexture(GLuint id);
-    static void ResetSampler(GLuint id);
-    static void ResetProgram(GLuint id);
-    static void ResetBuffer(GLuint id);
-    static void ResetVertexArray(GLuint id);
-    static void ResetFramebuffer(GLuint id);
+    /// Check the status of the current OpenGL read or draw framebuffer configuration
+    static GLenum CheckFBStatus(GLenum target);
+
+    /// Resets and unbinds any references to the given resource in the current OpenGL state
+    static void ResetTexture(GLuint handle);
+    static void ResetSampler(GLuint handle);
+    static void ResetProgram(GLuint handle);
+    static void ResetBuffer(GLuint handle);
+    static void ResetVertexArray(GLuint handle);
+    static void ResetFramebuffer(GLuint handle);
 
 private:
     static OpenGLState cur_state;
diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp
index 11c4d0daf..8f907593f 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.cpp
+++ b/src/video_core/renderer_opengl/renderer_opengl.cpp
@@ -107,7 +107,7 @@ void RendererOpenGL::SwapBuffers() {
     OpenGLState prev_state = OpenGLState::GetCurState();
     state.Apply();
 
-    for(int i : {0, 1}) {
+    for (int i : {0, 1}) {
         const auto& framebuffer = GPU::g_regs.framebuffer_config[i];
 
         // Main LCD (0): 0x1ED02204, Sub LCD (1): 0x1ED02A04
@@ -117,25 +117,25 @@ void RendererOpenGL::SwapBuffers() {
         LCD::Read(color_fill.raw, lcd_color_addr);
 
         if (color_fill.is_enabled) {
-            LoadColorToActiveGLTexture(color_fill.color_r, color_fill.color_g, color_fill.color_b, textures[i]);
+            LoadColorToActiveGLTexture(color_fill.color_r, color_fill.color_g, color_fill.color_b, screen_infos[i].texture);
 
             // Resize the texture in case the framebuffer size has changed
-            textures[i].width = 1;
-            textures[i].height = 1;
+            screen_infos[i].texture.width = 1;
+            screen_infos[i].texture.height = 1;
         } else {
-            if (textures[i].width != (GLsizei)framebuffer.width ||
-                textures[i].height != (GLsizei)framebuffer.height ||
-                textures[i].format != framebuffer.color_format) {
+            if (screen_infos[i].texture.width != (GLsizei)framebuffer.width ||
+                screen_infos[i].texture.height != (GLsizei)framebuffer.height ||
+                screen_infos[i].texture.format != framebuffer.color_format) {
                 // Reallocate texture if the framebuffer size has changed.
                 // This is expected to not happen very often and hence should not be a
                 // performance problem.
-                ConfigureFramebufferTexture(textures[i], framebuffer);
+                ConfigureFramebufferTexture(screen_infos[i].texture, framebuffer);
             }
-            LoadFBToActiveGLTexture(framebuffer, textures[i]);
+            LoadFBToScreenInfo(framebuffer, screen_infos[i]);
 
             // Resize the texture in case the framebuffer size has changed
-            textures[i].width = framebuffer.width;
-            textures[i].height = framebuffer.height;
+            screen_infos[i].texture.width = framebuffer.width;
+            screen_infos[i].texture.height = framebuffer.height;
         }
     }
 
@@ -166,8 +166,8 @@ void RendererOpenGL::SwapBuffers() {
 /**
  * Loads framebuffer from emulated memory into the active OpenGL texture.
  */
-void RendererOpenGL::LoadFBToActiveGLTexture(const GPU::Regs::FramebufferConfig& framebuffer,
-                                             const TextureInfo& texture) {
+void RendererOpenGL::LoadFBToScreenInfo(const GPU::Regs::FramebufferConfig& framebuffer,
+                                         ScreenInfo& screen_info) {
 
     const PAddr framebuffer_addr = framebuffer.active_fb == 0 ?
             framebuffer.address_left1 : framebuffer.address_left2;
@@ -177,8 +177,6 @@ void RendererOpenGL::LoadFBToActiveGLTexture(const GPU::Regs::FramebufferConfig&
         framebuffer_addr, (int)framebuffer.width,
         (int)framebuffer.height, (int)framebuffer.format);
 
-    const u8* framebuffer_data = Memory::GetPhysicalPointer(framebuffer_addr);
-
     int bpp = GPU::Regs::BytesPerPixel(framebuffer.color_format);
     size_t pixel_stride = framebuffer.stride / bpp;
 
@@ -189,24 +187,34 @@ void RendererOpenGL::LoadFBToActiveGLTexture(const GPU::Regs::FramebufferConfig&
     // only allows rows to have a memory alignement of 4.
     ASSERT(pixel_stride % 4 == 0);
 
-    state.texture_units[0].texture_2d = texture.handle;
-    state.Apply();
+    if (!Rasterizer()->AccelerateDisplay(framebuffer, framebuffer_addr, pixel_stride, screen_info)) {
+        // Reset the screen info's display texture to its own permanent texture
+        screen_info.display_texture = screen_info.texture.resource.handle;
+        screen_info.display_texcoords = MathUtil::Rectangle<float>(0.f, 0.f, 1.f, 1.f);
 
-    glActiveTexture(GL_TEXTURE0);
-    glPixelStorei(GL_UNPACK_ROW_LENGTH, (GLint)pixel_stride);
+        Memory::RasterizerFlushRegion(framebuffer_addr, framebuffer.stride * framebuffer.height);
 
-    // Update existing texture
-    // TODO: Test what happens on hardware when you change the framebuffer dimensions so that they
-    //       differ from the LCD resolution.
-    // TODO: Applications could theoretically crash Citra here by specifying too large
-    //       framebuffer sizes. We should make sure that this cannot happen.
-    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, framebuffer.width, framebuffer.height,
-                    texture.gl_format, texture.gl_type, framebuffer_data);
+        const u8* framebuffer_data = Memory::GetPhysicalPointer(framebuffer_addr);
 
-    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+        state.texture_units[0].texture_2d = screen_info.texture.resource.handle;
+        state.Apply();
 
-    state.texture_units[0].texture_2d = 0;
-    state.Apply();
+        glActiveTexture(GL_TEXTURE0);
+        glPixelStorei(GL_UNPACK_ROW_LENGTH, (GLint)pixel_stride);
+
+        // Update existing texture
+        // TODO: Test what happens on hardware when you change the framebuffer dimensions so that they
+        //       differ from the LCD resolution.
+        // TODO: Applications could theoretically crash Citra here by specifying too large
+        //       framebuffer sizes. We should make sure that this cannot happen.
+        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, framebuffer.width, framebuffer.height,
+                        screen_info.texture.gl_format, screen_info.texture.gl_type, framebuffer_data);
+
+        glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+
+        state.texture_units[0].texture_2d = 0;
+        state.Apply();
+    }
 }
 
 /**
@@ -216,7 +224,7 @@ void RendererOpenGL::LoadFBToActiveGLTexture(const GPU::Regs::FramebufferConfig&
  */
 void RendererOpenGL::LoadColorToActiveGLTexture(u8 color_r, u8 color_g, u8 color_b,
                                                 const TextureInfo& texture) {
-    state.texture_units[0].texture_2d = texture.handle;
+    state.texture_units[0].texture_2d = texture.resource.handle;
     state.Apply();
 
     glActiveTexture(GL_TEXTURE0);
@@ -224,6 +232,9 @@ void RendererOpenGL::LoadColorToActiveGLTexture(u8 color_r, u8 color_g, u8 color
 
     // Update existing texture
     glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, framebuffer_data);
+
+    state.texture_units[0].texture_2d = 0;
+    state.Apply();
 }
 
 /**
@@ -233,20 +244,22 @@ void RendererOpenGL::InitOpenGLObjects() {
     glClearColor(Settings::values.bg_red, Settings::values.bg_green, Settings::values.bg_blue, 0.0f);
 
     // Link shaders and get variable locations
-    program_id = GLShader::LoadProgram(vertex_shader, fragment_shader);
-    uniform_modelview_matrix = glGetUniformLocation(program_id, "modelview_matrix");
-    uniform_color_texture = glGetUniformLocation(program_id, "color_texture");
-    attrib_position = glGetAttribLocation(program_id, "vert_position");
-    attrib_tex_coord = glGetAttribLocation(program_id, "vert_tex_coord");
+    shader.Create(vertex_shader, fragment_shader);
+    state.draw.shader_program = shader.handle;
+    state.Apply();
+    uniform_modelview_matrix = glGetUniformLocation(shader.handle, "modelview_matrix");
+    uniform_color_texture = glGetUniformLocation(shader.handle, "color_texture");
+    attrib_position = glGetAttribLocation(shader.handle, "vert_position");
+    attrib_tex_coord = glGetAttribLocation(shader.handle, "vert_tex_coord");
 
     // Generate VBO handle for drawing
-    glGenBuffers(1, &vertex_buffer_handle);
+    vertex_buffer.Create();
 
     // Generate VAO
-    glGenVertexArrays(1, &vertex_array_handle);
+    vertex_array.Create();
 
-    state.draw.vertex_array = vertex_array_handle;
-    state.draw.vertex_buffer = vertex_buffer_handle;
+    state.draw.vertex_array = vertex_array.handle;
+    state.draw.vertex_buffer = vertex_buffer.handle;
     state.draw.uniform_buffer = 0;
     state.Apply();
 
@@ -258,13 +271,13 @@ void RendererOpenGL::InitOpenGLObjects() {
     glEnableVertexAttribArray(attrib_tex_coord);
 
     // Allocate textures for each screen
-    for (auto& texture : textures) {
-        glGenTextures(1, &texture.handle);
+    for (auto& screen_info : screen_infos) {
+        screen_info.texture.resource.Create();
 
         // Allocation of storage is deferred until the first frame, when we
         // know the framebuffer size.
 
-        state.texture_units[0].texture_2d = texture.handle;
+        state.texture_units[0].texture_2d = screen_info.texture.resource.handle;
         state.Apply();
 
         glActiveTexture(GL_TEXTURE0);
@@ -273,6 +286,8 @@ void RendererOpenGL::InitOpenGLObjects() {
         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+        screen_info.display_texture = screen_info.texture.resource.handle;
     }
 
     state.texture_units[0].texture_2d = 0;
@@ -327,30 +342,38 @@ void RendererOpenGL::ConfigureFramebufferTexture(TextureInfo& texture,
         UNIMPLEMENTED();
     }
 
-    state.texture_units[0].texture_2d = texture.handle;
+    state.texture_units[0].texture_2d = texture.resource.handle;
     state.Apply();
 
     glActiveTexture(GL_TEXTURE0);
     glTexImage2D(GL_TEXTURE_2D, 0, internal_format, texture.width, texture.height, 0,
             texture.gl_format, texture.gl_type, nullptr);
+
+    state.texture_units[0].texture_2d = 0;
+    state.Apply();
 }
 
 /**
  * Draws a single texture to the emulator window, rotating the texture to correct for the 3DS's LCD rotation.
  */
-void RendererOpenGL::DrawSingleScreenRotated(const TextureInfo& texture, float x, float y, float w, float h) {
+void RendererOpenGL::DrawSingleScreenRotated(const ScreenInfo& screen_info, float x, float y, float w, float h) {
+    auto& texcoords = screen_info.display_texcoords;
+
     std::array<ScreenRectVertex, 4> vertices = {{
-        ScreenRectVertex(x,   y,   1.f, 0.f),
-        ScreenRectVertex(x+w, y,   1.f, 1.f),
-        ScreenRectVertex(x,   y+h, 0.f, 0.f),
-        ScreenRectVertex(x+w, y+h, 0.f, 1.f),
+        ScreenRectVertex(x,   y,   texcoords.bottom, texcoords.left),
+        ScreenRectVertex(x+w, y,   texcoords.bottom, texcoords.right),
+        ScreenRectVertex(x,   y+h, texcoords.top, texcoords.left),
+        ScreenRectVertex(x+w, y+h, texcoords.top, texcoords.right),
     }};
 
-    state.texture_units[0].texture_2d = texture.handle;
+    state.texture_units[0].texture_2d = screen_info.display_texture;
     state.Apply();
 
     glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices.data());
     glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+
+    state.texture_units[0].texture_2d = 0;
+    state.Apply();
 }
 
 /**
@@ -362,9 +385,6 @@ void RendererOpenGL::DrawScreens() {
     glViewport(0, 0, layout.width, layout.height);
     glClear(GL_COLOR_BUFFER_BIT);
 
-    state.draw.shader_program = program_id;
-    state.Apply();
-
     // Set projection matrix
     std::array<GLfloat, 3 * 2> ortho_matrix = MakeOrthographicMatrix((float)layout.width,
         (float)layout.height);
@@ -374,9 +394,9 @@ void RendererOpenGL::DrawScreens() {
     glActiveTexture(GL_TEXTURE0);
     glUniform1i(uniform_color_texture, 0);
 
-    DrawSingleScreenRotated(textures[0], (float)layout.top_screen.left, (float)layout.top_screen.top,
+    DrawSingleScreenRotated(screen_infos[0], (float)layout.top_screen.left, (float)layout.top_screen.top,
         (float)layout.top_screen.GetWidth(), (float)layout.top_screen.GetHeight());
-    DrawSingleScreenRotated(textures[1], (float)layout.bottom_screen.left,(float)layout.bottom_screen.top,
+    DrawSingleScreenRotated(screen_infos[1], (float)layout.bottom_screen.left,(float)layout.bottom_screen.top,
         (float)layout.bottom_screen.GetWidth(), (float)layout.bottom_screen.GetHeight());
 
     m_current_frame++;
diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h
index fe4d142a5..5ca5255ac 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.h
+++ b/src/video_core/renderer_opengl/renderer_opengl.h
@@ -11,10 +11,28 @@
 #include "core/hw/gpu.h"
 
 #include "video_core/renderer_base.h"
+#include "video_core/renderer_opengl/gl_resource_manager.h"
 #include "video_core/renderer_opengl/gl_state.h"
 
 class EmuWindow;
 
+/// Structure used for storing information about the textures for each 3DS screen
+struct TextureInfo {
+    OGLTexture resource;
+    GLsizei width;
+    GLsizei height;
+    GPU::Regs::PixelFormat format;
+    GLenum gl_format;
+    GLenum gl_type;
+};
+
+/// Structure used for storing information about the display target for each 3DS screen
+struct ScreenInfo {
+    GLuint display_texture;
+    MathUtil::Rectangle<float> display_texcoords;
+    TextureInfo texture;
+};
+
 class RendererOpenGL : public RendererBase {
 public:
 
@@ -37,26 +55,16 @@ public:
     void ShutDown() override;
 
 private:
-    /// Structure used for storing information about the textures for each 3DS screen
-    struct TextureInfo {
-        GLuint handle;
-        GLsizei width;
-        GLsizei height;
-        GPU::Regs::PixelFormat format;
-        GLenum gl_format;
-        GLenum gl_type;
-    };
-
     void InitOpenGLObjects();
     void ConfigureFramebufferTexture(TextureInfo& texture,
                                      const GPU::Regs::FramebufferConfig& framebuffer);
     void DrawScreens();
-    void DrawSingleScreenRotated(const TextureInfo& texture, float x, float y, float w, float h);
+    void DrawSingleScreenRotated(const ScreenInfo& screen_info, float x, float y, float w, float h);
     void UpdateFramerate();
 
-    // Loads framebuffer from emulated memory into the active OpenGL texture.
-    void LoadFBToActiveGLTexture(const GPU::Regs::FramebufferConfig& framebuffer,
-                                 const TextureInfo& texture);
+    // Loads framebuffer from emulated memory into the display information structure
+    void LoadFBToScreenInfo(const GPU::Regs::FramebufferConfig& framebuffer,
+                             ScreenInfo& screen_info);
     // Fills active OpenGL texture with the given RGB color.
     void LoadColorToActiveGLTexture(u8 color_r, u8 color_g, u8 color_b,
                                     const TextureInfo& texture);
@@ -69,10 +77,10 @@ private:
     OpenGLState state;
 
     // OpenGL object IDs
-    GLuint vertex_array_handle;
-    GLuint vertex_buffer_handle;
-    GLuint program_id;
-    std::array<TextureInfo, 2> textures;          ///< Textures for top and bottom screens respectively
+    OGLVertexArray vertex_array;
+    OGLBuffer vertex_buffer;
+    OGLShader shader;
+    std::array<ScreenInfo, 2> screen_infos;          ///< Display information for top and bottom screens respectively
     // Shader uniform location indices
     GLuint uniform_modelview_matrix;
     GLuint uniform_color_texture;
diff --git a/src/video_core/swrasterizer.h b/src/video_core/swrasterizer.h
index 9a9a76d7a..090f899bc 100644
--- a/src/video_core/swrasterizer.h
+++ b/src/video_core/swrasterizer.h
@@ -11,16 +11,14 @@
 namespace VideoCore {
 
 class SWRasterizer : public RasterizerInterface {
-    void InitObjects() override {}
-    void Reset() override {}
     void AddTriangle(const Pica::Shader::OutputVertex& v0,
             const Pica::Shader::OutputVertex& v1,
             const Pica::Shader::OutputVertex& v2) override;
     void DrawTriangles() override {}
-    void FlushFramebuffer() override {}
     void NotifyPicaRegisterChanged(u32 id) override {}
+    void FlushAll() override {}
     void FlushRegion(PAddr addr, u32 size) override {}
-    void InvalidateRegion(PAddr addr, u32 size) override {}
+    void FlushAndInvalidateRegion(PAddr addr, u32 size) override {}
 };
 
 }