{"action":"created","comment":{"url":"https://api.github.com/repos/citra-emu/citra/pulls/comments/171274339","pull_request_review_id":100096189,"id":171274339,"diff_hunk":"@@ -159,641 +283,1324 @@ void RasterizerCacheOpenGL::BlitTextures(GLuint src_tex, GLuint dst_tex,\n         buffers = GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;\n     }\n \n-    glBlitFramebuffer(src_rect.left, src_rect.top, src_rect.right, src_rect.bottom, dst_rect.left,\n-                      dst_rect.top, dst_rect.right, dst_rect.bottom, buffers,\n+    glBlitFramebuffer(src_rect.left, src_rect.bottom, src_rect.right, src_rect.top, dst_rect.left,\n+                      dst_rect.bottom, dst_rect.right, dst_rect.top, buffers,\n                       buffers == GL_COLOR_BUFFER_BIT ? GL_LINEAR : GL_NEAREST);\n \n-    // Restore previous framebuffer bindings\n-    cur_state.draw.read_framebuffer = old_fbs[0];\n-    cur_state.draw.draw_framebuffer = old_fbs[1];\n-    cur_state.Apply();\n+    return true;\n }\n \n-bool RasterizerCacheOpenGL::TryBlitSurfaces(CachedSurface* src_surface,\n-                                            const MathUtil::Rectangle<int>& src_rect,\n-                                            CachedSurface* dst_surface,\n-                                            const MathUtil::Rectangle<int>& dst_rect) {\n+static bool FillSurface(const Surface& surface, const u8* fill_data,\n+                        const MathUtil::Rectangle<u32>& fill_rect, GLuint draw_fb_handle) {\n+    OpenGLState state = OpenGLState::GetCurState();\n+\n+    OpenGLState prev_state = state;\n+    SCOPE_EXIT({ prev_state.Apply(); });\n+\n+    state.ResetTexture(surface->texture.handle);\n+\n+    state.scissor.enabled = true;\n+    state.scissor.x = static_cast<GLint>(fill_rect.left);\n+    state.scissor.y = static_cast<GLint>(fill_rect.bottom);\n+    state.scissor.width = static_cast<GLsizei>(fill_rect.GetWidth());\n+    state.scissor.height = static_cast<GLsizei>(fill_rect.GetHeight());\n+\n+    state.draw.draw_framebuffer = draw_fb_handle;\n+    state.Apply();\n+\n+    if (surface->type == SurfaceType::Color || surface->type == SurfaceType::Texture) {\n+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,\n+                               surface->texture.handle, 0);\n+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0,\n+                               0);\n+\n+        Pica::Texture::TextureInfo tex_info{};\n+        tex_info.format = static_cast<Pica::TexturingRegs::TextureFormat>(surface->pixel_format);\n+        Math::Vec4<u8> color = Pica::Texture::LookupTexture(fill_data, 0, 0, tex_info);\n+\n+        std::array<GLfloat, 4> color_values = {color.x / 255.f, color.y / 255.f, color.z / 255.f,\n+                                               color.w / 255.f};\n+\n+        state.color_mask.red_enabled = GL_TRUE;\n+        state.color_mask.green_enabled = GL_TRUE;\n+        state.color_mask.blue_enabled = GL_TRUE;\n+        state.color_mask.alpha_enabled = GL_TRUE;\n+        state.Apply();\n+        glClearBufferfv(GL_COLOR, 0, &color_values[0]);\n+    } else if (surface->type == SurfaceType::Depth) {\n+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);\n+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,\n+                               surface->texture.handle, 0);\n+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);\n+\n+        u32 value_32bit = 0;\n+        GLfloat value_float;\n+\n+        if (surface->pixel_format == SurfaceParams::PixelFormat::D16) {\n+            std::memcpy(&value_32bit, fill_data, 2);\n+            value_float = value_32bit / 65535.0f; // 2^16 - 1\n+        } else if (surface->pixel_format == SurfaceParams::PixelFormat::D24) {\n+            std::memcpy(&value_32bit, fill_data, 3);\n+            value_float = value_32bit / 16777215.0f; // 2^24 - 1\n+        }\n+\n+        state.depth.write_mask = GL_TRUE;\n+        state.Apply();\n+        glClearBufferfv(GL_DEPTH, 0, &value_float);\n+    } else if (surface->type == SurfaceType::DepthStencil) {\n+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);\n+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,\n+                               surface->texture.handle, 0);\n+\n+        u32 value_32bit;\n+        std::memcpy(&value_32bit, fill_data, sizeof(u32));\n+\n+        GLfloat value_float = (value_32bit & 0xFFFFFF) / 16777215.0f; // 2^24 - 1\n+        GLint value_int = (value_32bit >> 24);\n+\n+        state.depth.write_mask = GL_TRUE;\n+        state.stencil.write_mask = -1;\n+        state.Apply();\n+        glClearBufferfi(GL_DEPTH_STENCIL, 0, value_float, value_int);\n+    }\n+    return true;\n+}\n+\n+SurfaceParams SurfaceParams::FromInterval(SurfaceInterval interval) const {\n+    SurfaceParams params = *this;\n+    const u32 tiled_size = is_tiled ? 8 : 1;\n+    const u32 stride_tiled_bytes = BytesInPixels(stride * tiled_size);\n+    PAddr aligned_start =\n+        addr + Common::AlignDown(boost::icl::first(interval) - addr, stride_tiled_bytes);\n+    PAddr aligned_end =\n+        addr + Common::AlignUp(boost::icl::last_next(interval) - addr, stride_tiled_bytes);\n+\n+    if (aligned_end - aligned_start > stride_tiled_bytes) {\n+        params.addr = aligned_start;\n+        params.height = (aligned_end - aligned_start) / BytesInPixels(stride);\n+    } else {\n+        // 1 row\n+        ASSERT(aligned_end - aligned_start == stride_tiled_bytes);\n+        const u32 tiled_alignment = BytesInPixels(is_tiled ? 8 * 8 : 1);\n+        aligned_start =\n+            addr + Common::AlignDown(boost::icl::first(interval) - addr, tiled_alignment);\n+        aligned_end =\n+            addr + Common::AlignUp(boost::icl::last_next(interval) - addr, tiled_alignment);\n+        params.addr = aligned_start;\n+        params.width = PixelsInBytes(aligned_end - aligned_start) / tiled_size;\n+        params.stride = params.width;\n+        params.height = tiled_size;\n+    }\n+    params.UpdateParams();\n \n-    if (!CachedSurface::CheckFormatsBlittable(src_surface->pixel_format,\n-                                              dst_surface->pixel_format)) {\n+    return params;\n+}\n+\n+SurfaceInterval SurfaceParams::GetSubRectInterval(MathUtil::Rectangle<u32> unscaled_rect) const {\n+    if (unscaled_rect.GetHeight() == 0 || unscaled_rect.GetWidth() == 0) {\n+        return {};\n+    }\n+\n+    if (is_tiled) {\n+        unscaled_rect.left = Common::AlignDown(unscaled_rect.left, 8) * 8;\n+        unscaled_rect.bottom = Common::AlignDown(unscaled_rect.bottom, 8) / 8;\n+        unscaled_rect.right = Common::AlignUp(unscaled_rect.right, 8) * 8;\n+        unscaled_rect.top = Common::AlignUp(unscaled_rect.top, 8) / 8;\n+    }\n+\n+    const u32 stride_tiled = !is_tiled ? stride : stride * 8;\n+\n+    const u32 pixel_offset =\n+        stride_tiled * (!is_tiled ? unscaled_rect.bottom : (height / 8) - unscaled_rect.top) +\n+        unscaled_rect.left;\n+\n+    const u32 pixels = (unscaled_rect.GetHeight() - 1) * stride_tiled + unscaled_rect.GetWidth();\n+\n+    return {addr + BytesInPixels(pixel_offset), addr + BytesInPixels(pixel_offset + pixels)};\n+}\n+\n+MathUtil::Rectangle<u32> SurfaceParams::GetSubRect(const SurfaceParams& sub_surface) const {\n+    const u32 begin_pixel_index = PixelsInBytes(sub_surface.addr - addr);\n+\n+    if (is_tiled) {\n+        const int x0 = (begin_pixel_index % (stride * 8)) / 8;\n+        const int y0 = (begin_pixel_index / (stride * 8)) * 8;\n+        // Top to bottom\n+        return MathUtil::Rectangle<u32>(x0, height - y0, x0 + sub_surface.width,\n+                                        height - (y0 + sub_surface.height));\n+    }\n+\n+    const int x0 = begin_pixel_index % stride;\n+    const int y0 = begin_pixel_index / stride;\n+    // Bottom to top\n+    return MathUtil::Rectangle<u32>(x0, y0 + sub_surface.height, x0 + sub_surface.width, y0);\n+}\n+\n+MathUtil::Rectangle<u32> SurfaceParams::GetScaledSubRect(const SurfaceParams& sub_surface) const {\n+    auto rect = GetSubRect(sub_surface);\n+    rect.left = rect.left * res_scale;\n+    rect.right = rect.right * res_scale;\n+    rect.top = rect.top * res_scale;\n+    rect.bottom = rect.bottom * res_scale;\n+    return rect;\n+}\n+\n+bool SurfaceParams::ExactMatch(const SurfaceParams& other_surface) const {\n+    return std::tie(other_surface.addr, other_surface.width, other_surface.height,\n+                    other_surface.stride, other_surface.pixel_format, other_surface.is_tiled) ==\n+               std::tie(addr, width, height, stride, pixel_format, is_tiled) &&\n+           pixel_format != PixelFormat::Invalid;\n+}\n+\n+bool SurfaceParams::CanSubRect(const SurfaceParams& sub_surface) const {\n+    return sub_surface.addr >= addr && sub_surface.end <= end &&\n+           sub_surface.pixel_format == pixel_format && pixel_format != PixelFormat::Invalid &&\n+           sub_surface.is_tiled == is_tiled &&\n+           (sub_surface.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 &&\n+           (sub_surface.stride == stride || sub_surface.height <= (is_tiled ? 8u : 1u)) &&\n+           GetSubRect(sub_surface).left + sub_surface.width <= stride;\n+}\n+\n+bool SurfaceParams::CanExpand(const SurfaceParams& expanded_surface) const {\n+    return pixel_format != PixelFormat::Invalid && pixel_format == expanded_surface.pixel_format &&\n+           addr <= expanded_surface.end && expanded_surface.addr <= end &&\n+           is_tiled == expanded_surface.is_tiled && stride == expanded_surface.stride &&\n+           (std::max(expanded_surface.addr, addr) - std::min(expanded_surface.addr, addr)) %\n+                   BytesInPixels(stride * (is_tiled ? 8 : 1)) ==\n+               0;\n+}\n+\n+bool SurfaceParams::CanTexCopy(const SurfaceParams& texcopy_params) const {\n+    if (pixel_format == PixelFormat::Invalid || addr > texcopy_params.addr ||\n+        end < texcopy_params.end) {\n         return false;\n     }\n+    if (texcopy_params.width != texcopy_params.stride) {\n+        const u32 tile_stride = BytesInPixels(stride * (is_tiled ? 8 : 1));\n+        return (texcopy_params.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 &&\n+               texcopy_params.width % BytesInPixels(is_tiled ? 64 : 1) == 0 &&\n+               (texcopy_params.height == 1 || texcopy_params.stride == tile_stride) &&\n+               ((texcopy_params.addr - addr) % tile_stride) + texcopy_params.width <= tile_stride;\n+    }\n+    return FromInterval(texcopy_params.GetInterval()).GetInterval() == texcopy_params.GetInterval();\n+}\n \n-    BlitTextures(src_surface->texture.handle, dst_surface->texture.handle,\n-                 CachedSurface::GetFormatType(src_surface->pixel_format), src_rect, dst_rect);\n-    return true;\n+bool CachedSurface::CanFill(const SurfaceParams& dest_surface,\n+                            SurfaceInterval fill_interval) const {\n+    if (type == SurfaceType::Fill && IsRegionValid(fill_interval) &&\n+        boost::icl::first(fill_interval) >= addr &&\n+        boost::icl::last_next(fill_interval) <= end && // dest_surface is within our fill range\n+        dest_surface.FromInterval(fill_interval).GetInterval() ==\n+            fill_interval) { // make sure interval is a rectangle in dest surface\n+        if (fill_size * 8 != dest_surface.GetFormatBpp()) {\n+            // Check if bits repeat for our fill_size\n+            const u32 dest_bytes_per_pixel = std::max(dest_surface.GetFormatBpp() / 8, 1u);\n+            std::vector<u8> fill_test(fill_size * dest_bytes_per_pixel);\n+\n+            for (u32 i = 0; i < dest_bytes_per_pixel; ++i)\n+                std::memcpy(&fill_test[i * fill_size], &fill_data[0], fill_size);\n+\n+            for (u32 i = 0; i < fill_size; ++i)\n+                if (std::memcmp(&fill_test[dest_bytes_per_pixel * i], &fill_test[0],\n+                                dest_bytes_per_pixel) != 0)\n+                    return false;\n+\n+            if (dest_surface.GetFormatBpp() == 4 && (fill_test[0] & 0xF) != (fill_test[0] >> 4))\n+                return false;\n+        }\n+        return true;\n+    }\n+    return false;\n }\n \n-static void AllocateSurfaceTexture(GLuint texture, CachedSurface::PixelFormat pixel_format,\n-                                   u32 width, u32 height) {\n-    // Allocate an uninitialized texture of appropriate size and format for the surface\n-    using SurfaceType = CachedSurface::SurfaceType;\n+bool CachedSurface::CanCopy(const SurfaceParams& dest_surface,\n+                            SurfaceInterval copy_interval) const {\n+    SurfaceParams subrect_params = dest_surface.FromInterval(copy_interval);\n+    ASSERT(subrect_params.GetInterval() == copy_interval);\n+    if (CanSubRect(subrect_params))\n+        return true;\n \n-    OpenGLState cur_state = OpenGLState::GetCurState();\n+    if (CanFill(dest_surface, copy_interval))\n+        return true;\n \n-    // Keep track of previous texture bindings\n-    GLuint old_tex = cur_state.texture_units[0].texture_2d;\n-    cur_state.texture_units[0].texture_2d = texture;\n-    cur_state.Apply();\n-    glActiveTexture(GL_TEXTURE0);\n+    return false;\n+}\n \n-    SurfaceType type = CachedSurface::GetFormatType(pixel_format);\n+SurfaceInterval SurfaceParams::GetCopyableInterval(const Surface& src_surface) const {\n+    SurfaceInterval result{};\n+    const auto valid_regions =\n+        SurfaceRegions(GetInterval() & src_surface->GetInterval()) - src_surface->invalid_regions;\n+    for (auto& valid_interval : valid_regions) {\n+        const SurfaceInterval aligned_interval{\n+            addr + Common::AlignUp(boost::icl::first(valid_interval) - addr,\n+                                   BytesInPixels(is_tiled ? 8 * 8 : 1)),\n+            addr + Common::AlignDown(boost::icl::last_next(valid_interval) - addr,\n+                                     BytesInPixels(is_tiled ? 8 * 8 : 1))};\n+\n+        if (BytesInPixels(is_tiled ? 8 * 8 : 1) > boost::icl::length(valid_interval) ||\n+            boost::icl::length(aligned_interval) == 0) {\n+            continue;\n+        }\n \n-    FormatTuple tuple;\n-    if (type == SurfaceType::Color) {\n-        ASSERT((size_t)pixel_format < fb_format_tuples.size());\n-        tuple = fb_format_tuples[(unsigned int)pixel_format];\n-    } else if (type == SurfaceType::Depth || type == SurfaceType::DepthStencil) {\n-        size_t tuple_idx = (size_t)pixel_format - 14;\n-        ASSERT(tuple_idx < depth_format_tuples.size());\n-        tuple = depth_format_tuples[tuple_idx];\n-    } else {\n-        tuple = {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE};\n+        // Get the rectangle within aligned_interval\n+        const u32 stride_bytes = BytesInPixels(stride) * (is_tiled ? 8 : 1);\n+        SurfaceInterval rect_interval{\n+            addr + Common::AlignUp(boost::icl::first(aligned_interval) - addr, stride_bytes),\n+            addr + Common::AlignDown(boost::icl::last_next(aligned_interval) - addr, stride_bytes),\n+        };\n+        if (boost::icl::first(rect_interval) > boost::icl::last_next(rect_interval)) {\n+            // 1 row\n+            rect_interval = aligned_interval;\n+        } else if (boost::icl::length(rect_interval) == 0) {\n+            // 2 rows that do not make a rectangle, return the larger one\n+            const SurfaceInterval row1{boost::icl::first(aligned_interval),\n+                                       boost::icl::first(rect_interval)};\n+            const SurfaceInterval row2{boost::icl::first(rect_interval),\n+                                       boost::icl::last_next(aligned_interval)};\n+            rect_interval = (boost::icl::length(row1) > boost::icl::length(row2)) ? row1 : row2;\n+        }\n+\n+        if (boost::icl::length(rect_interval) > boost::icl::length(result)) {\n+            result = rect_interval;\n+        }\n     }\n+    return result;\n+}\n \n-    glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, width, height, 0, tuple.format,\n-                 tuple.type, nullptr);\n+void RasterizerCacheOpenGL::CopySurface(const Surface& src_surface, const Surface& dst_surface,\n+                                        SurfaceInterval copy_interval) {\n+    SurfaceParams subrect_params = dst_surface->FromInterval(copy_interval);\n+    ASSERT(subrect_params.GetInterval() == copy_interval);\n \n-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);\n-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);\n-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n+    ASSERT(src_surface != dst_surface);\n \n-    // Restore previous texture bindings\n-    cur_state.texture_units[0].texture_2d = old_tex;\n-    cur_state.Apply();\n+    // This is only called when CanCopy is true, no need to run checks here\n+    if (src_surface->type == SurfaceType::Fill) {\n+        // FillSurface needs a 4 bytes buffer\n+        const u32 fill_offset =\n+            (boost::icl::first(copy_interval) - src_surface->addr) % src_surface->fill_size;\n+        std::array<u8, 4> fill_buffer;\n+\n+        u32 fill_buff_pos = fill_offset;\n+        for (int i : {0, 1, 2, 3})\n+            fill_buffer[i] = src_surface->fill_data[fill_buff_pos++ % src_surface->fill_size];\n+\n+        FillSurface(dst_surface, &fill_buffer[0], dst_surface->GetScaledSubRect(subrect_params),\n+                    draw_framebuffer.handle);\n+        return;\n+    }\n+    if (src_surface->CanSubRect(subrect_params)) {\n+        BlitTextures(src_surface->texture.handle, src_surface->GetScaledSubRect(subrect_params),\n+                     dst_surface->texture.handle, dst_surface->GetScaledSubRect(subrect_params),\n+                     src_surface->type, read_framebuffer.handle, draw_framebuffer.handle);\n+        return;\n+    }\n+    UNREACHABLE();\n }\n \n-MICROPROFILE_DEFINE(OpenGL_SurfaceUpload, \"OpenGL\", \"Surface Upload\", MP_RGB(128, 64, 192));\n-CachedSurface* RasterizerCacheOpenGL::GetSurface(const CachedSurface& params, bool match_res_scale,\n-                                                 bool load_if_create) {\n-    using PixelFormat = CachedSurface::PixelFormat;\n-    using SurfaceType = CachedSurface::SurfaceType;\n+MICROPROFILE_DEFINE(OpenGL_SurfaceLoad, \"OpenGL\", \"Surface Load\", MP_RGB(128, 64, 192));\n+void CachedSurface::LoadGLBuffer(PAddr load_start, PAddr load_end) {\n+    ASSERT(type != SurfaceType::Fill);\n \n-    if (params.addr == 0) {\n-        return nullptr;\n+    const u8* const texture_src_data = Memory::GetPhysicalPointer(addr);\n+    if (texture_src_data == nullptr)\n+        return;\n+\n+    if (gl_buffer == nullptr) {\n+        gl_buffer_size = width * height * GetGLBytesPerPixel(pixel_format);\n+        gl_buffer.reset(new u8[gl_buffer_size]);\n     }\n \n-    u32 params_size =\n-        params.width * params.height * CachedSurface::GetFormatBpp(params.pixel_format) / 8;\n+    // TODO: Should probably be done in ::Memory:: and check for other regions too\n+    if (load_start < Memory::VRAM_VADDR_END && load_end > Memory::VRAM_VADDR_END)\n+        load_end = Memory::VRAM_VADDR_END;\n \n-    // Check for an exact match in existing surfaces\n-    CachedSurface* best_exact_surface = nullptr;\n-    float exact_surface_goodness = -1.f;\n-\n-    auto surface_interval =\n-        boost::icl::interval<PAddr>::right_open(params.addr, params.addr + params_size);\n-    auto range = surface_cache.equal_range(surface_interval);\n-    for (auto it = range.first; it != range.second; ++it) {\n-        for (auto it2 = it->second.begin(); it2 != it->second.end(); ++it2) {\n-            CachedSurface* surface = it2->get();\n-\n-            // Check if the request matches the surface exactly\n-            if (params.addr == surface->addr && params.width == surface->width &&\n-                params.height == surface->height && params.pixel_format == surface->pixel_format) {\n-                // Make sure optional param-matching criteria are fulfilled\n-                bool tiling_match = (params.is_tiled == surface->is_tiled);\n-                bool res_scale_match = (params.res_scale_width == surface->res_scale_width &&\n-                                        params.res_scale_height == surface->res_scale_height);\n-                if (!match_res_scale || res_scale_match) {\n-                    // Prioritize same-tiling and highest resolution surfaces\n-                    float match_goodness =\n-                        (float)tiling_match + surface->res_scale_width * surface->res_scale_height;\n-                    if (match_goodness > exact_surface_goodness || surface->dirty) {\n-                        exact_surface_goodness = match_goodness;\n-                        best_exact_surface = surface;\n-                    }\n+    if (load_start < Memory::VRAM_VADDR && load_end > Memory::VRAM_VADDR)\n+        load_start = Memory::VRAM_VADDR;\n+\n+    MICROPROFILE_SCOPE(OpenGL_SurfaceLoad);\n+\n+    ASSERT(load_start >= addr && load_end <= end);\n+    const u32 start_offset = load_start - addr;\n+\n+    if (!is_tiled) {\n+        ASSERT(type == SurfaceType::Color);\n+        std::memcpy(&gl_buffer[start_offset], texture_src_data + start_offset,\n+                    load_end - load_start);\n+    } else {\n+        if (type == SurfaceType::Texture) {\n+            Pica::Texture::TextureInfo tex_info{};\n+            tex_info.width = width;\n+            tex_info.height = height;\n+            tex_info.format = static_cast<Pica::TexturingRegs::TextureFormat>(pixel_format);\n+            tex_info.SetDefaultStride();\n+            tex_info.physical_address = addr;\n+\n+            const SurfaceInterval load_interval(load_start, load_end);\n+            const auto rect = GetSubRect(FromInterval(load_interval));\n+            ASSERT(FromInterval(load_interval).GetInterval() == load_interval);\n+\n+            for (unsigned y = rect.bottom; y < rect.top; ++y) {\n+                for (unsigned x = rect.left; x < rect.right; ++x) {\n+                    auto vec4 =\n+                        Pica::Texture::LookupTexture(texture_src_data, x, height - 1 - y, tex_info);\n+                    const size_t offset = (x + (width * y)) * 4;\n+                    std::memcpy(&gl_buffer[offset], vec4.AsArray(), 4);\n                 }\n             }\n+        } else {\n+            morton_to_gl_fns[static_cast<size_t>(pixel_format)](stride, height, &gl_buffer[0], addr,\n+                                                                load_start, load_end);\n         }\n     }\n+}\n+\n+MICROPROFILE_DEFINE(OpenGL_SurfaceFlush, \"OpenGL\", \"Surface Flush\", MP_RGB(128, 192, 64));\n+void CachedSurface::FlushGLBuffer(PAddr flush_start, PAddr flush_end) {\n+    u8* const dst_buffer = Memory::GetPhysicalPointer(addr);\n+    if (dst_buffer == nullptr)\n+        return;\n+\n+    ASSERT(gl_buffer_size == width * height * GetGLBytesPerPixel(pixel_format));\n+\n+    // TODO: Should probably be done in ::Memory:: and check for other regions too\n+    // same as loadglbuffer()\n+    if (flush_start < Memory::VRAM_VADDR_END && flush_end > Memory::VRAM_VADDR_END)\n+        flush_end = Memory::VRAM_VADDR_END;\n \n-    // Return the best exact surface if found\n-    if (best_exact_surface != nullptr) {\n-        return best_exact_surface;\n+    if (flush_start < Memory::VRAM_VADDR && flush_end > Memory::VRAM_VADDR)\n+        flush_start = Memory::VRAM_VADDR;\n+\n+    MICROPROFILE_SCOPE(OpenGL_SurfaceFlush);\n+\n+    ASSERT(flush_start >= addr && flush_end <= end);\n+    const u32 start_offset = flush_start - addr;\n+    const u32 end_offset = flush_end - addr;\n+\n+    if (type == SurfaceType::Fill) {\n+        const u32 coarse_start_offset = start_offset - (start_offset % fill_size);\n+        const u32 backup_bytes = start_offset % fill_size;\n+        std::array<u8, 4> backup_data;\n+        if (backup_bytes)\n+            std::memcpy(&backup_data[0], &dst_buffer[coarse_start_offset], backup_bytes);\n+\n+        for (u32 offset = coarse_start_offset; offset < end_offset; offset += fill_size) {\n+            std::memcpy(&dst_buffer[offset], &fill_data[0],\n+                        std::min(fill_size, end_offset - offset));\n+        }\n+\n+        if (backup_bytes)\n+            std::memcpy(&dst_buffer[coarse_start_offset], &backup_data[0], backup_bytes);\n+    } else if (!is_tiled) {\n+        ASSERT(type == SurfaceType::Color);\n+        std::memcpy(dst_buffer + start_offset, &gl_buffer[start_offset], flush_end - flush_start);\n+    } else {\n+        gl_to_morton_fns[static_cast<size_t>(pixel_format)](stride, height, &gl_buffer[0], addr,\n+                                                            flush_start, flush_end);\n     }\n+}\n \n-    // No matching surfaces found, so create a new one\n-    u8* texture_src_data = Memory::GetPhysicalPointer(params.addr);\n-    if (texture_src_data == nullptr) {\n-        return nullptr;\n+MICROPROFILE_DEFINE(OpenGL_TextureUL, \"OpenGL\", \"Texture Upload\", MP_RGB(128, 64, 192));\n+void CachedSurface::UploadGLTexture(const MathUtil::Rectangle<u32>& rect, GLuint read_fb_handle,\n+                                    GLuint draw_fb_handle) {\n+    if (type == SurfaceType::Fill)\n+        return;\n+\n+    MICROPROFILE_SCOPE(OpenGL_TextureUL);\n+\n+    ASSERT(gl_buffer_size == width * height * GetGLBytesPerPixel(pixel_format));\n+\n+    // Load data from memory to the surface\n+    GLint x0 = static_cast<GLint>(rect.left);\n+    GLint y0 = static_cast<GLint>(rect.bottom);\n+    size_t buffer_offset = (y0 * stride + x0) * GetGLBytesPerPixel(pixel_format);\n+\n+    const FormatTuple& tuple = GetFormatTuple(pixel_format);\n+    GLuint target_tex = texture.handle;\n+\n+    // If not 1x scale, create 1x texture that we will blit from to replace texture subrect in\n+    // surface\n+    OGLTexture unscaled_tex;\n+    if (res_scale != 1) {\n+        x0 = 0;\n+        y0 = 0;\n+\n+        unscaled_tex.Create();\n+        AllocateSurfaceTexture(unscaled_tex.handle, tuple, rect.GetWidth(), rect.GetHeight());\n+        target_tex = unscaled_tex.handle;\n     }\n \n-    MICROPROFILE_SCOPE(OpenGL_SurfaceUpload);\n+    OpenGLState cur_state = OpenGLState::GetCurState();\n \n-    // Stride only applies to linear images.\n-    ASSERT(params.pixel_stride == 0 || !params.is_tiled);\n+    GLuint old_tex = cur_state.texture_units[0].texture_2d;\n+    cur_state.texture_units[0].texture_2d = target_tex;\n+    cur_state.Apply();\n \n-    std::shared_ptr<CachedSurface> new_surface = std::make_shared<CachedSurface>();\n+    // Ensure no bad interactions with GL_UNPACK_ALIGNMENT\n+    ASSERT(stride * GetGLBytesPerPixel(pixel_format) % 4 == 0);\n+    glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(stride));\n \n-    new_surface->addr = params.addr;\n-    new_surface->size = params_size;\n+    glActiveTexture(GL_TEXTURE0);\n+    glTexSubImage2D(GL_TEXTURE_2D, 0, x0, y0, static_cast<GLsizei>(rect.GetWidth()),\n+                    static_cast<GLsizei>(rect.GetHeight()), tuple.format, tuple.type,\n+                    &gl_buffer[buffer_offset]);\n \n-    new_surface->texture.Create();\n-    new_surface->width = params.width;\n-    new_surface->height = params.height;\n-    new_surface->pixel_stride = params.pixel_stride;\n-    new_surface->res_scale_width = params.res_scale_width;\n-    new_surface->res_scale_height = params.res_scale_height;\n+    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);\n \n-    new_surface->is_tiled = params.is_tiled;\n-    new_surface->pixel_format = params.pixel_format;\n-    new_surface->dirty = false;\n+    cur_state.texture_units[0].texture_2d = old_tex;\n+    cur_state.Apply();\n \n-    if (!load_if_create) {\n-        // Don't load any data; just allocate the surface's texture\n-        AllocateSurfaceTexture(new_surface->texture.handle, new_surface->pixel_format,\n-                               new_surface->GetScaledWidth(), new_surface->GetScaledHeight());\n-    } else {\n-        // TODO: Consider attempting subrect match in existing surfaces and direct blit here instead\n-        // of memory upload below if that's a common scenario in some game\n+    if (res_scale != 1) {\n+        auto scaled_rect = rect;\n+        scaled_rect.left *= res_scale;\n+        scaled_rect.top *= res_scale;\n+        scaled_rect.right *= res_scale;\n+        scaled_rect.bottom *= res_scale;\n+\n+        BlitTextures(unscaled_tex.handle, {0, rect.GetHeight(), rect.GetWidth(), 0}, texture.handle,\n+                     scaled_rect, type, read_fb_handle, draw_fb_handle);\n+    }\n+}\n \n-        Memory::RasterizerFlushRegion(params.addr, params_size);\n+MICROPROFILE_DEFINE(OpenGL_TextureDL, \"OpenGL\", \"Texture Download\", MP_RGB(128, 192, 64));\n+void CachedSurface::DownloadGLTexture(const MathUtil::Rectangle<u32>& rect, GLuint read_fb_handle,\n+                                      GLuint draw_fb_handle) {\n+    if (type == SurfaceType::Fill)\n+        return;\n \n-        // Load data from memory to the new surface\n-        OpenGLState cur_state = OpenGLState::GetCurState();\n+    MICROPROFILE_SCOPE(OpenGL_TextureDL);\n \n-        GLuint old_tex = cur_state.texture_units[0].texture_2d;\n-        cur_state.texture_units[0].texture_2d = new_surface->texture.handle;\n-        cur_state.Apply();\n-        glActiveTexture(GL_TEXTURE0);\n+    if (gl_buffer == nullptr) {\n+        gl_buffer_size = width * height * GetGLBytesPerPixel(pixel_format);\n+        gl_buffer.reset(new u8[gl_buffer_size]);\n+    }\n \n-        if (!new_surface->is_tiled) {\n-            // TODO: Ensure this will always be a color format, not a depth or other format\n-            ASSERT((size_t)new_surface->pixel_format < fb_format_tuples.size());\n-            const FormatTuple& tuple = fb_format_tuples[(unsigned int)params.pixel_format];\n+    OpenGLState state = OpenGLState::GetCurState();\n+    OpenGLState prev_state = state;\n+    SCOPE_EXIT({ prev_state.Apply(); });\n \n-            glPixelStorei(GL_UNPACK_ROW_LENGTH, (GLint)new_surface->pixel_stride);\n-            glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, params.width, params.height, 0,\n-                         tuple.format, tuple.type, texture_src_data);\n-            glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);\n+    const FormatTuple& tuple = GetFormatTuple(pixel_format);\n+\n+    // Ensure no bad interactions with GL_PACK_ALIGNMENT\n+    ASSERT(stride * GetGLBytesPerPixel(pixel_format) % 4 == 0);\n+    glPixelStorei(GL_PACK_ROW_LENGTH, static_cast<GLint>(stride));\n+    size_t buffer_offset = (rect.bottom * stride + rect.left) * GetGLBytesPerPixel(pixel_format);\n+\n+    // If not 1x scale, blit scaled texture to a new 1x texture and use that to flush\n+    if (res_scale != 1) {\n+        auto scaled_rect = rect;\n+        scaled_rect.left *= res_scale;\n+        scaled_rect.top *= res_scale;\n+        scaled_rect.right *= res_scale;\n+        scaled_rect.bottom *= res_scale;\n+\n+        OGLTexture unscaled_tex;\n+        unscaled_tex.Create();\n+\n+        MathUtil::Rectangle<u32> unscaled_tex_rect{0, rect.GetHeight(), rect.GetWidth(), 0};\n+        AllocateSurfaceTexture(unscaled_tex.handle, tuple, rect.GetWidth(), rect.GetHeight());\n+        BlitTextures(texture.handle, scaled_rect, unscaled_tex.handle, unscaled_tex_rect, type,\n+                     read_fb_handle, draw_fb_handle);\n+\n+        state.texture_units[0].texture_2d = unscaled_tex.handle;\n+        state.Apply();\n+\n+        glActiveTexture(GL_TEXTURE0);\n+        glGetTexImage(GL_TEXTURE_2D, 0, tuple.format, tuple.type, &gl_buffer[buffer_offset]);\n+    } else {\n+        state.ResetTexture(texture.handle);\n+        state.draw.read_framebuffer = read_fb_handle;\n+        state.Apply();\n+\n+        if (type == SurfaceType::Color || type == SurfaceType::Texture) {\n+            glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,\n+                                   texture.handle, 0);\n+            glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,\n+                                   0, 0);\n+        } else if (type == SurfaceType::Depth) {\n+            glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);\n+            glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,\n+                                   texture.handle, 0);\n+            glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);\n         } else {\n-            SurfaceType type = CachedSurface::GetFormatType(new_surface->pixel_format);\n-            if (type != SurfaceType::Depth && type != SurfaceType::DepthStencil) {\n-                FormatTuple tuple;\n-                if ((size_t)params.pixel_format < fb_format_tuples.size()) {\n-                    tuple = fb_format_tuples[(unsigned int)params.pixel_format];\n-                } else {\n-                    // Texture\n-                    tuple = {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE};\n-                }\n+            glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);\n+            glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,\n+                                   texture.handle, 0);\n+        }\n+        glReadPixels(static_cast<GLint>(rect.left), static_cast<GLint>(rect.bottom),\n+                     static_cast<GLsizei>(rect.GetWidth()), static_cast<GLsizei>(rect.GetHeight()),\n+                     tuple.format, tuple.type, &gl_buffer[buffer_offset]);\n+    }\n \n-                std::vector<Math::Vec4<u8>> tex_buffer(params.width * params.height);\n+    glPixelStorei(GL_PACK_ROW_LENGTH, 0);\n+}\n+\n+enum MatchFlags {\n+    Invalid = 1,      // Flag that can be applied to other match types, invalid matches require\n+                      // validation before they can be used\n+    Exact = 1 << 1,   // Surfaces perfectly match\n+    SubRect = 1 << 2, // Surface encompasses params\n+    Copy = 1 << 3,    // Surface we can copy from\n+    Expand = 1 << 4,  // Surface that can expand params\n+    TexCopy = 1 << 5  // Surface that will match a display transfer \"texture copy\" parameters\n+};\n \n-                Pica::Texture::TextureInfo tex_info;\n-                tex_info.width = params.width;\n-                tex_info.height = params.height;\n-                tex_info.format = (Pica::TexturingRegs::TextureFormat)params.pixel_format;\n-                tex_info.SetDefaultStride();\n-                tex_info.physical_address = params.addr;\n+constexpr MatchFlags operator|(MatchFlags lhs, MatchFlags rhs) {\n+    return static_cast<MatchFlags>(static_cast<int>(lhs) | static_cast<int>(rhs));\n+}\n \n-                for (unsigned y = 0; y < params.height; ++y) {\n-                    for (unsigned x = 0; x < params.width; ++x) {\n-                        tex_buffer[x + params.width * y] = Pica::Texture::LookupTexture(\n-                            texture_src_data, x, params.height - 1 - y, tex_info);\n-                    }\n+/// Get the best surface match (and its match type) for the given flags\n+template <MatchFlags find_flags>\n+Surface FindMatch(const SurfaceCache& surface_cache, const SurfaceParams& params,\n+                  ScaleMatch match_scale_type,\n+                  boost::optional<SurfaceInterval> validate_interval = boost::none) {\n+    Surface match_surface = nullptr;\n+    bool match_valid = false;\n+    u32 match_scale = 0;\n+    SurfaceInterval match_interval{};\n+\n+    for (auto& pair : RangeFromInterval(surface_cache, params.GetInterval())) {\n+        for (auto& surface : pair.second) {\n+            bool res_scale_matched = match_scale_type == ScaleMatch::Exact\n+                                         ? (params.res_scale == surface->res_scale)\n+                                         : (params.res_scale <= surface->res_scale);\n+            // validity will be checked in GetCopyableInterval\n+            bool is_valid =\n+                find_flags & MatchFlags::Copy\n+                    ? true\n+                    : surface->IsRegionValid(validate_interval.value_or(params.GetInterval()));\n+\n+            if (!(find_flags & MatchFlags::Invalid) && !is_valid)\n+                continue;\n+\n+            auto IsMatch_Helper = [&](auto check_type, auto match_fn) {\n+                if (!(find_flags & check_type))\n+                    return;\n+\n+                bool matched;\n+                SurfaceInterval surface_interval;\n+                std::tie(matched, surface_interval) = match_fn();\n+                if (!matched)\n+                    return;\n+\n+                if (!res_scale_matched && match_scale_type != ScaleMatch::Ignore &&\n+                    surface->type != SurfaceType::Fill)\n+                    return;\n+\n+                // Found a match, update only if this is better than the previous one\n+                auto UpdateMatch = [&] {\n+                    match_surface = surface;\n+                    match_valid = is_valid;\n+                    match_scale = surface->res_scale;\n+                    match_interval = surface_interval;\n+                };\n+\n+                if (surface->res_scale > match_scale) {\n+                    UpdateMatch();\n+                    return;\n+                } else if (surface->res_scale < match_scale) {\n+                    return;\n                 }\n \n-                glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, params.width, params.height,\n-                             0, GL_RGBA, GL_UNSIGNED_BYTE, tex_buffer.data());\n-            } else {\n-                // Depth/Stencil formats need special treatment since they aren't sampleable using\n-                // LookupTexture and can't use RGBA format\n-                size_t tuple_idx = (size_t)params.pixel_format - 14;\n-                ASSERT(tuple_idx < depth_format_tuples.size());\n-                const FormatTuple& tuple = depth_format_tuples[tuple_idx];\n+                if (is_valid && !match_valid) {\n+                    UpdateMatch();\n+                    return;\n+                } else if (is_valid != match_valid) {\n+                    return;\n+                }\n \n-                u32 bytes_per_pixel = CachedSurface::GetFormatBpp(params.pixel_format) / 8;\n+                if (boost::icl::length(surface_interval) > boost::icl::length(match_interval)) {\n+                    UpdateMatch();\n+                }\n+            };\n+            IsMatch_Helper(std::integral_constant<MatchFlags, MatchFlags::Exact>{}, [&] {\n+                return std::make_pair(surface->ExactMatch(params), surface->GetInterval());\n+            });\n+            IsMatch_Helper(std::integral_constant<MatchFlags, MatchFlags::SubRect>{}, [&] {\n+                return std::make_pair(surface->CanSubRect(params), surface->GetInterval());\n+            });\n+            IsMatch_Helper(std::integral_constant<MatchFlags, MatchFlags::Copy>{}, [&] {\n+                auto copy_interval =\n+                    params.FromInterval(*validate_interval).GetCopyableInterval(surface);\n+                bool matched = boost::icl::length(copy_interval & *validate_interval) != 0 &&\n+                               surface->CanCopy(params, copy_interval);\n+                return std::make_pair(matched, copy_interval);\n+            });\n+            IsMatch_Helper(std::integral_constant<MatchFlags, MatchFlags::Expand>{}, [&] {\n+                return std::make_pair(surface->CanExpand(params), surface->GetInterval());\n+            });\n+            IsMatch_Helper(std::integral_constant<MatchFlags, MatchFlags::TexCopy>{}, [&] {\n+                return std::make_pair(surface->CanTexCopy(params), surface->GetInterval());\n+            });\n+        }\n+    }\n+    return match_surface;\n+}\n \n-                // OpenGL needs 4 bpp alignment for D24 since using GL_UNSIGNED_INT as type\n-                bool use_4bpp = (params.pixel_format == PixelFormat::D24);\n+RasterizerCacheOpenGL::RasterizerCacheOpenGL() {\n+    read_framebuffer.Create();\n+    draw_framebuffer.Create();\n \n-                u32 gl_bytes_per_pixel = use_4bpp ? 4 : bytes_per_pixel;\n+    attributeless_vao.Create();\n \n-                std::vector<u8> temp_fb_depth_buffer(params.width * params.height *\n-                                                     gl_bytes_per_pixel);\n+    d24s8_abgr_buffer.Create();\n+    d24s8_abgr_buffer_size = 0;\n \n-                u8* temp_fb_depth_buffer_ptr =\n-                    use_4bpp ? temp_fb_depth_buffer.data() + 1 : temp_fb_depth_buffer.data();\n+    const char* vs_source = R\"(\n+#version 330 core\n+const vec2 vertices[4] = vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));\n+void main() {\n+    gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);\n+}\n+)\";\n+    const char* fs_source = R\"(\n+#version 330 core\n \n-                MortonCopyPixels(params.pixel_format, params.width, params.height, bytes_per_pixel,\n-                                 gl_bytes_per_pixel, texture_src_data, temp_fb_depth_buffer_ptr,\n-                                 true);\n+uniform samplerBuffer tbo;\n+uniform vec2 tbo_size;\n+uniform vec4 viewport;\n \n-                glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, params.width, params.height,\n-                             0, tuple.format, tuple.type, temp_fb_depth_buffer.data());\n-            }\n-        }\n+out vec4 color;\n \n-        // If not 1x scale, blit 1x texture to a new scaled texture and replace texture in surface\n-        if (new_surface->res_scale_width != 1.f || new_surface->res_scale_height != 1.f) {\n-            OGLTexture scaled_texture;\n-            scaled_texture.Create();\n-\n-            AllocateSurfaceTexture(scaled_texture.handle, new_surface->pixel_format,\n-                                   new_surface->GetScaledWidth(), new_surface->GetScaledHeight());\n-            BlitTextures(new_surface->texture.handle, scaled_texture.handle,\n-                         CachedSurface::GetFormatType(new_surface->pixel_format),\n-                         MathUtil::Rectangle<int>(0, 0, new_surface->width, new_surface->height),\n-                         MathUtil::Rectangle<int>(0, 0, new_surface->GetScaledWidth(),\n-                                                  new_surface->GetScaledHeight()));\n-\n-            new_surface->texture.Release();\n-            new_surface->texture.handle = scaled_texture.handle;\n-            scaled_texture.handle = 0;\n-            cur_state.texture_units[0].texture_2d = new_surface->texture.handle;\n-            cur_state.Apply();\n-        }\n+void main() {\n+    vec2 tbo_coord = (gl_FragCoord.xy - viewport.xy) * tbo_size / viewport.zw;\n+    int tbo_offset = int(tbo_coord.y) * int(tbo_size.x) + int(tbo_coord.x);\n+    color = texelFetch(tbo, tbo_offset).rabg;\n+}\n+)\";\n+    d24s8_abgr_shader.Create(vs_source, fs_source);\n+\n+    OpenGLState state = OpenGLState::GetCurState();\n+    GLuint old_program = state.draw.shader_program;\n+    state.draw.shader_program = d24s8_abgr_shader.handle;\n+    state.Apply();\n+\n+    GLint tbo_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, \"tbo\");\n+    ASSERT(tbo_u_id != -1);\n+    glUniform1i(tbo_u_id, 0);\n+\n+    state.draw.shader_program = old_program;\n+    state.Apply();\n+\n+    d24s8_abgr_tbo_size_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, \"tbo_size\");\n+    ASSERT(d24s8_abgr_tbo_size_u_id != -1);\n+    d24s8_abgr_viewport_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, \"viewport\");\n+    ASSERT(d24s8_abgr_viewport_u_id != -1);\n+}\n+\n+RasterizerCacheOpenGL::~RasterizerCacheOpenGL() {\n+    FlushAll();\n+    while (!surface_cache.empty())\n+        UnregisterSurface(*surface_cache.begin()->second.begin());\n+}\n+\n+bool RasterizerCacheOpenGL::BlitSurfaces(const Surface& src_surface,\n+                                         const MathUtil::Rectangle<u32>& src_rect,\n+                                         const Surface& dst_surface,\n+                                         const MathUtil::Rectangle<u32>& dst_rect) {\n+    if (!SurfaceParams::CheckFormatsBlittable(src_surface->pixel_format, dst_surface->pixel_format))\n+        return false;\n+\n+    return BlitTextures(src_surface->texture.handle, src_rect, dst_surface->texture.handle,\n+                        dst_rect, src_surface->type, read_framebuffer.handle,\n+                        draw_framebuffer.handle);\n+}\n \n-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);\n-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);\n-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n+void RasterizerCacheOpenGL::ConvertD24S8toABGR(GLuint src_tex,\n+                                               const MathUtil::Rectangle<u32>& src_rect,\n+                                               GLuint dst_tex,\n+                                               const MathUtil::Rectangle<u32>& dst_rect) {\n+    OpenGLState prev_state = OpenGLState::GetCurState();\n+    SCOPE_EXIT({ prev_state.Apply(); });\n \n-        cur_state.texture_units[0].texture_2d = old_tex;\n-        cur_state.Apply();\n+    OpenGLState state;\n+    state.draw.read_framebuffer = read_framebuffer.handle;\n+    state.draw.draw_framebuffer = draw_framebuffer.handle;\n+    state.Apply();\n+\n+    glBindBuffer(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer.handle);\n+\n+    GLsizeiptr target_pbo_size = src_rect.GetWidth() * src_rect.GetHeight() * 4;\n+    if (target_pbo_size > d24s8_abgr_buffer_size) {\n+        d24s8_abgr_buffer_size = target_pbo_size * 2;\n+        glBufferData(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer_size, nullptr, GL_STREAM_COPY);\n     }\n \n-    Memory::RasterizerMarkRegionCached(new_surface->addr, new_surface->size, 1);\n-    surface_cache.add(std::make_pair(boost::icl::interval<PAddr>::right_open(\n-                                         new_surface->addr, new_surface->addr + new_surface->size),\n-                                     std::set<std::shared_ptr<CachedSurface>>({new_surface})));\n-    return new_surface.get();\n+    glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);\n+    glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, src_tex,\n+                           0);\n+    glReadPixels(static_cast<GLint>(src_rect.left), static_cast<GLint>(src_rect.bottom),\n+                 static_cast<GLsizei>(src_rect.GetWidth()),\n+                 static_cast<GLsizei>(src_rect.GetHeight()), GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8,\n+                 0);\n+\n+    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);\n+\n+    // PBO now contains src_tex in RABG format\n+    state.draw.shader_program = d24s8_abgr_shader.handle;\n+    state.draw.vertex_array = attributeless_vao.handle;\n+    state.viewport.x = static_cast<GLint>(dst_rect.left);\n+    state.viewport.y = static_cast<GLint>(dst_rect.bottom);\n+    state.viewport.width = static_cast<GLsizei>(dst_rect.GetWidth());\n+    state.viewport.height = static_cast<GLsizei>(dst_rect.GetHeight());\n+    state.Apply();\n+\n+    OGLTexture tbo;\n+    tbo.Create();\n+    glActiveTexture(GL_TEXTURE0);\n+    glBindTexture(GL_TEXTURE_BUFFER, tbo.handle);\n+    glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, d24s8_abgr_buffer.handle);\n+\n+    glUniform2f(d24s8_abgr_tbo_size_u_id, static_cast<GLfloat>(src_rect.GetWidth()),\n+                static_cast<GLfloat>(src_rect.GetHeight()));\n+    glUniform4f(d24s8_abgr_viewport_u_id, static_cast<GLfloat>(state.viewport.x),\n+                static_cast<GLfloat>(state.viewport.y), static_cast<GLfloat>(state.viewport.width),\n+                static_cast<GLfloat>(state.viewport.height));\n+\n+    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0);\n+    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);\n+    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);\n+\n+    glBindTexture(GL_TEXTURE_BUFFER, 0);\n }\n \n-CachedSurface* RasterizerCacheOpenGL::GetSurfaceRect(const CachedSurface& params,\n-                                                     bool match_res_scale, bool load_if_create,\n-                                                     MathUtil::Rectangle<int>& out_rect) {\n-    if (params.addr == 0) {\n+Surface RasterizerCacheOpenGL::GetSurface(const SurfaceParams& params, ScaleMatch match_res_scale,\n+                                          bool load_if_create) {\n+    if (params.addr == 0 || params.height * params.width == 0) {\n         return nullptr;\n     }\n+    // Use GetSurfaceSubRect instead\n+    ASSERT(params.width == params.stride);\n+\n+    ASSERT(!params.is_tiled || (params.width % 8 == 0 && params.height % 8 == 0));\n \n-    u32 total_pixels = params.width * params.height;\n-    u32 params_size = total_pixels * CachedSurface::GetFormatBpp(params.pixel_format) / 8;\n-\n-    // Attempt to find encompassing surfaces\n-    CachedSurface* best_subrect_surface = nullptr;\n-    float subrect_surface_goodness = -1.f;\n-\n-    auto surface_interval =\n-        boost::icl::interval<PAddr>::right_open(params.addr, params.addr + params_size);\n-    auto cache_upper_bound = surface_cache.upper_bound(surface_interval);\n-    for (auto it = surface_cache.lower_bound(surface_interval); it != cache_upper_bound; ++it) {\n-        for (auto it2 = it->second.begin(); it2 != it->second.end(); ++it2) {\n-            CachedSurface* surface = it2->get();\n-\n-            // Check if the request is contained in the surface\n-            if (params.addr >= surface->addr &&\n-                params.addr + params_size - 1 <= surface->addr + surface->size - 1 &&\n-                params.pixel_format == surface->pixel_format) {\n-                // Make sure optional param-matching criteria are fulfilled\n-                bool tiling_match = (params.is_tiled == surface->is_tiled);\n-                bool res_scale_match = (params.res_scale_width == surface->res_scale_width &&\n-                                        params.res_scale_height == surface->res_scale_height);\n-                if (!match_res_scale || res_scale_match) {\n-                    // Prioritize same-tiling and highest resolution surfaces\n-                    float match_goodness =\n-                        (float)tiling_match + surface->res_scale_width * surface->res_scale_height;\n-                    if (match_goodness > subrect_surface_goodness || surface->dirty) {\n-                        subrect_surface_goodness = match_goodness;\n-                        best_subrect_surface = surface;\n-                    }\n+    // Check for an exact match in existing surfaces\n+    Surface surface =\n+        FindMatch<MatchFlags::Exact | MatchFlags::Invalid>(surface_cache, params, match_res_scale);\n+\n+    if (surface == nullptr) {\n+        u16 target_res_scale = params.res_scale;\n+        if (match_res_scale != ScaleMatch::Exact) {\n+            // This surface may have a subrect of another surface with a higher res_scale, find it\n+            // to adjust our params\n+            SurfaceParams find_params = params;\n+            Surface expandable = FindMatch<MatchFlags::Expand | MatchFlags::Invalid>(\n+                surface_cache, find_params, match_res_scale);\n+            if (expandable != nullptr && expandable->res_scale > target_res_scale) {\n+                target_res_scale = expandable->res_scale;\n+            }\n+            // Keep res_scale when reinterpreting d24s8 -> rgba8\n+            if (params.pixel_format == PixelFormat::RGBA8) {\n+                find_params.pixel_format = PixelFormat::D24S8;\n+                expandable = FindMatch<MatchFlags::Expand | MatchFlags::Invalid>(\n+                    surface_cache, find_params, match_res_scale);\n+                if (expandable != nullptr && expandable->res_scale > target_res_scale) {\n+                    target_res_scale = expandable->res_scale;\n                 }\n             }\n         }\n+        SurfaceParams new_params = params;\n+        new_params.res_scale = target_res_scale;\n+        surface = CreateSurface(new_params);\n+        RegisterSurface(surface);\n     }\n \n-    // Return the best subrect surface if found\n-    if (best_subrect_surface != nullptr) {\n-        unsigned int bytes_per_pixel =\n-            (CachedSurface::GetFormatBpp(best_subrect_surface->pixel_format) / 8);\n-\n-        int x0, y0;\n-\n-        if (!params.is_tiled) {\n-            u32 begin_pixel_index = (params.addr - best_subrect_surface->addr) / bytes_per_pixel;\n-            x0 = begin_pixel_index % best_subrect_surface->width;\n-            y0 = begin_pixel_index / best_subrect_surface->width;\n+    if (load_if_create) {\n+        ValidateSurface(surface, params.addr, params.size);\n+    }\n \n-            out_rect = MathUtil::Rectangle<int>(x0, y0, x0 + params.width, y0 + params.height);\n-        } else {\n-            u32 bytes_per_tile = 8 * 8 * bytes_per_pixel;\n-            u32 tiles_per_row = best_subrect_surface->width / 8;\n+    return surface;\n+}\n \n-            u32 begin_tile_index = (params.addr - best_subrect_surface->addr) / bytes_per_tile;\n-            x0 = begin_tile_index % tiles_per_row * 8;\n-            y0 = begin_tile_index / tiles_per_row * 8;\n+SurfaceRect_Tuple RasterizerCacheOpenGL::GetSurfaceSubRect(const SurfaceParams& params,\n+                                                           ScaleMatch match_res_scale,\n+                                                           bool load_if_create) {\n+    if (params.addr == 0 || params.height * params.width == 0) {\n+        return std::make_tuple(nullptr, MathUtil::Rectangle<u32>{});\n+    }\n \n-            // Tiled surfaces are flipped vertically in the rasterizer vs. 3DS memory.\n-            out_rect =\n-                MathUtil::Rectangle<int>(x0, best_subrect_surface->height - y0, x0 + params.width,\n-                                         best_subrect_surface->height - (y0 + params.height));\n+    // Attempt to find encompassing surface\n+    Surface surface = FindMatch<MatchFlags::SubRect | MatchFlags::Invalid>(surface_cache, params,\n+                                                                           match_res_scale);\n+\n+    // Check if FindMatch failed because of res scaling\n+    // If that's the case create a new surface with\n+    // the dimensions of the lower res_scale surface\n+    // to suggest it should not be used again","path":"src/video_core/renderer_opengl/gl_rasterizer_cache.cpp","position":1400,"original_position":1400,"commit_id":"18456ff9e6a8659adb2b859c556cfa17cf638ee1","original_commit_id":"18456ff9e6a8659adb2b859c556cfa17cf638ee1","user":{"login":"wwylele","id":4592895,"avatar_url":"https://avatars0.githubusercontent.com/u/4592895?v=4","gravatar_id":"","url":"https://api.github.com/users/wwylele","html_url":"https://github.com/wwylele","followers_url":"https://api.github.com/users/wwylele/followers","following_url":"https://api.github.com/users/wwylele/following{/other_user}","gists_url":"https://api.github.com/users/wwylele/gists{/gist_id}","starred_url":"https://api.github.com/users/wwylele/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wwylele/subscriptions","organizations_url":"https://api.github.com/users/wwylele/orgs","repos_url":"https://api.github.com/users/wwylele/repos","events_url":"https://api.github.com/users/wwylele/events{/privacy}","received_events_url":"https://api.github.com/users/wwylele/received_events","type":"User","site_admin":false},"body":"The line break is somewhat off.","created_at":"2018-02-28T15:07:06Z","updated_at":"2018-02-28T15:17:05Z","html_url":"https://github.com/citra-emu/citra/pull/3281#discussion_r171274339","pull_request_url":"https://api.github.com/repos/citra-emu/citra/pulls/3281","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/citra-emu/citra/pulls/comments/171274339"},"html":{"href":"https://github.com/citra-emu/citra/pull/3281#discussion_r171274339"},"pull_request":{"href":"https://api.github.com/repos/citra-emu/citra/pulls/3281"}}},"pull_request":{"url":"https://api.github.com/repos/citra-emu/citra/pulls/3281","id":157894381,"html_url":"https://github.com/citra-emu/citra/pull/3281","diff_url":"https://github.com/citra-emu/citra/pull/3281.diff","patch_url":"https://github.com/citra-emu/citra/pull/3281.patch","issue_url":"https://api.github.com/repos/citra-emu/citra/issues/3281","number":3281,"state":"open","locked":false,"title":"Texture Cache Rework","user":{"login":"jroweboy","id":952515,"avatar_url":"https://avatars1.githubusercontent.com/u/952515?v=4","gravatar_id":"","url":"https://api.github.com/users/jroweboy","html_url":"https://github.com/jroweboy","followers_url":"https://api.github.com/users/jroweboy/followers","following_url":"https://api.github.com/users/jroweboy/following{/other_user}","gists_url":"https://api.github.com/users/jroweboy/gists{/gist_id}","starred_url":"https://api.github.com/users/jroweboy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jroweboy/subscriptions","organizations_url":"https://api.github.com/users/jroweboy/orgs","repos_url":"https://api.github.com/users/jroweboy/repos","events_url":"https://api.github.com/users/jroweboy/events{/privacy}","received_events_url":"https://api.github.com/users/jroweboy/received_events","type":"User","site_admin":false},"body":"(Please note this code was written by @phanto-m and not me. I've studied it extensively so I can be ready to answer questions in review, but I am still noob and may not know everything/got something wrong. Please bear with me)\r\n\r\n### Goal:\r\n\r\nEliminate as many superfluous CPU <-> GPU flushes as possible. This is primarily accomplished by adding an Invalidate (without flush) method on the texture cache that is preferred to use instead of the InvalidateAndFlush that was in use everywhere before. This also adds support for hw texture copy which fixes upscaling in many games and makes several others full speed (and removes the need to use the CPU implementation.)\r\n\r\n### Whats new:\r\n\r\n* The entire texture cache interface got a face lift. Within the texture cache, lookup for surfaces is now done with easier to use helper methods to find the most appropriate surface. The parameters used to find/create a surface is now split from the actual cached surface as well, which should make it a bit easier to get a surface from the cache now.\r\n* Optimized morton copy to copy in tiles\r\n* Resolution scaling is now locked to integer sizes\r\n* GL_SCISSOR and viewport tests to try to avoid drawing out of the draw area if not needed\r\n* Invalidate without flushing which will attempt to revalidate the surface by copying from compatible surfaces if it can.\r\n* HW texture copy (yay!)\r\n\r\n### General Notes:\r\n\r\n* The coordinate system in the texture cache was changed to match GL_ORIGIN instead of the PICA coordinate system. I didn't bother to split this into its own commit. Please be aware of this when reviewing. Heres what phantom said when i asked them about it: \"getsurfacerect would return top-left origin coords when dealing with nontiled textures which made was a real PITA, now it's consistent and matches gl's origin. the flipping is now done in displaytransfer since it's the only place where that conversion is required. i kept the kinda broken texcoords in acceleratedisplay, it's weird how it expects rotated coors or i just didnt understand how the rotating is done in the rendered, didnt bother looking too much into it\"\r\n* The final commit is a hacky workaround that I'm going to leave up the reviewers to decide what to do with it. Its put at the end there so we can drop it if we decide its too much. It works by ignoring any regions that are dirty when revalidating (and being in the dirty region implies that the region was created only on the GPU), and if every place we are trying to validate in the interval is either copyable from another surface, or in the dirty regions, then it gets ignored. This makes things like the after match screen in smash 4 much faster with no visible differences, and also removes pokemon sun/moon outlines as well.\r\n\r\nI did my best to split all the changes into separate commits, so the reviewers can just read them one at a time, but as they can probably tell, I got a little tired of doing this towards the end. \"Add the rest of the cache methods\" \"Update the rasterizer to use the new cache\" are probably the most \"[draw the rest of the owl](http://i0.kym-cdn.com/photos/images/facebook/000/572/078/d6d.jpg)\" that this pr gets. I tried to at least make the diffs line up so they are easier to read :)\n\n<!-- Reviewable:start -->\n---\nThis change is [<img src=\"https://reviewable.io/review_button.svg\" height=\"34\" align=\"absmiddle\" alt=\"Reviewable\"/>](https://reviewable.io/reviews/citra-emu/citra/3281)\n<!-- Reviewable:end -->\n","created_at":"2017-12-12T17:42:40Z","updated_at":"2018-02-28T15:17:05Z","closed_at":null,"merged_at":null,"merge_commit_sha":"a7dd57b3ad4a899014c5e31d1f6f487cd701fec3","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":811880815,"url":"https://api.github.com/repos/citra-emu/citra/labels/canary-merge","name":"canary-merge","color":"11916f","default":false},{"id":444085593,"url":"https://api.github.com/repos/citra-emu/citra/labels/pr:needs-review","name":"pr:needs-review","color":"207de5","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/citra-emu/citra/pulls/3281/commits","review_comments_url":"https://api.github.com/repos/citra-emu/citra/pulls/3281/comments","review_comment_url":"https://api.github.com/repos/citra-emu/citra/pulls/comments{/number}","comments_url":"https://api.github.com/repos/citra-emu/citra/issues/3281/comments","statuses_url":"https://api.github.com/repos/citra-emu/citra/statuses/18456ff9e6a8659adb2b859c556cfa17cf638ee1","head":{"label":"jroweboy:texcache-pt2","ref":"texcache-pt2","sha":"18456ff9e6a8659adb2b859c556cfa17cf638ee1","user":{"login":"jroweboy","id":952515,"avatar_url":"https://avatars1.githubusercontent.com/u/952515?v=4","gravatar_id":"","url":"https://api.github.com/users/jroweboy","html_url":"https://github.com/jroweboy","followers_url":"https://api.github.com/users/jroweboy/followers","following_url":"https://api.github.com/users/jroweboy/following{/other_user}","gists_url":"https://api.github.com/users/jroweboy/gists{/gist_id}","starred_url":"https://api.github.com/users/jroweboy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jroweboy/subscriptions","organizations_url":"https://api.github.com/users/jroweboy/orgs","repos_url":"https://api.github.com/users/jroweboy/repos","events_url":"https://api.github.com/users/jroweboy/events{/privacy}","received_events_url":"https://api.github.com/users/jroweboy/received_events","type":"User","site_admin":false},"repo":{"id":31311785,"name":"citra","full_name":"jroweboy/citra","owner":{"login":"jroweboy","id":952515,"avatar_url":"https://avatars1.githubusercontent.com/u/952515?v=4","gravatar_id":"","url":"https://api.github.com/users/jroweboy","html_url":"https://github.com/jroweboy","followers_url":"https://api.github.com/users/jroweboy/followers","following_url":"https://api.github.com/users/jroweboy/following{/other_user}","gists_url":"https://api.github.com/users/jroweboy/gists{/gist_id}","starred_url":"https://api.github.com/users/jroweboy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jroweboy/subscriptions","organizations_url":"https://api.github.com/users/jroweboy/orgs","repos_url":"https://api.github.com/users/jroweboy/repos","events_url":"https://api.github.com/users/jroweboy/events{/privacy}","received_events_url":"https://api.github.com/users/jroweboy/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/jroweboy/citra","description":"Nintendo 3DS emulator/debugger","fork":true,"url":"https://api.github.com/repos/jroweboy/citra","forks_url":"https://api.github.com/repos/jroweboy/citra/forks","keys_url":"https://api.github.com/repos/jroweboy/citra/keys{/key_id}","collaborators_url":"https://api.github.com/repos/jroweboy/citra/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/jroweboy/citra/teams","hooks_url":"https://api.github.com/repos/jroweboy/citra/hooks","issue_events_url":"https://api.github.com/repos/jroweboy/citra/issues/events{/number}","events_url":"https://api.github.com/repos/jroweboy/citra/events","assignees_url":"https://api.github.com/repos/jroweboy/citra/assignees{/user}","branches_url":"https://api.github.com/repos/jroweboy/citra/branches{/branch}","tags_url":"https://api.github.com/repos/jroweboy/citra/tags","blobs_url":"https://api.github.com/repos/jroweboy/citra/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/jroweboy/citra/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/jroweboy/citra/git/refs{/sha}","trees_url":"https://api.github.com/repos/jroweboy/citra/git/trees{/sha}","statuses_url":"https://api.github.com/repos/jroweboy/citra/statuses/{sha}","languages_url":"https://api.github.com/repos/jroweboy/citra/languages","stargazers_url":"https://api.github.com/repos/jroweboy/citra/stargazers","contributors_url":"https://api.github.com/repos/jroweboy/citra/contributors","subscribers_url":"https://api.github.com/repos/jroweboy/citra/subscribers","subscription_url":"https://api.github.com/repos/jroweboy/citra/subscription","commits_url":"https://api.github.com/repos/jroweboy/citra/commits{/sha}","git_commits_url":"https://api.github.com/repos/jroweboy/citra/git/commits{/sha}","comments_url":"https://api.github.com/repos/jroweboy/citra/comments{/number}","issue_comment_url":"https://api.github.com/repos/jroweboy/citra/issues/comments{/number}","contents_url":"https://api.github.com/repos/jroweboy/citra/contents/{+path}","compare_url":"https://api.github.com/repos/jroweboy/citra/compare/{base}...{head}","merges_url":"https://api.github.com/repos/jroweboy/citra/merges","archive_url":"https://api.github.com/repos/jroweboy/citra/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/jroweboy/citra/downloads","issues_url":"https://api.github.com/repos/jroweboy/citra/issues{/number}","pulls_url":"https://api.github.com/repos/jroweboy/citra/pulls{/number}","milestones_url":"https://api.github.com/repos/jroweboy/citra/milestones{/number}","notifications_url":"https://api.github.com/repos/jroweboy/citra/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/jroweboy/citra/labels{/name}","releases_url":"https://api.github.com/repos/jroweboy/citra/releases{/id}","deployments_url":"https://api.github.com/repos/jroweboy/citra/deployments","created_at":"2015-02-25T12:05:21Z","updated_at":"2017-10-08T22:12:27Z","pushed_at":"2018-02-25T00:41:25Z","git_url":"git://github.com/jroweboy/citra.git","ssh_url":"git@github.com:jroweboy/citra.git","clone_url":"https://github.com/jroweboy/citra.git","svn_url":"https://github.com/jroweboy/citra","homepage":null,"size":23953,"stargazers_count":6,"watchers_count":6,"language":"C++","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"archived":false,"open_issues_count":2,"license":{"key":"gpl-2.0","name":"GNU General Public License v2.0","spdx_id":"GPL-2.0","url":"https://api.github.com/licenses/gpl-2.0"},"forks":4,"open_issues":2,"watchers":6,"default_branch":"master"}},"base":{"label":"citra-emu:master","ref":"master","sha":"c3c684cd2b282e4aceb5feadf8434f104d02c821","user":{"login":"citra-emu","id":7605670,"avatar_url":"https://avatars1.githubusercontent.com/u/7605670?v=4","gravatar_id":"","url":"https://api.github.com/users/citra-emu","html_url":"https://github.com/citra-emu","followers_url":"https://api.github.com/users/citra-emu/followers","following_url":"https://api.github.com/users/citra-emu/following{/other_user}","gists_url":"https://api.github.com/users/citra-emu/gists{/gist_id}","starred_url":"https://api.github.com/users/citra-emu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/citra-emu/subscriptions","organizations_url":"https://api.github.com/users/citra-emu/orgs","repos_url":"https://api.github.com/users/citra-emu/repos","events_url":"https://api.github.com/users/citra-emu/events{/privacy}","received_events_url":"https://api.github.com/users/citra-emu/received_events","type":"Organization","site_admin":false},"repo":{"id":19866730,"name":"citra","full_name":"citra-emu/citra","owner":{"login":"citra-emu","id":7605670,"avatar_url":"https://avatars1.githubusercontent.com/u/7605670?v=4","gravatar_id":"","url":"https://api.github.com/users/citra-emu","html_url":"https://github.com/citra-emu","followers_url":"https://api.github.com/users/citra-emu/followers","following_url":"https://api.github.com/users/citra-emu/following{/other_user}","gists_url":"https://api.github.com/users/citra-emu/gists{/gist_id}","starred_url":"https://api.github.com/users/citra-emu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/citra-emu/subscriptions","organizations_url":"https://api.github.com/users/citra-emu/orgs","repos_url":"https://api.github.com/users/citra-emu/repos","events_url":"https://api.github.com/users/citra-emu/events{/privacy}","received_events_url":"https://api.github.com/users/citra-emu/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/citra-emu/citra","description":"A Nintendo 3DS Emulator","fork":false,"url":"https://api.github.com/repos/citra-emu/citra","forks_url":"https://api.github.com/repos/citra-emu/citra/forks","keys_url":"https://api.github.com/repos/citra-emu/citra/keys{/key_id}","collaborators_url":"https://api.github.com/repos/citra-emu/citra/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/citra-emu/citra/teams","hooks_url":"https://api.github.com/repos/citra-emu/citra/hooks","issue_events_url":"https://api.github.com/repos/citra-emu/citra/issues/events{/number}","events_url":"https://api.github.com/repos/citra-emu/citra/events","assignees_url":"https://api.github.com/repos/citra-emu/citra/assignees{/user}","branches_url":"https://api.github.com/repos/citra-emu/citra/branches{/branch}","tags_url":"https://api.github.com/repos/citra-emu/citra/tags","blobs_url":"https://api.github.com/repos/citra-emu/citra/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/citra-emu/citra/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/citra-emu/citra/git/refs{/sha}","trees_url":"https://api.github.com/repos/citra-emu/citra/git/trees{/sha}","statuses_url":"https://api.github.com/repos/citra-emu/citra/statuses/{sha}","languages_url":"https://api.github.com/repos/citra-emu/citra/languages","stargazers_url":"https://api.github.com/repos/citra-emu/citra/stargazers","contributors_url":"https://api.github.com/repos/citra-emu/citra/contributors","subscribers_url":"https://api.github.com/repos/citra-emu/citra/subscribers","subscription_url":"https://api.github.com/repos/citra-emu/citra/subscription","commits_url":"https://api.github.com/repos/citra-emu/citra/commits{/sha}","git_commits_url":"https://api.github.com/repos/citra-emu/citra/git/commits{/sha}","comments_url":"https://api.github.com/repos/citra-emu/citra/comments{/number}","issue_comment_url":"https://api.github.com/repos/citra-emu/citra/issues/comments{/number}","contents_url":"https://api.github.com/repos/citra-emu/citra/contents/{+path}","compare_url":"https://api.github.com/repos/citra-emu/citra/compare/{base}...{head}","merges_url":"https://api.github.com/repos/citra-emu/citra/merges","archive_url":"https://api.github.com/repos/citra-emu/citra/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/citra-emu/citra/downloads","issues_url":"https://api.github.com/repos/citra-emu/citra/issues{/number}","pulls_url":"https://api.github.com/repos/citra-emu/citra/pulls{/number}","milestones_url":"https://api.github.com/repos/citra-emu/citra/milestones{/number}","notifications_url":"https://api.github.com/repos/citra-emu/citra/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/citra-emu/citra/labels{/name}","releases_url":"https://api.github.com/repos/citra-emu/citra/releases{/id}","deployments_url":"https://api.github.com/repos/citra-emu/citra/deployments","created_at":"2014-05-16T18:33:15Z","updated_at":"2018-02-28T03:17:19Z","pushed_at":"2018-02-28T14:25:34Z","git_url":"git://github.com/citra-emu/citra.git","ssh_url":"git@github.com:citra-emu/citra.git","clone_url":"https://github.com/citra-emu/citra.git","svn_url":"https://github.com/citra-emu/citra","homepage":"https://citra-emu.org","size":23775,"stargazers_count":3769,"watchers_count":3769,"language":"C++","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":868,"mirror_url":null,"archived":false,"open_issues_count":308,"license":{"key":"gpl-2.0","name":"GNU General Public License v2.0","spdx_id":"GPL-2.0","url":"https://api.github.com/licenses/gpl-2.0"},"forks":868,"open_issues":308,"watchers":3769,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/citra-emu/citra/pulls/3281"},"html":{"href":"https://github.com/citra-emu/citra/pull/3281"},"issue":{"href":"https://api.github.com/repos/citra-emu/citra/issues/3281"},"comments":{"href":"https://api.github.com/repos/citra-emu/citra/issues/3281/comments"},"review_comments":{"href":"https://api.github.com/repos/citra-emu/citra/pulls/3281/comments"},"review_comment":{"href":"https://api.github.com/repos/citra-emu/citra/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/citra-emu/citra/pulls/3281/commits"},"statuses":{"href":"https://api.github.com/repos/citra-emu/citra/statuses/18456ff9e6a8659adb2b859c556cfa17cf638ee1"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"wwylele"}}