#include "graphics/host_gpu/renderer/textureCache.h" #include "common/assert.h" #include "common/logging/log.h" #include "common/profiler.h" #include "graphics/guest_gpu/gpu_defs.h" #include "graphics/guest_gpu/gpu_format.h" #include "graphics/guest_gpu/graphicsRun.h" #include "graphics/guest_gpu/tile.h" #include "graphics/host_gpu/gpuTiler.h" #include "graphics/host_gpu/graphicContext.h" #include "graphics/host_gpu/objects/label.h" #include "graphics/host_gpu/objects/textureCommon.h" #include "graphics/host_gpu/renderer/bufferCache.h" #include "graphics/host_gpu/renderer/dummyTextureCache.h" #include "graphics/host_gpu/renderer/image.h" #include "graphics/host_gpu/renderer/imageView.h" #include "graphics/host_gpu/renderer/render.h" #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/host_gpu/renderer/renderTargetBarriers.h" #include "graphics/host_gpu/renderer/resourceMutex.h" #include "graphics/host_gpu/transfer.h" #include "graphics/host_gpu/vulkanCommon.h" #include "kernel/memory.h" #include #include #include #include #include #include #include #include #include namespace Libs::Graphics { namespace { thread_local const void* g_texture_cache_lock_owner = nullptr; thread_local const void* g_texture_fault_owner = nullptr; [[nodiscard]] bool GuestRangeIsZero(uint64_t address, uint64_t size) noexcept { const auto* bytes = reinterpret_cast(address); while (size >= sizeof(uint64_t)) { uint64_t word = 0; std::memcpy(&word, bytes, sizeof(word)); if (word != 0) { return false; } bytes += sizeof(word); size -= sizeof(word); } while (size-- != 0) { if (*bytes++ != 0) { return false; } } return true; } [[nodiscard]] uint64_t HashSampledImageEdges(const Image& image) { constexpr uint64_t page_mask = TRACKER_PAGE_SIZE - 1; const uint64_t begin = image.address; const uint64_t end = image.address + image.size; const uint64_t head_end = std::min(end, (begin + page_mask) & ~page_mask); const uint64_t tail_begin = std::max(begin, end & ~page_mask); const uint64_t head_size = head_end - begin; const uint64_t tail_size = tail_begin < head_end ? end - head_end : end - tail_begin; std::array bytes {}; if (head_size + tail_size > bytes.size()) { EXIT("TextureCache: sampled-image edge hash range overflow\n"); } if ((head_size != 0 && !LibKernel::Memory::TryReadBacking(begin, bytes.data(), head_size)) || (tail_size != 0 && !LibKernel::Memory::TryReadBacking(tail_begin < head_end ? head_end : tail_begin, bytes.data() + head_size, tail_size))) { EXIT("TextureCache: failed to hash sampled-image edge backing, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", image.address, image.size); } return XXH3_64bits(bytes.data(), static_cast(head_size + tail_size)); } class FaultSafeTextureLock final { public: FaultSafeTextureLock(const void* owner, TrackingSpinLock& mutex): m_mutex(mutex) { if (g_texture_cache_lock_owner != nullptr) { EXIT("TextureCache: recursive cache lock acquisition, current=%p\n", g_texture_cache_lock_owner); } g_texture_cache_lock_owner = owner; m_mutex.lock(); } ~FaultSafeTextureLock() { m_mutex.unlock(); g_texture_cache_lock_owner = nullptr; } private: TrackingSpinLock& m_mutex; }; enum class ImageRangeOverlap : uint8_t { None, PageOnly, Bytes }; ImageRangeOverlap ClassifyImageRangeOverlap(uint64_t left, uint64_t left_size, uint64_t right, uint64_t right_size) { if (ImageRangeOverlaps(left, left_size, right, right_size)) { return ImageRangeOverlap::Bytes; } return ImagePageRangesOverlap(left, left_size, right, right_size) ? ImageRangeOverlap::PageOnly : ImageRangeOverlap::None; } } // namespace bool IsExactRenderTargetMipStorage(const ImageInfo& sampled, const ImageInfo& storage, vk::Format sampled_view_format, vk::Format storage_image_format) noexcept { const auto render_target = Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget); const auto image_2d = Prospero::GpuEnumValue(Prospero::ImageType::kColor2D); if (sampled.address == 0 || sampled.size == 0 || sampled.width == 0 || sampled.height == 0 || (sampled.address & 0xffffu) != 0 || sampled.levels <= 1 || sampled.levels > 16 || sampled.base_level != 0 || sampled.view_levels != sampled.levels || sampled.tile != render_target || sampled.depth != 1 || sampled.type != image_2d || sampled.base_array != 0 || storage.address == 0 || storage.size == 0 || (storage.address & 0xffffu) != 0 || storage.width == 0 || storage.height == 0 || storage.base_level != 0 || storage.levels != 1 || storage.view_levels != 1 || storage.tile != render_target || storage.depth != 1 || storage.type != image_2d || storage.base_array != 0 || sampled.format != storage.format || sampled_view_format != storage_image_format) { return false; } const auto bytes_per_element = Prospero::NumBytesPerElement(sampled.format); if (bytes_per_element == 0) { return false; } TileSizeAlign sampled_layout {}; std::array level_layouts {}; std::array level_padded {}; if (!TileGetRenderTargetMipLayout(sampled.width, sampled.height, sampled.pitch, bytes_per_element, sampled.levels, sampled_layout, level_layouts.data(), level_padded.data()) || sampled_layout.align != 65536 || sampled_layout.size != sampled.size) { return false; } TileSizeAlign storage_layout {}; if (!TileGetRenderTargetSize(storage.width, storage.height, storage.pitch, bytes_per_element, storage_layout) || storage_layout.align != 65536 || storage_layout.size != storage.size) { return false; } for (uint32_t level = 0; level < sampled.levels; level++) { const uint64_t divisor = 1ull << level; const auto level_width = static_cast((static_cast(sampled.width) + divisor - 1) / divisor); const auto level_height = static_cast((static_cast(sampled.height) + divisor - 1) / divisor); const auto& layout = level_layouts[level]; const bool dedicated_allocation = layout.size == layout.src_size && layout.offset == layout.src_offset && layout.x == 0 && layout.y == 0; const auto& padded = level_padded[level]; const bool packed_tail_allocation = layout.src_size == 65536 && !dedicated_allocation && padded.width != 0 && padded.height != 0 && storage.width == padded.width && storage.height == padded.height && storage.pitch == padded.width && storage.size == layout.src_size && storage_layout.size == layout.src_size && layout.src_offset <= sampled.size && layout.src_size <= sampled.size - layout.src_offset && sampled.address <= UINT64_MAX - layout.src_offset && storage.address == sampled.address + layout.src_offset; if (packed_tail_allocation) { // A PS5 mip tail is one physical thin-64 KiB tile. The storage descriptor exposes // that complete padded block so its shader can address the packed mip locations. return true; } if (!dedicated_allocation || storage.width != level_width || storage.height != level_height || storage.pitch != TileGetRenderTargetPitch(level_width, bytes_per_element) || storage.size != layout.size || storage_layout.size != layout.size || sampled.address > UINT64_MAX - layout.offset || storage.address != sampled.address + layout.offset) { continue; } return true; } return false; } struct TextureCache::CachedImage { explicit CachedImage(GraphicContext& graphics): graphics(graphics) {} enum class Kind { Texture, StorageTexture, RenderTarget, DepthTarget, VideoOut } kind = Kind::Texture; Image info; RenderTargetInfo target; DepthTargetInfo depth; VideoOutInfo video_out; GraphicContext& graphics; VulkanImage* image = nullptr; bool gpu_modified = false; bool buffer_modified = false; bool stencil_initialized = false; bool registered = false; ~CachedImage() { if (image == nullptr || registered) { EXIT("TextureCache: cached image destroyed with invalid resources, image=%p " "kind=%u registered=%d\n", static_cast(image), static_cast(kind), registered); } ImageOps::Destroy(*image); image = nullptr; } [[nodiscard]] uint32_t RangeCount() const { return kind == Kind::DepthTarget && depth.stencil_address != 0 ? 2u : 1u; } [[nodiscard]] BufferImageBinding BufferBinding() const { switch (kind) { case Kind::Texture: return BufferImageBinding::Texture; case Kind::RenderTarget: return BufferImageBinding::RenderTarget; case Kind::VideoOut: return BufferImageBinding::VideoOut; case Kind::StorageTexture: return BufferImageBinding::StorageTexture; case Kind::DepthTarget: return BufferImageBinding::DepthTarget; } return BufferImageBinding::Unsupported; } [[nodiscard]] uint64_t Address(uint32_t index = 0) const { if (index >= RangeCount()) { EXIT("TextureCache: image address range index out of bounds, index=%u count=%u\n", index, RangeCount()); } if (index == 1) { return depth.stencil_address; } switch (kind) { case Kind::Texture: return info.address; case Kind::StorageTexture: return info.address; case Kind::RenderTarget: return target.address; case Kind::DepthTarget: return depth.address; case Kind::VideoOut: return video_out.address; } EXIT("TextureCache: unsupported cached image kind %u for address\n", static_cast(kind)); } [[nodiscard]] uint64_t Size(uint32_t index = 0) const { if (index >= RangeCount()) { EXIT("TextureCache: image size range index out of bounds, index=%u count=%u\n", index, RangeCount()); } if (index == 1) { return depth.stencil_size; } switch (kind) { case Kind::Texture: return info.size; case Kind::StorageTexture: return info.size; case Kind::RenderTarget: return target.size; case Kind::DepthTarget: return depth.size; case Kind::VideoOut: return video_out.size; } EXIT("TextureCache: unsupported cached image kind %u for size\n", static_cast(kind)); } [[nodiscard]] bool OverlapsRange(uint64_t address, uint64_t size, bool page) const { for (uint32_t i = 0; i < RangeCount(); i++) { if (page ? ImagePageRangesOverlap(address, size, Address(i), Size(i)) : ImageRangeOverlaps(address, size, Address(i), Size(i))) { return true; } } return false; } // A page-expanded region lookup identifies candidates before exact byte classification. Kyty's // tracker retains GPU ownership per 4 KiB page, so an edge-page // candidate must remain coherent even when the triggering byte lies just outside the image. [[nodiscard]] bool IsGpuReadbackPageCandidate(uint64_t address, uint64_t size) const { return gpu_modified && OverlapsRange(address, size, true); } [[nodiscard]] bool HasExactRange(uint64_t address, uint64_t size) const { for (uint32_t i = 0; i < RangeCount(); i++) { if (address == Address(i) && size == Size(i)) { return true; } } return false; } }; void TextureCache::RegisterImageLocked(CachedImage& image) { if (image.registered) { EXIT("TextureCache: registering an already registered image\n"); } std::vector ranges; ranges.reserve(image.RangeCount()); for (uint32_t range = 0; range < image.RangeCount(); range++) { ranges.push_back({image.Address(range), image.Size(range)}); } if (!m_image_owner_index.Register(&image, ranges)) { EXIT("TextureCache: invalid or duplicate image registration\n"); } image.registered = true; } void TextureCache::UnregisterImageLocked(CachedImage& image, bool release_tracking) { if (!image.registered) { EXIT("TextureCache: unregistering an unregistered image\n"); } std::vector final_releases; if (!m_image_owner_index.Unregister(&image, final_releases)) { EXIT("TextureCache: image missing from owner index\n"); } if (release_tracking) { for (const auto& release: final_releases) { m_memory_tracker.UntrackMemory(release.address, release.size); } } image.registered = false; } VulkanImage& TextureCache::PublishImage(CommandBuffer& command, std::shared_ptr image) { if (image == nullptr || image->image == nullptr || image->registered) { EXIT("TextureCache: invalid image publication, record=%p image=%p " "registered=%d\n", static_cast(image.get()), image != nullptr ? static_cast(image->image) : nullptr, image != nullptr && image->registered); } auto& result = *image->image; command.RetainResourceUntilFence(image); m_images.push_back(std::move(image)); RegisterImageLocked(*m_images.back()); return result; } std::shared_ptr TextureCache::GetImageOwner(const VulkanImage& image) { FaultSafeTextureLock lock(this, m_lock); const auto owner = std::ranges::find_if( m_images, [&](const auto& cached) { return cached->image == ℑ }); if (owner == m_images.end()) { EXIT("TextureCache: image has no cache owner, image=%p\n", static_cast(&image)); } return *owner; } std::vector TextureCache::FindImagesInRegionLocked(uint64_t vaddr, uint64_t size, bool page_overlap) { return page_overlap ? m_image_owner_index.QueryCandidates(vaddr, size) : m_image_owner_index.Query(vaddr, size); } struct TextureCache::ReadbackWorker { enum class State : uint32_t { Idle, Claimed, Requested, Ready, Installed, Completed, Stopping, Stopped }; struct ReadbackRange { uint64_t address; uint64_t size; }; struct ReadbackTransfer { std::array ranges {}; uint32_t count = 0; void Add(uint64_t address, uint64_t size) { if (address == 0 || size == 0 || count == ranges.size()) { EXIT("TextureCache: invalid image readback transfer range\n"); } ranges[count++] = {address, size}; } [[nodiscard]] std::span Ranges() const { return {ranges.data(), count}; } }; static_assert(std::atomic::is_always_lock_free); explicit ReadbackWorker(TextureCache& owner): cache(owner), thread([this] { Run(); }) {} ~ReadbackWorker() { auto expected = State::Idle; if (!state.compare_exchange_strong(expected, State::Stopping, std::memory_order_acq_rel)) { EXIT("TextureCache: cannot stop readback worker from state %u\n", static_cast(expected)); } state.notify_all(); thread.join(); if (state.load(std::memory_order_acquire) != State::Stopped) { EXIT("TextureCache: readback worker did not reach stopped state\n"); } } void Request(PageFaultAccess fault_access, uint64_t fault_vaddr, uint64_t fault_size) noexcept { const bool command_thread = GraphicsRunIsCommandProcessorThread(); const bool submissions_prepaused_now = GraphicsRunSubmissionLockHeld() || command_thread; const bool unsafe_gpu_lock = GraphicsRunGpuLockHeld() && !submissions_prepaused_now; if ((fault_access != PageFaultAccess::Read && fault_access != PageFaultAccess::Write) || unsafe_gpu_lock || LabelInCallback() || g_texture_cache_lock_owner != nullptr || g_texture_fault_owner != &cache) { EXIT("TextureCache: unsafe image readback request, access=%u command_thread=%d " "submission_lock=%d gpu_lock=%d label_callback=%d cache_lock=%p fault_owner=%p\n", static_cast(fault_access), command_thread, GraphicsRunSubmissionLockHeld(), GraphicsRunGpuLockHeld(), LabelInCallback(), g_texture_cache_lock_owner, g_texture_fault_owner); } State expected = State::Idle; while (!state.compare_exchange_weak(expected, State::Claimed, std::memory_order_acq_rel)) { if (expected == State::Stopping || expected == State::Stopped) { EXIT("TextureCache: image readback requested while worker is stopping, state=%u\n", static_cast(expected)); } state.wait(expected, std::memory_order_acquire); expected = State::Idle; } access = fault_access; vaddr = fault_vaddr; size = fault_size; submissions_prepaused = submissions_prepaused_now; state.store(State::Requested, std::memory_order_release); state.notify_all(); while (true) { const auto current = state.load(std::memory_order_acquire); if (current == State::Ready) { return; } if (current != State::Requested) { EXIT("TextureCache: invalid state while waiting for image readback, state=%u\n", static_cast(current)); } state.wait(current, std::memory_order_acquire); } } [[nodiscard]] bool Complete(PageFaultAccess fault_access, uint64_t fault_vaddr, uint64_t fault_size) noexcept { const auto current = state.load(std::memory_order_acquire); if (current == State::Idle || current == State::Stopping || current == State::Stopped) { return false; } if (current != State::Ready) { EXIT("TextureCache: active image readback has invalid completion state %u\n", static_cast(current)); } if (access != fault_access || vaddr != fault_vaddr || size != fault_size) { EXIT("TextureCache: mismatched active image readback completion\n"); } state.store(State::Installed, std::memory_order_release); state.notify_all(); return true; } [[nodiscard]] bool IsReady(PageFaultAccess fault_access, uint64_t fault_vaddr, uint64_t fault_size) const noexcept { return state.load(std::memory_order_acquire) == State::Ready && access == fault_access && vaddr == fault_vaddr && size == fault_size; } void Release(PageFaultAccess fault_access, uint64_t fault_vaddr, uint64_t fault_size) noexcept { const auto current = state.load(std::memory_order_acquire); if (current == State::Idle || current == State::Stopping || current == State::Stopped) { return; } if (current != State::Installed) { EXIT("TextureCache: active image readback has invalid release state %u\n", static_cast(current)); } if (access != fault_access || vaddr != fault_vaddr || size != fault_size) { EXIT("TextureCache: mismatched active image readback release\n"); } state.store(State::Completed, std::memory_order_release); state.notify_all(); while (true) { const auto current = state.load(std::memory_order_acquire); if (current == State::Idle) { break; } if (current != State::Completed) { EXIT("TextureCache: invalid state while releasing image readback, state=%u\n", static_cast(current)); } state.wait(current, std::memory_order_acquire); } } [[nodiscard]] ReadbackTransfer DownloadDepthTarget(CachedImage& cached, bool require_fault_range = true) { const auto& info = cached.depth; if (info.samples != 1 || cached.image->samples != 1) { EXIT("TextureCache: multisampled depth-image readback is unsupported, samples=%u/%u\n", info.samples, cached.image->samples); } const bool has_stencil = info.stencil_address != 0 || info.stencil_size != 0; const bool has_htile = info.htile_address != 0 || info.htile_size != 0; const bool fault_in_depth = vaddr >= info.address && vaddr < info.address + info.size && size <= info.address + info.size - vaddr; const bool fault_in_stencil = has_stencil && vaddr >= info.stencil_address && vaddr < info.stencil_address + info.stencil_size && size <= info.stencil_address + info.stencil_size - vaddr; const bool d16 = info.guest_format == Prospero::GpuEnumValue(Prospero::BufferFormat::k16UNorm) && info.bytes_per_element == 2; const bool d32 = info.guest_format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32Float) && info.bytes_per_element == 4; TileSizeAlign expected_stencil {}; TileSizeAlign expected_htile {}; TileSizeAlign expected_depth {}; const bool prospero_layout = (d16 || d32) && IsSupportedDepthReadbackFormat(info) && TileGetDepthSize(info.width, info.height, 0, Prospero::GpuEnumValue(d16 ? Prospero::DepthFormat::kZ16 : Prospero::DepthFormat::kZ32F), Prospero::GpuEnumValue(has_stencil ? Prospero::StencilFormat::k8UInt : Prospero::StencilFormat::kInvalid), has_htile, expected_stencil, expected_htile, expected_depth); const auto expected_pitch = prospero_layout ? TileGetTexturePitch(info.guest_format, info.width, 1, Prospero::GpuEnumValue(Prospero::TileMode::kDepth)) : 0u; const auto expected_stencil_pitch = prospero_layout && has_stencil ? TileGetTexturePitch(Prospero::GpuEnumValue(Prospero::BufferFormat::k8UInt), info.width, 1, Prospero::GpuEnumValue(Prospero::TileMode::kDepth)) : 0u; const bool layered_sizes = info.layers != 0 && info.size <= UINT32_MAX && expected_depth.size <= UINT64_MAX / info.layers && info.size == expected_depth.size * info.layers && (!has_stencil || (info.stencil_size <= UINT32_MAX && expected_stencil.size <= UINT64_MAX / info.layers && info.stencil_size == expected_stencil.size * info.layers)) && (!has_htile || (expected_htile.size <= UINT64_MAX / info.layers && info.htile_size == expected_htile.size * info.layers)); if ((require_fault_range && !fault_in_depth && !fault_in_stencil) || info.address == 0 || info.size == 0 || info.width == 0 || info.height == 0 || info.pitch < info.width || info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kDepth) || !prospero_layout || info.pitch != expected_pitch || !layered_sizes || expected_depth.align != 65536u || cached.image->layers != info.layers || (has_stencil && (info.stencil_address == 0 || expected_stencil_pitch < info.width || expected_stencil.align != 65536u)) || (has_htile && expected_htile.align != 32768u)) { EXIT("TextureCache: unsupported depth-image readback layout, fault=0x%016" PRIx64 "+0x%016" PRIx64 " depth=0x%016" PRIx64 "+0x%016" PRIx64 " stencil=0x%016" PRIx64 "+0x%016" PRIx64 " htile=0x%016" PRIx64 "+0x%016" PRIx64 " extent=%ux%u pitch=%u/%u layers=%u/%u tile=%u format=%d guest_format=%u bpe=%u " "expected_depth=0x%016" PRIx64 " expected_stencil=0x%016" PRIx64 " expected_htile=0x%016" PRIx64 "\n", vaddr, size, info.address, info.size, info.stencil_address, info.stencil_size, info.htile_address, info.htile_size, info.width, info.height, info.pitch, expected_pitch, info.layers, cached.image->layers, info.tile_mode, static_cast(info.format), info.guest_format, info.bytes_per_element, expected_depth.size, expected_stencil.size, expected_htile.size); } const auto rows = static_cast(info.height - 1); if (rows > (UINT64_MAX - info.width) / info.pitch || (has_stencil && rows > (UINT64_MAX - info.width) / expected_stencil_pitch)) { EXIT("TextureCache: depth-image readback size overflow\n"); } const auto depth_linear_elements = rows * info.pitch + info.width; const auto stencil_linear_elements = rows * expected_stencil_pitch + info.width; if (depth_linear_elements > UINT64_MAX / info.bytes_per_element) { EXIT("TextureCache: depth-image readback size overflow\n"); } const auto depth_linear_size = depth_linear_elements * info.bytes_per_element; const auto depth_slice_size = info.size / info.layers; const auto stencil_slice_size = has_stencil ? info.stencil_size / info.layers : 0; const bool meta_overlap = cache.HasMetaOverlapLocked(info.address, info.size) || (has_stencil && cache.HasMetaOverlapLocked(info.stencil_address, info.stencil_size)); const bool buffer_overlap = cache.m_buffer_cache.HasPageOverlap(info.address, info.size) || (has_stencil && cache.m_buffer_cache.HasPageOverlap( info.stencil_address, info.stencil_size)); const auto transfer_size = info.size + info.stencil_size; if (depth_linear_size > depth_slice_size || (has_stencil && stencil_linear_elements > stencil_slice_size) || transfer_size > UINT32_MAX || cached.image->format != info.format || cached.image->extent.width != info.width || cached.image->extent.height != info.height || meta_overlap || buffer_overlap) { EXIT("TextureCache: depth-image readback storage is unsupported, " "depth=0x%016" PRIx64 "+0x%016" PRIx64 " linear=0x%016" PRIx64 " format=%d/%d extent=%ux%u/%ux%u meta=%d buffer=%d\n", info.address, info.size, depth_linear_size, static_cast(cached.image->format), static_cast(info.format), cached.image->extent.width, cached.image->extent.height, info.width, info.height, meta_overlap, buffer_overlap); } auto regions = Transfer::MakeLayeredImageBufferCopies(info.layers, depth_slice_size, info.pitch, info.width, info.height, vk::ImageAspectFlagBits::eDepth); if (has_stencil) { auto stencil_regions = Transfer::MakeLayeredImageBufferCopies( info.layers, stencil_slice_size, expected_stencil_pitch, info.width, info.height, vk::ImageAspectFlagBits::eStencil); for (auto& region: stencil_regions) { region.offset += static_cast(info.size); } regions.insert(regions.end(), stencil_regions.begin(), stencil_regions.end()); } std::vector gpu_infos; TileBlockLayout depth_block {}; TileBlockLayout stencil_block {}; EXIT_NOT_IMPLEMENTED( !TileGetBlockLayout(TileBlockFamily::Depth64KB, info.bytes_per_element, depth_block) || (has_stencil && !TileGetBlockLayout(TileBlockFamily::Depth64KB, 1, stencil_block))); gpu_infos.reserve(regions.size()); for (uint32_t layer = 0; layer < info.layers; layer++) { const uint64_t offset = depth_slice_size * layer; GpuTileInfo tile {depth_block.family, depth_block.bytes_per_element, offset, depth_slice_size, offset, depth_slice_size, 0, info.width, info.height, 1, info.pitch}; tile.surface_z = layer; gpu_infos.push_back(tile); } if (has_stencil) { for (uint32_t layer = 0; layer < info.layers; layer++) { const uint64_t offset = info.size + stencil_slice_size * layer; GpuTileInfo tile {stencil_block.family, stencil_block.bytes_per_element, offset, stencil_slice_size, offset, stencil_slice_size, 0, info.width, info.height, 1, expected_stencil_pitch}; tile.surface_z = layer; gpu_infos.push_back(tile); } } guest.resize(transfer_size); Transfer::DownloadTiledImage(guest.data(), transfer_size, transfer_size, gpu_infos, regions, *cached.image, cached.image->layout); Libs::LibKernel::Memory::WriteBacking(info.address, guest.data(), info.size); ReadbackTransfer transfer; transfer.Add(info.address, info.size); if (has_stencil) { Libs::LibKernel::Memory::WriteBacking(info.stencil_address, guest.data() + info.size, info.stencil_size); transfer.Add(info.stencil_address, info.stencil_size); } return transfer; } [[nodiscard]] ReadbackTransfer DownloadColorImage(CachedImage& cached) { const bool storage = cached.kind == CachedImage::Kind::StorageTexture; const bool target = cached.kind == CachedImage::Kind::RenderTarget; const auto info = storage ? MakeColorImageTransferInfo(cached.info, VulkanFormat(cached.info.format), Prospero::NumBytesPerElement(cached.info.format)) : MakeColorImageTransferInfo(cached.target); const bool linear = info.tile_mode == Prospero::GpuEnumValue(Prospero::TileMode::kLinear); const bool tiled_target = target && IsTiledRenderTarget(cached.target); const bool tiled_storage = storage && info.tile_mode == Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget); bool single_layer_storage = false; if (storage) { switch (static_cast(cached.info.type)) { case Prospero::ImageType::kColor2D: case Prospero::ImageType::kColor2DArray: single_layer_storage = cached.info.depth == 1; break; default: break; } } const bool basic_storage = !storage || (single_layer_storage && cached.info.base_level == 0 && cached.info.levels == 1 && cached.info.base_array == 0 && (linear || tiled_storage)); const auto layers = target ? cached.target.layers : 1u; TileSizeAlign target_mip_layout {}; const bool target_mip_chain = target && info.levels > 1 && layers == 1 && tiled_target && TileGetRenderTargetMipLayout(info.width, info.height, info.pitch, info.bytes_per_element, info.levels, target_mip_layout, nullptr, nullptr) && target_mip_layout.align == 65536 && target_mip_layout.size == info.size && cached.image != nullptr && cached.image->format == info.format && cached.image->extent.width == info.width && cached.image->extent.height == info.height && cached.image->layers == 1 && cached.image->mip_levels == info.levels && cached.image->samples == 1; if (info.samples != 1 || (!linear && !tiled_target && !tiled_storage) || !basic_storage || (info.levels != 1 && !target_mip_chain) || layers == 0 || info.size > UINT32_MAX) { EXIT("TextureCache: unsupported color-image readback layout, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " extent=%ux%u pitch=%u bpe=%u levels=%u samples=%u tile=%u kind=%u\n", info.address, info.size, info.width, info.height, info.pitch, info.bytes_per_element, info.levels, info.samples, info.tile_mode, static_cast(cached.kind)); } const auto slice_size = info.size / layers; const bool meta_overlap = cache.HasMetaOverlapLocked(info.address, info.size); const bool buffer_overlap = cache.m_buffer_cache.HasPageOverlap(info.address, info.size); if (meta_overlap || buffer_overlap) { EXIT("TextureCache: color-image readback storage is unsupported, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " meta=%d buffer=%d kind=%u\n", info.address, info.size, meta_overlap, buffer_overlap, static_cast(cached.kind)); } std::vector regions; std::vector tiled_regions; TextureUploadLayout tiled_layout {}; const bool gpu_tiled = tiled_target || tiled_storage; if (gpu_tiled) { const auto format = storage ? cached.info.format : ImageOps::RenderTargetTransferFormat(info.bytes_per_element); tiled_layout = TextureCalcUploadLayout(format, info.width, info.height, info.levels, layers, info.pitch, info.tile_mode, info.size, false, false, "RenderTargetReadback"); if (target_mip_chain && (tiled_layout.tile_family != TileBlockFamily::RenderTarget64KB || tiled_layout.pitch != info.pitch)) { EXIT("TextureCache: inconsistent render-target readback layout, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " pitch=%u/%u levels=%u\n", info.address, info.size, info.pitch, tiled_layout.pitch, info.levels); } tiled_regions = TextureBuildUploadRegions(tiled_layout, info.format, info.width, info.height, layers, info.levels, layers > 1, false, TextureUploadDestination::MipLevels); regions = TextureBuildDownloadRegions(tiled_regions); } else { regions = Transfer::MakeLayeredImageBufferCopies(layers, slice_size, info.pitch, info.width, info.height); } if (gpu_tiled) { std::vector infos; const auto format = storage ? cached.info.format : ImageOps::RenderTargetTransferFormat(info.bytes_per_element); guest.resize(info.size); EXIT_NOT_IMPLEMENTED(!TextureBuildGpuTileInfos(info.size, tiled_regions, tiled_layout, format, layers, info.levels, infos)); Transfer::DownloadTiledImage(guest.data(), info.size, info.size, infos, regions, *cached.image, cached.image->layout); Libs::LibKernel::Memory::WriteBacking(info.address, guest.data(), info.size); } else { download.resize(info.size); std::fill(download.begin(), download.end(), 0); Transfer::DownloadImage(download.data(), info.size, regions, *cached.image, cached.image->layout); Libs::LibKernel::Memory::WriteBacking(info.address, download.data(), info.size); } ReadbackTransfer transfer; transfer.Add(info.address, info.size); return transfer; } void Run() noexcept { while (true) { auto current = state.load(std::memory_order_acquire); while (current == State::Idle) { state.wait(current, std::memory_order_acquire); current = state.load(std::memory_order_acquire); } if (current == State::Stopping) { state.store(State::Stopped, std::memory_order_release); state.notify_all(); return; } if (current != State::Requested) { EXIT("TextureCache: readback worker received unsupported state %u\n", static_cast(current)); } std::optional submissions; if (!submissions_prepaused) { submissions.emplace(); } FaultSafeTextureLock lock(&cache, cache.m_lock); CachedImage* selected = cache.FindGpuReadbackPageCandidateLocked(vaddr, size); const bool render_target = selected != nullptr && selected->kind == CachedImage::Kind::RenderTarget; const bool storage_texture = selected != nullptr && selected->kind == CachedImage::Kind::StorageTexture; const bool depth_target = selected != nullptr && selected->kind == CachedImage::Kind::DepthTarget; if ((!render_target && !storage_texture && !depth_target) || !selected->gpu_modified || selected->buffer_modified || selected->image == nullptr) { EXIT("TextureCache: unsupported GPU image readback owner, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " access=%u image=%p kind=%u gpu_modified=%d " "buffer_modified=%d vulkan_image=%p\n", vaddr, size, static_cast(access), static_cast(selected), selected != nullptr ? static_cast(selected->kind) : UINT32_MAX, selected != nullptr && selected->gpu_modified, selected != nullptr && selected->buffer_modified, selected != nullptr ? static_cast(selected->image) : nullptr); } const auto transfer = depth_target ? DownloadDepthTarget(*selected) : DownloadColorImage(*selected); if (!cache.m_memory_tracker.IsRegionGpuModified(vaddr, size)) { EXIT("TextureCache: readback fault page is not GPU-modified, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } const auto fault_page = vaddr & ~(TRACKER_PAGE_SIZE - 1); const auto page_end = fault_page + TRACKER_PAGE_SIZE; for (const auto& range: transfer.Ranges()) { const auto range_end = range.address + range.size; if (fault_page < range_end && page_end > range.address) { if (fault_page > range.address) { cache.m_memory_tracker.ForEachDownloadRange( range.address, fault_page - range.address, [](uint64_t, uint64_t) noexcept {}); } if (page_end < range_end) { cache.m_memory_tracker.ForEachDownloadRange( page_end, range_end - page_end, [](uint64_t, uint64_t) noexcept {}); } } else { cache.m_memory_tracker.ForEachDownloadRange( range.address, range.size, [](uint64_t, uint64_t) noexcept {}); } } state.store(State::Ready, std::memory_order_release); state.notify_all(); while ((current = state.load(std::memory_order_acquire)) != State::Completed) { if (current != State::Ready && current != State::Installed) { EXIT("TextureCache: invalid image readback completion state %u\n", static_cast(current)); } state.wait(current, std::memory_order_acquire); } for (const auto& range: transfer.Ranges()) { if (cache.m_memory_tracker.IsRegionGpuModified(range.address, range.size)) { EXIT("TextureCache: completed image readback retained GPU ownership, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", range.address, range.size); } } selected->gpu_modified = false; submissions_prepaused = false; state.store(State::Idle, std::memory_order_release); state.notify_all(); } } TextureCache& cache; std::atomic state {State::Idle}; PageFaultAccess access = PageFaultAccess::Unknown; uint64_t vaddr = 0; uint64_t size = 0; bool submissions_prepaused = false; std::vector download; std::vector guest; std::thread thread; }; void TextureCache::RequireRetirementIsolation(const std::vector& retire, const char* operation, uint64_t address, uint64_t size) const { if (retire.empty()) { return; } std::vector ranges; ranges.reserve(m_images.size() * 2); for (const auto& cached: m_images) { const bool retiring = std::find(retire.begin(), retire.end(), cached.get()) != retire.end(); for (uint32_t range = 0; range < cached->RangeCount(); range++) { ranges.push_back({cached->Address(range), cached->Size(range), retiring}); } } const auto conflict = FindImageRetirementConflict(ranges); if (conflict.Exists()) { const auto& retired = ranges[conflict.retired]; const auto& retained = ranges[conflict.retained]; EXIT("TextureCache: %s retirement leaves a tracked page alias, request=0x%016" PRIx64 "+0x%016" PRIx64 " retired=0x%016" PRIx64 "+0x%016" PRIx64 " retained=0x%016" PRIx64 "+0x%016" PRIx64 "\n", operation, address, size, retired.address, retired.size, retained.address, retained.size); } } void TextureCache::RetireImages(const std::vector& retire, const CachedImage* native_image_source) { if (retire.empty()) { return; } // FindTexture and FindRenderTarget retain cache records on their recording commands. Erasing // the lookup owner therefore defers Vulkan destruction until the last referencing fence. size_t removed = 0; bool native_image_retired = false; for (auto it = m_images.begin(); it != m_images.end();) { if (std::find(retire.begin(), retire.end(), it->get()) == retire.end()) { ++it; continue; } const bool sampled = (*it)->kind == CachedImage::Kind::Texture; const bool storage = (*it)->kind == CachedImage::Kind::StorageTexture; const bool target = (*it)->kind == CachedImage::Kind::RenderTarget || (*it)->kind == CachedImage::Kind::DepthTarget; const bool native_image = it->get() == native_image_source; if (native_image) { bool source_valid = (*it)->gpu_modified && !(*it)->buffer_modified; switch ((*it)->kind) { case CachedImage::Kind::StorageTexture: source_valid = source_valid && !(*it)->info.IsCpuDirty(); break; case CachedImage::Kind::RenderTarget: case CachedImage::Kind::DepthTarget: break; case CachedImage::Kind::Texture: case CachedImage::Kind::VideoOut: source_valid = false; break; } for (uint32_t range = 0; source_valid && range < (*it)->RangeCount(); range++) { source_valid = m_memory_tracker.IsRegionGpuModified((*it)->Address(range), (*it)->Size(range)); } if (!source_valid) { EXIT("TextureCache: invalid native image retirement, kind=%u gpu_modified=%d " "buffer_modified=%d cpu_dirty=%d\n", static_cast((*it)->kind), (*it)->gpu_modified, (*it)->buffer_modified, (*it)->kind == CachedImage::Kind::StorageTexture && (*it)->info.IsCpuDirty()); } (*it)->gpu_modified = false; native_image_retired = true; } if ((!sampled && !storage && !target && !native_image) || (*it)->gpu_modified || (storage && ((*it)->buffer_modified || (*it)->info.IsCpuDirty())) || (target && (*it)->buffer_modified)) { EXIT("TextureCache: invalid image retirement, kind=%u gpu_modified=%d " "buffer_modified=%d\n", static_cast((*it)->kind), (*it)->gpu_modified, (*it)->buffer_modified); } for (uint32_t range = 0; range < (*it)->RangeCount(); range++) { if (target && !native_image && m_memory_tracker.IsRegionGpuModified((*it)->Address(range), (*it)->Size(range))) { EXIT("TextureCache: clean target retirement retained tracker GPU ownership, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", (*it)->Address(range), (*it)->Size(range)); } } // A native transition keeps tracker ownership continuous until the replacement is // published. Normal retirement only releases 4 KiB pages with no remaining image owner. UnregisterImageLocked(**it, !native_image); it = m_images.erase(it); removed++; } if (removed != retire.size()) { EXIT("TextureCache: image retirement set mismatch, requested=%zu removed=%zu\n", retire.size(), removed); } if (native_image_source != nullptr && !native_image_retired) { EXIT("TextureCache: native image retirement source was not retired\n"); } } void TextureCache::RetireDepthMetadataLocked(const std::vector& retire, uint64_t preserve_address) { std::vector addresses; for (auto* cached: retire) { if (cached->kind != CachedImage::Kind::DepthTarget || cached->depth.htile_address == 0) { continue; } const bool retained_owner = std::any_of(m_images.begin(), m_images.end(), [&](const auto& other) { return std::find(retire.begin(), retire.end(), other.get()) == retire.end() && other->kind == CachedImage::Kind::DepthTarget && other->depth.htile_address == cached->depth.htile_address && other->depth.htile_size == cached->depth.htile_size; }); if (!retained_owner && cached->depth.htile_address != preserve_address && std::find(addresses.begin(), addresses.end(), cached->depth.htile_address) == addresses.end()) { addresses.push_back(cached->depth.htile_address); } } for (const auto address: addresses) { auto metadata = m_surface_metas.find(address); auto owner = std::find_if(retire.begin(), retire.end(), [&](const auto* cached) { return cached->kind == CachedImage::Kind::DepthTarget && cached->depth.htile_address == address; }); if (metadata == m_surface_metas.end() || owner == retire.end() || metadata->second.size != (*owner)->depth.htile_size) { EXIT("TextureCache: retiring depth target has invalid HTile metadata, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", address, owner != retire.end() ? (*owner)->depth.htile_size : 0); } if (metadata->second.gpu_modified) { m_metadata_tracker.ForEachDownloadRange(metadata->first, metadata->second.size, [](uint64_t, uint64_t) noexcept {}); } m_metadata_tracker.UntrackMemory(metadata->first, metadata->second.size); m_surface_metas.erase(metadata); } } void TextureCache::MaterializeImagesToGuestLocked( const std::vector>& images) { if (std::any_of(images.begin(), images.end(), [](const auto& cached) { return cached->gpu_modified; })) { Transfer::WaitForQueueIdle(); } for (const auto& cached: images) { if (!cached->gpu_modified) { continue; } if (cached->buffer_modified) { EXIT("TextureCache: image recreation has invalid ownership, kind=%u " "buffer_modified=%d\n", static_cast(cached->kind), cached->buffer_modified); } if (cached->kind != CachedImage::Kind::DepthTarget && cached->kind != CachedImage::Kind::RenderTarget && cached->kind != CachedImage::Kind::StorageTexture) { EXIT("TextureCache: unsupported image recreation source kind %u\n", static_cast(cached->kind)); } const auto transfer = cached->kind == CachedImage::Kind::DepthTarget ? m_readback->DownloadDepthTarget(*cached, false) : m_readback->DownloadColorImage(*cached); for (const auto& range: transfer.Ranges()) { m_memory_tracker.ForEachDownloadRange(range.address, range.size, [](uint64_t, uint64_t) noexcept {}); } cached->gpu_modified = false; } } namespace { bool Equal(const ImageInfo& left, const ImageInfo& right) { return left.address == right.address && left.size == right.size && left.format == right.format && left.width == right.width && left.height == right.height && left.pitch == right.pitch && left.base_level == right.base_level && left.levels == right.levels && left.view_levels == right.view_levels && left.tile == right.tile && left.swizzle == right.swizzle && left.depth == right.depth && left.type == right.type && left.base_array == right.base_array; } bool EqualStorageBacking(const ImageInfo& left, const ImageInfo& right) { return left.address == right.address && left.size == right.size && left.format == right.format && left.width == right.width && left.height == right.height && left.pitch == right.pitch && left.levels == right.levels && left.tile == right.tile && left.depth == right.depth && left.type == right.type && left.base_array == right.base_array; } bool Equal(const RenderTargetInfo& left, const RenderTargetInfo& right) { return left.address == right.address && left.size == right.size && left.format == right.format && left.width == right.width && left.height == right.height && left.pitch == right.pitch && left.bytes_per_element == right.bytes_per_element && left.tile_mode == right.tile_mode && left.levels == right.levels && left.layers == right.layers && left.samples == right.samples; } bool Equal(const DepthTargetInfo& left, const DepthTargetInfo& right) { return left.address == right.address && left.size == right.size && left.stencil_address == right.stencil_address && left.stencil_size == right.stencil_size && left.htile_address == right.htile_address && left.htile_size == right.htile_size && left.format == right.format && left.guest_format == right.guest_format && left.width == right.width && left.height == right.height && left.pitch == right.pitch && left.bytes_per_element == right.bytes_per_element && left.tile_mode == right.tile_mode && left.layers == right.layers && left.samples == right.samples && left.stencil_htile_compressed == right.stencil_htile_compressed; } [[nodiscard]] bool IsCoherentGuestImageSource(const BufferImageCopySource& source, uint64_t address, uint64_t size) { // The GPU tiler currently consumes coherent guest backing directly. return source.cpu_current && source.address == address && source.size == size && (source.buffer != nullptr || source.offset == 0); } [[nodiscard]] BufferImageCopySource SelectSourceRange(const BufferImageCopySource& source, uint64_t address, uint64_t size) { if (address < source.address || size > source.size || address - source.address > source.size - size || (source.buffer != nullptr && source.offset > UINT64_MAX - (address - source.address))) { EXIT("TextureCache: image source subrange is invalid\n"); } const auto offset = source.buffer != nullptr ? source.offset + address - source.address : 0; return {source.buffer, offset, address, size, source.cpu_current}; } [[nodiscard]] DepthTargetInfo SelectDepthLayers(const DepthTargetInfo& info, uint32_t base_layer, uint32_t layer_count) { if (info.layers == 0 || base_layer >= info.layers || layer_count == 0 || layer_count > info.layers - base_layer || info.size % info.layers != 0 || info.stencil_size % info.layers != 0 || info.htile_size % info.layers != 0) { EXIT("TextureCache: depth layer range is invalid\n"); } auto result = info; result.layers = layer_count; for (auto [address, size]: {std::pair {&result.address, &result.size}, std::pair {&result.stencil_address, &result.stencil_size}, std::pair {&result.htile_address, &result.htile_size}}) { const auto slice_size = *size / info.layers; *address += slice_size * base_layer; *size = slice_size * layer_count; } return result; } void AppendLayerCopies(std::vector& regions, VulkanImage& source, vk::ImageAspectFlags aspect) { for (uint32_t layer = 0; layer < source.layers; layer++) { for (uint32_t level = 0; level < source.mip_levels; level++) { ImageImageCopy region {source}; region.src_aspect = aspect; region.dst_aspect = aspect; region.src_level = level; region.dst_level = level; region.src_layer = layer; region.dst_layer = layer; region.width = std::max(source.extent.width >> level, 1u); region.height = std::max(source.extent.height >> level, 1u); regions.push_back(region); } } } } // namespace TextureCache::TextureCache(GraphicContext& graphics, PageManager& page_manager, BufferCache& buffer_cache, ResourceMutex& resource_mutex) : m_graphics(graphics), m_memory_tracker(page_manager), m_metadata_tracker(page_manager, PageWatchMode::Write), m_buffer_cache(buffer_cache), m_resource_mutex(resource_mutex) { if (!Common::Thread::IsMainThread()) { EXIT("TextureCache: construction is restricted to the main thread\n"); } m_dummy_textures = std::make_unique(); m_readback = std::make_unique(*this); } TextureCache::CachedImage* TextureCache::FindGpuReadbackPageCandidateLocked(uint64_t vaddr, uint64_t size) { CachedImage* selected = nullptr; for (auto* cached: FindImagesInRegionLocked(vaddr, size, true)) { if (!cached->IsGpuReadbackPageCandidate(vaddr, size)) { continue; } if (selected != nullptr) { EXIT("TextureCache: CPU fault has multiple GPU-modified image page candidates, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " first=%p second=%p\n", vaddr, size, static_cast(selected), static_cast(cached)); } selected = cached; } return selected; } void TextureCache::MarkSampledAliasesCpuDirtyLocked(uint64_t vaddr, uint64_t size) { for (auto* cached: FindImagesInRegionLocked(vaddr, size, true)) { if (cached->kind != CachedImage::Kind::Texture) { continue; } if (cached->gpu_modified && ImageRangeOverlaps(cached->info.address, cached->info.size, vaddr, size)) { EXIT("TextureCache: CPU write overlaps a GPU-modified sampled texture, " "write=0x%016" PRIx64 "+0x%016" PRIx64 " image=0x%016" PRIx64 "+0x%016" PRIx64 "\n", vaddr, size, cached->info.address, cached->info.size); } cached->info.InvalidateCpuWrite(vaddr, size); if (cached->info.NeedsMaybeCpuHash()) { cached->info.SetMaybeCpuHash(HashSampledImageEdges(cached->info)); } } } void TextureCache::RetireSampledTargetAliases(const ImageInfo& requested) { std::vector retire; bool wait_idle = false; for (auto* cached: FindImagesInRegionLocked(requested.address, requested.size, true)) { if (cached->kind != CachedImage::Kind::RenderTarget) { continue; } switch (ClassifySampledRenderTargetOverlap(requested, cached->target, cached->buffer_modified)) { case RenderTargetOverlap::None: continue; case RenderTargetOverlap::RetireTarget: wait_idle |= cached->gpu_modified; retire.push_back(cached); break; case RenderTargetOverlap::RetireSampled: case RenderTargetOverlap::RetireStorage: case RenderTargetOverlap::PreserveStorage: case RenderTargetOverlap::ExpandTarget: case RenderTargetOverlap::Unsupported: EXIT("TextureCache: unsupported sampled/render-target alias, sampled=0x%016" PRIx64 "+0x%016" PRIx64 " target=0x%016" PRIx64 "+0x%016" PRIx64 " gpu=%d buffer=%d\n", requested.address, requested.size, cached->target.address, cached->target.size, cached->gpu_modified, cached->buffer_modified); } } for (auto* cached: FindImagesInRegionLocked(requested.address, requested.size, true)) { if (cached->kind != CachedImage::Kind::DepthTarget) { continue; } const bool overlaps_depth = ImageRangeOverlaps(requested.address, requested.size, cached->depth.address, cached->depth.size); const bool overlaps_stencil = cached->depth.stencil_address != 0 && ImageRangeOverlaps(requested.address, requested.size, cached->depth.stencil_address, cached->depth.stencil_size); if (!overlaps_depth && !overlaps_stencil) { continue; } const auto delta = cached->depth.address >= requested.address ? cached->depth.address - requested.address : UINT64_MAX; const bool contained = delta <= requested.size && cached->depth.size <= requested.size - delta; const bool sampled_expansion = IsSampledDepthExpansion(requested, cached->depth); const bool exact_range = requested.address == cached->depth.address && requested.size == cached->depth.size; const bool depth_tracker_gpu = m_memory_tracker.IsRegionGpuModified(cached->depth.address, cached->depth.size); const bool stencil_tracker_gpu = cached->depth.stencil_address != 0 && m_memory_tracker.IsRegionGpuModified(cached->depth.stencil_address, cached->depth.stencil_size); const bool clean_pool_alias = CanRetireGuestCurrentDepthForSampled( requested, cached->depth, cached->gpu_modified, cached->buffer_modified, depth_tracker_gpu, stencil_tracker_gpu); const bool native_transition = !cached->buffer_modified && cached->depth.stencil_address == 0 && cached->depth.stencil_size == 0 && cached->depth.layers == 1 && contained && (exact_range || sampled_expansion); const bool supported = clean_pool_alias || native_transition; if (!supported) { EXIT("TextureCache: unsupported sampled/depth-target alias, sampled=0x%016" PRIx64 "+0x%016" PRIx64 " depth=0x%016" PRIx64 "+0x%016" PRIx64 " overlaps=%d/%d contained=%d gpu=%d/%d/%d buffer=%d" " stencil=0x%016" PRIx64 "+0x%016" PRIx64 " layers=%u expansion=%d" " sampled_info={fmt=%u extent=%ux%u pitch=%u levels=%u/%u tile=%u type=%u}" " depth_info={fmt=%u host=%d extent=%ux%u pitch=%u tile=%u htile=0x%016" PRIx64 "+0x%016" PRIx64 "}\n", requested.address, requested.size, cached->depth.address, cached->depth.size, overlaps_depth, overlaps_stencil, contained, cached->gpu_modified, depth_tracker_gpu, stencil_tracker_gpu, cached->buffer_modified, cached->depth.stencil_address, cached->depth.stencil_size, cached->depth.layers, sampled_expansion, requested.format, requested.width, requested.height, requested.pitch, requested.levels, requested.view_levels, requested.tile, requested.type, cached->depth.guest_format, static_cast(cached->depth.format), cached->depth.width, cached->depth.height, cached->depth.pitch, cached->depth.tile_mode, cached->depth.htile_address, cached->depth.htile_size); } wait_idle |= cached->gpu_modified; retire.push_back(cached); } if (retire.empty()) { return; } RequireRetirementIsolation(retire, "sampled target", requested.address, requested.size); if (wait_idle) { Transfer::WaitForQueueIdle(); } for (auto* cached: retire) { if (!cached->gpu_modified) { continue; } const auto transfer = cached->kind == CachedImage::Kind::DepthTarget ? m_readback->DownloadDepthTarget(*cached, false) : m_readback->DownloadColorImage(*cached); for (const auto& range: transfer.Ranges()) { m_memory_tracker.ForEachDownloadRange(range.address, range.size, [](uint64_t, uint64_t) noexcept {}); } cached->gpu_modified = false; } RetireDepthMetadataLocked(retire); RetireImages(retire); } void TextureCache::ResolveStorageImageOverlaps(const ImageInfo& requested) { std::vector retire; for (auto* cached: FindImagesInRegionLocked(requested.address, requested.size, true)) { const bool tracker_gpu = m_memory_tracker.IsRegionGpuModified(cached->Address(), cached->Size()); switch (ClassifyStorageImageOverlap( requested.address, requested.size, cached->Address(), cached->Size(), cached->kind == CachedImage::Kind::Texture, cached->gpu_modified, cached->buffer_modified, tracker_gpu)) { case StorageImageOverlap::None: continue; case StorageImageOverlap::RetireSampled: retire.push_back(cached); continue; case StorageImageOverlap::PageNeighbor: continue; case StorageImageOverlap::Unsupported: EXIT("TextureCache: unsupported storage-image byte alias, requested=0x%016" PRIx64 "+0x%016" PRIx64 " existing=0x%016" PRIx64 "+0x%016" PRIx64 " kind=%u gpu=%d/%d buffer=%d\n", requested.address, requested.size, cached->Address(), cached->Size(), static_cast(cached->kind), cached->gpu_modified, tracker_gpu, cached->buffer_modified); } } // Every exact byte overlap was classified above: only clean sampled images are retired and all // retained byte aliases remain fatal. A retired full mip chain can still contain a cached, // byte-disjoint subresource outside this storage request. The multi-owner index keeps those // retained pages tracked and UnregisterImageLocked releases only pages whose final owner left. for (auto* cached: retire) { if (!cached->gpu_modified) { continue; } const auto transfer = m_readback->DownloadColorImage(*cached); for (const auto& range: transfer.Ranges()) { m_memory_tracker.ForEachDownloadRange(range.address, range.size, [](uint64_t, uint64_t) noexcept {}); } cached->gpu_modified = false; } RetireImages(retire); } void TextureCache::RetireStorageDepthAliasLocked(const ImageInfo& requested) { CachedImage* selected = nullptr; for (auto* entry: FindImagesInRegionLocked(requested.address, requested.size, true)) { auto& cached = *entry; if (cached.kind != CachedImage::Kind::DepthTarget || !cached.OverlapsRange(requested.address, requested.size, true)) { continue; } const auto& depth = cached.depth; const bool exact_d32_uint = cached.gpu_modified && !cached.buffer_modified && depth.address == requested.address && depth.size == requested.size && depth.stencil_address == 0 && depth.stencil_size == 0 && depth.htile_address == 0 && depth.htile_size == 0 && depth.layers == 1 && depth.format == vk::Format::eD32Sfloat && depth.guest_format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32Float) && depth.bytes_per_element == 4 && depth.width == requested.width && depth.height == requested.height && depth.pitch == requested.pitch && requested.format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32UInt) && requested.tile == Prospero::GpuEnumValue(Prospero::TileMode::kDepth) && requested.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor2D) && requested.depth == 1 && requested.levels == 1; if (!exact_d32_uint || selected != nullptr) { EXIT("TextureCache: unsupported storage/depth-target alias, requested=0x%016" PRIx64 "+0x%016" PRIx64 " depth=0x%016" PRIx64 "+0x%016" PRIx64 " exact=%d ambiguous=%d\n", requested.address, requested.size, depth.address, depth.size, exact_d32_uint, selected != nullptr); } selected = &cached; } if (selected == nullptr) { return; } Transfer::WaitForQueueIdle(); const auto transfer = m_readback->DownloadDepthTarget(*selected, false); for (const auto& range: transfer.Ranges()) { m_memory_tracker.ForEachDownloadRange(range.address, range.size, [](uint64_t, uint64_t) noexcept {}); } selected->gpu_modified = false; RetireImages({selected}); } TextureCache::~TextureCache() { m_readback.reset(); if (!m_images.empty()) { Transfer::WaitForQueueIdle(); } for (const auto& image: m_images) { UnregisterImageLocked(*image, false); } m_images.clear(); m_dummy_textures.reset(); } VulkanImage& TextureCache::FindTexture(CommandBuffer& command, const ImageInfo& info, bool metadata_read) { if (info.address == 0 || info.size == 0 || info.address >= TRACKER_ADDRESS_SIZE || info.size > TRACKER_ADDRESS_SIZE - info.address || info.width == 0 || info.height == 0 || info.depth == 0 || info.levels == 0 || info.levels >= 16 || info.view_levels == 0 || info.base_level + info.view_levels > info.levels) { EXIT("TextureCache: invalid sampled-image request, command=%p addr=0x%016" PRIx64 " size=0x%016" PRIx64 " extent=%ux%ux%u levels=%u\n", static_cast(&command), info.address, info.size, info.width, info.height, info.depth, info.levels); } std::lock_guard transaction(m_resource_mutex); { FaultSafeTextureLock lock(this, m_lock); RetireSampledTargetAliases(info); } BufferImageCopySource source {nullptr, 0, info.address, info.size, true}; if (m_buffer_cache.HasPageOverlap(info.address, info.size)) { // ObtainBufferForImage publishes dirty native-buffer bytes into coherent guest backing. source = m_buffer_cache.ObtainBufferForImage(info.address, info.size); if (!IsCoherentGuestImageSource(source, info.address, info.size)) { EXIT("TextureCache: sampled-image buffer source is inconsistent, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " source=%p source_addr=0x%016" PRIx64 " source_size=0x%016" PRIx64 " current=%d\n", info.address, info.size, static_cast(source.buffer), source.address, source.size, source.cpu_current); } } FaultSafeTextureLock lock(this, m_lock); if (HasMetaOverlapLocked(info.address, info.size) != metadata_read) { EXIT("TextureCache: sampled-image metadata classification mismatch, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " requested=%d\n", info.address, info.size, metadata_read); } std::shared_ptr storage_match; std::vector storage_retire; const auto requested_view_format = TextureGetFormat(info.format); for (auto& cached: m_images) { if (cached->kind != CachedImage::Kind::StorageTexture) { continue; } const bool tracker_gpu = m_memory_tracker.IsRegionGpuModified(cached->info.address, cached->info.size); const bool cpu_dirty = cached->info.IsCpuDirty() || m_memory_tracker.IsRegionCpuModified(cached->info.address, cached->info.size); const bool exact_mip = IsExactRenderTargetMipStorage( info, cached->info, requested_view_format, cached->image->format); switch (ClassifyStorageSampledOverlap( info, cached->info, requested_view_format, cached->image->format, cached->gpu_modified, cpu_dirty, exact_mip, cached->buffer_modified, tracker_gpu)) { case StorageSampledOverlap::None: break; case StorageSampledOverlap::ExactImage: if (storage_match != nullptr) { EXIT("TextureCache: duplicate exact storage image for sampled binding, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", info.address, info.size); } storage_match = cached; break; case StorageSampledOverlap::RetireStorage: storage_retire.push_back(cached.get()); break; case StorageSampledOverlap::Unsupported: EXIT("TextureCache: unsupported sampled/storage image alias, " "requested=0x%016" PRIx64 "+0x%016" PRIx64 " storage=0x%016" PRIx64 "+0x%016" PRIx64 " gpu_modified=%d buffer_modified=%d tracker_gpu=%d cpu_dirty=%d" " exact_mip=%d" " requested_info={format=%u extent=%ux%ux%u pitch=%u base=%u levels=%u" " view_levels=%u tile=%u swizzle=0x%03x type=%u base_array=%u}" " storage_info={format=%u extent=%ux%ux%u pitch=%u base=%u levels=%u" " view_levels=%u tile=%u swizzle=0x%03x type=%u base_array=%u}\n", info.address, info.size, cached->info.address, cached->info.size, cached->gpu_modified, cached->buffer_modified, tracker_gpu, cpu_dirty, exact_mip, info.format, info.width, info.height, info.depth, info.pitch, info.base_level, info.levels, info.view_levels, info.tile, info.swizzle, info.type, info.base_array, cached->info.format, cached->info.width, cached->info.height, cached->info.depth, cached->info.pitch, cached->info.base_level, cached->info.levels, cached->info.view_levels, cached->info.tile, cached->info.swizzle, cached->info.type, cached->info.base_array); } } if (storage_match != nullptr && !storage_retire.empty()) { EXIT("TextureCache: sampled binding has both exact and mip storage owners, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " mip_owners=%zu\n", info.address, info.size, storage_retire.size()); } if (storage_match != nullptr) { if (m_memory_tracker.IsRegionCpuModified(info.address, info.size) || !m_memory_tracker.IsRegionGpuModified(info.address, info.size) || storage_match->image->type != VulkanImageType::StorageTexture || storage_match->image->image_view[VulkanImage::VIEW_DEFAULT] == nullptr) { EXIT("TextureCache: sampled storage-image ownership or view is inconsistent, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", info.address, info.size); } command.RetainResourceUntilFence(storage_match); return *storage_match->image; } if (!storage_retire.empty()) { // Resolve a modified cached mip before replacing it with the containing image. // Kyty does not yet copy between those independently allocated Vulkan images, so use the // existing synchronized tiled readback seam and rebuild the complete chain from coherent // guest backing. This is an uncommon ownership transition, not a frame lookup fast path. Transfer::WaitForQueueIdle(); for (auto* cached: storage_retire) { if (!cached->gpu_modified || cached->buffer_modified || cached->info.IsCpuDirty() || !m_memory_tracker.IsRegionGpuModified(cached->info.address, cached->info.size) || m_memory_tracker.IsRegionCpuModified(cached->info.address, cached->info.size)) { EXIT("TextureCache: sampled mip-storage ownership changed during transition, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " gpu=%d buffer=%d dirty=%d\n", cached->info.address, cached->info.size, cached->gpu_modified, cached->buffer_modified, cached->info.IsCpuDirty()); } const auto transfer = m_readback->DownloadColorImage(*cached); for (const auto& range: transfer.Ranges()) { m_memory_tracker.ForEachDownloadRange(range.address, range.size, [](uint64_t, uint64_t) noexcept {}); } cached->gpu_modified = false; } RetireImages(storage_retire); } if (m_memory_tracker.IsRegionCpuModified(info.address, info.size)) { MarkSampledAliasesCpuDirtyLocked(info.address, info.size); } std::shared_ptr match; for (auto& cached: m_images) { if (cached->kind != CachedImage::Kind::Texture || !Equal(info, cached->info)) { continue; } if (match != nullptr || cached->gpu_modified) { EXIT("TextureCache: invalid exact sampled-image cache match, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " duplicate=%d gpu_modified=%d\n", info.address, info.size, match != nullptr, cached->gpu_modified); } match = cached; } if (match != nullptr) { if (match->info.IsMaybeCpuDirty() && !match->info.IsDefinitelyCpuDirty()) { const bool changed = match->info.ResolveMaybeCpuHash(HashSampledImageEdges(match->info)); if (!changed) { m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, []() noexcept {}); } } const bool buffer_dirty = match->buffer_modified; if (buffer_dirty) { if (match->gpu_modified || !IsCoherentGuestImageSource(source, info.address, info.size)) { EXIT("TextureCache: sampled-image refresh has invalid buffer ownership, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " source=%p current=%d gpu_modified=%d\n", info.address, info.size, static_cast(source.buffer), source.cpu_current, match->gpu_modified); } m_memory_tracker.MarkRegionAsCpuModified(info.address, info.size); } if (match->info.IsCpuDirty() || buffer_dirty) { m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { m_tiler.DetileImage(*static_cast(match->image), match->info, source, true, false); }); if (match->info.IsCpuDirty()) { match->info.RefreshComplete(); } match->buffer_modified = false; } command.RetainResourceUntilFence(match); return *match->image; } for (const auto& cached: m_images) { if (cached->kind != CachedImage::Kind::Texture) { if (!cached->OverlapsRange(info.address, info.size, true)) { continue; } EXIT("TextureCache: sampled image overlaps GPU target, requested=0x%016" PRIx64 "+0x%016" PRIx64 " target=0x%016" PRIx64 "+0x%016" PRIx64 " target_kind=%u requested_info={format=%u extent=%ux%ux%u pitch=%u levels=%u" " tile=%u type=%u}\n", info.address, info.size, cached->Address(), cached->Size(), static_cast(cached->kind), info.format, info.width, info.height, info.depth, info.pitch, info.levels, info.tile, info.type); } const auto overlap = ClassifySampledOverlap(info, cached->info, cached->gpu_modified); if (overlap == SampledOverlap::Unsupported) { EXIT("TextureCache: unsupported sampled-texture alias, requested=0x%016" PRIx64 "+0x%016" PRIx64 " existing=0x%016" PRIx64 "+0x%016" PRIx64 " gpu_modified=%d\n", info.address, info.size, cached->info.address, cached->info.size, cached->gpu_modified); } } m_images.reserve(m_images.size() + 1); auto cached = std::make_shared(m_graphics); cached->kind = CachedImage::Kind::Texture; cached->info = info; vk::ComponentMapping components {}; cached->image = ImageOps::CreateTexture(info, false, components); m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { m_tiler.DetileImage(*static_cast(cached->image), cached->info, source, false, false); }); ImageOps::CreateTextureViews(*static_cast(cached->image), info, false, components); return PublishImage(command, std::move(cached)); } StorageTextureVulkanImage& TextureCache::FindStorageTexture(CommandBuffer& command, const ImageInfo& info) { const bool supported_type = info.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor2D) || info.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor2DArray) || info.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor3D); const bool supported_depth_tile = info.tile == Prospero::GpuEnumValue(Prospero::TileMode::kDepth) && IsSupportedStorageDepthTile(info.format, info.type, info.width, info.height, info.depth); if (info.address == 0 || info.size == 0 || info.address >= TRACKER_ADDRESS_SIZE || info.size > TRACKER_ADDRESS_SIZE - info.address || info.width == 0 || info.height == 0 || info.depth == 0 || info.levels == 0 || info.levels > 16 || info.base_level >= info.levels || info.view_levels != 1 || info.base_array != 0 || !supported_type || (info.tile != Prospero::GpuEnumValue(Prospero::TileMode::kLinear) && info.tile != Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget) && !supported_depth_tile) || !IsSupportedStorageSwizzle(info.format, info.swizzle)) { EXIT("TextureCache: unsupported storage-image request, command=%p " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " extent=%ux%ux%u levels=%u base_level=%u base_array=%u type=%u tile=%u " "swizzle=0x%03x\n", static_cast(&command), info.address, info.size, info.width, info.height, info.depth, info.levels, info.base_level, info.base_array, info.type, info.tile, info.swizzle); } if ((info.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor2D) && info.depth != 1) || (info.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor3D) && info.depth == 0) || (info.type == Prospero::GpuEnumValue(Prospero::ImageType::kColor2DArray) && info.base_array >= info.depth)) { EXIT("TextureCache: storage-image type and depth disagree, type=%u depth=%u\n", info.type, info.depth); } std::lock_guard transaction(m_resource_mutex); const bool buffer_overlap = m_buffer_cache.HasPageOverlap(info.address, info.size); BufferImageCopySource source {nullptr, 0, info.address, info.size, true}; if (buffer_overlap) { source = m_buffer_cache.ObtainBufferForImage(info.address, info.size); if (!IsCoherentGuestImageSource(source, info.address, info.size)) { EXIT("TextureCache: storage-image buffer source is inconsistent, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " source=%p source_addr=0x%016" PRIx64 " source_size=0x%016" PRIx64 " current=%d\n", info.address, info.size, static_cast(source.buffer), source.address, source.size, source.cpu_current); } } FaultSafeTextureLock lock(this, m_lock); ResolveImageMetadataOverlapsLocked(info.address, info.size); if (supported_depth_tile && info.format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32UInt)) { RetireStorageDepthAliasLocked(info); } std::shared_ptr match; for (auto& cached: m_images) { if (cached->kind != CachedImage::Kind::StorageTexture || !EqualStorageBacking(info, cached->info)) { continue; } if (match != nullptr || cached->info.IsCpuDirty()) { EXIT("TextureCache: invalid exact storage-image cache match, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " duplicate=%d cpu_dirty=%d\n", info.address, info.size, match != nullptr, cached->info.IsCpuDirty()); } match = cached; } if (match != nullptr) { const bool cpu_modified = m_memory_tracker.IsRegionCpuModified(info.address, info.size); const bool gpu_modified = m_memory_tracker.IsRegionGpuModified(info.address, info.size); const auto rebind = ClassifyStorageBufferRebind( buffer_overlap, match->gpu_modified, match->buffer_modified, gpu_modified, cpu_modified, IsCoherentGuestImageSource(source, info.address, info.size)); if (rebind == StorageBufferRebind::Unsupported) { EXIT("TextureCache: storage-image ownership is inconsistent, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " cached_gpu_modified=%d buffer_modified=%d tracker_gpu_modified=%d " "cpu_modified=%d buffer_overlap=%d source_current=%d\n", info.address, info.size, match->gpu_modified, match->buffer_modified, gpu_modified, cpu_modified, buffer_overlap, source.cpu_current); } if (rebind == StorageBufferRebind::RefreshFromBacking) { // The formatted buffer may cover only one page of a packed mip chain. The // synchronization path published the complete tiled backing before that write, // and ObtainBufferForImage has now folded its dirty subrange back into it. m_memory_tracker.MarkRegionAsCpuModified(info.address, info.size); m_memory_tracker.ForEachUploadRange( info.address, info.size, true, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { m_tiler.DetileImage(*static_cast(match->image), match->info, source, true, true); }); match->buffer_modified = false; match->gpu_modified = true; } else if (!gpu_modified) { if (cpu_modified) { m_memory_tracker.ForEachUploadRange( info.address, info.size, true, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { m_tiler.DetileImage( *static_cast(match->image), match->info, {nullptr, 0, match->info.address, match->info.size, true}, true, true); }); } else { // A readback leaves both copies current. Reclaim ownership without an unnecessary // upload through the clean UpdateImage storage-binding path. m_memory_tracker.MarkRegionAsGpuModified(info.address, info.size); } match->gpu_modified = true; } command.RetainResourceUntilFence(match); return *static_cast(match->image); } ResolveStorageImageOverlaps(info); m_images.reserve(m_images.size() + 1); auto cached = std::make_shared(m_graphics); cached->kind = CachedImage::Kind::StorageTexture; cached->info = info; vk::ComponentMapping components {}; cached->image = ImageOps::CreateTexture(info, true, components); m_memory_tracker.ForEachUploadRange( info.address, info.size, true, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { m_tiler.DetileImage(*static_cast(cached->image), cached->info, source, false, true); }); cached->gpu_modified = true; ImageOps::CreateTextureViews(*static_cast(cached->image), info, true, components); return static_cast(PublishImage(command, std::move(cached))); } RenderTextureVulkanImage& TextureCache::FindRenderTarget(CommandBuffer& command, const RenderTargetInfo& info) { const bool standard64 = IsSupportedStandard64RenderTarget(info); const bool valid_samples = info.samples == 1 || info.samples == 2 || info.samples == 4 || info.samples == 8; if (info.address == 0 || info.size == 0 || info.address >= TRACKER_ADDRESS_SIZE || info.size > TRACKER_ADDRESS_SIZE - info.address || info.format == vk::Format::eUndefined || info.width == 0 || info.height == 0 || info.pitch < info.width || info.bytes_per_element == 0 || info.levels == 0 || info.levels > 16 || info.layers == 0 || info.size % info.layers != 0 || !valid_samples || (info.samples > 1 && (info.levels != 1 || info.layers != 1 || info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget))) || (info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kLinear) && info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget) && !standard64)) { EXIT("TextureCache: invalid render-target request, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " extent=%ux%u pitch=%u bpe=%u samples=%u tile=%u format=%d\n", info.address, info.size, info.width, info.height, info.pitch, info.bytes_per_element, info.samples, info.tile_mode, static_cast(info.format)); } if (info.levels == 1 && info.tile_mode == Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget)) { TileSizeAlign layout {}; const auto encoded_samples = static_cast(std::countr_zero(info.samples)); if (!TileGetRenderTargetSize(info.width, info.height, info.pitch, info.bytes_per_element, layout, encoded_samples) || layout.size > UINT64_MAX / info.layers || static_cast(layout.size) * info.layers != info.size) { EXIT("TextureCache: invalid render-target block layout, size=0x%016" PRIx64 " expected=0x%08x extent=%ux%u pitch=%u bpe=%u samples=%u\n", info.size, layout.size, info.width, info.height, info.pitch, info.bytes_per_element, info.samples); } } if (info.levels > 1) { TileSizeAlign layout {}; if (info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget) || !TileGetRenderTargetMipLayout(info.width, info.height, info.pitch, info.bytes_per_element, info.levels, layout, nullptr, nullptr) || layout.size > UINT64_MAX / info.layers || static_cast(layout.size) * info.layers != info.size) { EXIT("TextureCache: unsupported render-target mip layout, size=0x%016" PRIx64 " expected=0x%08x extent=%ux%u pitch=%u bpe=%u levels=%u tile=%u\n", info.size, layout.size, info.width, info.height, info.pitch, info.bytes_per_element, info.levels, info.tile_mode); } } if (standard64) { const auto format = ImageOps::RenderTargetTransferFormat(info.bytes_per_element); const auto expected_pitch = TileGetTexturePitch(format, info.width, info.levels, info.tile_mode); TileSizeAlign layout {}; TileGetTextureSize(format, info.width, info.height, expected_pitch, info.levels, info.tile_mode, &layout, nullptr, nullptr); if (expected_pitch != info.pitch || layout.align != 65536 || layout.size != info.size) { EXIT("TextureCache: invalid Standard64KB render-target layout," " size=0x%016" PRIx64 " expected=0x%08x align=0x%08x pitch=%u/%u\n", info.size, layout.size, layout.align, info.pitch, expected_pitch); } } const auto rows = static_cast(info.height - 1); if (rows > (UINT64_MAX - info.width) / info.pitch) { EXIT("TextureCache: render-target element count overflow, extent=%ux%u pitch=%u\n", info.width, info.height, info.pitch); } const auto elements = rows * info.pitch + info.width; if (elements > UINT64_MAX / info.bytes_per_element || elements * info.bytes_per_element > UINT64_MAX / info.layers || info.size < elements * info.bytes_per_element * info.layers) { EXIT("TextureCache: invalid render-target storage, size=0x%016" PRIx64 " elements=0x%016" PRIx64 " bpe=%u\n", info.size, elements, info.bytes_per_element); } std::lock_guard transaction(m_resource_mutex); BufferImageCopySource target_source {nullptr, 0, info.address, info.size, true}; const bool target_buffer_overlap = m_buffer_cache.HasPageOverlap(info.address, info.size); if (target_buffer_overlap) { // A render target may use a containing native buffer after dirty bytes are published or // coherent guest backing for a clean partial view. ObtainBufferForImage keeps GPU-dirty // partial ownership and inconsistent state as hard failures. target_source = m_buffer_cache.ObtainBufferForImage(info.address, info.size); } FaultSafeTextureLock lock(this, m_lock); std::shared_ptr match; for (auto& cached: m_images) { if (cached->kind != CachedImage::Kind::RenderTarget || (!Equal(info, cached->target) && !IsCompatibleRenderTargetView(cached->target, info) && !IsCompatibleRenderTargetBacking(cached->target, info))) { continue; } match = cached; } if (match != nullptr) { ResolveImageMetadataOverlapsLocked(info.address, info.size); if (info.samples > 1 && (match->buffer_modified || m_memory_tracker.IsRegionCpuModified(info.address, info.size))) { EXIT("TextureCache: multisampled render-target refresh is unsupported, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " samples=%u\n", info.address, info.size, info.samples); } if (match->buffer_modified) { if (match->gpu_modified || !IsCoherentGuestImageSource(target_source, info.address, info.size)) { EXIT("TextureCache: render-target refresh has invalid buffer ownership, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " source=%p current=%d gpu_modified=%d\n", info.address, info.size, static_cast(target_source.buffer), target_source.cpu_current, match->gpu_modified); } m_memory_tracker.MarkRegionAsCpuModified(info.address, info.size); } if (!match->gpu_modified && m_memory_tracker.IsRegionCpuModified(info.address, info.size)) { m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { ImageOps::UploadRenderTarget( *static_cast(match->image), info, true); }); match->buffer_modified = false; } command.RetainResourceUntilFence(match); return *static_cast(match->image); } std::vector retire; std::vector> recreated_depth_sources; std::shared_ptr native_image_source; for (const auto& entry: m_images) { auto& cached = *entry; if (!cached.OverlapsRange(info.address, info.size, true)) { continue; } RenderTargetOverlap overlap = RenderTargetOverlap::Unsupported; switch (cached.kind) { case CachedImage::Kind::Texture: overlap = ClassifyRenderTargetOverlap(cached.info, cached.gpu_modified, info); break; case CachedImage::Kind::StorageTexture: overlap = ClassifyStorageRenderTargetOverlap( cached.info, cached.image->format, cached.gpu_modified, cached.buffer_modified, cached.info.IsCpuDirty() || m_memory_tracker.IsRegionCpuModified(cached.info.address, cached.info.size), m_memory_tracker.IsRegionGpuModified(cached.info.address, cached.info.size), info); break; case CachedImage::Kind::RenderTarget: overlap = ClassifyRenderTargetOverlap( cached.target, cached.gpu_modified, cached.buffer_modified, m_memory_tracker.IsRegionGpuModified(cached.target.address, cached.target.size), IsCoherentGuestImageSource(target_source, info.address, info.size), info); break; case CachedImage::Kind::DepthTarget: { bool tracker_gpu_modified = false; for (uint32_t range = 0; range < cached.RangeCount(); range++) { tracker_gpu_modified |= m_memory_tracker.IsRegionGpuModified( cached.Address(range), cached.Size(range)); } if (CanRecreateDepthForRenderTarget( cached.depth, cached.gpu_modified, cached.buffer_modified, tracker_gpu_modified, IsCoherentGuestImageSource(target_source, info.address, info.size), info)) { overlap = RenderTargetOverlap::RetireTarget; } } break; case CachedImage::Kind::VideoOut: break; } if (overlap == RenderTargetOverlap::None) { continue; } bool supported = false; switch (overlap) { case RenderTargetOverlap::RetireSampled: supported = cached.kind == CachedImage::Kind::Texture; break; case RenderTargetOverlap::RetireStorage: supported = cached.kind == CachedImage::Kind::StorageTexture; break; case RenderTargetOverlap::PreserveStorage: supported = cached.kind == CachedImage::Kind::StorageTexture && native_image_source == nullptr; if (supported) { native_image_source = entry; } break; case RenderTargetOverlap::ExpandTarget: supported = cached.kind == CachedImage::Kind::RenderTarget && native_image_source == nullptr; if (supported) { native_image_source = entry; } break; case RenderTargetOverlap::RetireTarget: supported = cached.kind == CachedImage::Kind::RenderTarget || cached.kind == CachedImage::Kind::DepthTarget; if (supported && cached.kind == CachedImage::Kind::DepthTarget) { recreated_depth_sources.push_back(entry); } break; case RenderTargetOverlap::None: case RenderTargetOverlap::Unsupported: break; } if (!supported) { EXIT("TextureCache: unsupported render-target alias, requested=0x%016" PRIx64 "+0x%016" PRIx64 " existing_kind=%u existing=0x%016" PRIx64 "+0x%016" PRIx64 " gpu_modified=%d" " sampled_format=%u extent=%ux%ux%u pitch=%u levels=%u base_level=%u" " tile=%u type=%u base_array=%u\n", info.address, info.size, static_cast(cached.kind), cached.Address(), cached.Size(), cached.gpu_modified, cached.info.format, cached.info.width, cached.info.height, cached.info.depth, cached.info.pitch, cached.info.levels, cached.info.base_level, cached.info.tile, cached.info.type, cached.info.base_array); } retire.push_back(&cached); } RequireRetirementIsolation(retire, "render target", info.address, info.size); MaterializeImagesToGuestLocked(recreated_depth_sources); for (const auto& cached: recreated_depth_sources) { // The exact guest range owns the current bytes. Clear a stale buffer marker only after // source-coherence validation so retirement can replace the obsolete Vulkan shape. cached->buffer_modified = false; } RetireDepthMetadataLocked(retire); RetireImages(retire, native_image_source.get()); ResolveImageMetadataOverlapsLocked(info.address, info.size); auto cached = std::make_shared(m_graphics); cached->kind = CachedImage::Kind::RenderTarget; cached->target = info; cached->image = ImageOps::CreateRenderTarget(info); const bool preserve_native = native_image_source != nullptr; if (preserve_native) { command.RetainResourceUntilFence(native_image_source); if (native_image_source->kind == CachedImage::Kind::RenderTarget) { const auto& old = native_image_source->target; const auto tail_address = info.address + old.size; const auto tail_size = info.size - old.size; if (old.layers >= info.layers || tail_size == 0 || !IsCoherentGuestImageSource(target_source, info.address, info.size)) { EXIT("TextureCache: invalid render-target expansion source, old_layers=%u " "new_layers=%u old_size=0x%016" PRIx64 " new_size=0x%016" PRIx64 "\n", old.layers, info.layers, old.size, info.size); } m_memory_tracker.ForEachUploadRange( tail_address, tail_size, true, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { ImageOps::UploadRenderTargetLayers( *static_cast(cached->image), info, old.layers, info.layers - old.layers, false); }); } std::vector regions; regions.reserve(static_cast(native_image_source->image->layers) * native_image_source->image->mip_levels); AppendLayerCopies(regions, *native_image_source->image, vk::ImageAspectFlagBits::eColor); Transfer::CopyImage(command, regions, *cached->image, RENDER_COLOR_IMAGE_LAYOUT); } else { if (info.samples > 1) { if (!IsCoherentGuestImageSource(target_source, info.address, info.size) || !GuestRangeIsZero(info.address, info.size)) { EXIT("TextureCache: nonzero multisampled render-target upload is unsupported, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " samples=%u\n", info.address, info.size, info.samples); } m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, []() noexcept {}); static_cast(cached->image)->initial_clear_pending = true; } else { m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { ImageOps::UploadRenderTarget( *static_cast(cached->image), info, false); }); } } cached->gpu_modified = preserve_native; return static_cast(PublishImage(command, std::move(cached))); } DepthStencilVulkanImage& TextureCache::FindDepthTarget(CommandBuffer& command, const DepthTargetInfo& info) { const bool has_stencil = info.stencil_address != 0 || info.stencil_size != 0; const bool has_htile = info.htile_address != 0 || info.htile_size != 0; const bool valid_samples = info.samples == 1 || info.samples == 2 || info.samples == 4 || info.samples == 8; if (info.address == 0 || info.size == 0 || info.address >= TRACKER_ADDRESS_SIZE || info.size > TRACKER_ADDRESS_SIZE - info.address || (info.address & 0xffffu) != 0 || info.width == 0 || info.height == 0 || info.pitch < info.width || info.layers == 0 || !valid_samples || (info.samples > 1 && info.layers != 1) || info.size % info.layers != 0 || info.size > UINT32_MAX || info.stencil_size > UINT32_MAX || info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kDepth) || (has_stencil && (info.stencil_address == 0 || info.stencil_size == 0 || info.stencil_size % info.layers != 0 || info.stencil_address >= TRACKER_ADDRESS_SIZE || info.stencil_size > TRACKER_ADDRESS_SIZE - info.stencil_address || (info.stencil_address & 0xffffu) != 0 || ImagePageRangesOverlap(info.address, info.size, info.stencil_address, info.stencil_size))) || ((info.htile_address == 0) != (info.htile_size == 0)) || (has_htile && (info.htile_size % info.layers != 0 || info.htile_address >= TRACKER_ADDRESS_SIZE || info.htile_size > TRACKER_ADDRESS_SIZE - info.htile_address || (info.htile_address & 0x7fffu) != 0 || ImagePageRangesOverlap(info.address, info.size, info.htile_address, info.htile_size) || (has_stencil && ImagePageRangesOverlap(info.stencil_address, info.stencil_size, info.htile_address, info.htile_size)))) || (info.stencil_htile_compressed && (!has_stencil || !has_htile)) || !IsSupportedDepthTargetFormat(info)) { EXIT("TextureCache: unsupported depth target, command=%p depth=0x%016" PRIx64 "+0x%016" PRIx64 " stencil=0x%016" PRIx64 "+0x%016" PRIx64 " htile=0x%016" PRIx64 "+0x%016" PRIx64 " extent=%ux%u pitch=%u samples=%u tile=%u format=%d guest_format=%u bpe=%u\n", static_cast(&command), info.address, info.size, info.stencil_address, info.stencil_size, info.htile_address, info.htile_size, info.width, info.height, info.pitch, info.samples, info.tile_mode, static_cast(info.format), info.guest_format, info.bytes_per_element); } const auto* depth_policy = FindGuestDepthFormatPolicy(info.guest_format); TileSizeAlign expected_depth {}; TileSizeAlign expected_stencil {}; TileSizeAlign expected_htile {}; const auto encoded_samples = static_cast(std::countr_zero(info.samples)); if (depth_policy == nullptr || depth_policy->bytes_per_element != info.bytes_per_element || info.pitch != TileGetDepthPitch(info.width, info.bytes_per_element, encoded_samples) || !TileGetDepthSize( info.width, info.height, 0, Prospero::GpuEnumValue(depth_policy->depth_format), Prospero::GpuEnumValue(has_stencil ? Prospero::StencilFormat::k8UInt : Prospero::StencilFormat::kInvalid), has_htile, expected_stencil, expected_htile, expected_depth, encoded_samples) || expected_depth.size > UINT64_MAX / info.layers || static_cast(expected_depth.size) * info.layers != info.size || (has_stencil && (expected_stencil.size > UINT64_MAX / info.layers || static_cast(expected_stencil.size) * info.layers != info.stencil_size)) || (has_htile && (expected_htile.size > UINT64_MAX / info.layers || static_cast(expected_htile.size) * info.layers != info.htile_size))) { EXIT("TextureCache: invalid depth-target block layout, depth=0x%016" PRIx64 "+0x%016" PRIx64 " stencil=0x%016" PRIx64 "+0x%016" PRIx64 " htile=0x%016" PRIx64 "+0x%016" PRIx64 " extent=%ux%u pitch=%u samples=%u\n", info.address, info.size, info.stencil_address, info.stencil_size, info.htile_address, info.htile_size, info.width, info.height, info.pitch, info.samples); } const auto rows = static_cast(info.height - 1); if (rows > (UINT64_MAX - info.width) / info.pitch) { EXIT("TextureCache: depth-target element count overflow, extent=%ux%u pitch=%u\n", info.width, info.height, info.pitch); } const auto elements = rows * info.pitch + info.width; if (elements > UINT64_MAX / info.bytes_per_element / info.samples || elements * info.bytes_per_element * info.samples > UINT64_MAX / info.layers || info.size < elements * info.bytes_per_element * info.samples * info.layers) { EXIT("TextureCache: depth storage is too small, size=0x%016" PRIx64 " elements=0x%016" PRIx64 " bpe=%u\n", info.size, elements, info.bytes_per_element); } if (has_stencil && (elements > UINT64_MAX / info.samples / info.layers || info.stencil_size < elements * info.samples * info.layers)) { EXIT("TextureCache: stencil storage is too small, size=0x%016" PRIx64 " elements=0x%016" PRIx64 "\n", info.stencil_size, elements); } if (has_stencil && !info.stencil_load_clear && !CanLoadRawStencilPlane(info)) { EXIT("TextureCache: HTile-compressed stencil load is unsupported, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " htile=0x%016" PRIx64 "+0x%016" PRIx64 "\n", info.stencil_address, info.stencil_size, info.htile_address, info.htile_size); } if (has_htile) { RegisterMeta(info.htile_address, info.htile_size, info.layers); } std::lock_guard transaction(m_resource_mutex); // BufferCache treats an untracked guest range as CPU-current. That is useful when // uploading from guest memory, but it is not evidence that a clean native image is // stale. Only a genuinely overlapping buffer participates in transition selection. const bool depth_buffer_overlap = m_buffer_cache.HasPageOverlap(info.address, info.size); const auto depth_source = m_buffer_cache.ObtainBufferForImage(info.address, info.size); const auto stencil_source = has_stencil ? m_buffer_cache.ObtainBufferForImage(info.stencil_address, info.stencil_size) : BufferImageCopySource {}; FaultSafeTextureLock lock(this, m_lock); ResolveImageMetadataOverlapsLocked(info.address, info.size); if (has_stencil) { ResolveImageMetadataOverlapsLocked(info.stencil_address, info.stencil_size); } std::shared_ptr match; for (auto& cached: m_images) { if (cached->kind != CachedImage::Kind::DepthTarget || (!Equal(info, cached->depth) && !IsCompatibleDepthTargetBacking(cached->depth, info))) { continue; } match = cached; } if (match != nullptr) { const bool depth_cpu_modified = m_memory_tracker.IsRegionCpuModified(info.address, info.size); const bool stencil_cpu_modified = has_stencil && m_memory_tracker.IsRegionCpuModified(info.stencil_address, info.stencil_size); if (RequiresMultisampleDepthRefresh(info, match->buffer_modified, depth_cpu_modified, stencil_cpu_modified)) { EXIT("TextureCache: multisampled depth-target refresh is unsupported, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " samples=%u access=%d/%d clear=%d/%d buffer=%d cpu=%d/%d\n", info.address, info.size, info.samples, info.depth_access, info.stencil_access, info.depth_load_clear, info.stencil_load_clear, match->buffer_modified, depth_cpu_modified, stencil_cpu_modified); } if (!CanLoadStencilAttachment(info, match->stencil_initialized)) { EXIT("TextureCache: stencil load requires initialized contents, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", info.stencil_address, info.stencil_size); } if (has_stencil && info.stencil_load_clear) { match->stencil_initialized = true; } if (match->buffer_modified) { if (match->gpu_modified || !depth_buffer_overlap || !IsCoherentGuestImageSource(depth_source, info.address, info.size)) { EXIT("TextureCache: depth-target refresh has invalid buffer ownership, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " gpu_modified=%d overlap=%d source_current=%d\n", info.address, info.size, match->gpu_modified, depth_buffer_overlap, depth_source.cpu_current); } m_memory_tracker.MarkRegionAsCpuModified(info.address, info.size); m_memory_tracker.ForEachUploadRange( info.address, info.size, true, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { m_tiler.DetileImage(*static_cast(match->image), info, depth_source, true); }); match->buffer_modified = false; match->gpu_modified = true; } else if (!match->gpu_modified && depth_source.cpu_dirty) { m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { m_tiler.DetileImage(*static_cast(match->image), info, depth_source, true); }); } if (!match->gpu_modified && has_stencil && stencil_source.cpu_dirty) { m_memory_tracker.ForEachUploadRange( info.stencil_address, info.stencil_size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { if (!info.stencil_load_clear) { m_tiler.DetileStencil(*static_cast(match->image), info, stencil_source, true); match->stencil_initialized = true; } }); } command.RetainResourceUntilFence(match); return *static_cast(match->image); } std::vector retire; std::shared_ptr sampled_depth_source; std::shared_ptr native_depth_source; std::shared_ptr discarded_depth_source; std::shared_ptr retired_storage_source; std::vector> recreated_target_sources; for (const auto& entry: m_images) { auto& cached = *entry; const bool overlaps = cached.OverlapsRange(info.address, info.size, true) || (has_stencil && cached.OverlapsRange(info.stencil_address, info.stencil_size, true)); if (!overlaps) { continue; } DepthOverlap overlap = DepthOverlap::Unsupported; switch (cached.kind) { case CachedImage::Kind::Texture: { const auto native_overlap = ClassifyDepthOverlap(cached.info, cached.gpu_modified, info); const bool overlaps_depth = ImageRangeOverlaps( cached.info.address, cached.info.size, info.address, info.size); const bool overlaps_stencil = has_stencil && ImageRangeOverlaps(cached.info.address, cached.info.size, info.stencil_address, info.stencil_size); const bool guest_source_current = (!overlaps_depth || info.depth_load_clear || IsCoherentGuestImageSource(depth_source, info.address, info.size)) && (!overlaps_stencil || info.stencil_load_clear || IsCoherentGuestImageSource(stencil_source, info.stencil_address, info.stencil_size)); overlap = native_overlap; if (overlap == DepthOverlap::Unsupported && CanRetireGuestCurrentSampledForDepth( cached.info, info, cached.gpu_modified, cached.buffer_modified, cached.info.IsCpuDirty(), m_memory_tracker.IsRegionGpuModified(cached.info.address, cached.info.size), guest_source_current)) { overlap = DepthOverlap::RetireSampled; } if (overlap == DepthOverlap::RetireSampled && !info.depth_load_clear && native_overlap == DepthOverlap::RetireSampled && sampled_depth_source == nullptr) { sampled_depth_source = entry; } } break; case CachedImage::Kind::DepthTarget: overlap = ClassifyDepthTargetOverlap(cached.depth, cached.gpu_modified, cached.buffer_modified, info); break; case CachedImage::Kind::StorageTexture: overlap = ClassifyStorageDepthOverlap( cached.info, cached.gpu_modified, cached.buffer_modified, cached.info.IsCpuDirty() || m_memory_tracker.IsRegionCpuModified(cached.info.address, cached.info.size), m_memory_tracker.IsRegionGpuModified(cached.info.address, cached.info.size), info); break; case CachedImage::Kind::RenderTarget: { const bool overlaps_depth = ImageRangeOverlaps( cached.target.address, cached.target.size, info.address, info.size); const bool overlaps_stencil = has_stencil && ImageRangeOverlaps(cached.target.address, cached.target.size, info.stencil_address, info.stencil_size); const bool guest_source_current = (!overlaps_depth || info.depth_load_clear || IsCoherentGuestImageSource(depth_source, info.address, info.size)) && (!overlaps_stencil || info.stencil_load_clear || IsCoherentGuestImageSource(stencil_source, info.stencil_address, info.stencil_size)); overlap = CanRecreateRenderTargetForDepth( cached.target, cached.gpu_modified, cached.buffer_modified, m_memory_tracker.IsRegionGpuModified(cached.target.address, cached.target.size), guest_source_current, info) ? DepthOverlap::RecreateTarget : DepthOverlap::Unsupported; } break; case CachedImage::Kind::VideoOut: break; } bool supported = false; switch (overlap) { case DepthOverlap::RetireSampled: supported = cached.kind == CachedImage::Kind::Texture; break; case DepthOverlap::RetireStorage: supported = cached.kind == CachedImage::Kind::StorageTexture && retired_storage_source == nullptr && native_depth_source == nullptr && discarded_depth_source == nullptr && recreated_target_sources.empty(); if (supported) { retired_storage_source = entry; } break; case DepthOverlap::ExpandTarget: supported = cached.kind == CachedImage::Kind::DepthTarget && native_depth_source == nullptr && retired_storage_source == nullptr && recreated_target_sources.empty(); if (supported) { native_depth_source = entry; } break; case DepthOverlap::DiscardTarget: supported = cached.kind == CachedImage::Kind::DepthTarget && discarded_depth_source == nullptr && native_depth_source == nullptr; supported = supported && retired_storage_source == nullptr && recreated_target_sources.empty(); if (supported) { discarded_depth_source = entry; } break; case DepthOverlap::RecreateTarget: supported = (cached.kind == CachedImage::Kind::DepthTarget || cached.kind == CachedImage::Kind::RenderTarget) && native_depth_source == nullptr && discarded_depth_source == nullptr && retired_storage_source == nullptr; if (supported) { recreated_target_sources.push_back(entry); } break; case DepthOverlap::None: case DepthOverlap::Unsupported: break; } if (!supported) { EXIT("TextureCache: unsupported depth-target alias, depth=0x%016" PRIx64 "+0x%016" PRIx64 " existing_kind=%u existing=0x%016" PRIx64 "+0x%016" PRIx64 "\n", info.address, info.size, static_cast(cached.kind), cached.Address(), cached.Size()); } retire.push_back(&cached); } RequireRetirementIsolation(retire, "depth target", info.address, info.size); MaterializeImagesToGuestLocked(recreated_target_sources); if (retired_storage_source != nullptr) { if (retired_storage_source->gpu_modified) { m_memory_tracker.UnmarkRegionAsGpuModified(retired_storage_source->info.address, retired_storage_source->info.size); retired_storage_source->gpu_modified = false; } } const auto* transition_source = native_depth_source != nullptr ? native_depth_source.get() : (discarded_depth_source != nullptr ? discarded_depth_source.get() : nullptr); RetireDepthMetadataLocked(retire, info.htile_address); RetireImages(retire, transition_source); const bool coherent_guest_stencil = has_stencil && IsCoherentGuestImageSource(stencil_source, info.stencil_address, info.stencil_size); // Native expansion must preserve old layers. Guest data initializes only a wholly new image. const bool stencil_contents_available = native_depth_source != nullptr ? native_depth_source->stencil_initialized : coherent_guest_stencil; if (!CanLoadStencilAttachment(info, stencil_contents_available)) { EXIT("TextureCache: new stencil target requires a clear before stencil access, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", info.stencil_address, info.stencil_size); } if (info.samples > 1 && (native_depth_source != nullptr || sampled_depth_source != nullptr || retired_storage_source != nullptr)) { EXIT("TextureCache: multisampled depth native transition is unsupported, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " samples=%u\n", info.address, info.size, info.samples); } const bool initial_depth_clear = info.samples > 1 && !info.depth_load_clear; const bool initial_stencil_clear = info.samples > 1 && has_stencil && !info.stencil_load_clear; if (initial_depth_clear && (!IsCoherentGuestImageSource(depth_source, info.address, info.size) || !GuestRangeIsZero(info.address, info.size))) { EXIT("TextureCache: nonzero multisampled depth upload is unsupported, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " samples=%u\n", info.address, info.size, info.samples); } if (initial_stencil_clear && (!coherent_guest_stencil || !GuestRangeIsZero(info.stencil_address, info.stencil_size))) { EXIT("TextureCache: nonzero multisampled stencil upload is unsupported, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " samples=%u\n", info.stencil_address, info.stencil_size, info.samples); } auto cached = std::make_shared(m_graphics); cached->kind = CachedImage::Kind::DepthTarget; cached->depth = info; cached->stencil_initialized = !has_stencil || info.stencil_load_clear || initial_stencil_clear; cached->image = ImageOps::CreateDepthTarget(info); auto* depth_image = static_cast(cached->image); depth_image->initial_depth_clear_pending = initial_depth_clear; depth_image->initial_stencil_clear_pending = initial_stencil_clear; if (discarded_depth_source != nullptr) { command.RetainResourceUntilFence(discarded_depth_source); } if (retired_storage_source != nullptr) { command.RetainResourceUntilFence(retired_storage_source); } if (native_depth_source != nullptr) { const auto& old = native_depth_source->depth; if (old.layers >= info.layers || native_depth_source->image->layers != old.layers) { EXIT("TextureCache: invalid depth expansion source, old_layers=%u new_layers=%u\n", old.layers, info.layers); } command.RetainResourceUntilFence(native_depth_source); const auto tail = SelectDepthLayers(info, old.layers, info.layers - old.layers); const auto depth_tail = SelectSourceRange(depth_source, tail.address, tail.size); if (!info.depth_load_clear && !IsCoherentGuestImageSource(depth_tail, tail.address, tail.size)) { EXIT("TextureCache: depth expansion tail is not CPU-current\n"); } m_memory_tracker.ForEachUploadRange( tail.address, tail.size, true, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { if (!info.depth_load_clear) { m_tiler.DetileImage(*static_cast(cached->image), tail, depth_tail, false, old.layers); } }); if (has_stencil) { const auto stencil_tail = SelectSourceRange(stencil_source, tail.stencil_address, tail.stencil_size); if (!info.stencil_load_clear && !IsCoherentGuestImageSource(stencil_tail, tail.stencil_address, tail.stencil_size)) { EXIT("TextureCache: stencil expansion tail is not CPU-current\n"); } m_memory_tracker.ForEachUploadRange( tail.stencil_address, tail.stencil_size, true, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { if (!info.stencil_load_clear) { m_tiler.DetileStencil(*static_cast(cached->image), tail, stencil_tail, false, old.layers); } }); cached->stencil_initialized = true; } std::vector regions; regions.reserve(native_depth_source->image->layers * (has_stencil ? 2u : 1u)); AppendLayerCopies(regions, *native_depth_source->image, vk::ImageAspectFlagBits::eDepth); if (has_stencil) { AppendLayerCopies(regions, *native_depth_source->image, vk::ImageAspectFlagBits::eStencil); } Transfer::CopyImage(command, regions, *cached->image, vk::ImageLayout::eDepthStencilAttachmentOptimal); cached->gpu_modified = true; } else { if (info.samples > 1) { // Multisample guest planes cannot be copied through Vulkan buffer-image commands. // Their verified-zero initial contents are materialized by the first render pass. m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, []() noexcept {}); if (has_stencil) { m_memory_tracker.ForEachUploadRange( info.stencil_address, info.stencil_size, false, [](uint64_t, uint64_t) noexcept {}, []() noexcept {}); } return static_cast(PublishImage(command, std::move(cached))); } if (sampled_depth_source != nullptr && (sampled_depth_source->image->type != VulkanImageType::Texture || sampled_depth_source->image->format != VulkanFormat(info.guest_format) || sampled_depth_source->image->extent.width != info.width || sampled_depth_source->image->extent.height != info.height)) { EXIT("TextureCache: sampled-depth native source is inconsistent\n"); } const auto transition_source = SelectDepthTransitionSource( info.depth_load_clear, sampled_depth_source != nullptr, sampled_depth_source != nullptr && sampled_depth_source->info.IsCpuDirty(), sampled_depth_source != nullptr && sampled_depth_source->buffer_modified, depth_buffer_overlap, depth_source.cpu_dirty); m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { switch (transition_source) { case DepthTransitionSource::None: break; case DepthTransitionSource::Guest: m_tiler.DetileImage(*static_cast(cached->image), info, depth_source, false); break; case DepthTransitionSource::Native: command.RetainResourceUntilFence(sampled_depth_source); Transfer::CopyImageViaBuffer( command, *sampled_depth_source->image, vk::ImageAspectFlagBits::eColor, *cached->image, vk::ImageAspectFlagBits::eDepth, info.bytes_per_element, vk::ImageLayout::eDepthStencilAttachmentOptimal); break; } }); if (has_stencil) { m_memory_tracker.ForEachUploadRange( info.stencil_address, info.stencil_size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { if (!info.stencil_load_clear) { m_tiler.DetileStencil(*static_cast(cached->image), info, stencil_source, false); } }); cached->stencil_initialized = true; } } return static_cast(PublishImage(command, std::move(cached))); } std::vector TextureCache::RegisterVideoOutSurfaces(const std::vector& infos) { if (infos.empty()) { EXIT("TextureCache: video-out registration requires surfaces\n"); } for (const auto& info: infos) { ImageOps::ValidateVideoOut(info); } for (size_t i = 0; i < infos.size(); i++) { for (size_t j = i + 1; j < infos.size(); j++) { if (ImagePageRangesOverlap(infos[i].address, infos[i].size, infos[j].address, infos[j].size)) { EXIT("TextureCache: video-out surfaces share pages, first=%zu second=%zu\n", i, j); } } } std::lock_guard transaction(m_resource_mutex); for (const auto& info: infos) { if (m_buffer_cache.HasPageOverlap(info.address, info.size)) { EXIT("TextureCache: video-out surface aliases buffer pages, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", info.address, info.size); } } FaultSafeTextureLock lock(this, m_lock); for (const auto& info: infos) { ResolveImageMetadataOverlapsLocked(info.address, info.size); for (const auto& cached: m_images) { if (cached->OverlapsRange(info.address, info.size, true)) { EXIT( "TextureCache: video-out surface aliases cached image pages, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " existing_kind=%u\n", info.address, info.size, static_cast(cached->kind)); } } } m_images.reserve(m_images.size() + infos.size()); std::vector result; result.reserve(infos.size()); for (const auto& info: infos) { auto cached = std::make_shared(m_graphics); cached->kind = CachedImage::Kind::VideoOut; cached->video_out = info; cached->image = ImageOps::CreateVideoOut(info); if (info.compression == VideoOutCompression::Uncompressed) { m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { ImageOps::UploadVideoOut(*static_cast(cached->image), info, false); }); } else { // A compressed guest surface cannot be decoded without its DCC metadata. Establish the // normal tracked range, but leave the shared native image to be initialized by a GPU // render/clear before any sampled or presentation read. m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, []() noexcept {}); } result.push_back(static_cast(cached->image)); m_images.push_back(std::move(cached)); RegisterImageLocked(*m_images.back()); } return result; } void TextureCache::RefreshVideoOut(VideoOutVulkanImage& image, bool render_target) { std::lock_guard transaction(m_resource_mutex); FaultSafeTextureLock lock(this, m_lock); const auto it = std::find_if(m_images.begin(), m_images.end(), [&image](const auto& cached) { return cached->image == ℑ }); if (it == m_images.end() || (*it)->kind != CachedImage::Kind::VideoOut) { EXIT("TextureCache: video-out image is not registered, image=%p\n", static_cast(&image)); } auto& cached = **it; if (cached.gpu_modified) { return; } const auto& info = cached.video_out; const bool image_dirty = m_memory_tracker.IsRegionCpuModified(info.address, info.size); const bool buffer_overlap = m_buffer_cache.HasPageOverlap(info.address, info.size); const bool buffer_dirty = cached.buffer_modified || (buffer_overlap && (m_buffer_cache.IsRegionCpuModified(info.address, info.size) || m_buffer_cache.IsRegionGpuModified(info.address, info.size))); if (!image_dirty && !buffer_dirty) { if (info.compression == VideoOutCompression::Uncompressed || CanUseVideoOutNativeWithoutUpload(info.compression, render_target, false, false)) { return; } EXIT("TextureCache: compressed video-out read requires native GPU contents, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", info.address, info.size); } if (info.compression != VideoOutCompression::Uncompressed) { EXIT("TextureCache: compressed video-out guest refresh is unsupported, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " image_dirty=%d buffer_dirty=%d render_target=%d\n", info.address, info.size, image_dirty, buffer_dirty, render_target); } if (buffer_dirty) { const auto source = m_buffer_cache.ObtainBufferForImage(info.address, info.size); if (!IsCoherentGuestImageSource(source, info.address, info.size)) { EXIT("TextureCache: invalid video-out source, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " buffer=%p current=%d source=0x%016" PRIx64 "+0x%016" PRIx64 " buffer_modified=%d\n", info.address, info.size, static_cast(source.buffer), source.cpu_current, source.address, source.size, cached.buffer_modified); } cached.buffer_modified = false; } m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { ImageOps::UploadVideoOut(image, info, true); }); } void TextureCache::UnregisterVideoOutSurfaces(const std::vector& images) { if (images.empty()) { EXIT("TextureCache: video-out unregistration requires surfaces\n"); } std::lock_guard transaction(m_resource_mutex); FaultSafeTextureLock lock(this, m_lock); std::vector selected; selected.reserve(images.size()); for (size_t i = 0; i < images.size(); i++) { auto* image = images[i]; if (image == nullptr || std::find(images.begin(), images.begin() + i, image) != images.begin() + i) { EXIT("TextureCache: invalid or duplicate video-out image at index %zu, image=%p\n", i, static_cast(image)); } auto it = std::find_if(m_images.begin(), m_images.end(), [image](const auto& cached) { return cached->image == image; }); if (it == m_images.end() || (*it)->kind != CachedImage::Kind::VideoOut) { EXIT("TextureCache: video-out image is not registered, index=%zu image=%p\n", i, static_cast(image)); } selected.push_back(it->get()); } for (auto* cached: selected) { if (cached->buffer_modified) { EXIT("TextureCache: cannot unregister a buffer-dirty video-out surface, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", cached->Address(), cached->Size()); } } Transfer::WaitForQueueIdle(); for (auto* cached: selected) { if (cached->gpu_modified) { m_memory_tracker.UnmarkRegionAsGpuModified(cached->Address(), cached->Size()); cached->gpu_modified = false; } UnregisterImageLocked(*cached, true); } for (auto* image: images) { auto it = std::find_if(m_images.begin(), m_images.end(), [image](const auto& cached) { return cached->image == image; }); m_images.erase(it); } } bool TextureCache::ClearImageFromBuffer(CommandBuffer& command, uint64_t vaddr, uint64_t size, uint32_t packed_clear) { if (command.IsInvalid() || vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid compute image clear, command=%p addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", static_cast(&command), vaddr, size); } m_buffer_cache.ValidateGpuAccess(vaddr, size, false, true); std::lock_guard transaction(m_resource_mutex); FaultSafeTextureLock lock(this, m_lock); enum class ClearAspect : uint8_t { None, Color, Depth, Stencil }; auto classify = [vaddr, size](const CachedImage& cached) { const bool color = cached.kind == CachedImage::Kind::VideoOut || cached.kind == CachedImage::Kind::RenderTarget; if (color && cached.Address() == vaddr && cached.Size() == size) { return ClearAspect::Color; } if (cached.kind != CachedImage::Kind::DepthTarget) { return ClearAspect::None; } if (CanNativeClearDepthFromBuffer(cached.depth, vaddr, size)) { return ClearAspect::Depth; } if (cached.RangeCount() == 2 && cached.Address(1) == vaddr && cached.Size(1) == size) { return ClearAspect::Stencil; } return ClearAspect::None; }; std::shared_ptr match; ClearAspect aspect = ClearAspect::None; bool incompatible = false; uint32_t matches = 0; for (const auto& cached: m_images) { if (!cached->OverlapsRange(vaddr, size, true)) { continue; } const auto candidate = classify(*cached); if (candidate == ClearAspect::None) { incompatible = true; continue; } if (aspect == ClearAspect::None) { aspect = candidate; match = cached; } else if (candidate != aspect) { incompatible = true; continue; } matches++; } if (matches > 1) { EXIT("TextureCache: compute image clear has ambiguous exact aliases, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " aspect=%u matches=%u\n", vaddr, size, static_cast(aspect), matches); } if (incompatible || match == nullptr) { return false; } float depth_clear = 0.0f; if (aspect == ClearAspect::Depth && !DecodePackedDepthClear(match->image->format, packed_clear, depth_clear)) { return false; } uint8_t stencil_clear = 0; if (aspect == ClearAspect::Stencil && !DecodePackedStencilClear(packed_clear, stencil_clear)) { return false; } RequireNoMetaOverlapLocked(vaddr, size); const bool buffer_overlap = m_buffer_cache.HasPageOverlap(vaddr, size); const bool buffer_cpu_modified = buffer_overlap && m_buffer_cache.IsRegionCpuModified(vaddr, size); const bool buffer_gpu_modified = buffer_overlap && m_buffer_cache.IsRegionGpuModified(vaddr, size); BufferImageCopySource source {nullptr, 0, vaddr, size, true}; if (buffer_overlap && !buffer_cpu_modified && !buffer_gpu_modified) { source = m_buffer_cache.ObtainBufferForImage(vaddr, size); } const bool buffer_source_valid = !buffer_overlap || IsCoherentGuestImageSource(source, vaddr, size); const bool image_cpu_modified = m_memory_tracker.IsRegionCpuModified(vaddr, size); const bool invalid_common = !buffer_source_valid || buffer_cpu_modified || buffer_gpu_modified || match->buffer_modified || image_cpu_modified; if (aspect == ClearAspect::Color && invalid_common) { EXIT("TextureCache: compute image clear requires exclusive GPU image ownership, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " buffer_overlap=%d source_valid=%d buffer_cpu_modified=%d" " buffer_gpu_modified=%d buffer_modified=%d image_cpu_modified=%d\n", vaddr, size, buffer_overlap, buffer_source_valid, buffer_cpu_modified, buffer_gpu_modified, match->buffer_modified, image_cpu_modified); } if (aspect != ClearAspect::Color && (!match->gpu_modified || invalid_common)) { const bool depth = aspect == ClearAspect::Depth; const char* plane = depth ? "depth" : "stencil"; const char* source_name = depth ? "buffer" : "stencil"; EXIT("TextureCache: compute %s clear requires a GPU-owned depth image and a clean " "%s source, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " gpu_modified=%d buffer_overlap=%d source_valid=%d buffer_cpu_modified=%d" " buffer_gpu_modified=%d buffer_modified=%d image_cpu_modified=%d\n", plane, source_name, vaddr, size, match->gpu_modified, buffer_overlap, buffer_source_valid, buffer_cpu_modified, buffer_gpu_modified, match->buffer_modified, image_cpu_modified); } auto vk_buffer = command.Handle(); if (aspect == ClearAspect::Color) { vk::ClearColorValue clear {}; if (!DecodePackedColorClear(match->image->format, packed_clear, clear)) { return false; } GraphicsRenderColorImageBarrier(vk_buffer, *match->image, vk::ImageLayout::eTransferDstOptimal); const vk::ImageSubresourceRange range {vk::ImageAspectFlagBits::eColor, 0, VK_REMAINING_MIP_LEVELS, 0, match->image->layers}; vk_buffer.clearColorImage(match->image->image, match->image->layout, &clear, 1, &range); GraphicsRenderColorImageBarrier(vk_buffer, *match->image, RENDER_COLOR_IMAGE_LAYOUT); if (!match->gpu_modified) { m_memory_tracker.MarkRegionAsGpuModified(vaddr, size); match->gpu_modified = true; } command.RetainResourceUntilFence(match); return true; } if (match->image->layout == vk::ImageLayout::eUndefined || match->image->layout == vk::ImageLayout::ePreinitialized) { EXIT("TextureCache: compute %s clear has invalid source layout %u\n", aspect == ClearAspect::Depth ? "depth" : "stencil", static_cast(match->image->layout)); } const auto old_layout = match->image->layout; GraphicsRenderDepthStencilImageBarrier(vk_buffer, *match->image, vk::ImageLayout::eTransferDstOptimal); const vk::ClearDepthStencilValue clear {depth_clear, stencil_clear}; const auto clear_aspect = static_cast( aspect == ClearAspect::Depth ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eStencil); const vk::ImageSubresourceRange range {clear_aspect, 0, VK_REMAINING_MIP_LEVELS, 0, match->image->layers}; vk_buffer.clearDepthStencilImage(match->image->image, match->image->layout, &clear, 1, &range); GraphicsRenderDepthStencilImageBarrier(vk_buffer, *match->image, old_layout); if (aspect == ClearAspect::Stencil) { match->stencil_initialized = true; } command.RetainResourceUntilFence(match); return true; } void TextureCache::MarkGpuWritten(VulkanImage& image) { std::lock_guard transaction(m_resource_mutex); FaultSafeTextureLock lock(this, m_lock); for (auto& cached: m_images) { if (cached->image != &image) { continue; } if (cached->kind != CachedImage::Kind::RenderTarget && cached->kind != CachedImage::Kind::DepthTarget && cached->kind != CachedImage::Kind::VideoOut) { EXIT("TextureCache: sampled texture cannot be marked GPU-written, image=%p kind=%u\n", static_cast(&image), static_cast(cached->kind)); } for (uint32_t i = 0; i < cached->RangeCount(); i++) { if (m_buffer_cache.HasPageOverlap(cached->Address(i), cached->Size(i))) { const bool cpu_modified = m_buffer_cache.IsRegionCpuModified(cached->Address(i), cached->Size(i)); const bool gpu_modified = m_buffer_cache.IsRegionGpuModified(cached->Address(i), cached->Size(i)); if (cpu_modified || gpu_modified) { EXIT( "TextureCache: GPU-written image aliases a dirty buffer, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " cpu_modified=%d gpu_modified=%d\n", cached->Address(i), cached->Size(i), cpu_modified, gpu_modified); } } if (m_memory_tracker.IsRegionCpuModified(cached->Address(i), cached->Size(i))) { EXIT("TextureCache: GPU-write begins while image range is CPU-modified, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", cached->Address(i), cached->Size(i)); } } if (cached->kind == CachedImage::Kind::DepthTarget) { if ((cached->depth.htile_address == 0) != (cached->depth.htile_size == 0)) { EXIT("TextureCache: depth target has incomplete HTile range, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", cached->depth.htile_address, cached->depth.htile_size); } if (cached->depth.htile_address != 0) { auto meta = m_surface_metas.find(cached->depth.htile_address); if (meta == m_surface_metas.end() || meta->second.size != cached->depth.htile_size) { EXIT("TextureCache: depth target HTile metadata is missing or mismatched, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", cached->depth.htile_address, cached->depth.htile_size); } m_buffer_cache.ValidateGpuAccess(cached->depth.htile_address, cached->depth.htile_size, false, true); m_metadata_tracker.ForEachUploadRange( cached->depth.htile_address, cached->depth.htile_size, true, [](uint64_t, uint64_t) noexcept {}, []() noexcept {}); meta->second.gpu_modified = true; } } if (!cached->gpu_modified) { for (uint32_t i = 0; i < cached->RangeCount(); i++) { m_memory_tracker.MarkRegionAsGpuModified(cached->Address(i), cached->Size(i)); } cached->gpu_modified = true; } return; } EXIT("TextureCache: GPU-written image is not registered, image=%p\n", static_cast(&image)); } void TextureCache::PrepareHostWrite(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid host-write range, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } FaultSafeTextureLock lock(this, m_lock); const bool metadata_overlap = HasMetaOverlapLocked(vaddr, size); bool found = false; for (const auto& cached: m_images) { for (uint32_t range = 0; range < cached->RangeCount(); range++) { // CPU-current depth targets use the same tracked guest refresh path as sampled and // color images. GPU-owned targets and metadata aliases remain unsupported below. const bool host_refreshable = (cached->kind == CachedImage::Kind::Texture || cached->kind == CachedImage::Kind::RenderTarget || cached->kind == CachedImage::Kind::DepthTarget) && !cached->buffer_modified; switch (ClassifyHostWriteOverlap(vaddr, size, cached->Address(range), cached->Size(range), host_refreshable, cached->gpu_modified, metadata_overlap)) { case HostWriteOverlap::None: break; case HostWriteOverlap::InvalidateImage: found = true; break; case HostWriteOverlap::Unsupported: EXIT("TextureCache: host write aliases unsupported image, " "write=0x%016" PRIx64 "+0x%016" PRIx64 " image=0x%016" PRIx64 "+0x%016" PRIx64 " kind=%u gpu_modified=%d buffer_modified=%d metadata_overlap=%d\n", vaddr, size, cached->Address(range), cached->Size(range), static_cast(cached->kind), cached->gpu_modified, cached->buffer_modified, metadata_overlap); } } } if (!found) { EXIT("TextureCache: host write expected a refreshable image overlap, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } // Explicitly invalidate on a write fault. The resource transaction is already held here, so // writing first would recursively enter the cache. m_memory_tracker.MarkRegionAsCpuModified(vaddr, size); MarkSampledAliasesCpuDirtyLocked(vaddr, size); } void TextureCache::SynchronizeColorImageToBufferLocked(CachedImage& cached, uint64_t write_address, uint64_t write_size) { const bool render_target = cached.kind == CachedImage::Kind::RenderTarget; const bool video_out = cached.kind == CachedImage::Kind::VideoOut; const bool storage = cached.kind == CachedImage::Kind::StorageTexture; RenderTargetInfo target = cached.target; if (storage) { const auto& info = cached.info; target.address = info.address; target.size = info.size; target.format = VulkanFormat(info.format); target.width = info.width; target.height = info.height; target.pitch = info.pitch; target.bytes_per_element = Prospero::RenderTargetBytesPerElement(info.format); target.tile_mode = info.tile; target.levels = info.levels; } if (video_out) { const auto& info = cached.video_out; target.address = info.address; target.size = info.size; target.format = info.format; target.width = info.width; target.height = info.height; target.pitch = info.pitch; target.bytes_per_element = info.bytes_per_element; target.tile_mode = info.tile_mode; } const bool linear = target.tile_mode == Prospero::GpuEnumValue(Prospero::TileMode::kLinear); const bool tiled = IsTiledRenderTarget(target); const bool bgra16 = video_out && cached.video_out.bgra16; TileSizeAlign exact {}; bool single_slice = false; if (IsSupportedStandard64RenderTarget(target)) { exact = {static_cast(target.size), 65536}; single_slice = true; } else { single_slice = TileGetRenderTargetSize(target.width, target.height, target.pitch, target.bytes_per_element, exact); } const bool layered_size = single_slice && static_cast(exact.size) * target.layers == target.size; if (storage && target.levels > 1) { single_slice = TileGetRenderTargetMipLayout(target.width, target.height, target.pitch, target.bytes_per_element, target.levels, exact, nullptr, nullptr); } const bool exact_tiled = tiled && exact.align == 65536 && (storage ? single_slice && exact.size == target.size : layered_size); const bool valid_kind = render_target || storage || (video_out && cached.video_out.compression == VideoOutCompression::Uncompressed); if (!valid_kind || !cached.gpu_modified || cached.buffer_modified || (!storage && target.levels != 1) || target.size > UINT32_MAX || (!linear && !exact_tiled) || HasMetaOverlapLocked(target.address, target.size)) { EXIT("TextureCache: unsupported color-image buffer synchronization, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " extent=%ux%u pitch=%u bpe=%u levels=%u layers=%u tile=%u" " kind=%u compression=%u gpu_modified=%d buffer_modified=%d\n", target.address, target.size, target.width, target.height, target.pitch, target.bytes_per_element, target.levels, target.layers, target.tile_mode, static_cast(cached.kind), video_out ? static_cast(cached.video_out.compression) : 0, cached.gpu_modified, cached.buffer_modified); } if (write_address < target.address || write_size == 0 || write_address - target.address > target.size || write_size > target.size - (write_address - target.address)) { EXIT("TextureCache: image synchronization write is outside backing, " "write=0x%016" PRIx64 "+0x%016" PRIx64 " image=0x%016" PRIx64 "+0x%016" PRIx64 "\n", write_address, write_size, target.address, target.size); } const auto slice_size = target.size / target.layers; if (cached.image->format != target.format || cached.image->extent.width != target.width || cached.image->extent.height != target.height || (tiled && !IsSupportedRenderTargetElementSize(target.bytes_per_element)) || HasMetaOverlapLocked(target.address, target.size)) { EXIT("TextureCache: color-image buffer synchronization storage mismatch, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " linear=0x%016" PRIx64 "\n", target.address, target.size); } Transfer::WaitForQueueIdle(); std::vector regions; std::vector tiled_regions; TextureUploadLayout tiled_layout {}; uint32_t tiled_format = 0; if (storage) { const bool array_texture = TextureIsLayeredTexture(cached.info.type); const bool volume_texture = TextureIs3DTexture(cached.info.type); tiled_format = cached.info.format; tiled_layout = TextureCalcUploadLayout( cached.info.format, cached.info.width, cached.info.height, cached.info.levels, cached.info.depth, cached.info.pitch, cached.info.tile, cached.info.size, true, volume_texture, "StorageTextureReadback"); tiled_regions = TextureBuildUploadRegions( tiled_layout, cached.image->format, cached.info.width, cached.info.height, cached.info.depth, cached.info.levels, array_texture, volume_texture, TextureUploadDestination::MipLevels); regions = TextureBuildDownloadRegions(tiled_regions); } else if (tiled && !bgra16) { tiled_format = ImageOps::RenderTargetTransferFormat(target.bytes_per_element); tiled_layout = TextureCalcUploadLayout( tiled_format, target.width, target.height, target.levels, target.layers, target.pitch, target.tile_mode, target.size, false, false, "ColorBufferTransition"); tiled_regions = TextureBuildUploadRegions( tiled_layout, target.format, target.width, target.height, target.layers, target.levels, target.layers > 1, false, TextureUploadDestination::MipLevels); regions = TextureBuildDownloadRegions(tiled_regions); } else { regions = Transfer::MakeLayeredImageBufferCopies(target.layers, slice_size, target.pitch, target.width, target.height); } if (tiled && !bgra16) { std::vector infos; const uint32_t depth = storage ? cached.info.depth : target.layers; EXIT_NOT_IMPLEMENTED(!TextureBuildGpuTileInfos(target.size, tiled_regions, tiled_layout, tiled_format, depth, target.levels, infos)); m_buffer_transition_guest.resize(target.size); Transfer::DownloadTiledImage(m_buffer_transition_guest.data(), target.size, target.size, infos, regions, *cached.image, cached.image->layout); Libs::LibKernel::Memory::WriteBacking(target.address, m_buffer_transition_guest.data(), target.size); } else if (tiled) { std::vector linear_data(target.size); Transfer::DownloadImage(linear_data.data(), target.size, regions, *cached.image, cached.image->layout); ImageOps::SwapVideoOutBgra16(linear_data.data(), target.size); TileBlockLayout block {}; EXIT_NOT_IMPLEMENTED(!TileGetBlockLayout(TileBlockFamily::RenderTarget64KB, target.bytes_per_element, block)); const GpuTileInfo info { block.family, block.bytes_per_element, 0, target.size, 0, target.size, 0, target.width, target.height, 1, target.pitch}; m_buffer_transition_guest.resize(target.size); GpuTile(linear_data.data(), m_buffer_transition_guest.data(), target.size, target.size, std::span(&info, 1)); Libs::LibKernel::Memory::WriteBacking(target.address, m_buffer_transition_guest.data(), target.size); } else { Transfer::ProcessDownloadedImage(target.size, regions, *cached.image, cached.image->layout, [&](std::span linear_data) { Libs::LibKernel::Memory::WriteBacking( target.address, linear_data.data(), target.size); }); } m_memory_tracker.ForEachDownloadRange(target.address, target.size, [](uint64_t, uint64_t) noexcept {}); // Only the impending buffer-write range needs publication into BufferCache ownership. The // complete image backing was reconstructed above so a later partial-buffer rebind can detile // the coherent mip chain without requiring one cached buffer to contain the whole image. m_buffer_cache.PublishImageBacking(write_address, write_size); cached.gpu_modified = false; cached.buffer_modified = true; } void TextureCache::SynchronizeDepthImageToBufferLocked(CachedImage& cached, uint64_t write_address, uint64_t write_size) { const auto& info = cached.depth; const bool has_stencil = info.stencil_address != 0 || info.stencil_size != 0; const bool has_htile = info.htile_address != 0 || info.htile_size != 0; TileSizeAlign expected_stencil {}; TileSizeAlign expected_htile {}; TileSizeAlign expected_depth {}; const bool d16 = info.guest_format == Prospero::GpuEnumValue(Prospero::BufferFormat::k16UNorm) && info.format == vk::Format::eD16Unorm && info.bytes_per_element == 2; const bool d32 = info.guest_format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32Float) && info.format == vk::Format::eD32Sfloat && info.bytes_per_element == 4; const bool layout = (d16 || d32) && TileGetDepthSize(info.width, info.height, 0, Prospero::GpuEnumValue(d16 ? Prospero::DepthFormat::kZ16 : Prospero::DepthFormat::kZ32F), Prospero::GpuEnumValue(Prospero::StencilFormat::kInvalid), false, expected_stencil, expected_htile, expected_depth); if (cached.kind != CachedImage::Kind::DepthTarget || !cached.gpu_modified || cached.buffer_modified || write_address != info.address || write_size != info.size || has_stencil || has_htile || info.layers != 1 || !layout || expected_depth.align != 65536 || expected_depth.size != info.size || cached.image->format != info.format || cached.image->extent.width != info.width || cached.image->extent.height != info.height || HasMetaOverlapLocked(info.address, info.size)) { EXIT("TextureCache: unsupported depth-image buffer synchronization, " "write=0x%016" PRIx64 "+0x%016" PRIx64 " depth=0x%016" PRIx64 "+0x%016" PRIx64 " extent=%ux%u layers=%u format=%d guest=%u bpe=%u" " stencil=%d htile=%d gpu_modified=%d buffer_modified=%d\n", write_address, write_size, info.address, info.size, info.width, info.height, info.layers, static_cast(info.format), info.guest_format, info.bytes_per_element, has_stencil, has_htile, cached.gpu_modified, cached.buffer_modified); } Transfer::WaitForQueueIdle(); const auto regions = Transfer::MakeLayeredImageBufferCopies( 1, info.size, info.pitch, info.width, info.height, vk::ImageAspectFlagBits::eDepth); TileBlockLayout block {}; EXIT_NOT_IMPLEMENTED( !TileGetBlockLayout(TileBlockFamily::Depth64KB, info.bytes_per_element, block)); const GpuTileInfo tile_info {block.family, block.bytes_per_element, 0, info.size, 0, info.size, 0, info.width, info.height, 1, info.pitch}; m_buffer_transition_guest.resize(info.size); Transfer::DownloadTiledImage(m_buffer_transition_guest.data(), info.size, info.size, std::span(&tile_info, 1), regions, *cached.image, cached.image->layout); Libs::LibKernel::Memory::WriteBacking(info.address, m_buffer_transition_guest.data(), info.size); m_memory_tracker.ForEachDownloadRange(info.address, info.size, [](uint64_t, uint64_t) noexcept {}); m_buffer_cache.PublishImageBacking(write_address, write_size); cached.gpu_modified = false; cached.buffer_modified = true; } bool TextureCache::InvalidateMemoryFromGPU(uint64_t vaddr, uint64_t size, bool formatted_buffer_write) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid GPU invalidation range, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } FaultSafeTextureLock lock(this, m_lock); for (auto it = m_surface_metas.begin(); it != m_surface_metas.end();) { switch (ClassifyImageRangeOverlap(vaddr, size, it->first, it->second.size)) { case ImageRangeOverlap::None: case ImageRangeOverlap::PageOnly: ++it; continue; case ImageRangeOverlap::Bytes: break; } LOGF("TextureCache: discarding overwritten virtual metadata, write=0x%016" PRIx64 "+0x%016" PRIx64 " metadata=0x%016" PRIx64 "+0x%016" PRIx64 "\n", vaddr, size, it->first, it->second.size); if (it->second.gpu_modified) { m_metadata_tracker.ForEachDownloadRange(it->first, it->second.size, [](uint64_t, uint64_t) noexcept {}); } m_metadata_tracker.UntrackMemory(it->first, it->second.size); it = m_surface_metas.erase(it); } auto match = m_images.end(); BufferImageWrite action = BufferImageWrite::None; for (auto it = m_images.begin(); it != m_images.end(); ++it) { auto& cached = **it; if (!cached.OverlapsRange(vaddr, size, false)) { continue; } const auto next = ClassifyBufferImageWrite(vaddr, size, cached.Address(), cached.Size(), cached.BufferBinding(), cached.gpu_modified, formatted_buffer_write, cached.buffer_modified); if (match != m_images.end() || next == BufferImageWrite::None || next == BufferImageWrite::Unsupported) { EXIT("TextureCache: unsupported GPU invalidation alias, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " cached_kind=%u cached=0x%016" PRIx64 "+0x%016" PRIx64 " gpu_modified=%d buffer_modified=%d formatted=%d ambiguous=%d\n", vaddr, size, static_cast(cached.kind), cached.Address(), cached.Size(), cached.gpu_modified, cached.buffer_modified, formatted_buffer_write, match != m_images.end()); } match = it; action = next; } if (match == m_images.end()) { return false; } auto& cached = **match; switch (action) { case BufferImageWrite::InvalidateTexture: case BufferImageWrite::InvalidateVideoOut: case BufferImageWrite::InvalidateStorageTexture: case BufferImageWrite::InvalidateDepthTarget: case BufferImageWrite::InvalidateRenderTarget: cached.buffer_modified = true; return true; case BufferImageWrite::SynchronizeRenderTarget: case BufferImageWrite::SynchronizeStorageTexture: SynchronizeColorImageToBufferLocked(cached, vaddr, size); return true; case BufferImageWrite::SynchronizeDepthTarget: SynchronizeDepthImageToBufferLocked(cached, vaddr, size); return true; case BufferImageWrite::SynchronizeVideoOut: SynchronizeColorImageToBufferLocked(cached, vaddr, size); return true; case BufferImageWrite::None: case BufferImageWrite::Unsupported: EXIT("TextureCache: invalid GPU invalidation action %u\n", static_cast(action)); } return false; } DepthStencilVulkanImage* TextureCache::FindDepthTargetByRange(CommandBuffer& command, uint64_t vaddr, uint64_t size, bool allow_containing_sampled) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid depth-target range query, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } FaultSafeTextureLock lock(this, m_lock); CachedImage* found = nullptr; for (auto* cached: FindImagesInRegionLocked(vaddr, size, false)) { if (cached->kind != CachedImage::Kind::DepthTarget || !cached->OverlapsRange(vaddr, size, false)) { continue; } const bool containing_sampled = allow_containing_sampled && vaddr == cached->depth.address && size > cached->depth.size; if ((!IsDepthTargetRangeCompatible(cached->depth, vaddr, size) && !containing_sampled) || found != nullptr) { EXIT("TextureCache: incompatible or ambiguous depth-target range, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " cached=0x%016" PRIx64 "+0x%016" PRIx64 " previous=%p\n", vaddr, size, cached->depth.address, cached->depth.size, static_cast(found)); } if (containing_sampled) { return nullptr; } const bool stencil_range = vaddr == cached->depth.stencil_address && size == cached->depth.stencil_size; if (stencil_range && !cached->stencil_initialized) { EXIT("TextureCache: sampled stencil range is uninitialized, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } found = cached; } if (found == nullptr) { return nullptr; } const auto owner = std::find_if(m_images.begin(), m_images.end(), [found](const auto& image) { return image.get() == found; }); if (owner == m_images.end()) { EXIT("TextureCache: page-table depth target has no cache owner\n"); } command.RetainResourceUntilFence(*owner); return static_cast(found->image); } RenderTextureVulkanImage* TextureCache::FindRenderTargetByRange(CommandBuffer& command, uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid render-target range query, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } FaultSafeTextureLock lock(this, m_lock); CachedImage* found = nullptr; for (auto* cached: FindImagesInRegionLocked(vaddr, size, false)) { if (cached->kind != CachedImage::Kind::RenderTarget || !ImageRangeOverlaps(vaddr, size, cached->Address(), cached->Size())) { continue; } const auto slice_size = cached->target.layers != 0 ? cached->target.size / cached->target.layers : 0; const bool contained = vaddr == cached->target.address && slice_size != 0 && cached->target.size % cached->target.layers == 0 && size <= cached->target.size && size % slice_size == 0; if (!contained) { continue; } if (found != nullptr) { EXIT("TextureCache: incompatible or ambiguous render-target range, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " cached=0x%016" PRIx64 "+0x%016" PRIx64 " previous=%p\n", vaddr, size, cached->Address(), cached->Size(), static_cast(found)); } found = cached; } if (found == nullptr) { return nullptr; } const auto owner = std::find_if(m_images.begin(), m_images.end(), [found](const auto& image) { return image.get() == found; }); if (owner == m_images.end()) { EXIT("TextureCache: page-table render target has no cache owner\n"); } command.RetainResourceUntilFence(*owner); return static_cast(found->image); } TextureCache::RegionInfo TextureCache::QueryRegion(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid region query, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } FaultSafeTextureLock lock(this, m_lock); RegionInfo result; const auto candidates = FindImagesInRegionLocked(vaddr, size, true); result.image_pages = !candidates.empty(); for (const auto* cached: candidates) { const bool bytes = cached->OverlapsRange(vaddr, size, false); result.image_bytes |= bytes; result.gpu_image_bytes |= bytes && cached->gpu_modified; result.non_sampled_pages |= cached->kind != CachedImage::Kind::Texture; } for (const auto& [address, metadata]: m_surface_metas) { result.metadata_pages |= ImagePageRangesOverlap(vaddr, size, address, metadata.size); result.metadata_bytes |= ImageRangeOverlaps(vaddr, size, address, metadata.size); } result.gpu_metadata_bytes = result.metadata_bytes && m_metadata_tracker.IsRegionGpuModified(vaddr, size); return result; } bool TextureCache::ResolveMetaRange(uint64_t vaddr, uint64_t size, MetaRangeInfo& info) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { return false; } FaultSafeTextureLock lock(this, m_lock); MetaRangeInfo found {}; bool matched = false; for (const auto& [address, metadata]: m_surface_metas) { const auto slice_size = metadata.size / metadata.layers; const bool full = vaddr == address && size == metadata.size; const auto offset = vaddr >= address ? vaddr - address : UINT64_MAX; const bool slice = !full && size == slice_size && offset < metadata.size && offset % slice_size == 0; if (!full && !slice) { continue; } MetaRangeInfo candidate {.metadata_address = address, .metadata_size = metadata.size, .slice = full ? 0u : static_cast(offset / slice_size), .full = full}; if (matched && (candidate.metadata_address != found.metadata_address || candidate.metadata_size != found.metadata_size || candidate.slice != found.slice || candidate.full != found.full)) { EXIT("TextureCache: ambiguous exact metadata range, request=0x%016" PRIx64 "+0x%016" PRIx64 " first=0x%016" PRIx64 "+0x%016" PRIx64 " slice=%u full=%d second=0x%016" PRIx64 "+0x%016" PRIx64 " slice=%u full=%d\n", vaddr, size, found.metadata_address, found.metadata_size, found.slice, found.full, candidate.metadata_address, candidate.metadata_size, candidate.slice, candidate.full); } found = candidate; matched = true; } if (matched) { info = found; } return matched; } void TextureCache::RegisterMeta(uint64_t vaddr, uint64_t size, uint32_t layers) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr || (vaddr & 0x7fffu) != 0 || layers == 0 || layers > 32 || size % layers != 0) { EXIT("TextureCache: invalid metadata registration, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " layers=%u\n", vaddr, size, layers); } std::lock_guard transaction(m_resource_mutex); if (m_buffer_cache.HasPageOverlap(vaddr, size)) { // Register virtual surface metadata independently of an earlier buffer view. Kyty's split // caches first publish any dirty buffer bytes; clean partial views use guest backing. const auto source = m_buffer_cache.ObtainBufferForImage(vaddr, size); if (!IsCoherentGuestImageSource(source, vaddr, size)) { EXIT("TextureCache: metadata buffer source is inconsistent, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " source=%p source_addr=0x%016" PRIx64 " source_size=0x%016" PRIx64 " current=%d\n", vaddr, size, static_cast(source.buffer), source.address, source.size, source.cpu_current); } } FaultSafeTextureLock lock(this, m_lock); auto existing = m_surface_metas.find(vaddr); uint64_t range_vaddr = vaddr; uint64_t range_size = size; if (existing != m_surface_metas.end()) { const auto slice_size = size / layers; const auto old_slice_size = existing->second.size / existing->second.layers; if (slice_size != old_slice_size || (size > existing->second.size) != (layers > existing->second.layers)) { EXIT("TextureCache: incompatible metadata backing growth\n"); } if (layers <= existing->second.layers) { return; } range_vaddr += existing->second.size; range_size -= existing->second.size; } for (const auto& [address, meta]: m_surface_metas) { if (address != vaddr && ImagePageRangesOverlap(range_vaddr, range_size, address, meta.size)) { LOGF("TextureCache: registering overlapping metadata views, requested=0x%016" PRIx64 "+0x%016" PRIx64 " existing=0x%016" PRIx64 "+0x%016" PRIx64 "\n", range_vaddr, range_size, address, meta.size); } } std::vector retire; for (const auto& cached: m_images) { if (!cached->OverlapsRange(range_vaddr, range_size, true)) { continue; } const bool sampled = cached->kind == CachedImage::Kind::Texture; const bool writable_image = cached->kind == CachedImage::Kind::StorageTexture || cached->kind == CachedImage::Kind::RenderTarget || cached->kind == CachedImage::Kind::DepthTarget; const bool gpu_modified = cached->gpu_modified || m_memory_tracker.IsRegionGpuModified( cached->Address(), cached->Size()); const bool cpu_dirty = cached->kind == CachedImage::Kind::StorageTexture && (cached->info.IsCpuDirty() || m_memory_tracker.IsRegionCpuModified(cached->info.address, cached->info.size)); const auto overlap = ClassifyMetaImageOverlap(sampled, writable_image, gpu_modified, cached->buffer_modified, cpu_dirty); if (overlap == MetaImageOverlap::Unsupported) { EXIT("TextureCache: metadata aliases image pages, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " image_kind=%u image=0x%016" PRIx64 "+0x%016" PRIx64 " gpu_modified=%d buffer_modified=%d\n", range_vaddr, range_size, static_cast(cached->kind), cached->Address(), cached->Size(), cached->gpu_modified, cached->buffer_modified); } if (overlap == MetaImageOverlap::RetireImage) { retire.push_back(cached.get()); continue; } LOGF("TextureCache: metadata aliases a CPU-current sampled image, metadata=0x%016" PRIx64 "+0x%016" PRIx64 " image=0x%016" PRIx64 "+0x%016" PRIx64 "\n", range_vaddr, range_size, cached->Address(), cached->Size()); } if (!retire.empty()) { RequireRetirementIsolation(retire, "metadata", range_vaddr, range_size); for (const auto* cached: retire) { LOGF("TextureCache: retiring a guest-current image for metadata reuse, " "metadata=0x%016" PRIx64 "+0x%016" PRIx64 " image=0x%016" PRIx64 "+0x%016" PRIx64 " kind=%u\n", range_vaddr, range_size, cached->Address(), cached->Size(), static_cast(cached->kind)); } RetireDepthMetadataLocked(retire, vaddr); RetireImages(retire); } if (existing == m_surface_metas.end()) { m_surface_metas.emplace(vaddr, MetaDataInfo {.size = size, .layers = layers}); } else { existing->second.size = size; existing->second.layers = layers; } } bool TextureCache::IsMeta(uint64_t vaddr) { FaultSafeTextureLock lock(this, m_lock); return m_surface_metas.contains(vaddr); } bool TextureCache::IsMetaRange(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { return false; } FaultSafeTextureLock lock(this, m_lock); const auto it = m_surface_metas.find(vaddr); return it != m_surface_metas.end() && it->second.size == size; } bool TextureCache::HasMetaOverlapLocked(uint64_t vaddr, uint64_t size) const { for (const auto& [address, meta]: m_surface_metas) { if (ImagePageRangesOverlap(vaddr, size, address, meta.size)) { return true; } } return false; } void TextureCache::RequireNoMetaOverlapLocked(uint64_t vaddr, uint64_t size) const { for (const auto& [address, meta]: m_surface_metas) { if (!ImagePageRangesOverlap(vaddr, size, address, meta.size)) { continue; } const auto owner = std::find_if(m_images.begin(), m_images.end(), [&](const auto& cached) { return cached->kind == CachedImage::Kind::DepthTarget && cached->depth.htile_address == address && cached->depth.htile_size == meta.size; }); EXIT("TextureCache: image range overlaps virtual metadata, image=0x%016" PRIx64 "+0x%016" PRIx64 " metadata=0x%016" PRIx64 "+0x%016" PRIx64 " layers=%u gpu=%d clear=0x%08x owner=%p owner_depth=0x%016" PRIx64 "+0x%016" PRIx64 " owner_gpu=%d owner_buffer=%d\n", vaddr, size, address, meta.size, meta.layers, meta.gpu_modified, meta.clear_mask, owner != m_images.end() ? static_cast(owner->get()) : nullptr, owner != m_images.end() ? (*owner)->depth.address : 0, owner != m_images.end() ? (*owner)->depth.size : 0, owner != m_images.end() && (*owner)->gpu_modified, owner != m_images.end() && (*owner)->buffer_modified); } } void TextureCache::ResolveImageMetadataOverlapsLocked(uint64_t vaddr, uint64_t size) { std::vector retire; for (const auto& [address, meta]: m_surface_metas) { if (!ImagePageRangesOverlap(vaddr, size, address, meta.size)) { continue; } bool found_owner = false; for (const auto& cached: m_images) { if (cached->kind != CachedImage::Kind::DepthTarget || cached->depth.htile_address != address) { continue; } found_owner = true; bool tracker_gpu_modified = false; for (uint32_t range = 0; range < cached->RangeCount(); range++) { tracker_gpu_modified |= m_memory_tracker.IsRegionGpuModified(cached->Address(range), cached->Size(range)); } const bool metadata_tracker_gpu_modified = m_metadata_tracker.IsRegionGpuModified(address, meta.size); if (cached->depth.htile_size != meta.size || !CanRetireGuestCurrentDepthForMetadataReuse( cached->gpu_modified, cached->buffer_modified, tracker_gpu_modified, meta.gpu_modified, metadata_tracker_gpu_modified, meta.clear_mask)) { EXIT("TextureCache: image range overlaps owned metadata, image=0x%016" PRIx64 "+0x%016" PRIx64 " metadata=0x%016" PRIx64 "+0x%016" PRIx64 " layers=%u clear=0x%08x gpu=%d/%d/%d buffer=%d\n", vaddr, size, address, meta.size, meta.layers, meta.clear_mask, cached->gpu_modified, tracker_gpu_modified, metadata_tracker_gpu_modified, cached->buffer_modified); } if (std::find(retire.begin(), retire.end(), cached.get()) == retire.end()) { retire.push_back(cached.get()); } } if (!found_owner) { EXIT("TextureCache: image range overlaps unowned metadata, image=0x%016" PRIx64 "+0x%016" PRIx64 " metadata=0x%016" PRIx64 "+0x%016" PRIx64 " layers=%u gpu=%d clear=0x%08x\n", vaddr, size, address, meta.size, meta.layers, meta.gpu_modified, meta.clear_mask); } } RequireRetirementIsolation(retire, "image metadata", vaddr, size); RetireDepthMetadataLocked(retire); RetireImages(retire); RequireNoMetaOverlapLocked(vaddr, size); } bool TextureCache::IsMetaCleared(uint64_t vaddr, uint32_t slice) { FaultSafeTextureLock lock(this, m_lock); const auto it = m_surface_metas.find(vaddr); if (it == m_surface_metas.end()) { return false; } if (slice >= it->second.layers) { EXIT("TextureCache: metadata clear slice out of range, addr=0x%016" PRIx64 " slice=%u layers=%u\n", vaddr, slice, it->second.layers); } if ((it->second.clear_mask & (1u << slice)) == 0) { return false; } const auto slice_size = it->second.size / it->second.layers; const auto slice_addr = vaddr + slice_size * slice; if (!it->second.gpu_modified || !m_metadata_tracker.IsRegionGpuModified(slice_addr, slice_size) || m_metadata_tracker.IsRegionCpuModified(slice_addr, slice_size)) { EXIT("TextureCache: cleared metadata slice is not GPU-owned\n"); } return true; } bool TextureCache::ClearMeta(uint64_t vaddr) { std::lock_guard transaction(m_resource_mutex); FaultSafeTextureLock lock(this, m_lock); const auto it = m_surface_metas.find(vaddr); if (it == m_surface_metas.end()) { return false; } m_metadata_tracker.ForEachUploadRange( vaddr, it->second.size, true, [](uint64_t, uint64_t) noexcept {}, []() noexcept {}); it->second.gpu_modified = true; it->second.clear_mask = it->second.layers == 32 ? UINT32_MAX : (1u << it->second.layers) - 1u; return true; } bool TextureCache::TouchMeta(uint64_t vaddr, uint32_t slice, bool is_clear) { FaultSafeTextureLock lock(this, m_lock); const auto it = m_surface_metas.find(vaddr); if (it == m_surface_metas.end()) { return false; } if (slice >= it->second.layers) { EXIT("TextureCache: metadata update slice out of range\n"); } const auto slice_size = it->second.size / it->second.layers; const auto slice_addr = vaddr + slice_size * slice; if (!it->second.gpu_modified || !m_metadata_tracker.IsRegionGpuModified(slice_addr, slice_size)) { EXIT("TextureCache: metadata update requires GPU ownership, addr=0x%016" PRIx64 "\n", vaddr); } if (m_metadata_tracker.IsRegionCpuModified(slice_addr, slice_size)) { EXIT("TextureCache: metadata update races CPU modification, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", slice_addr, slice_size); } if (is_clear) { it->second.clear_mask |= 1u << slice; } else { it->second.clear_mask &= ~(1u << slice); } return true; } bool TextureCache::InvalidateMemory(PageFaultAccess access, uint64_t vaddr, uint64_t size, PageFaultPhase phase) noexcept { if (access != PageFaultAccess::Read && access != PageFaultAccess::Write) { EXIT("TextureCache: unsupported page-fault access %u, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " phase=%u\n", static_cast(access), vaddr, size, static_cast(phase)); } if (phase == PageFaultPhase::Invalidate) { if (g_texture_fault_owner != nullptr) { EXIT("TextureCache: recursive page-fault invalidation, owner=%p\n", g_texture_fault_owner); } m_fault_mutex.lock(); g_texture_fault_owner = this; const bool metadata = access == PageFaultAccess::Write && m_metadata_tracker.InvalidateVirtualGpuWrite(access, vaddr, size, phase); CpuFaultAction action = CpuFaultAction::Untracked; bool needs_readback = false; { FaultSafeTextureLock lock(this, m_lock); if (access == PageFaultAccess::Write) { MarkSampledAliasesCpuDirtyLocked(vaddr, size); } needs_readback = FindGpuReadbackPageCandidateLocked(vaddr, size) != nullptr; if (!needs_readback) { action = m_memory_tracker.BeginCpuFault(vaddr, size, access); } } if (needs_readback) { if (GraphicsRunIsCommandProcessorThread()) { GraphicsRunFinishScheduler(); } m_readback->Request(access, vaddr, size); action = m_memory_tracker.BeginCpuFault(vaddr, size, access); if (action != CpuFaultAction::Download) { EXIT("TextureCache: downloaded image did not retain the active fault page " "tracking, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " action=%u\n", vaddr, size, static_cast(action)); } } else if (action == CpuFaultAction::Download) { EXIT("generic region invalidation cannot download GPU-dirty memory\n"); } return metadata || action != CpuFaultAction::Untracked; } if (g_texture_fault_owner != this) { EXIT("TextureCache: page-fault phase has no matching invalidation, phase=%u owner=%p\n", static_cast(phase), g_texture_fault_owner); } if (phase == PageFaultPhase::Complete) { const bool metadata = access == PageFaultAccess::Write && m_metadata_tracker.InvalidateVirtualGpuWrite(access, vaddr, size, phase); const bool downloaded = m_readback->IsReady(access, vaddr, size); const bool image = m_memory_tracker.CompleteCpuFault(vaddr, size, access, downloaded); const bool readback = m_readback->Complete(access, vaddr, size); if (readback && !image) { EXIT("TextureCache: image readback completed without a matching tracker fault\n"); } return metadata || image || readback; } if (phase != PageFaultPhase::Release) { EXIT("TextureCache: unsupported page-fault phase %u\n", static_cast(phase)); } if (access == PageFaultAccess::Write) { (void)m_metadata_tracker.InvalidateVirtualGpuWrite(access, vaddr, size, phase); } m_readback->Release(access, vaddr, size); g_texture_fault_owner = nullptr; m_fault_mutex.unlock(); return true; } void TextureCache::UnmapMemory(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid unmap range, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } std::lock_guard transaction(m_resource_mutex); FaultSafeTextureLock lock(this, m_lock); const auto end = vaddr + size; for (const auto& [address, meta]: m_surface_metas) { if (ImagePageRangesOverlap(vaddr, size, address, meta.size) && (vaddr > address || end < address + meta.size)) { EXIT("TextureCache: partial metadata unmap is unsupported, unmap=0x%016" PRIx64 "+0x%016" PRIx64 " metadata=0x%016" PRIx64 "+0x%016" PRIx64 "\n", vaddr, size, address, meta.size); } } for (const auto& cached: m_images) { for (uint32_t i = 0; i < cached->RangeCount(); i++) { if (ImagePageRangesOverlap(vaddr, size, cached->Address(i), cached->Size(i)) && (vaddr > cached->Address(i) || end < cached->Address(i) + cached->Size(i))) { EXIT("TextureCache: partial image unmap is unsupported, unmap=0x%016" PRIx64 "+0x%016" PRIx64 " image=0x%016" PRIx64 "+0x%016" PRIx64 "\n", vaddr, size, cached->Address(i), cached->Size(i)); } } if (cached->OverlapsRange(vaddr, size, false) && cached->kind == CachedImage::Kind::VideoOut) { EXIT("TextureCache: registered video-out surface must be unregistered before unmap, " "unmap=0x%016" PRIx64 "+0x%016" PRIx64 " image=0x%016" PRIx64 "+0x%016" PRIx64 " buffer_modified=%d\n", vaddr, size, cached->Address(), cached->Size(), cached->buffer_modified); } } std::vector retire_depth_metadata; for (const auto& cached: m_images) { if (cached->kind != CachedImage::Kind::DepthTarget || !cached->OverlapsRange(vaddr, size, false) || cached->depth.htile_address == 0) { continue; } const bool retained_owner = std::any_of(m_images.begin(), m_images.end(), [&](const auto& other) { return other.get() != cached.get() && other->kind == CachedImage::Kind::DepthTarget && !other->OverlapsRange(vaddr, size, false) && other->depth.htile_address == cached->depth.htile_address && other->depth.htile_size == cached->depth.htile_size; }); if (!retained_owner && std::find(retire_depth_metadata.begin(), retire_depth_metadata.end(), cached->depth.htile_address) == retire_depth_metadata.end()) { retire_depth_metadata.push_back(cached->depth.htile_address); } } for (const auto address: retire_depth_metadata) { const auto meta = m_surface_metas.find(address); const auto owner = std::find_if(m_images.begin(), m_images.end(), [&](const auto& cached) { return cached->kind == CachedImage::Kind::DepthTarget && cached->OverlapsRange(vaddr, size, false) && cached->depth.htile_address == address; }); if (meta == m_surface_metas.end() || owner == m_images.end() || meta->second.size != (*owner)->depth.htile_size) { EXIT("TextureCache: retiring depth image has invalid HTile registration, " "unmap=0x%016" PRIx64 "+0x%016" PRIx64 " metadata=0x%016" PRIx64 " registered=0x%016" PRIx64 " expected=0x%016" PRIx64 "\n", vaddr, size, address, meta != m_surface_metas.end() ? meta->second.size : 0, owner != m_images.end() ? (*owner)->depth.htile_size : 0); } } std::vector metadata_ranges; metadata_ranges.reserve(m_surface_metas.size()); for (const auto& [address, meta]: m_surface_metas) { const bool retiring = ImageRangeOverlaps(vaddr, size, address, meta.size) || std::find(retire_depth_metadata.begin(), retire_depth_metadata.end(), address) != retire_depth_metadata.end(); metadata_ranges.push_back({address, meta.size, retiring}); } const auto metadata_conflict = FindImageRetirementConflict(metadata_ranges); if (metadata_conflict.Exists()) { const auto& retired = metadata_ranges[metadata_conflict.retired]; const auto& retained = metadata_ranges[metadata_conflict.retained]; EXIT("TextureCache: depth-image retirement leaves a tracked metadata alias, " "unmap=0x%016" PRIx64 "+0x%016" PRIx64 " retired=0x%016" PRIx64 "+0x%016" PRIx64 " retained=0x%016" PRIx64 "+0x%016" PRIx64 "\n", vaddr, size, retired.address, retired.size, retained.address, retained.size); } bool wait_idle = false; for (auto& cached: m_images) { if (cached->OverlapsRange(vaddr, size, false)) { wait_idle = true; } } if (wait_idle) { Transfer::WaitForQueueIdle(); } for (auto& cached: m_images) { if (!cached->OverlapsRange(vaddr, size, false) || !cached->gpu_modified) { continue; } for (uint32_t i = 0; i < cached->RangeCount(); i++) { m_memory_tracker.UnmarkRegionAsGpuModified(cached->Address(i), cached->Size(i)); } cached->gpu_modified = false; } for (auto it = m_surface_metas.begin(); it != m_surface_metas.end();) { const bool allocation_unmapped = ImageRangeOverlaps(vaddr, size, it->first, it->second.size); const bool depth_owner_retired = std::find(retire_depth_metadata.begin(), retire_depth_metadata.end(), it->first) != retire_depth_metadata.end(); if (!allocation_unmapped && !depth_owner_retired) { ++it; continue; } if (depth_owner_retired) { LOGF("TextureCache: retiring HTile metadata with depth image, " "unmap=0x%016" PRIx64 "+0x%016" PRIx64 " metadata=0x%016" PRIx64 "+0x%016" PRIx64 "\n", vaddr, size, it->first, it->second.size); } if (it->second.gpu_modified) { m_metadata_tracker.ForEachDownloadRange(it->first, it->second.size, [](uint64_t, uint64_t) noexcept {}); } m_metadata_tracker.UntrackMemory(it->first, it->second.size); it = m_surface_metas.erase(it); } m_memory_tracker.UntrackMemory(vaddr, size); for (auto it = m_images.begin(); it != m_images.end();) { if (!(**it).OverlapsRange(vaddr, size, false)) { ++it; continue; } UnregisterImageLocked(**it, false); it = m_images.erase(it); } } VulkanImage& TextureCache::GetDummySampledTexture(bool uint_format, bool image_3d) { KYTY_PROFILER_BLOCK("TextureCache::GetDummySampledTexture"); return m_dummy_textures->Get(DummyTextureCache::Usage::Sampled, uint_format, image_3d); } VulkanImage& TextureCache::GetDummyStorageTexture(bool uint_format, bool image_3d) { KYTY_PROFILER_BLOCK("TextureCache::GetDummyStorageTexture"); return m_dummy_textures->Get(DummyTextureCache::Usage::Storage, uint_format, image_3d); } } // namespace Libs::Graphics