#include "graphics/host_gpu/renderer/cache/textureCache.h" #include "common/alignment.h" #include "common/assert.h" #include "common/emulatorConfig.h" #include "common/logging/log.h" #include "common/profiler.h" #include "graphics/guest_gpu/gpu_format.h" #include "graphics/guest_gpu/tile.h" #include "graphics/host_gpu/graphicContext.h" #include "graphics/host_gpu/renderer/cache/bufferCache.h" #include "graphics/host_gpu/renderer/commandScheduler.h" #include "graphics/host_gpu/renderer/image/imageView.h" #include "graphics/host_gpu/renderer/image/textureCommon.h" #include "graphics/host_gpu/renderer/image/tiler.h" #include "graphics/host_gpu/renderer/render.h" #include "kernel/memory.h" #include #include #include #include #include #include #include #include #include #include namespace Libs::Graphics { namespace { constexpr uint64_t NumFramesBeforeRemoval = 32; [[nodiscard]] bool DecodeDccClear(const TextureCache::ImageDesc& desc, vk::Format format, uint32_t fill, vk::ClearColorValue& clear) { const auto code = static_cast(fill); if (fill != static_cast(code) * 0x01010101u) { return false; } const auto& metadata = desc.info.metadata; if (code == 0x20) { // Clear-to-register is a color-buffer operation; the texture pipe cannot decode it. return desc.type == TextureCache::BindingType::RenderTarget && metadata.dcc_clear_register_valid && DecodePackedColorClear(format, metadata.dcc_clear_word, clear); } if (code != 0x00 && code != 0x40 && code != 0x80 && code != 0xc0) { return false; } clear = {}; if (code == 0x00) { return true; } switch (format) { case vk::Format::eR8Unorm: case vk::Format::eR8G8Unorm: case vk::Format::eR8G8B8A8Unorm: case vk::Format::eR8G8B8A8Srgb: case vk::Format::eB8G8R8A8Unorm: case vk::Format::eB8G8R8A8Srgb: case vk::Format::eA2B10G10R10UnormPack32: case vk::Format::eA2R10G10B10UnormPack32: case vk::Format::eR5G6B5UnormPack16: case vk::Format::eA1R5G5B5UnormPack16: case vk::Format::eR4G4B4A4UnormPack16: case vk::Format::eR16Unorm: case vk::Format::eR16G16Unorm: case vk::Format::eR16G16B16A16Unorm: case vk::Format::eR16Sfloat: case vk::Format::eR16G16Sfloat: case vk::Format::eR16G16B16A16Sfloat: case vk::Format::eR32Sfloat: case vk::Format::eR32G32Sfloat: case vk::Format::eR32G32B32A32Sfloat: case vk::Format::eB10G11R11UfloatPack32: break; default: return false; } const float rgb = (code & 0x80u) != 0 ? 1.0f : 0.0f; const float alpha = (code & 0x40u) != 0 ? 1.0f : 0.0f; std::array channels {rgb, rgb, rgb, alpha}; if (!metadata.dcc_alpha_msb) { std::swap(channels[0], channels[3]); } // DCC clear decoding clamps missing lanes before applying the format swizzle. const auto components = vk::componentCount(format); if (components == 1) { channels[0] = channels[3]; } else if (components == 2) { channels[1] = channels[3]; } switch (format) { case vk::Format::eB8G8R8A8Unorm: case vk::Format::eB8G8R8A8Srgb: case vk::Format::eA2R10G10B10UnormPack32: case vk::Format::eA1R5G5B5UnormPack16: case vk::Format::eR5G6B5UnormPack16: std::swap(channels[0], channels[2]); break; case vk::Format::eR4G4B4A4UnormPack16: std::reverse(channels.begin(), channels.end()); break; default: break; } clear.float32 = channels; return true; } [[nodiscard]] const char* BindingTypeName(TextureCache::BindingType type) { switch (type) { case TextureCache::BindingType::Texture: return "Texture"; case TextureCache::BindingType::Storage: return "StorageTexture"; case TextureCache::BindingType::RenderTarget: return "ColorTarget"; case TextureCache::BindingType::DepthTarget: return "DepthTarget"; case TextureCache::BindingType::VideoOut: return "VideoOut"; } return "Image"; } void NameImageBinding(GraphicContext& graphics, Image& image, vk::ImageView view, TextureCache::BindingType type, const ImageViewInfo& view_info) { const auto* role = BindingTypeName(type); SetVulkanObjectNameF( graphics.device, image.backing.image, "Kyty.{}.Image[guest=0x{:016x} size=0x{:x} extent={}x{}x{} format={} mips={} layers={} " "samples={}]", role, image.info.data.address, image.info.data.size, image.info.extent.width, image.info.extent.height, image.info.extent.depth, static_cast(image.info.pixel_format), image.info.resources.levels, image.info.resources.layers, image.info.samples); SetVulkanObjectNameF( graphics.device, view, "Kyty.{}.View[guest=0x{:016x} format={} aspect=0x{:x} mip={}+{} layer={}+{}]", role, image.info.data.address, static_cast(view_info.format), static_cast(view_info.aspect), view_info.base_level, view_info.level_count, view_info.base_layer, view_info.layer_count); } [[nodiscard]] std::vector BuildDepthCopies(const ImageInfo& info, uint64_t slice_stride) { std::vector copies(info.resources.layers); for (uint32_t layer = 0; layer < info.resources.layers; ++layer) { auto& copy = copies[layer]; copy.bufferOffset = slice_stride * layer; copy.bufferRowLength = info.pitch; copy.bufferImageHeight = info.extent.height; copy.imageSubresource = {vk::ImageAspectFlagBits::eDepth, 0, layer, 1}; copy.imageExtent = {info.extent.width, info.extent.height, 1}; } return copies; } [[nodiscard]] std::vector BuildDepthTiles(const ImageInfo& info) { TileBlockLayout block {}; EXIT_NOT_IMPLEMENTED( !TileGetBlockLayout(TileBlockFamily::Depth64KB, info.bytes_per_block, block)); const auto full_slice_size = info.data.size / info.resources.layers; std::vector tiles; tiles.reserve(info.resources.layers); for (uint32_t layer = 0; layer < info.resources.layers; ++layer) { const auto offset = full_slice_size * layer; tiles.push_back({block.family, block.bytes_per_element, offset, full_slice_size, offset, full_slice_size, 0, info.extent.width, info.extent.height, 1, info.pitch}); tiles.back().surface_z = layer; } return tiles; } } // namespace TextureCache::TextureCache(GraphicContext& graphics, CommandScheduler& scheduler, PageManager& page_manager, BufferCache& buffer_cache) : m_graphics(graphics), m_scheduler(scheduler), m_page_manager(page_manager), m_blit_helper(graphics, scheduler), m_tiler(graphics, scheduler, buffer_cache.GetUtilityBuffer(MemoryUsage::Stream)), m_buffer_cache(buffer_cache), m_readback_linear_images(Config::ReadbackLinearImagesEnabled()) { if (m_graphics.CanReportMemoryUsage()) { constexpr int64_t GiB = 1024ll * 1024 * 1024; const auto budget = static_cast(std::min(m_graphics.GetTotalMemoryBudget(), INT64_MAX)); const auto threshold = std::min(budget, 8 * GiB); m_pressure_gc_memory = static_cast( std::max(std::min(budget - 6 * threshold / 10, budget - GiB), GiB + GiB / 2)); m_critical_gc_memory = static_cast( std::max(std::min(budget - 2 * threshold / 10, budget - GiB / 2), 3 * GiB)); m_trigger_gc_memory = static_cast(std::max((budget - threshold) / 2, 0)); } } TextureCache::~TextureCache() { m_slot_images.ForEach([&](ImageId id, const Image& image) { if (image.registered) { UnregisterImage(id); } }); } bool TextureCache::SameBacking(const ImageInfo& cached, const ImageInfo& requested, bool exact_format) { if (cached.data.address != requested.data.address) { return false; } if (cached.data.size != requested.data.size) { return false; } if (cached.extent != requested.extent) { return false; } if (cached.samples != requested.samples) { return false; } if (cached.bytes_per_block != requested.bytes_per_block) { return false; } if (cached.tile_mode != requested.tile_mode) { return false; } if (!ImageViewOps::FormatsCompatible(cached.pixel_format, requested.pixel_format) || (cached.type != requested.type && requested.extent != vk::Extent3D {1, 1, 1})) { return false; } if (exact_format && cached.pixel_format != requested.pixel_format) { return false; } return true; } TextureCache::BindingType TextureCache::UploadBinding(const Image& image) { if (image.info.IsDepth()) { if (image.info.tile_mode == Prospero::TileMode::kDepth || image.info.tile_mode == Prospero::TileMode::kLinear) { return BindingType::DepthTarget; } return BindingType::Texture; } if (image.usage.render_target) { return BindingType::RenderTarget; } if (image.usage.video_out) { return BindingType::VideoOut; } return image.usage.storage ? BindingType::Storage : BindingType::Texture; } bool TextureCache::SafeToDownload(const Image& image) { if (!image.SafeToDownload()) { return false; } const auto range = image.info.data; return !m_buffer_cache.HasGpuDirtyBytes(range.address, range.size); } ImageId TextureCache::InsertImage(const ImageInfo& info) { const auto id = m_slot_images.insert(m_graphics, m_scheduler, info); if (!info.data.Empty()) { RegisterImage(id); } return id; } void TextureCache::RegisterImage(ImageId id) { auto& image = m_slot_images[id]; if (image.registered || image.info.data.Empty()) { EXIT("TextureCache: invalid image registration\n"); } ImagePageTable::PageRange pages {}; if (!ImagePageTable::TryGetPageRange(image.info.data.address, image.info.data.size, pages)) { EXIT("TextureCache: image registration is outside the guest address space\n"); } ForEachPage(image.info.data.address, image.info.data.size, [this, id](uint64_t page) { m_image_page_table[page].push_back(id); }); image.registered = true; image.lru_id = m_lru_cache.Insert(id, m_gc_tick); m_total_used_memory += image.AccountedSize(); } void TextureCache::UnregisterImage(ImageId id) { auto& image = m_slot_images[id]; if (!image.registered) { return; } UntrackImage(id); ImagePageTable::PageRange pages {}; if (!ImagePageTable::TryGetPageRange(image.info.data.address, image.info.data.size, pages)) { EXIT("TextureCache: registered image is outside the guest address space\n"); } ForEachPage(image.info.data.address, image.info.data.size, [this, id](uint64_t page) { auto* owners = m_image_page_table.Find(page); if (owners == nullptr || !owners->Erase(id)) { EXIT("TextureCache: image missing from page owner index\n"); } }); m_lru_cache.Free(image.lru_id); const auto accounted = image.AccountedSize(); if (accounted > m_total_used_memory) { EXIT("TextureCache: image accounting underflow\n"); } m_total_used_memory -= accounted; image.registered = false; } void TextureCache::DeleteImage(ImageId id) { auto* image = m_slot_images.try_get(id); if (image == nullptr || !image->registered) { return; } if (!image->depth_id) { std::vector associations; m_slot_images.ForEach([&](ImageId candidate, const Image& associated) { if (associated.depth_id == id) { associations.push_back(candidate); } }); for (const auto association: associations) { FreeImage(association); } } if (image->IsGpuModified()) { EXIT("TextureCache: deleting a GPU-modified image without resolving its contents\n"); } m_download_images.erase(id); if (image->info.HasMetadata()) { const auto metadata = m_surface_metas.find(image->info.metadata.range.address); if (metadata != m_surface_metas.end() && ((image->info.metadata.kind == ImageMetadataKind::Dcc && metadata->second.type == MetaDataInfo::Type::Dcc) || (image->info.metadata.kind == ImageMetadataKind::Htile && metadata->second.type == MetaDataInfo::Type::HTile))) { // A later binding may have reused this address for another metadata type. m_surface_metas.erase(metadata); } } UnregisterImage(id); if (m_scheduler.Active()) { m_scheduler.DeferOperation([this, id] { m_slot_images.erase(id); }); } else { m_slot_images.erase(id); } } void TextureCache::FreeImage(ImageId id) { auto& image = m_slot_images[id]; if (image.IsGpuModified()) { image.ClearGpuModified(); } DeleteImage(id); } void TextureCache::TouchImage(Image& image) { if (image.registered) { m_lru_cache.Touch(image.lru_id, m_gc_tick); } } void TextureCache::MarkAsMaybeDirty(ImageId id, Image& image) { image.MarkMaybeCpuDirty(); if (image.NeedsMaybeCpuHash()) { image.SetMaybeCpuHash(image.HashGuestEdges()); } UntrackImage(id); } void TextureCache::TrackImage(ImageId id) { auto& image = m_slot_images[id]; if (!image.registered) { return; } const auto image_begin = image.info.data.address; const auto image_end = image.info.data.End(); if (image_begin == image.track_addr && image_end == image.track_addr_end) { return; } if (!image.IsTracked()) { image.track_addr = image_begin; image.track_addr_end = image_end; m_page_manager.UpdatePageWatchers(image_begin, image.info.data.size); return; } if (image_begin < image.track_addr) { TrackImageHead(id); } if (image.track_addr_end < image_end) { TrackImageTail(id); } } void TextureCache::TrackImageHead(ImageId id) { auto& image = m_slot_images[id]; if (!image.registered) { return; } const auto image_begin = image.info.data.address; if (image_begin == image.track_addr) { return; } if (!image.IsTracked() || image_begin > image.track_addr) { EXIT("TextureCache: invalid image head tracking range\n"); } const auto size = image.track_addr - image_begin; image.track_addr = image_begin; m_page_manager.UpdatePageWatchers(image_begin, size); } void TextureCache::TrackImageTail(ImageId id) { auto& image = m_slot_images[id]; if (!image.registered) { return; } const auto image_end = image.info.data.End(); if (image_end == image.track_addr_end) { return; } if (!image.IsTracked() || image.track_addr_end > image_end) { EXIT("TextureCache: invalid image tail tracking range\n"); } const auto address = image.track_addr_end; const auto size = image_end - address; image.track_addr_end = image_end; m_page_manager.UpdatePageWatchers(address, size); } void TextureCache::UntrackImage(ImageId id) { auto& image = m_slot_images[id]; if (!image.IsTracked()) { return; } const auto address = image.track_addr; const auto size = image.track_addr_end - image.track_addr; image.track_addr = 0; image.track_addr_end = 0; if (size != 0) { m_page_manager.UpdatePageWatchers(address, size); } } void TextureCache::UntrackImageHead(ImageId id) { auto& image = m_slot_images[id]; const auto begin = image.info.data.address; if (!image.IsTracked() || begin < image.track_addr) { return; } const auto address = Common::AlignDown(begin + TRACKER_PAGE_SIZE, TRACKER_PAGE_SIZE); const auto size = address - begin; image.track_addr = address; if (image.track_addr == image.track_addr_end) { MarkAsMaybeDirty(id, image); } if (size != 0) { m_page_manager.UpdatePageWatchers(begin, size); } } void TextureCache::UntrackImageTail(ImageId id) { auto& image = m_slot_images[id]; const auto end = image.info.data.End(); if (!image.IsTracked() || image.track_addr_end < end) { return; } const auto address = Common::AlignDown(end, TRACKER_PAGE_SIZE); const auto size = end - address; image.track_addr_end = address; if (image.track_addr == image.track_addr_end) { MarkAsMaybeDirty(id, image); } if (size != 0) { m_page_manager.UpdatePageWatchers(address, size); } } void TextureCache::TrackImageDownload(ImageId id, Image& image) { if (m_readback_linear_images && !image.info.IsTiled() && !image.info.data.Empty()) { if (!image.IsGpuModified()) { EXIT("TextureCache: cannot enroll a non-GPU-owned image for download\n"); } m_download_images.insert(id); } } TextureCache::ImageIds TextureCache::FindImagesInRegion(uint64_t address, uint64_t size, bool page_overlap) const { ImagePageTable::PageRange pages {}; if (!ImagePageTable::TryGetPageRange(address, size, pages)) { return {}; } uint32_t query_epoch = ++m_image_query_epoch; if (query_epoch == 0) { m_slot_images.ForEach([](ImageId, const Image& image) { image.query_epoch = 0; }); query_epoch = ++m_image_query_epoch; } ImageIds result; ForEachPage(address, size, [&](uint64_t page) { const auto* owners = m_image_page_table.Find(page); if (owners == nullptr) { return; } owners->ForEach([&](ImageId id) { auto* image = m_slot_images.try_get(id); if (image == nullptr) { return; } if (image->query_epoch == query_epoch) { return; } image->query_epoch = query_epoch; if (image->Overlaps(address, size, page_overlap)) { result.push_back(id); } }); }); return result; } ImageId TextureCache::GetNullImage(const ImageDesc& desc) { const auto format = desc.info.pixel_format; if (const auto found = m_null_images.find(format); found != m_null_images.end()) { return found->second; } ImageInfo info {}; info.pixel_format = desc.info.pixel_format; info.guest_format = desc.info.guest_format; info.type = Prospero::ImageType::kColor2D; info.extent = {1, 1, 1}; info.resources = {1, 1}; info.pitch = 1; info.bytes_per_block = std::max(desc.info.bytes_per_block, 1u); info.samples = 1; info.tile_mode = Prospero::TileMode::kLinear; info.mip_layout[0] = {0, info.bytes_per_block, 1, 1}; const auto id = InsertImage(info); m_null_images.emplace(format, id); return id; } void TextureCache::ValidateImageDesc(const ImageDesc& desc) const { ImageOps::Validate(desc.info); if (desc.view_info.format == vk::Format::eUndefined || desc.view_info.level_count == 0 || desc.view_info.layer_count == 0 || desc.view_info.base_level >= desc.info.resources.levels || desc.view_info.level_count > desc.info.resources.levels - desc.view_info.base_level || (!desc.info.IsVolume() && (desc.view_info.base_layer >= desc.info.resources.layers || desc.view_info.layer_count > desc.info.resources.layers - desc.view_info.base_layer))) { EXIT("TextureCache: invalid image view description\n"); } if (desc.type == BindingType::DepthTarget && !IsSupportedDepthTargetFormat(desc.info)) { EXIT("TextureCache: unsupported depth image description\n"); } if (desc.type == BindingType::VideoOut && !IsSupportedVideoOutFormat(desc.info)) { EXIT("TextureCache: unsupported video-out image description\n"); } if (desc.type == BindingType::VideoOut && desc.info.metadata.compression == VideoOutCompression::Unsupported) { EXIT("TextureCache: unsupported compressed video-out description\n"); } } void TextureCache::PrepareImageCopy(Image& image) { if (image.IsCpuDirty()) { image.RefreshComplete(); } } void TextureCache::RefreshCopySource(ImageId id) { auto& image = m_slot_images[id]; RefreshImage(id); if (image.IsDefinitelyCpuDirty()) { EXIT("TextureCache: image copy source remained CPU-dirty after refresh\n"); } } bool TextureCache::CopyD16(Image& destination, Image& source) { const bool source_depth = source.info.IsDepth(); const bool destination_depth = destination.info.IsDepth(); if (source_depth == destination_depth) { return false; } auto& depth = source_depth ? source : destination; auto& color = source_depth ? destination : source; const auto transfer_bytes = DepthAspectTransferBytes(depth.backing.format); if (depth.info.bytes_per_block != sizeof(uint16_t) || color.info.bytes_per_block != sizeof(uint16_t) || transfer_bytes != sizeof(uint32_t)) { return false; } EXIT_IF(source.backing.samples != 1 || destination.backing.samples != 1 || source.info.resources.levels != 1 || destination.info.resources.levels != 1 || source.info.extent != destination.info.extent || source.info.resources.layers != destination.info.resources.layers); const auto layers = depth.info.resources.layers; const uint64_t depth_slice = static_cast(depth.info.pitch) * depth.info.extent.height * transfer_bytes; const uint64_t color_slice = static_cast(color.info.pitch) * color.info.extent.height * sizeof(uint16_t); EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || color_slice > UINT64_MAX / layers); const auto depth_size = depth_slice * layers; const auto color_size = color_slice * layers; std::vector depth_copies(layers); std::vector color_copies(layers); for (uint32_t layer = 0; layer < layers; layer++) { depth_copies[layer].bufferOffset = depth_slice * layer; depth_copies[layer].bufferRowLength = depth.info.pitch; depth_copies[layer].bufferImageHeight = depth.info.extent.height; depth_copies[layer].imageSubresource = {vk::ImageAspectFlagBits::eDepth, 0, layer, 1}; depth_copies[layer].imageExtent = depth.info.extent; color_copies[layer].bufferOffset = color_slice * layer; color_copies[layer].bufferRowLength = color.info.pitch; color_copies[layer].bufferImageHeight = color.info.extent.height; color_copies[layer].imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, layer, 1}; color_copies[layer].imageExtent = color.info.extent; } auto depth_buffer = m_tiler.GetScratchBuffer(depth_size); auto color_buffer = m_tiler.GetScratchBuffer(color_size); const TileManager::D16Layout promote_layout { .width = depth.info.extent.width, .height = depth.info.extent.height, .layers = layers, .source_row_stride = static_cast(color.info.pitch) * sizeof(uint16_t), .target_row_stride = static_cast(depth.info.pitch) * transfer_bytes, .source_slice_stride = color_slice, .target_slice_stride = depth_slice, }; const bool d32 = DepthAspectTransferFormat(depth.backing.format) == vk::Format::eD32Sfloat; if (source_depth) { source.Download(depth_copies, depth_buffer.buffer, depth_buffer.offset, depth_buffer.size); m_tiler.ConvertD16(depth_buffer, color_buffer, TileManager::D16Direction::Demote, d32, {.width = promote_layout.width, .height = promote_layout.height, .layers = promote_layout.layers, .source_row_stride = promote_layout.target_row_stride, .target_row_stride = promote_layout.source_row_stride, .source_slice_stride = promote_layout.target_slice_stride, .target_slice_stride = promote_layout.source_slice_stride}); destination.Upload(color_copies, color_buffer.buffer, color_buffer.offset, color_buffer.size); } else { source.Download(color_copies, color_buffer.buffer, color_buffer.offset, color_buffer.size); m_tiler.ConvertD16(color_buffer, depth_buffer, TileManager::D16Direction::Promote, d32, promote_layout); destination.Upload(depth_copies, depth_buffer.buffer, depth_buffer.offset, depth_buffer.size); } return true; } void TextureCache::CopyImage(ImageId destination_id, ImageId source_id) { RefreshCopySource(source_id); auto& destination = m_slot_images[destination_id]; auto& source = m_slot_images[source_id]; TrackImage(destination_id); if (source.backing.samples != destination.backing.samples) { EXIT("TextureCache: cannot issue an unequal-sample image copy\n"); } PrepareImageCopy(destination); if (source.IsBufferModified()) { if (source.info.data == destination.info.data) { destination.MarkBufferModified(); } return; } const bool source_depth = source.info.IsDepth(); const bool dest_depth = destination.info.IsDepth(); const bool direct_copy = source.backing.format == destination.backing.format || (!source_depth && !dest_depth && vk::blockSize(source.backing.format) == vk::blockSize(destination.backing.format)); if (direct_copy) { destination.CopyImage(source); } else if (!CopyD16(destination, source)) { if (source.backing.samples != 1 || destination.backing.samples != 1) { EXIT("TextureCache: cross-format multisample image copy is unsupported\n"); } auto& copy_buffer = m_buffer_cache.GetUtilityBuffer(MemoryUsage::DeviceLocal); destination.CopyImageWithBuffer(source, copy_buffer); } if (source.IsGpuModified()) { destination.MarkGpuModified(); } destination.ClearBufferModified(); } void TextureCache::CopyImageMip(ImageId destination_id, ImageId source_id, uint32_t mip, uint32_t layer) { RefreshCopySource(source_id); auto& destination = m_slot_images[destination_id]; auto& source = m_slot_images[source_id]; TrackImage(destination_id); if (source.IsBufferModified() || source.backing.samples != destination.backing.samples) { EXIT("TextureCache: invalid mip-copy ownership or sample count\n"); } destination.CopyMip(source, mip, layer); if (source.IsGpuModified()) { destination.MarkGpuModified(); } } ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingType binding, ImageId cached_id) { auto& cached = m_slot_images[cached_id]; if (!cached.info.IsDepth() && !requested.IsDepth()) { return {}; } const bool stencil_match = requested.HasStencil() == cached.info.HasStencil(); const bool bpp_match = requested.bytes_per_block == cached.info.bytes_per_block; // PPSA04264 const bool raw_d16_texture = binding == BindingType::Texture && cached.info.IsDepth() && cached.info.guest_format == Prospero::BufferFormat::k16UNorm && requested.guest_format == Prospero::BufferFormat::k16UInt && requested.pixel_format == vk::Format::eR16Uint && cached.backing.samples == 1 && requested.samples == 1 && requested.data == cached.info.data && requested.extent == cached.info.extent && requested.resources == cached.info.resources && requested.type == cached.info.type && requested.pitch == cached.info.pitch && requested.tile_mode == cached.info.tile_mode && !requested.HasStencil() && !cached.info.HasStencil() && !requested.HasMetadata() && !cached.info.HasMetadata(); // PPSA04264 const bool retain_cached_layout = requested.samples == 1 && cached.info.samples == 1 && cached.backing.samples == 1 && requested.bytes_per_block == cached.info.bytes_per_block && requested.data.address == cached.info.data.address && requested.data.size < cached.info.data.size && requested.extent == cached.info.extent && requested.resources.levels == 1 && cached.info.resources.levels == 1 && requested.resources.layers != 0 && cached.info.resources.layers != 0 && requested.resources.layers < cached.info.resources.layers && requested.type == cached.info.type && requested.pitch == cached.info.pitch && requested.tile_mode == cached.info.tile_mode && requested.mip_layout[0].offset == 0 && cached.info.mip_layout[0].offset == 0 && requested.mip_layout[0].size == requested.data.size && cached.info.mip_layout[0].size == cached.info.data.size && requested.data.size % requested.resources.layers == 0 && cached.info.data.size % cached.info.resources.layers == 0 && requested.data.size / requested.resources.layers == cached.info.data.size / cached.info.resources.layers && !requested.HasStencil() && !cached.info.HasStencil() && !requested.HasMetadata() && !cached.info.HasMetadata(); bool recreate = cached.info.resources < requested.resources; switch (binding) { case BindingType::Texture: recreate |= requested.IsDepth() && !cached.info.IsDepth(); recreate |= raw_d16_texture; break; case BindingType::Storage: recreate |= cached.info.IsDepth(); break; case BindingType::RenderTarget: recreate |= cached.info.IsDepth(); break; case BindingType::DepthTarget: recreate |= !cached.info.IsDepth(); recreate |= cached.info.IsDepth() && !(stencil_match && bpp_match); break; case BindingType::VideoOut: recreate |= cached.info.IsDepth(); break; } if (!recreate) { return cached_id; } RefreshImage(cached_id); auto info = requested; if (retain_cached_layout) { info.data = cached.info.data; info.resources = cached.info.resources; info.mip_layout = cached.info.mip_layout; } else { info.resources = std::max(requested.resources, cached.info.resources); } info.htile_clear_mask = 0; const auto replacement_id = InsertImage(info); auto& replacement = m_slot_images[replacement_id]; replacement.usage = cached.usage; if (cached.binding.is_bound || cached.binding.is_target) { cached.binding.needs_rebind = true; } if (cached.backing.samples == replacement.backing.samples) { const bool copy_supported = cached.backing.samples == 1 || cached.backing.format == replacement.backing.format || (!cached.info.IsDepth() && !replacement.info.IsDepth() && ImageViewOps::FormatsCompatible(cached.backing.format, replacement.backing.format)); if (copy_supported) { CopyImage(replacement_id, cached_id); } else { LOGF_COLOR(Log::Color::BrightYellow, "TextureCache: unsupported cross-format multisample depth copy\n"); } } else if (cached.backing.samples == 1 && replacement.backing.samples > 1 && replacement.info.IsDepth()) { RefreshCopySource(cached_id); if (cached.IsBufferModified() || cached.IsDefinitelyCpuDirty()) { EXIT("TextureCache: multisample depth conversion source is not native-current\n"); } PrepareImageCopy(replacement); m_blit_helper.ReinterpretColorAsMsDepth(cached, replacement); CommitGpuWrite(replacement); } else { LOGF_COLOR(Log::Color::BrightYellow, "TextureCache: unsupported unequal-sample depth overlap copy (%u -> %u)\n", cached.backing.samples, replacement.backing.samples); } FreeImage(cached_id); return replacement_id; } TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& requested, BindingType binding, ImageId cached_id, ImageId merged_id) { auto owner = m_slot_images.try_get(cached_id); if (owner == nullptr) { return {merged_id}; } auto& cached = *owner; const auto current_tick = m_scheduler.CurrentTick(); const bool safe_to_delete = current_tick - std::min(current_tick, cached.tick_accessed_last) > NumFramesBeforeRemoval; const uint32_t requested_block = requested.bytes_per_block * requested.samples; const uint32_t cached_block = cached.info.bytes_per_block * cached.info.samples; if (requested.data.address == cached.info.data.address && requested.BlockExtent() == cached.info.BlockExtent() && requested_block == cached_block) { if (const auto depth_id = ResolveDepthOverlap(requested, binding, cached_id)) { return {depth_id}; } if (requested.IsBlock() && !cached.info.IsBlock()) { return {ExpandImage(requested, cached_id)}; } if (requested.data.size == cached.info.data.size && (requested.IsVolume() || cached.info.IsVolume())) { return {ExpandImage(requested, cached_id)}; } // Equal pitch does not imply equal mip placement: a changed extent can move // a level into or out of the mip tail. These are separate guest layouts. if (requested.tile_mode != cached.info.tile_mode || (requested.resources == cached.info.resources && requested.mip_layout != cached.info.mip_layout)) { if (safe_to_delete) { FreeImage(cached_id); } return {merged_id}; } // PPSA08394 if (requested.data.size == cached.info.data.size && requested.resources == cached.info.resources && requested.type == cached.info.type && requested.extent.width > cached.info.extent.width && requested.extent.height >= cached.info.extent.height && requested.extent.depth >= cached.info.extent.depth && ImageViewOps::FormatsCompatible(cached.info.pixel_format, requested.pixel_format)) { return {ExpandImage(requested, cached_id)}; } if (requested.pixel_format != cached.info.pixel_format || requested.data.size <= cached.info.data.size) { const auto result_id = merged_id ? merged_id : cached_id; const auto result = m_slot_images.try_get(result_id); return {result != nullptr && ImageViewOps::FormatsCompatible(result->info.pixel_format, requested.pixel_format) ? result_id : ImageId {}}; } if (requested.type == cached.info.type && requested.resources > cached.info.resources) { return {ExpandImage(requested, cached_id)}; } EXIT("TextureCache: unresolvable equal-address image overlap, address=0x%016" PRIx64 " requested=%ux%u " "cached=%ux%u requested_size=0x%016" PRIx64 " cached_size=0x%016" PRIx64 " type=%u/%u tile=%u/%u\n", requested.data.address, requested.resources.levels, requested.resources.layers, cached.info.resources.levels, cached.info.resources.layers, requested.data.size, cached.info.data.size, static_cast(requested.type), static_cast(cached.info.type), static_cast(requested.tile_mode), static_cast(cached.info.tile_mode)); } const int32_t requested_mip = requested.MipOf(cached.info); if (requested_mip >= 0) { const int32_t layer = requested.SliceOf(cached.info, requested_mip); return {cached_id, requested_mip, layer}; } const int32_t mip = cached.info.MipOf(requested); if (mip >= 0) { const int32_t layer = cached.info.SliceOf(requested, mip); if (!merged_id) { return {ExpandImage(requested, cached_id)}; } cached.binding.needs_rebind |= cached.binding.is_bound || cached.binding.is_target; m_slot_images[merged_id].binding.is_target |= cached.binding.is_target; CopyImageMip(merged_id, cached_id, static_cast(mip), static_cast(layer)); FreeImage(cached_id); return {merged_id}; } if (requested.data.address >= cached.info.data.address && safe_to_delete) { FreeImage(cached_id); } return {merged_id}; } ImageId TextureCache::ExpandImage(const ImageInfo& info, ImageId source_id) { RefreshCopySource(source_id); const auto expanded_id = InsertImage(info); auto& expanded = m_slot_images[expanded_id]; auto& source = m_slot_images[source_id]; expanded.usage = source.usage; if (source.binding.is_bound || source.binding.is_target) { source.binding.needs_rebind = true; } InitializeImage(expanded_id); const int32_t mip = source.info.MipOf(info); const int32_t layer = source.info.SliceOf(info, mip); if (layer >= 0) { CopyImageMip(expanded_id, source_id, static_cast(mip), static_cast(layer)); } else { CopyImage(expanded_id, source_id); } FreeImage(source_id); return expanded_id; } struct TextureCache::TextureTransfer { TextureUploadLayout layout; std::vector regions; std::vector tiles; bool swap_bgra16 = false; bool valid = false; [[nodiscard]] uint64_t LinearSize() const { uint64_t size = 0; for (const auto& tile: tiles) { size = std::max(size, tile.linear_offset + tile.linear_size); } return size; } }; struct TextureCache::ImageDownload { TextureTransfer texture; bool depth_target = false; bool valid = false; }; TextureCache::TextureTransfer TextureCache::BuildTextureTransfer(const Image& image, BindingType binding, TransferDirection direction) const { const auto& info = image.info; const bool upload = direction == TransferDirection::Upload; const bool render_target = binding == BindingType::RenderTarget; const bool video_out = binding == BindingType::VideoOut; auto format = info.guest_format; uint32_t layers = info.TransferLayers(); bool volume = info.IsVolume(); bool allow_depth_tile = upload; const char* owner = "TextureCache readback"; TextureTransfer transfer; transfer.swap_bgra16 = info.bgra16 && (!upload || render_target || video_out); if (render_target) { format = ImageOps::RenderTargetTransferFormat(info.bytes_per_block); } if (video_out) { allow_depth_tile = false; } else if (render_target || binding == BindingType::Storage) { allow_depth_tile = true; } if (upload) { if ((render_target || video_out) && (info.resources.layers == 0 || info.data.size % info.resources.layers != 0 || info.samples != 1 || image.backing.samples != 1)) { EXIT("TextureCache: invalid color-attachment upload\n"); } owner = "TextureCache"; if (render_target) { owner = "RenderTarget"; } else if (binding == BindingType::Storage) { owner = "StorageTextureCache"; } else if (video_out) { if (info.metadata.compression != VideoOutCompression::Uncompressed) { EXIT("TextureCache: invalid color-attachment upload\n"); } layers = info.resources.layers; volume = false; owner = "VideoOut"; } } transfer.layout = TextureCalcUploadLayout(format, info.extent.width, info.extent.height, info.resources.levels, layers, info.tile_mode, info.data.size, allow_depth_tile, volume, owner); transfer.regions = TextureBuildImageCopies(transfer.layout); if (info.IsDepth()) { for (auto& region: transfer.regions) { region.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eDepth; } } if (transfer.layout.surface.description.tile_mode != Prospero::TileMode::kLinear) { if (!TextureBuildGpuTileInfos(info.data.size, transfer.regions, transfer.layout, info.resources.levels, transfer.tiles)) { return transfer; } } transfer.valid = true; return transfer; } TextureCache::ImageDownload TextureCache::BuildDownload(const Image& image) const { const auto& info = image.info; const auto binding = UploadBinding(image); ImageDownload transfer {.depth_target = binding == BindingType::DepthTarget}; if (info.samples != 1 || image.backing.samples != 1) { return transfer; } if (transfer.depth_target) { transfer.valid = IsSupportedDepthPlaneReadback(info) && info.resources.layers != 0 && info.data.size % info.resources.layers == 0 && Prospero::NumBytesPerElement(info.guest_format) == info.bytes_per_block; return transfer; } if (info.metadata.compression != VideoOutCompression::Uncompressed) { return transfer; } transfer.texture = BuildTextureTransfer(image, binding, TransferDirection::Download); transfer.valid = transfer.texture.valid; return transfer; } void TextureCache::UploadImage(Image& image, Buffer& source, uint64_t source_offset) { const auto& info = image.info; const auto binding = UploadBinding(image); const auto upload = [&](std::vector& copies, TileManager::Result linear) { for (auto& copy: copies) { copy.bufferOffset += linear.offset; } image.Upload(copies, linear.buffer, linear.offset, linear.size); }; if (binding != BindingType::DepthTarget) { auto transfer = BuildTextureTransfer(image, binding, TransferDirection::Upload); if (!transfer.valid) { EXIT("TextureCache: invalid texture upload: binding=%u addr=0x%016" PRIx64 " size=0x%016" PRIx64 " format=%u tile=%u family=%u extent=%ux%ux%u " "pitch=%u levels=%u layers=%u samples=%u\n", static_cast(binding), info.data.address, info.data.size, static_cast(info.guest_format), static_cast(info.tile_mode), static_cast(transfer.layout.surface.texture.block.family), info.extent.width, info.extent.height, info.extent.depth, info.pitch, info.resources.levels, info.resources.layers, info.samples); } TileManager::Result linear {source.Handle(), source_offset, info.data.size}; if (!transfer.tiles.empty()) { linear = m_tiler.Detile(source.Handle(), source_offset, info.data.size, transfer.LinearSize(), transfer.tiles); } if (transfer.swap_bgra16) { linear = m_tiler.SwapBgra16(linear); } upload(transfer.regions, linear); return; } if (info.samples != 1 || image.backing.samples != 1 || info.resources.layers == 0 || info.data.size % info.resources.layers != 0 || Prospero::NumBytesPerElement(info.guest_format) != info.bytes_per_block) { EXIT("TextureCache: invalid depth upload\n"); } const auto layers = info.resources.layers; const auto full_slice_size = info.data.size / layers; auto copies = BuildDepthCopies(info, full_slice_size); TileManager::Result linear {source.Handle(), source_offset, source.Size() - source_offset}; if (info.IsTiled()) { const auto tiles = BuildDepthTiles(info); linear = m_tiler.Detile(source.Handle(), source_offset, info.data.size, info.data.size, tiles); } const auto transfer_bytes = DepthAspectTransferBytes(info.pixel_format); if (transfer_bytes != info.bytes_per_block) { const uint64_t texels_per_slice = static_cast(info.pitch) * info.extent.height; EXIT_NOT_IMPLEMENTED(info.bytes_per_block != sizeof(uint16_t) || transfer_bytes != sizeof(uint32_t) || texels_per_slice > UINT32_MAX || texels_per_slice > UINT64_MAX / transfer_bytes); const uint64_t transfer_slice = texels_per_slice * transfer_bytes; EXIT_NOT_IMPLEMENTED(transfer_slice > UINT64_MAX / layers); auto promoted = m_tiler.GetScratchBuffer(transfer_slice * layers); m_tiler.ConvertD16( linear, promoted, TileManager::D16Direction::Promote, info.pixel_format == vk::Format::eD32SfloatS8Uint, {.width = info.extent.width, .height = info.extent.height, .layers = layers, .source_row_stride = static_cast(info.pitch) * sizeof(uint16_t), .target_row_stride = static_cast(info.pitch) * sizeof(uint32_t), .source_slice_stride = full_slice_size, .target_slice_stride = transfer_slice}); linear = promoted; for (uint32_t layer = 0; layer < layers; layer++) { copies[layer].bufferOffset = transfer_slice * layer; } } upload(copies, linear); } void TextureCache::InitializeImage(ImageId id) { auto& image = m_slot_images[id]; if (image.info.data.Empty()) { return; } TrackImage(id); if (image.info.metadata.compression != VideoOutCompression::Uncompressed) { if (image.IsCpuDirty()) { image.RefreshComplete(); } return; } if (image.info.samples > 1) { return; } const bool upload = image.IsBufferModified() || image.IsCpuDirty(); if (upload) { const auto [source, source_offset] = m_buffer_cache.ObtainBufferForImage(image.info.data.address, image.info.data.size); if (source == nullptr) { EXIT("TextureCache: failed to obtain image upload source\n"); } UploadImage(image, *source, source_offset); image.ClearBufferModified(); } if (image.IsCpuDirty()) { image.RefreshComplete(); } } void TextureCache::PrepareDccClear(ImageId id, const ImageDesc& desc) { if (desc.info.metadata.kind != ImageMetadataKind::Dcc) { return; } auto& image = m_slot_images[id]; image.info.metadata = desc.info.metadata; auto [entry, inserted] = m_surface_metas.try_emplace( desc.info.metadata.range.address, MetaDataInfo {.type = MetaDataInfo::Type::Dcc}); auto& metadata = entry->second; if (!inserted && metadata.type == MetaDataInfo::Type::PendingDcc) { metadata.type = MetaDataInfo::Type::Dcc; } else if (metadata.type != MetaDataInfo::Type::Dcc) { EXIT("TextureCache: image reuses non-DCC metadata\n"); } if (metadata.clear_mask == 0 || image.info.resources.levels != 1 || desc.info.metadata.range.size == 0 || metadata.fill_size < desc.info.metadata.range.size) { return; } vk::ClearValue clear {}; if (!DecodeDccClear(desc, image.backing.format, metadata.fill_value, clear.color)) { return; } const auto& view = desc.view_info; const bool volume_texture = image.info.IsVolume() && view.type == vk::ImageViewType::e3D; const auto first = volume_texture ? 0u : view.base_layer; const auto count = volume_texture ? std::max(image.info.extent.depth >> view.base_level, 1u) : view.layer_count; if (first >= 32 || count > 32 - first) { return; } // The metadata fill covers the complete allocation. Consume each layer only after its // native image contents exist; already materialized layers may have been rendered since. for (uint32_t layer = first; layer < first + count;) { if ((metadata.clear_mask & (1u << layer)) == 0) { layer++; continue; } const auto start = layer; uint32_t mask = 0; do { mask |= 1u << layer++; } while (layer < first + count && (metadata.clear_mask & (1u << layer)) != 0); ClearImage(m_scheduler.Current(), id, {vk::ImageAspectFlagBits::eColor, view.base_level, view.level_count, start, layer - start}, clear); metadata.clear_mask &= ~mask; } } void TextureCache::RefreshImage(ImageId id) { TrackImage(id); auto& image = m_slot_images[id]; if (image.IsMaybeCpuDirty()) { const auto hash = image.HashGuestEdges(); if (image.NeedsMaybeCpuHash()) { image.SetMaybeCpuHash(hash); return; } (void)image.ResolveMaybeCpuHash(hash); } bool cpu_dirty = image.IsBufferModified() || image.IsDefinitelyCpuDirty(); if (image.info.metadata.compression != VideoOutCompression::Uncompressed) { if (cpu_dirty) { EXIT("TextureCache: compressed guest image refresh is unsupported\n"); } return; } if (!cpu_dirty) { return; } InitializeImage(id); } void TextureCache::AssociateStencil(ImageId depth_id, GuestRange stencil) { if (!stencil.Valid()) { EXIT("TextureCache: invalid stencil association range\n"); } auto& depth = m_slot_images[depth_id]; if (!depth.info.IsDepth() || !depth.info.HasStencil()) { EXIT("TextureCache: stencil association requires a depth/stencil image\n"); } ImageId association {}; for (const auto id: FindImagesInRegion(stencil.address, stencil.size, false)) { const auto owner = m_slot_images.try_get(id); if (owner != nullptr && owner->info.data.address == stencil.address) { association = id; } } if (!association) { ImageInfo info {}; info.data = stencil; info.extent = depth.info.extent; association = InsertImage(info); } auto& record = m_slot_images[association]; TouchImage(record); record.depth_id = depth_id; } ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) { auto& command = m_scheduler.Current(); if (command.IsInvalid()) { EXIT("TextureCache: image lookup requires a valid command buffer\n"); } ValidateImageDesc(desc); if (desc.info.data.Empty()) { std::scoped_lock lock {m_lock}; return GetNullImage(desc); } ImageId result {}; { std::scoped_lock lock {m_lock}; const auto candidates = FindImagesInRegion(desc.info.data.address, desc.info.data.size, false); for (const auto id: candidates) { const auto& image = m_slot_images[id]; if (SameBacking(image.info, desc.info, exact_format)) { result = id; } } int32_t view_mip = -1; int32_t view_layer = -1; if (!result) { for (const auto candidate: candidates) { view_mip = -1; view_layer = -1; const auto& merged_info = result ? m_slot_images[result].info : desc.info; const auto overlap = ResolveOverlap(merged_info, desc.type, candidate, result); if (overlap.image) { result = overlap.image; view_mip = overlap.mip; view_layer = overlap.layer; } } } if (result) { auto& resolved = m_slot_images[result]; if (exact_format && resolved.info.pixel_format != desc.info.pixel_format) { result = {}; } else if (resolved.info.resources < desc.info.resources) { FreeImage(result); result = {}; } } if (!result) { result = InsertImage(desc.info); auto& inserted = m_slot_images[result]; if (m_buffer_cache.HasGpuDirtyBytes(inserted.info.data.address, inserted.info.data.size)) { inserted.MarkBufferModified(); } } auto& image = m_slot_images[result]; if (desc.type == BindingType::VideoOut && desc.info.metadata.compression != VideoOutCompression::Uncompressed) { const bool guest_dirty = image.IsBufferModified() || image.IsCpuDirty(); const bool native_current = (image.usage.render_target || image.IsGpuModified()) && !guest_dirty; if (!native_current) { EXIT("TextureCache: compressed video-out read requires clean native GPU " "contents\n"); } } if (view_mip >= 0) { desc.view_info.base_level = static_cast(view_mip); } if (view_layer >= 0) { desc.view_info.base_layer = static_cast(view_layer); } image.tick_accessed_last = m_scheduler.CurrentTick(); TouchImage(image); } return result; } void TextureCache::UpdateImage(ImageId id) { std::scoped_lock lock {m_lock}; auto& image = m_slot_images[id]; TouchImage(image); RefreshImage(id); } ImageId TextureCache::FindImageFromRange(uint64_t address, uint64_t size, bool ensure_valid) { if (!GuestRange {address, size}.Valid()) { return {}; } std::scoped_lock lock {m_lock}; ImageIds matches; for (const auto id: FindImagesInRegion(address, size, false)) { auto owner = m_slot_images.try_get(id); if (owner == nullptr || owner->info.data.address != address) { continue; } if (ensure_valid && owner->depth_id) { owner = m_slot_images.try_get(owner->depth_id); } if (owner == nullptr || (ensure_valid && !SafeToDownload(*owner))) { continue; } matches.push_back(id); } ImageId selected {}; if (matches.size() == 1) { selected = matches.front(); } else { for (const auto id: matches) { const auto& image = m_slot_images[id]; if (image.info.data.size == size) { selected = id; break; } } } if (selected && ensure_valid) { const auto owner = m_slot_images.try_get(selected); if (owner != nullptr && owner->depth_id) { selected = owner->depth_id; } } return selected; } vk::ImageView TextureCache::FindTexture(ImageId id, const ImageDesc& desc) { std::scoped_lock lock {m_lock}; auto& image = m_slot_images[id]; TouchImage(image); if (!image.info.data.Empty()) { if (!image.registered || image.depth_id || image.binding.needs_rebind) { EXIT("TextureCache: texture requires rediscovery before final acquisition\n"); } } if (desc.type == BindingType::Storage) { image.MarkGpuModified(); } if (!image.info.data.Empty()) { PrepareDccClear(id, desc); RefreshImage(id); } switch (desc.type) { case BindingType::Texture: break; case BindingType::Storage: if (!image.info.data.Empty()) { CommitGpuWrite(image); } TrackImageDownload(id, image); break; default: EXIT("TextureCache: invalid texture binding\n"); } const auto view = image.FindView(desc.view_info); NameImageBinding(m_graphics, image, view, desc.type, desc.view_info); return view; } vk::ImageView TextureCache::FindRenderTarget(ImageId id, const ImageDesc& desc) { if (desc.type != BindingType::RenderTarget) { EXIT("TextureCache: invalid color-target binding\n"); } std::scoped_lock lock {m_lock}; auto& image = m_slot_images[id]; if (!image.registered || image.depth_id || image.binding.needs_rebind) { EXIT("TextureCache: color target requires rediscovery before final acquisition\n"); } TouchImage(image); image.MarkGpuModified(); image.usage.render_target = true; PrepareDccClear(id, desc); RefreshImage(id); CommitGpuWrite(image); TrackImageDownload(id, image); const auto view = image.FindView(desc.view_info); NameImageBinding(m_graphics, image, view, desc.type, desc.view_info); return view; } vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) { if (desc.type != BindingType::DepthTarget) { EXIT("TextureCache: invalid depth-target binding\n"); } std::scoped_lock lock {m_lock}; auto& image = m_slot_images[id]; if (!image.registered || image.depth_id || image.binding.needs_rebind) { EXIT("TextureCache: depth target requires rediscovery before final acquisition\n"); } TouchImage(image); image.MarkGpuModified(); image.usage.depth_target = true; RefreshImage(id); if (desc.info.HasMetadata()) { image.info.metadata = desc.info.metadata; auto [metadata, inserted] = m_surface_metas.try_emplace(desc.info.metadata.range.address, MetaDataInfo {.type = MetaDataInfo::Type::HTile, .clear_mask = image.info.htile_clear_mask}); if (!inserted && metadata->second.type != MetaDataInfo::Type::HTile) { // PS5 allocations can reuse DCC storage as HTile while the old color image is cached. // The depth binding defines the new type; incompatible fill state cannot carry over. metadata->second = {.type = MetaDataInfo::Type::HTile, .clear_mask = image.info.htile_clear_mask}; } } CommitGpuWrite(image); if (desc.info.HasStencil()) { AssociateStencil(id, desc.info.stencil); } const auto view = image.FindView(desc.view_info); NameImageBinding(m_graphics, image, view, desc.type, desc.view_info); return view; } void TextureCache::MarkGpuWritten(ImageId id) { std::scoped_lock lock {m_lock}; auto& image = m_slot_images[id]; if (!image.registered || image.depth_id) { EXIT("TextureCache: cannot mark an unavailable image GPU-written\n"); } TrackImage(id); CommitGpuWrite(image); } void TextureCache::CommitGpuWrite(Image& image) { if (image.depth_id || image.backing.image == nullptr) { EXIT("TextureCache: stencil association cannot own image contents\n"); } image.ClearBufferModified(); if (image.IsCpuDirty()) { image.RefreshComplete(); } image.MarkGpuModified(); } bool TextureCache::ClearImageFromBuffer(CommandBuffer& command, uint64_t address, uint64_t size, uint32_t packed_clear) { if (command.IsInvalid() || !GuestRange {address, size}.Valid()) { EXIT("TextureCache: invalid image clear\n"); } std::scoped_lock lock {m_lock}; ImageId selected {}; vk::ImageAspectFlags aspect {}; for (const auto id: FindImagesInRegion(address, size, false)) { auto owner = m_slot_images.try_get(id); if (owner == nullptr) { continue; } vk::ImageAspectFlags candidate {}; ImageId candidate_id = id; if (owner->depth_id && owner->info.data.address == address && owner->info.data.size == size) { candidate = vk::ImageAspectFlagBits::eStencil; candidate_id = owner->depth_id; owner = m_slot_images.try_get(candidate_id); if (owner == nullptr || owner->backing.image == nullptr || !owner->info.HasStencil()) { continue; } } else if (!owner->depth_id && owner->info.data.address == address && owner->info.data.size == size) { candidate = owner->info.IsDepth() ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor; } if (!candidate) { continue; } if (selected && selected != candidate_id) { return false; } selected = candidate_id; aspect = candidate; } if (!selected) { return false; } auto& image = m_slot_images[selected]; vk::ClearValue clear {}; if (aspect == vk::ImageAspectFlagBits::eColor) { if (!DecodePackedColorClear(image.info.pixel_format, packed_clear, clear.color)) { return false; } } else { uint8_t stencil_clear = 0; if ((aspect == vk::ImageAspectFlagBits::eDepth && !DecodePackedDepthClear(image.info.pixel_format, packed_clear, clear.depthStencil.depth)) || (aspect == vk::ImageAspectFlagBits::eStencil && !DecodePackedStencilClear(packed_clear, stencil_clear))) { return false; } clear.depthStencil.stencil = stencil_clear; } ClearImage(command, selected, {aspect, 0, image.info.resources.levels, 0, image.info.TransferLayers()}, clear); return true; } void TextureCache::ClearImage(CommandBuffer& command, ImageId id, const vk::ImageSubresourceRange& range, const vk::ClearValue& clear) { auto& image = m_slot_images[id]; const auto aspects = image.info.IsDepth() ? ImageViewOps::DepthAspectMask(image.backing.format) : vk::ImageAspectFlagBits::eColor; EXIT_IF(range.baseMipLevel >= image.info.resources.levels); const auto layers = image.info.IsVolume() ? std::max(image.info.extent.depth >> range.baseMipLevel, 1u) : image.backing.layers; EXIT_IF(command.IsInvalid() || image.depth_id || !range.aspectMask || range.levelCount == 0 || range.levelCount > image.info.resources.levels - range.baseMipLevel || range.layerCount == 0 || range.baseArrayLayer >= layers || range.layerCount > layers - range.baseArrayLayer || (range.aspectMask & aspects) != range.aspectMask); const bool full_image = range.aspectMask == aspects && range.baseMipLevel == 0 && range.levelCount == image.info.resources.levels && range.baseArrayLayer == 0 && range.layerCount == layers; TrackImage(id); if (!full_image && (image.IsBufferModified() || image.IsCpuDirty())) { InitializeImage(id); if (image.info.samples == 1 && (image.IsBufferModified() || image.IsCpuDirty())) { EXIT("TextureCache: image clear retained guest ownership\n"); } } command.EndRendering(); if (image.info.IsVolume() && !full_image) { EXIT_NOT_IMPLEMENTED(range.aspectMask != vk::ImageAspectFlagBits::eColor || range.levelCount != 1); ImageViewInfo view {}; view.format = image.backing.format; view.type = range.layerCount == 1 ? vk::ImageViewType::e2D : vk::ImageViewType::e2DArray; view.base_level = range.baseMipLevel; view.base_layer = range.baseArrayLayer; view.layer_count = range.layerCount; view.usage = vk::ImageUsageFlagBits::eColorAttachment; image.Transit(vk::ImageLayout::eColorAttachmentOptimal, vk::AccessFlagBits2::eColorAttachmentWrite, {}, command.Handle()); vk::RenderingAttachmentInfo attachment {}; attachment.imageView = image.FindView(view); attachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; attachment.loadOp = vk::AttachmentLoadOp::eClear; attachment.storeOp = vk::AttachmentStoreOp::eStore; attachment.clearValue = clear; vk::RenderingInfo rendering {}; rendering.renderArea.extent = { std::max(image.info.extent.width >> range.baseMipLevel, 1u), std::max(image.info.extent.height >> range.baseMipLevel, 1u)}; rendering.layerCount = range.layerCount; rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &attachment; command.Handle().beginRendering(&rendering); command.Handle().endRendering(); CommitGpuWrite(image); return; } image.Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {}, command.Handle()); auto native_range = range; if (image.info.IsVolume()) { native_range.baseArrayLayer = 0; native_range.layerCount = 1; } if (range.aspectMask == vk::ImageAspectFlagBits::eColor) { command.Handle().clearColorImage(image.backing.image, vk::ImageLayout::eTransferDstOptimal, &clear.color, 1, &native_range); } else { command.Handle().clearDepthStencilImage(image.backing.image, vk::ImageLayout::eTransferDstOptimal, &clear.depthStencil, 1, &native_range); } CommitGpuWrite(image); } void TextureCache::InvalidateMemory(uint64_t address, uint64_t size) { if (!GuestRange {address, size}.Valid()) { EXIT("TextureCache: invalid memory-invalidation range\n"); } std::scoped_lock lock {m_lock}; InvalidateCpuAliases(address, size); } void TextureCache::DownloadDepth(Image& image, Buffer& destination, uint64_t destination_offset) { const auto& info = image.info; const auto layers = info.resources.layers; const auto full_slice_size = info.data.size / layers; const auto transfer_bytes = DepthAspectTransferBytes(info.pixel_format); const uint64_t texels_per_slice = static_cast(info.pitch) * info.extent.height; EXIT_NOT_IMPLEMENTED(transfer_bytes == 0 || texels_per_slice > UINT32_MAX || texels_per_slice > UINT64_MAX / transfer_bytes || texels_per_slice > UINT64_MAX / info.bytes_per_block); const uint64_t transfer_slice = texels_per_slice * transfer_bytes; const uint64_t guest_slice = texels_per_slice * info.bytes_per_block; EXIT_NOT_IMPLEMENTED(transfer_slice > UINT64_MAX / layers); const uint64_t transfer_size = transfer_slice * layers; EXIT_NOT_IMPLEMENTED(guest_slice > full_slice_size); auto copies = BuildDepthCopies(info, full_slice_size); if (transfer_bytes == info.bytes_per_block) { if (!info.IsTiled()) { for (auto& copy: copies) { copy.bufferOffset += destination_offset; } image.Download(copies, destination.Handle(), destination_offset, info.data.size); return; } const auto tiles = BuildDepthTiles(info); m_tiler.TileImage(image, copies, destination.Handle(), destination_offset, info.data.size, info.data.size, tiles); return; } EXIT_NOT_IMPLEMENTED(info.bytes_per_block != sizeof(uint16_t) || transfer_bytes != sizeof(uint32_t)); for (uint32_t layer = 0; layer < layers; layer++) { copies[layer].bufferOffset = transfer_slice * layer; } auto host_linear = m_tiler.GetScratchBuffer(transfer_size); image.Download(copies, host_linear.buffer, 0, host_linear.size); const bool tiled = info.IsTiled(); auto guest_linear = tiled ? m_tiler.GetScratchBuffer(info.data.size) : TileManager::Result {destination.Handle(), destination_offset, destination.Size() - destination_offset}; m_tiler.ConvertD16(host_linear, guest_linear, TileManager::D16Direction::Demote, DepthAspectTransferFormat(info.pixel_format) == vk::Format::eD32Sfloat, {.width = info.extent.width, .height = info.extent.height, .layers = layers, .source_row_stride = static_cast(info.pitch) * sizeof(uint32_t), .target_row_stride = static_cast(info.pitch) * sizeof(uint16_t), .source_slice_stride = transfer_slice, .target_slice_stride = full_slice_size}); if (!tiled) { return; } const auto tiles = BuildDepthTiles(info); m_tiler.Tile(guest_linear.buffer, guest_linear.offset, info.data.size, destination.Handle(), destination_offset, info.data.size, tiles); } void TextureCache::DownloadImage(Image& image, Buffer& destination, uint64_t destination_offset, uint64_t destination_size, ImageDownload transfer) { if (!transfer.valid) { EXIT("TextureCache: invalid image download transfer\n"); } if (transfer.depth_target) { if (destination_size != image.info.data.size) { EXIT("TextureCache: partial depth image download is unsupported\n"); } DownloadDepth(image, destination, destination_offset); return; } auto& texture = transfer.texture; const auto transform = texture.swap_bgra16 ? TileManager::ColorTransform::SwapBgra16 : TileManager::ColorTransform::None; if (texture.tiles.empty()) { if (transform == TileManager::ColorTransform::SwapBgra16) { auto linear = m_tiler.GetScratchBuffer(destination_size); image.Download(texture.regions, linear.buffer, 0, linear.size); m_tiler.SwapBgra16(linear, {destination.Handle(), destination_offset, destination_size}); return; } for (auto& copy: texture.regions) { copy.bufferOffset += destination_offset; } image.Download(texture.regions, destination.Handle(), destination_offset, destination_size); return; } m_tiler.TileImage(image, texture.regions, destination.Handle(), destination_offset, destination_size, texture.LinearSize(), texture.tiles, transform); } bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uint64_t size) { const auto selected = m_texture_cache.FindImageFromRange(vaddr, size); if (!selected) { return false; } std::scoped_lock lock {m_texture_cache.m_lock}; auto& image = m_texture_cache.m_slot_images[selected]; // The GPU thread owns image retirement; CPU invalidation can dirty this image after lookup. if (!m_texture_cache.SafeToDownload(image)) { return false; } if (!buffer.IsInBounds(image.info.data.address, 1)) { return false; } const auto buf_offset = buffer.Offset(image.info.data.address); const auto available = buffer.Size() - buf_offset; uint32_t levels = 0; uint64_t copy_size = 0; if (image.info.IsVolume()) { // Volume mips contain strided block slices, so a mip's linear span cannot prove that // every retained slice fits. Keep volume synchronization whole-image only. if (!buffer.IsInBounds(image.info.data.address, image.info.data.size)) { return false; } levels = image.info.resources.levels; copy_size = image.info.data.size; } else { for (; levels < image.info.resources.levels; ++levels) { const auto& mip = image.info.mip_layout[levels]; if (mip.size == 0 || mip.offset > available || mip.size > available - mip.offset) { break; } copy_size = std::max(copy_size, mip.offset + mip.size); } } if (copy_size == 0) { return false; } auto transfer = m_texture_cache.BuildDownload(image); if (!transfer.valid) { return false; } if (transfer.depth_target && copy_size != image.info.data.size) { return false; } if (!transfer.depth_target && levels < image.info.resources.levels) { auto& texture = transfer.texture; std::erase_if(texture.regions, [levels](const vk::BufferImageCopy& region) { return region.imageSubresource.mipLevel >= levels; }); if (texture.regions.empty()) { return false; } if (!texture.tiles.empty()) { texture.tiles.clear(); if (!TextureBuildGpuTileInfos(copy_size, texture.regions, texture.layout, levels, texture.tiles)) { return false; } } } m_texture_cache.DownloadImage(image, buffer, buf_offset, copy_size, std::move(transfer)); return true; } bool TextureCache::DownloadImageMemory(ImageId id) { auto& image = m_slot_images[id]; if (image.depth_id) { return false; } auto transfer = BuildDownload(image); if (!transfer.valid || !SafeToDownload(image)) { return false; } const auto range = image.info.data; auto& download = m_buffer_cache.GetUtilityBuffer(MemoryUsage::Download); auto [mapped, offset] = download.Map(range.size, std::max(image.info.bytes_per_block, 4)); if (mapped == nullptr) { EXIT("TextureCache: failed to map reusable download buffer\n"); } download.Commit(); if (!LibKernel::Memory::TryReadBacking(range.address, mapped, range.size)) { return false; } download.Flush(offset, range.size); DownloadImage(image, download, offset, range.size, std::move(transfer)); vk::BufferMemoryBarrier barrier {}; barrier.srcAccessMask = vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eTransferWrite | vk::AccessFlagBits::eShaderWrite; barrier.dstAccessMask = vk::AccessFlagBits::eHostRead; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.buffer = download.Handle(); barrier.offset = offset; barrier.size = range.size; m_scheduler.EndRendering(); m_scheduler.Current().Handle().pipelineBarrier(vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &barrier, 0, nullptr); m_scheduler.DeferPriorityOperation([&download, range, mapped, offset] { download.Invalidate(offset, range.size); LibKernel::Memory::WriteBacking(range.address, mapped, range.size); }); return true; } void TextureCache::InvalidateMemoryFromGPU(uint64_t address, uint64_t size) { if (!GuestRange {address, size}.Valid()) { return; } std::scoped_lock lock {m_lock}; for (const auto id: FindImagesInRegion(address, size, true)) { auto& image = m_slot_images[id]; if (image.depth_id || !image.Overlaps(address, size)) { continue; } if (image.IsGpuModified()) { image.ClearGpuModified(); } image.MarkBufferModified(); } } bool TextureCache::IsRegionGpuModified(uint64_t address, uint64_t size) { if (!GuestRange {address, size}.Valid()) { return false; } std::scoped_lock lock {m_lock}; for (const auto id: FindImagesInRegion(address, size, false)) { const auto& image = m_slot_images[id]; if (!image.depth_id && image.IsGpuModified()) { return true; } } return false; } void TextureCache::InvalidateCpuAliases(uint64_t address, uint64_t size) { const auto page_begin = Common::AlignDown(address, TRACKER_PAGE_SIZE); const auto page_end = Common::AlignUp(address + size, TRACKER_PAGE_SIZE); for (const auto id: FindImagesInRegion(address, size, true)) { auto owner = m_slot_images.try_get(id); if (owner == nullptr) { continue; } if (owner->Overlaps(address, size)) { owner->InvalidateCpuWrite(address, size); UntrackImage(id); continue; } const auto image_begin = owner->info.data.address; const auto image_end = owner->info.data.End(); if (page_end < image_end) { UntrackImageHead(id); } else if (image_begin < page_begin) { UntrackImageTail(id); } else { MarkAsMaybeDirty(id, *owner); } } } bool TextureCache::IsMeta(uint64_t address) { std::scoped_lock lock {m_lock}; const auto found = m_surface_metas.find(address); return found != m_surface_metas.end() && found->second.type != MetaDataInfo::Type::PendingDcc; } bool TextureCache::IsMetaCleared(uint64_t address, uint32_t slice, uint32_t* fill_value) { std::scoped_lock lock {m_lock}; const auto found = m_surface_metas.find(address); if (found == m_surface_metas.end() || found->second.type == MetaDataInfo::Type::PendingDcc || slice >= 32) { return false; } if (fill_value != nullptr) { *fill_value = found->second.fill_value; } return (found->second.clear_mask & (1u << slice)) != 0; } bool TextureCache::ClearMeta(uint64_t address) { std::scoped_lock lock {m_lock}; const auto found = m_surface_metas.find(address); if (found == m_surface_metas.end() || found->second.type == MetaDataInfo::Type::PendingDcc || found->second.type == MetaDataInfo::Type::Dcc) { // Preserve the broad metadata-clear operation for CMask/FMask/HTile. DCC requires a // validated fill value, so an arbitrary compute write must not clear it. return false; } found->second.clear_mask = UINT32_MAX; return true; } void TextureCache::TrackDccFill(uint64_t address, uint64_t size, uint32_t fill_value) { if (!GuestRange {address, size}.Valid()) { EXIT("TextureCache: invalid DCC fill range\n"); } // DCC fills use a repeated byte code. Require all four bytes of the detected dword to agree, // and mark only recognized deferred-clear encodings as logically clear. const auto dcc_clear_mask = [fill_value] { const auto code = static_cast(fill_value); if (fill_value != static_cast(code) * 0x01010101u) { return 0u; } switch (code) { case 0x00: case 0x20: case 0x40: case 0x80: case 0xc0: return UINT32_MAX; default: return 0u; } }(); std::scoped_lock lock {m_lock}; // The guest dispatch still writes metadata. An unknown address remains PendingDcc until an // image descriptor confirms its role; never reinterpret CMask/FMask/HTile as DCC. const auto found = m_surface_metas.try_emplace(address).first; if (found->second.type == MetaDataInfo::Type::PendingDcc || found->second.type == MetaDataInfo::Type::Dcc) { found->second.clear_mask = dcc_clear_mask; found->second.fill_value = fill_value; found->second.fill_size = size; } } bool TextureCache::TouchMeta(uint64_t address, uint32_t slice, bool is_clear) { std::scoped_lock lock {m_lock}; const auto found = m_surface_metas.find(address); if (found == m_surface_metas.end() || found->second.type == MetaDataInfo::Type::PendingDcc || slice >= 32) { return false; } if (is_clear) { found->second.clear_mask |= 1u << slice; } else { found->second.clear_mask &= ~(1u << slice); } return true; } void TextureCache::UnmapMemory(uint64_t address, uint64_t size) { if (!GuestRange {address, size}.Valid()) { EXIT("TextureCache: invalid unmap range\n"); } std::scoped_lock lock {m_lock}; const auto end = address + size; for (auto metadata = m_surface_metas.lower_bound(address); metadata != m_surface_metas.end() && metadata->first < end;) { metadata = m_surface_metas.erase(metadata); } auto images = FindImagesInRegion(address, size, false); for (const auto id: images) { auto owner = m_slot_images.try_get(id); if (owner == nullptr) { continue; } FreeImage(id); } } void TextureCache::RunGarbageCollector() { std::scoped_lock lock {m_lock}; const uint64_t tick = m_gc_tick++; if (m_graphics.CanReportMemoryUsage()) { m_total_used_memory = m_graphics.GetDeviceMemoryUsage(); } if (m_total_used_memory < m_trigger_gc_memory) { return; } const auto collect = [&](bool allow_aggressive) { bool pressured = m_total_used_memory >= m_pressure_gc_memory; bool aggressive = allow_aggressive && m_total_used_memory >= m_critical_gc_memory; const uint64_t age = std::min(aggressive ? 160 : pressured ? 80 : 16, tick); size_t deletions = aggressive ? 40 : pressured ? 20 : 10; std::vector candidates; candidates.reserve(deletions); // Deleting depth recursively deletes its stencil association, so finish LRU traversal // first. m_lru_cache.ForEachItemBelow(tick - age, [&](ImageId id) { candidates.push_back(id); return candidates.size() == deletions; }); for (const auto id: candidates) { if (deletions == 0) { break; } --deletions; auto owner = m_slot_images.try_get(id); if (owner == nullptr || !owner->registered || owner->depth_id) { continue; } if (owner->IsGpuModified()) { const bool safe = SafeToDownload(*owner); if (safe && owner->info.IsTiled()) { continue; } if (safe && !pressured) { continue; } if (safe && !DownloadImageMemory(id)) { continue; } } FreeImage(id); if (m_total_used_memory < m_critical_gc_memory && aggressive) { deletions >>= 2; aggressive = false; } if (m_total_used_memory < m_pressure_gc_memory && pressured) { deletions >>= 1; pressured = false; } } }; collect(false); if (m_total_used_memory >= m_critical_gc_memory) { collect(true); } } void TextureCache::ProcessDownloadImages() { std::scoped_lock lock {m_lock}; for (const auto id: m_download_images) { const auto owner = m_slot_images.try_get(id); if (owner != nullptr && owner->registered && owner->IsGpuModified()) { (void)DownloadImageMemory(id); } } m_download_images.clear(); } } // namespace Libs::Graphics