From e051e572519751eb3f7660e6190db920e6f5f5fc Mon Sep 17 00:00:00 2001 From: iwubcode Date: Mon, 31 Aug 2026 01:39:00 -0500 Subject: [PATCH] VideoCommon: refactor texture cache frame buffer copies, in preparation for a future enhancement --- Source/Core/VideoBackends/Null/TextureCache.h | 4 +- .../VideoBackends/Software/TextureCache.h | 4 +- Source/Core/VideoCommon/BPStructs.cpp | 43 +- Source/Core/VideoCommon/TextureCacheBase.cpp | 657 ++++++++++-------- Source/Core/VideoCommon/TextureCacheBase.h | 69 +- 5 files changed, 461 insertions(+), 316 deletions(-) diff --git a/Source/Core/VideoBackends/Null/TextureCache.h b/Source/Core/VideoBackends/Null/TextureCache.h index 78e910b74b..f0137619de 100644 --- a/Source/Core/VideoBackends/Null/TextureCache.h +++ b/Source/Core/VideoBackends/Null/TextureCache.h @@ -10,8 +10,8 @@ namespace Null class TextureCache final : public TextureCacheBase { protected: - void CopyEFB(AbstractStagingTexture* dst, const EFBCopyParams& params, u32 native_width, - u32 bytes_per_row, u32 num_blocks_y, u32 memory_stride, + void CopyEFB(AbstractStagingTexture* dst, FramebufferManager*, const EFBCopyParams& params, + u32 native_width, u32 bytes_per_row, u32 num_blocks_y, u32 memory_stride, const MathUtil::Rectangle& src_rect, bool scale_by_half, bool linear_filter, float y_scale, float gamma, bool clamp_top, bool clamp_bottom, const std::array& filter_coefficients) override diff --git a/Source/Core/VideoBackends/Software/TextureCache.h b/Source/Core/VideoBackends/Software/TextureCache.h index 7fc38ed9e8..80c84b8a88 100644 --- a/Source/Core/VideoBackends/Software/TextureCache.h +++ b/Source/Core/VideoBackends/Software/TextureCache.h @@ -10,8 +10,8 @@ namespace SW class TextureCache : public TextureCacheBase { protected: - void CopyEFB(AbstractStagingTexture* dst, const EFBCopyParams& params, u32 native_width, - u32 bytes_per_row, u32 num_blocks_y, u32 memory_stride, + void CopyEFB(AbstractStagingTexture* dst, FramebufferManager*, const EFBCopyParams& params, + u32 native_width, u32 bytes_per_row, u32 num_blocks_y, u32 memory_stride, const MathUtil::Rectangle& src_rect, bool scale_by_half, bool linear_filter, float y_scale, float gamma, bool clamp_top, bool clamp_bottom, const std::array& filter_coefficients) override diff --git a/Source/Core/VideoCommon/BPStructs.cpp b/Source/Core/VideoCommon/BPStructs.cpp index 180bc86151..1bcfb84206 100644 --- a/Source/Core/VideoCommon/BPStructs.cpp +++ b/Source/Core/VideoCommon/BPStructs.cpp @@ -300,17 +300,31 @@ static void BPWritten(PixelShaderManager& pixel_shader_manager, XFStateManager& if (destStride != 0) { + // bpmem.zcontrol.pixel_format to PixelFormat::Z24 is when the game wants to copy from + // ZBuffer (Zbuffer uses 24-bit Format) + bool is_depth_copy = bpmem.zcontrol.pixel_format == PixelFormat::Z24; + + FramebufferCopyRawData framebuffer_copy_data{ + .dest_address = destAddr, + .copy_width = copy_width, + .dest_stride = destStride, + .is_depth_format = is_depth_copy, + .gamma = s_gammaLUT[PE_copy.gamma], + .src_rect = srcRect, + .clamp_top = bpmem.triggerEFBCopy.clamp_top, + .clamp_bottom = bpmem.triggerEFBCopy.clamp_bottom, + .filter_coefficients = bpmem.copyfilter.GetCoefficients()}; + // Check if we are to copy from the EFB or draw to the XFB if (PE_copy.copy_to_xfb == 0) { - // bpmem.zcontrol.pixel_format to PixelFormat::Z24 is when the game wants to copy from - // ZBuffer (Zbuffer uses 24-bit Format) - bool is_depth_copy = bpmem.zcontrol.pixel_format == PixelFormat::Z24; - g_texture_cache->CopyRenderTargetToTexture( - destAddr, PE_copy.tp_realFormat(), copy_width, copy_height, destStride, is_depth_copy, - srcRect, PE_copy.intensity_fmt && PE_copy.auto_conv, PE_copy.half_scale, 1.0f, - s_gammaLUT[PE_copy.gamma], bpmem.triggerEFBCopy.clamp_top, - bpmem.triggerEFBCopy.clamp_bottom, bpmem.copyfilter.GetCoefficients()); + framebuffer_copy_data.efbcopy_format = PE_copy.tp_realFormat(); + framebuffer_copy_data.is_intensity_format = PE_copy.intensity_fmt && PE_copy.auto_conv; + framebuffer_copy_data.half_scale = PE_copy.half_scale; + framebuffer_copy_data.y_scale = 1.0; + framebuffer_copy_data.copy_height = copy_height; + auto resolved_data = g_texture_cache->ResolveFramebufferCopyData(framebuffer_copy_data); + g_texture_cache->CopyRenderTargetToTexture(std::ref(resolved_data)); } else { @@ -335,11 +349,14 @@ static void BPWritten(PixelShaderManager& pixel_shader_manager, XFStateManager& destAddr, srcRect.left, srcRect.top, srcRect.right, srcRect.bottom, bpmem.copyTexSrcWH.x + 1, destStride, height, yScale); - bool is_depth_copy = bpmem.zcontrol.pixel_format == PixelFormat::Z24; - g_texture_cache->CopyRenderTargetToTexture( - destAddr, EFBCopyFormat::XFB, copy_width, height, destStride, is_depth_copy, srcRect, - false, false, yScale, s_gammaLUT[PE_copy.gamma], bpmem.triggerEFBCopy.clamp_top, - bpmem.triggerEFBCopy.clamp_bottom, bpmem.copyfilter.GetCoefficients()); + framebuffer_copy_data.efbcopy_format = EFBCopyFormat::XFB; + framebuffer_copy_data.is_intensity_format = false; + framebuffer_copy_data.half_scale = false; + framebuffer_copy_data.y_scale = yScale; + framebuffer_copy_data.copy_height = height; + auto resolved_data = g_texture_cache->ResolveFramebufferCopyData(framebuffer_copy_data); + + g_texture_cache->CopyRenderTargetToTexture(std::ref(resolved_data)); auto& system = Core::System::GetInstance(); diff --git a/Source/Core/VideoCommon/TextureCacheBase.cpp b/Source/Core/VideoCommon/TextureCacheBase.cpp index 4e4567da8e..54fc177a8d 100644 --- a/Source/Core/VideoCommon/TextureCacheBase.cpp +++ b/Source/Core/VideoCommon/TextureCacheBase.cpp @@ -2145,11 +2145,7 @@ bool TextureCacheBase::CopyFilterCanOverflow(const std::array& coefficie return coefficients[0] + coefficients[1] + coefficients[2] >= 128; } -void TextureCacheBase::CopyRenderTargetToTexture( - u32 dstAddr, EFBCopyFormat dstFormat, u32 width, u32 height, u32 dstStride, bool is_depth_copy, - const MathUtil::Rectangle& srcRect, bool isIntensity, bool scaleByHalf, float y_scale, - float gamma, bool clamp_top, bool clamp_bottom, - const CopyFilterCoefficients::Values& filter_coefficients) +void TextureCacheBase::CopyRenderTargetToTexture(FramebufferCopyResolvedData& resolved_data) { // Emulation methods: // @@ -2209,71 +2205,25 @@ void TextureCacheBase::CopyRenderTargetToTexture( // // Disadvantage of all methods: Calling this function requires the GPU to perform a pipeline flush // which stalls any further CPU processing. - const bool is_xfb_copy = !is_depth_copy && !isIntensity && dstFormat == EFBCopyFormat::XFB; - bool copy_to_vram = g_backend_info.bSupportsCopyToVram && !g_ActiveConfig.bDisableCopyToVRAM; - bool copy_to_ram = - !(is_xfb_copy ? g_ActiveConfig.bSkipXFBCopyToRam : g_ActiveConfig.bSkipEFBCopyToRam) || - !copy_to_vram; - - // tex_w and tex_h are the native size of the texture in the GC memory. - // The size scaled_* represents the emulated texture. Those differ - // because of upscaling and because of yscaling of XFB copies. - // For the latter, we keep the EFB resolution for the virtual XFB blit. - u32 tex_w = width; - u32 tex_h = height; - u32 scaled_tex_w = g_framebuffer_manager->EFBToScaledX(width); - u32 scaled_tex_h = g_framebuffer_manager->EFBToScaledY(height); - - if (scaleByHalf) - { - tex_w /= 2; - tex_h /= 2; - scaled_tex_w /= 2; - scaled_tex_h /= 2; - } - - if (!is_xfb_copy && !g_ActiveConfig.bCopyEFBScaled) - { - // No upscaling - scaled_tex_w = tex_w; - scaled_tex_h = tex_h; - } - - // Get the base (in memory) format of this efb copy. - TextureFormat baseFormat = TexDecoder_GetEFBCopyBaseFormat(dstFormat); - - u32 blockH = TexDecoder_GetBlockHeightInTexels(baseFormat); - const u32 blockW = TexDecoder_GetBlockWidthInTexels(baseFormat); - - // Round up source height to multiple of block size - u32 actualHeight = Common::AlignUp(tex_h, blockH); - const u32 actualWidth = Common::AlignUp(tex_w, blockW); - - u32 num_blocks_y = actualHeight / blockH; - const u32 num_blocks_x = actualWidth / blockW; - - // RGBA takes two cache lines per block; all others take one - const u32 bytes_per_block = baseFormat == TextureFormat::RGBA8 ? 64 : 32; - - const u32 bytes_per_row = num_blocks_x * bytes_per_block; - const u32 covered_range = num_blocks_y * dstStride; auto& system = Core::System::GetInstance(); auto& memory = system.GetMemory(); - u8* dst = memory.GetPointerForRange(dstAddr, covered_range); + u8* dst = memory.GetPointerForRange(resolved_data.source_data.dest_address, + resolved_data.covered_range); if (dst == nullptr) { - ERROR_LOG_FMT(VIDEO, "Trying to copy from EFB to invalid address {:#010x}", dstAddr); + ERROR_LOG_FMT(VIDEO, "Trying to copy from EFB to invalid address {:#010x}", + resolved_data.source_data.dest_address); return; } if (g_ActiveConfig.bGraphicMods) { FBInfo info; - info.m_width = tex_w; - info.m_height = tex_h; - info.m_texture_format = baseFormat; - if (is_xfb_copy) + info.m_width = resolved_data.memory_width; + info.m_height = resolved_data.memory_height; + info.m_texture_format = resolved_data.memory_format; + if (resolved_data.is_xfb) { for (const auto& action : g_graphics_mod_manager->GetXFBActions(info)) { @@ -2283,253 +2233,54 @@ void TextureCacheBase::CopyRenderTargetToTexture( else { bool skip = false; - GraphicsModActionData::PreEFB efb{tex_w, tex_h, &skip, &scaled_tex_w, &scaled_tex_h}; + GraphicsModActionData::PreEFB efb{resolved_data.memory_width, resolved_data.memory_height, + &skip, &resolved_data.scaled_memory_width, + &resolved_data.scaled_memory_height}; for (const auto& action : g_graphics_mod_manager->GetEFBActions(info)) { action->BeforeEFB(&efb); } if (skip == true) { - if (copy_to_ram) - UninitializeEFBMemory(dst, dstStride, bytes_per_row, num_blocks_y); + if (resolved_data.copy_to_ram) + { + UninitializeEFBMemory(dst, resolved_data.source_data.dest_stride, + resolved_data.bytes_per_row, resolved_data.num_blocks_y); + } return; } } } - if (dstStride < bytes_per_row) - { - // This kind of efb copy results in a scrambled image. - // I'm pretty sure no game actually wants to do this, it might be caused by a - // programming bug in the game, or a CPU/Bounding box emulation issue with dolphin. - // The copy_to_ram code path above handles this "correctly" and scrambles the image - // but the copy_to_vram code path just saves and uses unscrambled texture instead. - - // To avoid a "incorrect" result, we simply skip doing the copy_to_vram code path - // so if the game does try to use the scrambled texture, dolphin will grab the scrambled - // texture (or black if copy_to_ram is also disabled) out of ram. - ERROR_LOG_FMT(VIDEO, "Memory stride too small ({} < {})", dstStride, bytes_per_row); - copy_to_vram = false; - } - - // We also linear filtering for both box filtering and downsampling higher resolutions to 1x. - // TODO: This only produces perfect downsampling for 2x IR, other resolutions will need more - // complex down filtering to average all pixels and produce the correct result. - const bool linear_filter = - !is_depth_copy && - (scaleByHalf || g_framebuffer_manager->GetEFBScale() != 1 || y_scale > 1.0f); - RcTcacheEntry entry; - if (copy_to_vram) + if (resolved_data.copy_to_vram) { - // create the texture - const TextureConfig config(scaled_tex_w, scaled_tex_h, 1, g_framebuffer_manager->GetEFBLayers(), - 1, AbstractTextureFormat::RGBA8, AbstractTextureFlag_RenderTarget, - AbstractTextureType::Texture_2DArray); - entry = AllocateCacheEntry(config); + entry = SetupCopyEntry(g_framebuffer_manager.get(), resolved_data); + if (entry) - { - entry->SetGeneralParameters(dstAddr, 0, baseFormat, is_xfb_copy); - entry->SetDimensions(tex_w, tex_h, 1); - entry->frameCount = FRAMECOUNT_INVALID; - if (is_xfb_copy) - { - entry->should_force_safe_hashing = is_xfb_copy; - entry->SetXfbCopy(dstStride); - } - else - { - entry->SetEfbCopy(dstStride); - } - entry->may_have_overlapping_textures = false; - entry->is_custom_tex = false; - - CopyEFBToCacheEntry(entry, is_depth_copy, srcRect, scaleByHalf, linear_filter, dstFormat, - isIntensity, gamma, clamp_top, clamp_bottom, - GetVRAMCopyFilterCoefficients(filter_coefficients)); - - if (g_ActiveConfig.bGraphicMods) - { - FBInfo info; - info.m_width = tex_w; - info.m_height = tex_h; - info.m_texture_format = baseFormat; - if (!is_xfb_copy) - { - GraphicsModActionData::PostEFB efb; - for (const auto& action : g_graphics_mod_manager->GetEFBActions(info)) - { - action->AfterEFB(&efb); - if (efb.material) - { - ApplyMaterialToCacheEntry(*efb.material, entry.get()); - } - } - } - } - - if (is_xfb_copy && (g_ActiveConfig.bDumpXFBTarget || g_ActiveConfig.bGraphicMods)) - { - const std::string id = fmt::format("{}x{}", tex_w, tex_h); - if (g_ActiveConfig.bGraphicMods) - { - entry->texture_info_name = fmt::format("{}_{}", XFB_DUMP_PREFIX, id); - } - - if (g_ActiveConfig.bDumpXFBTarget) - { - entry->texture->Save(fmt::format("{}{}_n{:06}_{}.png", - File::GetUserPath(D_DUMPTEXTURES_IDX), XFB_DUMP_PREFIX, - xfb_count++, id), - 0); - } - } - else if (g_ActiveConfig.bDumpEFBTarget || g_ActiveConfig.bGraphicMods) - { - const std::string id = fmt::format("{}x{}_{}", tex_w, tex_h, static_cast(baseFormat)); - if (g_ActiveConfig.bGraphicMods) - { - entry->texture_info_name = fmt::format("{}_{}", EFB_DUMP_PREFIX, id); - } - - if (g_ActiveConfig.bDumpEFBTarget) - { - static int efb_count = 0; - entry->texture->Save(fmt::format("{}{}_n{:06}_{}.png", - File::GetUserPath(D_DUMPTEXTURES_IDX), EFB_DUMP_PREFIX, - efb_count++, id), - 0); - } - } - } + CopyRenderTargetToVRam(entry, g_framebuffer_manager.get(), resolved_data); } - if (copy_to_ram) + if (resolved_data.copy_to_ram) { - const std::array coefficients = GetRAMCopyFilterCoefficients(filter_coefficients); - PixelFormat srcFormat = bpmem.zcontrol.pixel_format; - EFBCopyParams format(srcFormat, dstFormat, is_depth_copy, isIntensity, - AllCopyFilterCoefsNeeded(coefficients), - CopyFilterCanOverflow(coefficients), gamma != 1.0); - - std::unique_ptr staging_texture = GetEFBCopyStagingTexture(); - if (staging_texture) - { - CopyEFB(staging_texture.get(), format, tex_w, bytes_per_row, num_blocks_y, dstStride, srcRect, - scaleByHalf, linear_filter, y_scale, gamma, clamp_top, clamp_bottom, coefficients); - - // We can't defer if there is no VRAM copy (since we need to update the hash). - if (!copy_to_vram || !g_ActiveConfig.bDeferEFBCopies) - { - // Immediately flush it. - WriteEFBCopyToRAM(dst, bytes_per_row / sizeof(u32), num_blocks_y, dstStride, - std::move(staging_texture)); - } - else - { - // Defer the flush until later. - entry->pending_efb_copy = std::move(staging_texture); - entry->pending_efb_copy_width = bytes_per_row / sizeof(u32); - entry->pending_efb_copy_height = num_blocks_y; - m_pending_efb_copies.push_back(entry); - } - } + const bool defer_write = g_ActiveConfig.bDeferEFBCopies; + CopyRenderTargetToRam(entry, g_framebuffer_manager.get(), dst, resolved_data, defer_write); } else { - if (is_xfb_copy) + if (resolved_data.is_xfb) { - UninitializeXFBMemory(dst, dstStride, bytes_per_row, num_blocks_y); + UninitializeXFBMemory(dst, resolved_data.source_data.dest_stride, resolved_data.bytes_per_row, + resolved_data.num_blocks_y); } else { - UninitializeEFBMemory(dst, dstStride, bytes_per_row, num_blocks_y); + UninitializeEFBMemory(dst, resolved_data.source_data.dest_stride, resolved_data.bytes_per_row, + resolved_data.num_blocks_y); } } - // Invalidate all textures, if they are either fully overwritten by our efb copy, or if they - // have a different stride than our efb copy. Partly overwritten textures with the same stride - // as our efb copy are marked to check them for partial texture updates. - // TODO: The logic to detect overlapping strided efb copies is not 100% accurate. - bool strided_efb_copy = dstStride != bytes_per_row; - auto iter = FindOverlappingTextures(dstAddr, covered_range); - while (iter.first != iter.second) - { - RcTcacheEntry& overlapping_entry = iter.first->second; - - if (overlapping_entry->addr == dstAddr && overlapping_entry->is_xfb_copy) - { - for (auto& reference : overlapping_entry->references) - { - reference->reference_changed = true; - } - } - - if (overlapping_entry->OverlapsMemoryRange(dstAddr, covered_range)) - { - u32 overlap_range = std::min(overlapping_entry->addr + overlapping_entry->size_in_bytes, - dstAddr + covered_range) - - std::max(overlapping_entry->addr, dstAddr); - if (!copy_to_vram || overlapping_entry->memory_stride != dstStride || - (!strided_efb_copy && overlapping_entry->size_in_bytes == overlap_range) || - (strided_efb_copy && overlapping_entry->size_in_bytes == overlap_range && - overlapping_entry->addr == dstAddr)) - { - // Pending EFB copies which are completely covered by this new copy can simply be tossed, - // instead of having to flush them later on, since this copy will write over everything. - iter.first = InvalidateTexture(iter.first, true); - continue; - } - - // We don't want to change the may_have_overlapping_textures flag on XFB container entries - // because otherwise they can't be re-used/updated, leaking textures for several frames. - if (!overlapping_entry->is_xfb_container) - overlapping_entry->may_have_overlapping_textures = true; - - // There are cases (Rogue Squadron 2 / Texas Holdem on Wiiware) where - // for xfb copies the textures overlap which causes the hash of the first copy - // to be different (from when it was originally created). This has no implications - // for XFB2Tex because the underlying memory doesn't change (dummy values) but - // can affect XFB2Ram when we compare the texture cache copy hash with the - // newly computed hash - // By calculating the hash when we receive overlapping xfbs, we are able - // to mitigate this - if (overlapping_entry->is_xfb_copy && copy_to_ram) - { - overlapping_entry->hash = overlapping_entry->CalculateHash(); - } - - // Do not load textures by hash, if they were at least partly overwritten by an efb copy. - // In this case, comparing the hash is not enough to check, if two textures are identical. - if (overlapping_entry->textures_by_hash_iter != m_textures_by_hash.end()) - { - m_textures_by_hash.erase(overlapping_entry->textures_by_hash_iter); - overlapping_entry->textures_by_hash_iter = m_textures_by_hash.end(); - } - } - ++iter.first; - } - - if (OpcodeDecoder::g_record_fifo_data) - { - // Mark the memory behind this efb copy as dynamically generated for the Fifo log - u32 address = dstAddr; - for (u32 i = 0; i < num_blocks_y; i++) - { - Core::System::GetInstance().GetFifoRecorder().UseMemory(address, bytes_per_row, - MemoryUpdate::Type::TextureMap, true); - address += dstStride; - } - } - - // Even if the copy is deferred, still compute the hash. This way if the copy is used as a texture - // in a subsequent draw before it is flushed, it will have the same hash. - if (entry) - { - const u64 hash = entry->CalculateHash(); - entry->SetHashes(hash, hash); - m_textures_by_address.emplace(dstAddr, std::move(entry)); - } + FinalizeFramebufferCopy(entry, resolved_data); } void TextureCacheBase::FlushEFBCopies() @@ -2931,15 +2682,16 @@ void TextureCacheBase::CopyEFBToCacheEntry(RcTcacheEntry& entry, bool is_depth_c entry->texture->FinishedRendering(); } -void TextureCacheBase::CopyEFB(AbstractStagingTexture* dst, const EFBCopyParams& params, - u32 native_width, u32 bytes_per_row, u32 num_blocks_y, - u32 memory_stride, const MathUtil::Rectangle& src_rect, - bool scale_by_half, bool linear_filter, float y_scale, float gamma, - bool clamp_top, bool clamp_bottom, - const std::array& filter_coefficients) +void TextureCacheBase::CopyEFB(AbstractStagingTexture* dst, + FramebufferManager* frame_buffer_manager, + const EFBCopyParams& params, u32 native_width, u32 bytes_per_row, + u32 num_blocks_y, u32 memory_stride, + const MathUtil::Rectangle& src_rect, bool scale_by_half, + bool linear_filter, float y_scale, float gamma, bool clamp_top, + bool clamp_bottom, const std::array& filter_coefficients) { // Flush EFB pokes first, as they're expected to be included. - g_framebuffer_manager->FlushEFBPokes(); + frame_buffer_manager->FlushEFBPokes(); // Get the pipeline which we will be using. If the compilation failed, this will be null. const AbstractPipeline* copy_pipeline = g_shader_cache->GetEFBCopyToRAMPipeline(params); @@ -2949,12 +2701,12 @@ void TextureCacheBase::CopyEFB(AbstractStagingTexture* dst, const EFBCopyParams& return; } - const auto scaled_src_rect = g_framebuffer_manager->ConvertEFBRectangle(src_rect); + const auto scaled_src_rect = frame_buffer_manager->ConvertEFBRectangle(src_rect); const auto framebuffer_rect = g_gfx->ConvertFramebufferRectangle( - scaled_src_rect, g_framebuffer_manager->GetEFBFramebuffer()); + scaled_src_rect, frame_buffer_manager->GetEFBFramebuffer()); AbstractTexture* src_texture = - params.depth ? g_framebuffer_manager->ResolveEFBDepthTexture(framebuffer_rect) : - g_framebuffer_manager->ResolveEFBColorTexture(framebuffer_rect); + params.depth ? frame_buffer_manager->ResolveEFBDepthTexture(framebuffer_rect) : + frame_buffer_manager->ResolveEFBColorTexture(framebuffer_rect); g_gfx->BeginUtilityDrawing(); src_texture->FinishedRendering(); @@ -2971,7 +2723,7 @@ void TextureCacheBase::CopyEFB(AbstractStagingTexture* dst, const EFBCopyParams& u32 padding; }; Uniforms encoder_params; - const u32 efb_height = g_framebuffer_manager->GetEFBHeight(); + const u32 efb_height = frame_buffer_manager->GetEFBHeight(); const float rcp_efb_height = 1.0f / static_cast(efb_height); encoder_params.position_uniform[0] = src_rect.left; encoder_params.position_uniform[1] = src_rect.top; @@ -3007,6 +2759,9 @@ void TextureCacheBase::CopyEFB(AbstractStagingTexture* dst, const EFBCopyParams& dst->CopyFromTexture(m_efb_encoding_texture.get(), encode_rect, 0, 0, encode_rect); g_gfx->EndUtilityDrawing(); + // Rebind to a non global version... + frame_buffer_manager->BindEFBFramebuffer(); + // Flush if there's sufficient draws between this copy and the last. g_vertex_manager->OnEFBCopyToRAM(); } @@ -3288,3 +3043,323 @@ TextureCacheBase::TexPoolEntry::TexPoolEntry(std::unique_ptr te : texture(std::move(tex)), framebuffer(std::move(fb)) { } + +FramebufferCopyResolvedData +TextureCacheBase::ResolveFramebufferCopyData(const FramebufferCopyRawData& copy_data) +{ + FramebufferCopyResolvedData result; + + result.is_xfb = !copy_data.is_depth_format && !copy_data.is_intensity_format && + copy_data.efbcopy_format == EFBCopyFormat::XFB; + result.copy_to_vram = g_backend_info.bSupportsCopyToVram && !g_ActiveConfig.bDisableCopyToVRAM; + result.copy_to_ram = + !(result.is_xfb ? g_ActiveConfig.bSkipXFBCopyToRam : g_ActiveConfig.bSkipEFBCopyToRam) || + !result.copy_to_vram; + + // memory_width and memory_height are the native size of the texture in the GC memory. + // The size scaled_* represents the emulated texture. Those differ + // because of upscaling and because of yscaling of XFB copies. + // For the latter, we keep the EFB resolution for the virtual XFB blit. + result.memory_width = copy_data.copy_width; + result.memory_height = copy_data.copy_height; + result.scaled_memory_width = g_framebuffer_manager->EFBToScaledX(copy_data.copy_width); + result.scaled_memory_height = g_framebuffer_manager->EFBToScaledY(copy_data.copy_height); + + if (copy_data.half_scale) + { + result.memory_width /= 2; + result.memory_height /= 2; + result.scaled_memory_width /= 2; + result.scaled_memory_height /= 2; + } + + if (!result.is_xfb && !g_ActiveConfig.bCopyEFBScaled) + { + // No upscaling + result.scaled_memory_width = result.memory_width; + result.scaled_memory_height = result.memory_height; + } + + // Get the base (in memory) format of this efb copy. + result.memory_format = TexDecoder_GetEFBCopyBaseFormat(copy_data.efbcopy_format); + + u32 blockH = TexDecoder_GetBlockHeightInTexels(result.memory_format); + const u32 blockW = TexDecoder_GetBlockWidthInTexels(result.memory_format); + + // Round up source height to multiple of block size + u32 actualHeight = Common::AlignUp(result.memory_height, blockH); + const u32 actualWidth = Common::AlignUp(result.memory_width, blockW); + + result.num_blocks_y = actualHeight / blockH; + const u32 num_blocks_x = actualWidth / blockW; + + // RGBA takes two cache lines per block; all others take one + const u32 bytes_per_block = result.memory_format == TextureFormat::RGBA8 ? 64 : 32; + + result.bytes_per_row = num_blocks_x * bytes_per_block; + result.covered_range = result.num_blocks_y * copy_data.dest_stride; + + // We also linear filtering for both box filtering and downsampling higher resolutions to 1x. + // TODO: This only produces perfect downsampling for 2x IR, other resolutions will need more + // complex down filtering to average all pixels and produce the correct result. + result.linear_filter = !copy_data.is_depth_format && + (copy_data.half_scale || g_framebuffer_manager->GetEFBScale() != 1 || + copy_data.y_scale > 1.0f); + + if (copy_data.dest_stride < result.bytes_per_row) + { + // This kind of efb copy results in a scrambled image. + // I'm pretty sure no game actually wants to do this, it might be caused by a + // programming bug in the game, or a CPU/Bounding box emulation issue with dolphin. + // The copy_to_ram code path above handles this "correctly" and scrambles the image + // but the copy_to_vram code path just saves and uses unscrambled texture instead. + + // To avoid a "incorrect" result, we simply skip doing the copy_to_vram code path + // so if the game does try to use the scrambled texture, dolphin will grab the scrambled + // texture (or black if copy_to_ram is also disabled) out of ram. + ERROR_LOG_FMT(VIDEO, "Memory stride too small ({} < {})", copy_data.dest_stride, + result.bytes_per_row); + result.copy_to_vram = false; + } + + result.pixel_format = bpmem.zcontrol.pixel_format; + + // Copy the remaining fields from the raw data: + result.source_data = copy_data; + + return result; +} + +RcTcacheEntry TextureCacheBase::SetupCopyEntry(FramebufferManager* frame_buffer_manager, + const FramebufferCopyResolvedData& resolved_data) +{ + // create the texture + const TextureConfig config(resolved_data.scaled_memory_width, resolved_data.scaled_memory_height, + 1, frame_buffer_manager->GetEFBLayers(), 1, + AbstractTextureFormat::RGBA8, AbstractTextureFlag_RenderTarget, + AbstractTextureType::Texture_2DArray); + RcTcacheEntry entry = AllocateCacheEntry(config); + if (entry) + { + entry->SetGeneralParameters(resolved_data.source_data.dest_address, 0, + resolved_data.memory_format, resolved_data.is_xfb); + entry->SetDimensions(resolved_data.memory_width, resolved_data.memory_height, 1); + entry->frameCount = FRAMECOUNT_INVALID; + if (resolved_data.is_xfb) + { + entry->should_force_safe_hashing = resolved_data.is_xfb; + entry->SetXfbCopy(resolved_data.source_data.dest_stride); + } + else + { + entry->SetEfbCopy(resolved_data.source_data.dest_stride); + } + entry->may_have_overlapping_textures = false; + entry->is_custom_tex = false; + + if (g_ActiveConfig.bGraphicMods) + { + if (resolved_data.is_xfb) + { + const std::string id = + fmt::format("{}x{}", resolved_data.memory_width, resolved_data.memory_height); + entry->texture_info_name = fmt::format("{}_{}", XFB_DUMP_PREFIX, id); + } + else + { + const std::string id = + fmt::format("{}x{}_{}", resolved_data.memory_width, resolved_data.memory_height, + static_cast(resolved_data.memory_format)); + entry->texture_info_name = fmt::format("{}_{}", EFB_DUMP_PREFIX, id); + } + } + } + return entry; +} + +void TextureCacheBase::CopyRenderTargetToVRam(RcTcacheEntry& entry, + FramebufferManager* frame_buffer_manager, + const FramebufferCopyResolvedData& resolved_data) +{ + CopyEFBToCacheEntry(entry, resolved_data.source_data.is_depth_format, + resolved_data.source_data.src_rect, resolved_data.source_data.half_scale, + resolved_data.linear_filter, resolved_data.source_data.efbcopy_format, + resolved_data.source_data.is_intensity_format, + resolved_data.source_data.gamma, resolved_data.source_data.clamp_top, + resolved_data.source_data.clamp_bottom, + GetVRAMCopyFilterCoefficients(resolved_data.source_data.filter_coefficients)); + + if (g_ActiveConfig.bGraphicMods) + { + FBInfo info; + info.m_width = resolved_data.memory_width; + info.m_height = resolved_data.memory_height; + info.m_texture_format = resolved_data.memory_format; + if (!resolved_data.is_xfb) + { + GraphicsModActionData::PostEFB efb; + for (const auto& action : g_graphics_mod_manager->GetEFBActions(info)) + { + action->AfterEFB(&efb); + if (efb.material) + { + ApplyMaterialToCacheEntry(*efb.material, entry.get()); + } + } + } + } + + if (g_ActiveConfig.bDumpXFBTarget) + { + const std::string id = + fmt::format("{}x{}", resolved_data.memory_width, resolved_data.memory_height); + entry->texture->Save(fmt::format("{}{}_n{:06}_{}.png", File::GetUserPath(D_DUMPTEXTURES_IDX), + XFB_DUMP_PREFIX, xfb_count++, id), + 0); + } + else if (g_ActiveConfig.bDumpEFBTarget) + { + const std::string id = + fmt::format("{}x{}_{}", resolved_data.memory_width, resolved_data.memory_height, + static_cast(resolved_data.memory_format)); + static int efb_count = 0; + entry->texture->Save(fmt::format("{}{}_n{:06}_{}.png", File::GetUserPath(D_DUMPTEXTURES_IDX), + EFB_DUMP_PREFIX, efb_count++, id), + 0); + } +} + +void TextureCacheBase::CopyRenderTargetToRam(RcTcacheEntry& entry, + FramebufferManager* frame_buffer_manager, u8* dst, + const FramebufferCopyResolvedData& resolved_data, + bool defer_write) +{ + std::unique_ptr staging_texture = GetEFBCopyStagingTexture(); + if (staging_texture) + { + const std::array coefficients = + GetRAMCopyFilterCoefficients(resolved_data.source_data.filter_coefficients); + EFBCopyParams format( + resolved_data.pixel_format, resolved_data.source_data.efbcopy_format, + resolved_data.source_data.is_depth_format, resolved_data.source_data.is_intensity_format, + AllCopyFilterCoefsNeeded(coefficients), CopyFilterCanOverflow(coefficients), + resolved_data.source_data.gamma != 1.0); + CopyEFB(staging_texture.get(), frame_buffer_manager, format, resolved_data.memory_width, + resolved_data.bytes_per_row, resolved_data.num_blocks_y, + resolved_data.source_data.dest_stride, resolved_data.source_data.src_rect, + resolved_data.source_data.half_scale, resolved_data.linear_filter, + resolved_data.source_data.y_scale, resolved_data.source_data.gamma, + resolved_data.source_data.clamp_top, resolved_data.source_data.clamp_bottom, + coefficients); + + // We can't defer if there is no VRAM copy (since we need to update the hash). + if (!resolved_data.copy_to_vram || !defer_write) + { + // Immediately flush it. + WriteEFBCopyToRAM(dst, resolved_data.bytes_per_row / sizeof(u32), resolved_data.num_blocks_y, + resolved_data.source_data.dest_stride, std::move(staging_texture)); + } + else + { + // Defer the flush until later. + entry->pending_efb_copy = std::move(staging_texture); + entry->pending_efb_copy_width = resolved_data.bytes_per_row / sizeof(u32); + entry->pending_efb_copy_height = resolved_data.num_blocks_y; + m_pending_efb_copies.push_back(entry); + } + } +} + +void TextureCacheBase::FinalizeFramebufferCopy(RcTcacheEntry& entry, + const FramebufferCopyResolvedData& resolved_data) +{ + const u32 covered_range = resolved_data.num_blocks_y * resolved_data.source_data.dest_stride; + + // Invalidate all textures, if they are either fully overwritten by our efb copy, or if they + // have a different stride than our efb copy. Partly overwritten textures with the same stride + // as our efb copy are marked to check them for partial texture updates. + // TODO: The logic to detect overlapping strided efb copies is not 100% accurate. + bool strided_efb_copy = resolved_data.source_data.dest_stride != resolved_data.bytes_per_row; + auto iter = FindOverlappingTextures(resolved_data.source_data.dest_address, covered_range); + while (iter.first != iter.second) + { + RcTcacheEntry& overlapping_entry = iter.first->second; + + if (overlapping_entry->addr == resolved_data.source_data.dest_address && + overlapping_entry->is_xfb_copy) + { + for (auto& reference : overlapping_entry->references) + { + reference->reference_changed = true; + } + } + + if (overlapping_entry->OverlapsMemoryRange(resolved_data.source_data.dest_address, + covered_range)) + { + u32 overlap_range = std::min(overlapping_entry->addr + overlapping_entry->size_in_bytes, + resolved_data.source_data.dest_address + covered_range) - + std::max(overlapping_entry->addr, resolved_data.source_data.dest_address); + if (!resolved_data.copy_to_vram || + overlapping_entry->memory_stride != resolved_data.source_data.dest_stride || + (!strided_efb_copy && overlapping_entry->size_in_bytes == overlap_range) || + (strided_efb_copy && overlapping_entry->size_in_bytes == overlap_range && + overlapping_entry->addr == resolved_data.source_data.dest_address)) + { + // Pending EFB copies which are completely covered by this new copy can simply be tossed, + // instead of having to flush them later on, since this copy will write over everything. + iter.first = InvalidateTexture(iter.first, true); + continue; + } + + // We don't want to change the may_have_overlapping_textures flag on XFB container entries + // because otherwise they can't be re-used/updated, leaking textures for several frames. + if (!overlapping_entry->is_xfb_container) + overlapping_entry->may_have_overlapping_textures = true; + + // There are cases (Rogue Squadron 2 / Texas Holdem on Wiiware) where + // for xfb copies the textures overlap which causes the hash of the first copy + // to be different (from when it was originally created). This has no implications + // for XFB2Tex because the underlying memory doesn't change (dummy values) but + // can affect XFB2Ram when we compare the texture cache copy hash with the + // newly computed hash + // By calculating the hash when we receive overlapping xfbs, we are able + // to mitigate this + if (overlapping_entry->is_xfb_copy && resolved_data.copy_to_ram) + { + overlapping_entry->hash = overlapping_entry->CalculateHash(); + } + + // Do not load textures by hash, if they were at least partly overwritten by an efb copy. + // In this case, comparing the hash is not enough to check, if two textures are identical. + if (overlapping_entry->textures_by_hash_iter != m_textures_by_hash.end()) + { + m_textures_by_hash.erase(overlapping_entry->textures_by_hash_iter); + overlapping_entry->textures_by_hash_iter = m_textures_by_hash.end(); + } + } + ++iter.first; + } + + if (OpcodeDecoder::g_record_fifo_data) + { + // Mark the memory behind this efb copy as dynamically generated for the Fifo log + u32 address = resolved_data.source_data.dest_address; + for (u32 i = 0; i < resolved_data.num_blocks_y; i++) + { + Core::System::GetInstance().GetFifoRecorder().UseMemory(address, resolved_data.bytes_per_row, + MemoryUpdate::Type::TextureMap, true); + address += resolved_data.source_data.dest_stride; + } + } + + // Even if the copy is deferred, still compute the hash. This way if the copy is used as a + // texture in a subsequent draw before it is flushed, it will have the same hash. + if (entry) + { + const u64 hash = entry->CalculateHash(); + entry->SetHashes(hash, hash); + + m_textures_by_address.emplace(resolved_data.source_data.dest_address, entry); + } +} diff --git a/Source/Core/VideoCommon/TextureCacheBase.h b/Source/Core/VideoCommon/TextureCacheBase.h index 7e9d0613d4..1bb3804d40 100644 --- a/Source/Core/VideoCommon/TextureCacheBase.h +++ b/Source/Core/VideoCommon/TextureCacheBase.h @@ -33,6 +33,7 @@ class AbstractFramebuffer; class AbstractStagingTexture; +class FramebufferManager; class PointerWrap; struct SamplerState; struct VideoConfig; @@ -98,6 +99,49 @@ struct EFBCopyParams bool apply_gamma; }; +struct FramebufferCopyRawData +{ + u32 dest_address; + u32 copy_width; + u32 copy_height; + + u32 dest_stride; + EFBCopyFormat efbcopy_format; + + bool is_depth_format; + bool is_intensity_format; + bool half_scale; + float y_scale; + float gamma; + + MathUtil::Rectangle src_rect; + + bool clamp_top; + bool clamp_bottom; + CopyFilterCoefficients::Values filter_coefficients; +}; + +struct FramebufferCopyResolvedData +{ + // Source details + FramebufferCopyRawData source_data; + PixelFormat pixel_format; + + // Derived details + bool is_xfb; + bool copy_to_vram; + bool copy_to_ram; + u32 memory_width; + u32 memory_height; + u32 scaled_memory_width; + u32 scaled_memory_height; + TextureFormat memory_format; + u32 bytes_per_row; + u32 num_blocks_y; + u32 covered_range; + bool linear_filter; +}; + template <> struct fmt::formatter { @@ -285,12 +329,7 @@ public: MathUtil::Rectangle* display_rect); virtual void BindTextures(BitSet32 used_textures, const std::array& samplers); - void CopyRenderTargetToTexture(u32 dstAddr, EFBCopyFormat dstFormat, u32 width, u32 height, - u32 dstStride, bool is_depth_copy, - const MathUtil::Rectangle& srcRect, bool isIntensity, - bool scaleByHalf, float y_scale, float gamma, bool clamp_top, - bool clamp_bottom, - const CopyFilterCoefficients::Values& filter_coefficients); + void CopyRenderTargetToTexture(FramebufferCopyResolvedData& resolved_data); void ScaleTextureCacheEntryTo(RcTcacheEntry& entry, u32 new_width, u32 new_height); @@ -314,6 +353,9 @@ public: static SamplerState GetSamplerState(u32 index, float custom_tex_scale, bool custom_tex, bool has_arbitrary_mips); + static FramebufferCopyResolvedData + ResolveFramebufferCopyData(const FramebufferCopyRawData& copy_data); + static void RenderPaletteEntry(u32 texel_buffer_offset, const RcTcacheEntry& entry, AbstractTexture* texture, TLUTFormat tlutfmt); static void RenderReinterpretEntry(const RcTcacheEntry& entry, AbstractTexture* texture, @@ -330,8 +372,9 @@ protected: u32 aligned_height, u32 row_stride, const u8* palette, TLUTFormat palette_format); - virtual void CopyEFB(AbstractStagingTexture* dst, const EFBCopyParams& params, u32 native_width, - u32 bytes_per_row, u32 num_blocks_y, u32 memory_stride, + virtual void CopyEFB(AbstractStagingTexture* dst, FramebufferManager* frame_buffer_manager, + const EFBCopyParams& params, u32 native_width, u32 bytes_per_row, + u32 num_blocks_y, u32 memory_stride, const MathUtil::Rectangle& src_rect, bool scale_by_half, bool linear_filter, float y_scale, float gamma, bool clamp_top, bool clamp_bottom, const std::array& filter_coefficients); @@ -417,6 +460,16 @@ private: void ApplyMaterialToCacheEntry(const VideoCommon::MaterialResource& material, TCacheEntry* entry); + RcTcacheEntry SetupCopyEntry(FramebufferManager* frame_buffer_manager, + const FramebufferCopyResolvedData& resolved_data); + void CopyRenderTargetToVRam(RcTcacheEntry& entry, FramebufferManager* frame_buffer_manager, + const FramebufferCopyResolvedData& resolved_data); + void CopyRenderTargetToRam(RcTcacheEntry& entry, FramebufferManager* frame_buffer_manager, + u8* dst, const FramebufferCopyResolvedData& resolved_data, + bool defer_write); + void FinalizeFramebufferCopy(RcTcacheEntry& entry, + const FramebufferCopyResolvedData& resolved_data); + // m_textures_by_address is the authoritive version of what's actually "in" the texture cache // but it's possible for invalidated TCache entries to live on elsewhere TexAddrCache m_textures_by_address;