#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/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/framebufferCache.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/resourceMutex.h" #include "graphics/host_gpu/utils.h" #include "graphics/shader/shader.h" #include "kernel/memory.h" #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; 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; }; bool Overlaps(uint64_t left, uint64_t left_size, uint64_t right, uint64_t right_size) { return left < right + right_size && right < left + left_size; } bool PageOverlaps(uint64_t left, uint64_t left_size, uint64_t right, uint64_t right_size) { return ImagePageRangesOverlap(left, left_size, right, right_size); } 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 (Overlaps(left, left_size, right, right_size)) { return ImageRangeOverlap::Bytes; } return PageOverlaps(left, left_size, right, right_size) ? ImageRangeOverlap::PageOnly : ImageRangeOverlap::None; } } // namespace struct TextureCache::CachedImage { enum class Kind { Texture, StorageTexture, RenderTarget, DepthTarget, VideoOut } kind = Kind::Texture; Image info; RenderTargetInfo target; DepthTargetInfo depth; VideoOutInfo video_out; GraphicContext* ctx = nullptr; VulkanImage* image = nullptr; VulkanMemory memory; bool gpu_modified = false; bool buffer_modified = false; bool stencil_initialized = false; ~CachedImage() { if (ctx == nullptr || image == nullptr) { EXIT("TextureCache: cached image destroyed with invalid resources, ctx=%p image=%p " "kind=%u\n", static_cast(ctx), static_cast(image), static_cast(kind)); } switch (kind) { case Kind::Texture: case Kind::StorageTexture: TextureCache::DeleteGpuTexture(ctx, static_cast(image), &memory); break; case Kind::RenderTarget: TextureCache::DeleteRenderTexture( ctx, static_cast(image), &memory); break; case Kind::DepthTarget: TextureCache::DeleteDepthStencil(ctx, static_cast(image), &memory); break; case Kind::VideoOut: TextureCache::DeleteVideoOut(ctx, static_cast(image), &memory); break; } 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: case Kind::DepthTarget: return BufferImageBinding::Unsupported; } 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 ? PageOverlaps(address, size, Address(i), Size(i)) : Overlaps(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; } }; struct TextureCache::ReadbackWorker { enum class State : uint32_t { Idle, Claimed, Requested, Ready, Installed, Completed, Stopping, Stopped }; struct ReadbackRange { uint64_t address; uint64_t size; }; 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]] ReadbackRange DownloadDepthTarget(CachedImage& cached) { 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; const bool fault_in_depth = vaddr >= info.address && vaddr < info.address + info.size && size <= info.address + info.size - vaddr; const bool d16 = info.guest_format == Prospero::GpuEnumValue(Prospero::BufferFormat::k16UNorm) && info.format == VK_FORMAT_D16_UNORM && info.bytes_per_element == 2; const bool d32 = info.guest_format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32Float) && info.format == VK_FORMAT_D32_SFLOAT && info.bytes_per_element == 4; TileSizeAlign expected_stencil {}; TileSizeAlign expected_htile {}; TileSizeAlign expected_depth {}; const bool prospero_layout = (d16 || d32) && TileGetDepthSize(info.width, info.height, 0, Prospero::GpuEnumValue(d16 ? Prospero::DepthFormat::kZ16 : Prospero::DepthFormat::kZ32F), Prospero::GpuEnumValue(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 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_htile || (expected_htile.size <= UINT64_MAX / info.layers && info.htile_size == expected_htile.size * info.layers)); if (!fault_in_depth || has_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_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_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_htile.size); } const auto rows = static_cast(info.height - 1); if (rows > (UINT64_MAX - info.width) / info.pitch) { EXIT("TextureCache: depth-image readback size overflow\n"); } const auto linear_elements = rows * info.pitch + info.width; if (linear_elements > UINT64_MAX / info.bytes_per_element) { EXIT("TextureCache: depth-image readback size overflow\n"); } const auto linear_size = linear_elements * info.bytes_per_element; const auto slice_size = info.size / info.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 (linear_size > slice_size || 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, 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); } download.resize(info.size); std::fill(download.begin(), download.end(), 0); const auto regions = MakeLayeredImageBufferCopies(info.layers, slice_size, info.pitch, info.width, info.height, VK_IMAGE_ASPECT_DEPTH_BIT); UtilFillBuffer(cached.ctx, download.data(), info.size, regions, cached.image, cached.image->layout); guest.resize(info.size); cache.m_tiler.TileImage(guest.data(), download.data(), info); Libs::LibKernel::Memory::WriteBacking(info.address, guest.data(), info.size); return {info.address, info.size}; } [[nodiscard]] ReadbackRange 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, Prospero::SurfaceFormat(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 && IsTiledRenderTarget(cached.target); 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); const auto layers = target ? cached.target.layers : 1u; const auto rows = static_cast(info.height - 1); if ((!linear && !tiled) || !basic_storage || info.address == 0 || info.size == 0 || info.width == 0 || info.height == 0 || info.levels != 1 || info.pitch < info.width || info.bytes_per_element == 0 || layers == 0 || info.size > UINT32_MAX || info.size % layers != 0 || cached.image->layers != layers || rows > (UINT64_MAX - info.width) / info.pitch) { EXIT("TextureCache: unsupported color-image readback layout, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " extent=%ux%u pitch=%u bpe=%u levels=%u tile=%u kind=%u\n", info.address, info.size, info.width, info.height, info.pitch, info.bytes_per_element, info.levels, info.tile_mode, static_cast(cached.kind)); } const auto linear_elements = rows * info.pitch + info.width; if (linear_elements > UINT64_MAX / info.bytes_per_element) { EXIT("TextureCache: color-image readback size overflow\n"); } const auto linear_size = linear_elements * info.bytes_per_element; 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 (linear_size > slice_size || cached.image->format != info.format || cached.image->extent.width != info.width || cached.image->extent.height != info.height || (tiled && !IsSupportedRenderTargetElementSize(info.bytes_per_element)) || meta_overlap || buffer_overlap) { EXIT("TextureCache: color-image readback storage is unsupported, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " linear=0x%016" PRIx64 " format=%d/%d extent=%ux%u/%ux%u bpe=%u tiled=%d meta=%d kind=%u\n", info.address, info.size, linear_size, static_cast(cached.image->format), static_cast(info.format), cached.image->extent.width, cached.image->extent.height, info.width, info.height, info.bytes_per_element, tiled, meta_overlap, static_cast(cached.kind)); } download.resize(info.size); std::fill(download.begin(), download.end(), 0); const auto regions = MakeLayeredImageBufferCopies(layers, slice_size, info.pitch, info.width, info.height); UtilFillBuffer(cached.ctx, download.data(), info.size, regions, cached.image, cached.image->layout); if (tiled) { guest.resize(info.size); cache.m_tiler.TileImage(guest.data(), download.data(), cached.target); Libs::LibKernel::Memory::WriteBacking(info.address, guest.data(), info.size); } else { Libs::LibKernel::Memory::WriteBacking(info.address, download.data(), info.size); } return {info.address, info.size}; } 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->ctx == nullptr || 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 ctx=%p 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->ctx) : nullptr, 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 target_end = transfer.address + transfer.size; const auto page_end = fault_page + TRACKER_PAGE_SIZE; if (fault_page > transfer.address) { cache.m_memory_tracker.ForEachDownloadRange( transfer.address, fault_page - transfer.address, [](uint64_t, uint64_t) noexcept {}); } if (page_end < target_end) { cache.m_memory_tracker.ForEachDownloadRange( page_end, target_end - page_end, [](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); } if (cache.m_memory_tracker.IsRegionGpuModified(transfer.address, transfer.size)) { EXIT("TextureCache: completed image readback retained GPU ownership, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", transfer.address, transfer.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; 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 image transition transfers this registration to the replacement cache // record. Keeping it GPU-owned also keeps guest page protection continuous. if (!native_image) { m_memory_tracker.UntrackMemory((*it)->Address(range), (*it)->Size(range)); } } 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"); } } 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 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; } 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.stencil_htile_compressed == right.stencil_htile_compressed; } [[nodiscard]] bool IsCoherentGuestImageSource(const BufferImageCopySource& source, uint64_t address, uint64_t size) { // The current PS5 Tiler consumes coherent guest backing directly. A native buffer is an // optional future GPU-detiler source or staging fallback. 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, VkImageAspectFlags aspect) { for (uint32_t layer = 0; layer < source->layers; layer++) { for (uint32_t level = 0; level < source->mip_levels; level++) { ImageImageCopy region {}; region.src_image = 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); } } } VkImageAspectFlags DepthAspectMask(VkFormat format) { return VK_IMAGE_ASPECT_DEPTH_BIT | (format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT || format == VK_FORMAT_D32_SFLOAT_S8_UINT ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); } TextureImageCreateParams MakeImageParams(const ImageInfo& info, bool storage) { TextureImageCreateParams params {}; params.fmt = info.format; params.width = info.width; params.height = info.height; params.base_level = info.base_level; params.levels = info.levels; params.depth = info.depth; params.type = info.type; // Storage image views use identity component mapping. The guest storage write mapping is // validated before this point and intentionally does not become a Vulkan view swizzle. params.swizzle = storage ? DstSel(4, 5, 6, 7) : info.swizzle; params.format_usage = TextureFormatUsage::Sampled | TextureFormatUsage::Storage; params.required_format_usage = storage ? TextureFormatUsage::Sampled | TextureFormatUsage::Storage : TextureFormatUsage::Sampled; params.view_usage = storage ? TextureFormatUsage::Sampled | TextureFormatUsage::Storage : TextureFormatUsage::Sampled; params.image_layout = TextureUploadDestination::MipLevels; params.allow_cube_view = !storage; params.compatible_format_views = storage && IsRgba8SrgbViewFormat(TextureGetFormat(info.format, params.format_usage)); params.owner = storage ? "StorageTextureCache" : "TextureCache"; return params; } bool FormatSupportsStorage(GraphicContext* ctx, VkFormat format) { VkFormatProperties properties {}; vkGetPhysicalDeviceFormatProperties(ctx->physical_device, format, &properties); return (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0; } bool RenderTargetSupportsStorage(GraphicContext* ctx, VkFormat format, VkImageCreateFlags flags) { const auto compatible = BgraSrgbStorageViewFormat(format); const auto required_flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT; const bool compatible_views = (flags & required_flags) == required_flags; return FormatSupportsStorage(ctx, format) || (compatible_views && compatible != VK_FORMAT_UNDEFINED && FormatSupportsStorage(ctx, compatible)); } VkImageCreateFlags RenderTargetCreateFlags(VkFormat format) { const bool compatible_format_view = IsRgba8SrgbViewFormat(format) || BgraToRgbaSampledViewFormat(format) != VK_FORMAT_UNDEFINED; return compatible_format_view ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : VkImageCreateFlags {0}; } VkImageUsageFlags RenderTargetUsage(GraphicContext* ctx, VkFormat format, VkImageCreateFlags flags) { auto usage = static_cast(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) | static_cast(VK_IMAGE_USAGE_TRANSFER_SRC_BIT) | static_cast(VK_IMAGE_USAGE_TRANSFER_DST_BIT) | static_cast(VK_IMAGE_USAGE_SAMPLED_BIT); if (RenderTargetSupportsStorage(ctx, format, flags)) { usage |= VK_IMAGE_USAGE_STORAGE_BIT; } VkImageFormatProperties properties {}; if (vkGetPhysicalDeviceImageFormatProperties(ctx->physical_device, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, flags, &properties) != VK_SUCCESS) { EXIT("TextureCache: render-target format does not support required usage, format=%d " "usage=0x%x\n", static_cast(format), usage); } return usage; } void CreateRenderTargetView(GraphicContext* ctx, VulkanImage* image, int index, VkComponentSwizzle r, VkComponentSwizzle g, VkComponentSwizzle b, VkComponentSwizzle a, VkImageViewType type = VK_IMAGE_VIEW_TYPE_2D, VkFormat view_format = VK_FORMAT_UNDEFINED, VkImageUsageFlags view_usage = 0, uint32_t level_count = 0) { const auto layer_count = type == VK_IMAGE_VIEW_TYPE_2D_ARRAY ? image->layers : 1u; UtilCreateImageView(ctx, image, index, type, VK_IMAGE_ASPECT_COLOR_BIT, {r, g, b, a}, 0, 0, layer_count, level_count == 0 ? image->mip_levels : level_count, view_format, view_usage); } void CreateRenderAttachmentViews(GraphicContext* ctx, RenderTextureVulkanImage* image) { for (uint32_t level = 0; level < image->mip_levels; level++) { VkImageViewCreateInfo create {}; create.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; create.image = image->image; create.viewType = VK_IMAGE_VIEW_TYPE_2D; create.format = image->format; create.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; create.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; create.subresourceRange.baseMipLevel = level; create.subresourceRange.levelCount = 1; create.subresourceRange.baseArrayLayer = 0; create.subresourceRange.layerCount = 1; const auto result = vkCreateImageView(ctx->device, &create, nullptr, &image->render_view[level]); if (result != VK_SUCCESS || image->render_view[level] == nullptr) { EXIT("TextureCache: failed to create render-target mip view, result=%d level=%u\n", static_cast(result), level); } } } void CreateRenderTargetViews(GraphicContext* ctx, RenderTextureVulkanImage* image) { CreateRenderTargetView(ctx, image, VulkanImage::VIEW_DEFAULT, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY); CreateRenderTargetView(ctx, image, VulkanImage::VIEW_BGRA, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_IDENTITY); CreateRenderTargetView(ctx, image, VulkanImage::VIEW_R001, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ONE); CreateRenderTargetView(ctx, image, VulkanImage::VIEW_RGB1, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_ONE); CreateRenderTargetView(ctx, image, VulkanImage::VIEW_R000, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO); CreateRenderTargetView(ctx, image, VulkanImage::VIEW_RG01, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ONE); CreateRenderTargetView(ctx, image, VulkanImage::VIEW_000R, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_R); if (image->layers > 1) { CreateRenderTargetView(ctx, image, VulkanImage::VIEW_DEFAULT_ARRAY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_IMAGE_VIEW_TYPE_2D_ARRAY); CreateRenderTargetView(ctx, image, VulkanImage::VIEW_BGRA_ARRAY, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_IDENTITY, VK_IMAGE_VIEW_TYPE_2D_ARRAY); } if (FormatSupportsStorage(ctx, image->format)) { CreateRenderTargetView(ctx, image, VulkanImage::VIEW_STORAGE, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_IMAGE_VIEW_TYPE_2D, VK_FORMAT_UNDEFINED, 0, 1); if (image->layers > 1) { CreateRenderTargetView(ctx, image, VulkanImage::VIEW_STORAGE_ARRAY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_IMAGE_VIEW_TYPE_2D_ARRAY, VK_FORMAT_UNDEFINED, 0, 1); } } const auto rgba_view_format = BgraToRgbaSampledViewFormat(image->format); if (rgba_view_format != VK_FORMAT_UNDEFINED) { CreateRenderTargetView(ctx, image, VulkanImage::VIEW_BGRA_TO_RGBA, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_A, VK_IMAGE_VIEW_TYPE_2D, rgba_view_format, VK_IMAGE_USAGE_SAMPLED_BIT); } CreateRenderAttachmentViews(ctx, image); } void CreateVideoOutViews(GraphicContext* ctx, VideoOutVulkanImage* image) { CreateRenderTargetView(ctx, image, VulkanImage::VIEW_DEFAULT, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY); CreateRenderTargetView(ctx, image, VulkanImage::VIEW_BGRA, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_IDENTITY); CreateRenderTargetView(ctx, image, VulkanImage::VIEW_DEFAULT_ARRAY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_IMAGE_VIEW_TYPE_2D_ARRAY); CreateRenderTargetView(ctx, image, VulkanImage::VIEW_BGRA_ARRAY, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_IDENTITY, VK_IMAGE_VIEW_TYPE_2D_ARRAY); } [[nodiscard]] uint32_t RenderTargetTransferFormat(uint32_t bytes_per_element) { switch (bytes_per_element) { case 1: return Prospero::GpuEnumValue(Prospero::BufferFormat::k8UNorm); case 2: return Prospero::GpuEnumValue(Prospero::BufferFormat::k16UNorm); case 4: return Prospero::GpuEnumValue(Prospero::BufferFormat::k32Float); case 8: return Prospero::GpuEnumValue(Prospero::BufferFormat::k16_16_16_16Float); case 16: return Prospero::GpuEnumValue(Prospero::BufferFormat::k32_32_32_32Float); default: EXIT("TextureCache: unsupported render-target element size: %u\n", bytes_per_element); } } void UploadRenderTargetLayers(GraphicContext* ctx, RenderTextureVulkanImage* image, const RenderTargetInfo& info, uint32_t base_layer, uint32_t layer_count, bool refresh) { if (info.layers == 0 || info.size % info.layers != 0 || layer_count == 0 || base_layer >= info.layers || layer_count > info.layers - base_layer || image == nullptr || base_layer >= image->layers || layer_count > image->layers - base_layer) { EXIT("TextureCache: invalid render-target layer upload, base=%u count=%u " "info_layers=%u image_layers=%u size=0x%016" PRIx64 "\n", base_layer, layer_count, info.layers, image != nullptr ? image->layers : 0, info.size); } if (refresh) { VulkanDeviceWaitIdle(ctx); } const auto slice_size = info.size / info.layers; const auto upload_size = slice_size * layer_count; const bool standard64 = IsSupportedStandard64RenderTarget(info); if (standard64 || info.levels > 1 || info.layers > 1) { const auto format = RenderTargetTransferFormat(info.bytes_per_element); auto layout = TextureCalcUploadLayout(format, info.width, info.height, info.levels, layer_count, info.pitch, info.tile_mode, upload_size, false, false, false, "TextureCache render target"); const bool render_target_tiled = info.tile_mode == Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget); if ((render_target_tiled && !layout.fmt_tiled_render_target) || (standard64 && !layout.fmt_tiled_standard64kb) || layout.pitch != info.pitch) { EXIT("TextureCache: unsupported render-target mip upload layout, pitch=%u/%u tile=%u\n", info.pitch, layout.pitch, info.tile_mode); } auto regions = TextureBuildUploadRegions( layout, info.format, info.width, info.height, layer_count, info.levels, true, false, TextureUploadDestination::MipLevels, TextureUploadSliceLayout::MipChainPerSlice); for (auto& region: regions) { region.dst_layer += base_layer; } const auto source_address = info.address + slice_size * base_layer; TextureUploadGuestImage( ctx, image, reinterpret_cast(source_address), upload_size, regions, layout, format, info.width, info.height, layer_count, info.levels, TextureUploadSliceLayout::MipChainPerSlice, "TextureCache render target", static_cast(VK_IMAGE_LAYOUT_GENERAL)); return; } if (info.tile_mode == Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget) && UtilBufferIsTiled(info.address, slice_size)) { UtilScratchBuffer scratch(slice_size); TileConvertTiledToLinearRenderTarget( scratch.Data(), reinterpret_cast(info.address), info.width, info.height, info.pitch, info.bytes_per_element, slice_size); UtilFillImage(ctx, image, scratch.Data(), slice_size, info.pitch, static_cast(VK_IMAGE_LAYOUT_GENERAL)); } else { UtilFillImage(ctx, image, reinterpret_cast(info.address), slice_size, info.pitch, static_cast(VK_IMAGE_LAYOUT_GENERAL)); } } void UploadRenderTarget(GraphicContext* ctx, RenderTextureVulkanImage* image, const RenderTargetInfo& info, bool refresh) { UploadRenderTargetLayers(ctx, image, info, 0, info.layers, refresh); } RenderTextureVulkanImage* CreateRenderTarget(GraphicContext* ctx, const RenderTargetInfo& info, VulkanMemory* memory) { auto* image = new RenderTextureVulkanImage; image->extent.width = info.width; image->extent.height = info.height; image->format = info.format; image->mip_levels = info.levels; image->layers = info.layers; image->layout = VK_IMAGE_LAYOUT_UNDEFINED; UtilResetImageViews(image); VkImageCreateInfo create {}; create.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; create.flags = RenderTargetCreateFlags(info.format); create.imageType = VK_IMAGE_TYPE_2D; create.extent = {info.width, info.height, 1}; create.mipLevels = info.levels; create.arrayLayers = info.layers; create.format = info.format; create.tiling = VK_IMAGE_TILING_OPTIMAL; create.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; create.usage = RenderTargetUsage(ctx, info.format, create.flags); create.sharingMode = VK_SHARING_MODE_EXCLUSIVE; create.samples = VK_SAMPLE_COUNT_1_BIT; memory->property = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; if (!VulkanCreateImage(ctx, &create, image, memory)) { EXIT("TextureCache: failed to create render target, addr=0x%016" PRIx64 " extent=%ux%u format=%d\n", info.address, info.width, info.height, static_cast(info.format)); } image->memory = *memory; CreateRenderTargetViews(ctx, image); return image; } void CreateDepthViews(GraphicContext* ctx, DepthStencilVulkanImage* image) { UtilCreateImageView(ctx, image, VulkanImage::VIEW_DEFAULT, VK_IMAGE_VIEW_TYPE_2D, DepthAspectMask(image->format), {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}, 0, 0, 1, 1); UtilCreateImageView(ctx, image, VulkanImage::VIEW_DEPTH_TEXTURE, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_ASPECT_DEPTH_BIT, {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_R}, 0, 0, 1, 1); UtilCreateImageView(ctx, image, VulkanImage::VIEW_R000, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_ASPECT_DEPTH_BIT, {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO}, 0, 0, 1, 1); UtilCreateImageView(ctx, image, VulkanImage::VIEW_R001, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_ASPECT_DEPTH_BIT, {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ONE}, 0, 0, 1, 1); if (image->layers > 1) { UtilCreateImageView(ctx, image, VulkanImage::VIEW_DEFAULT_ARRAY, VK_IMAGE_VIEW_TYPE_2D_ARRAY, DepthAspectMask(image->format), {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}, 0, 0, image->layers, 1); UtilCreateImageView(ctx, image, VulkanImage::VIEW_DEPTH_TEXTURE_ARRAY, VK_IMAGE_VIEW_TYPE_2D_ARRAY, VK_IMAGE_ASPECT_DEPTH_BIT, {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_R}, 0, 0, image->layers, 1); UtilCreateImageView(ctx, image, VulkanImage::VIEW_R000_ARRAY, VK_IMAGE_VIEW_TYPE_2D_ARRAY, VK_IMAGE_ASPECT_DEPTH_BIT, {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO}, 0, 0, image->layers, 1); UtilCreateImageView(ctx, image, VulkanImage::VIEW_R001_ARRAY, VK_IMAGE_VIEW_TYPE_2D_ARRAY, VK_IMAGE_ASPECT_DEPTH_BIT, {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ONE}, 0, 0, image->layers, 1); } } DepthStencilVulkanImage* CreateDepthTarget(GraphicContext* ctx, const DepthTargetInfo& info, VulkanMemory* memory) { VkImageCreateInfo create {}; create.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; create.imageType = VK_IMAGE_TYPE_2D; create.extent = {info.width, info.height, 1}; create.mipLevels = 1; create.arrayLayers = info.layers; create.format = info.format; create.tiling = VK_IMAGE_TILING_OPTIMAL; create.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; create.usage = DepthTargetImageUsage(); create.sharingMode = VK_SHARING_MODE_EXCLUSIVE; create.samples = VK_SAMPLE_COUNT_1_BIT; VkImageFormatProperties properties {}; if (vkGetPhysicalDeviceImageFormatProperties(ctx->physical_device, info.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, create.usage, 0, &properties) != VK_SUCCESS) { EXIT("TextureCache: depth format does not support required usage, format=%d usage=0x%x\n", static_cast(info.format), create.usage); } auto* image = new DepthStencilVulkanImage; image->extent.width = info.width; image->extent.height = info.height; image->guest_pitch = info.pitch; image->layers = info.layers; image->format = info.format; image->layout = VK_IMAGE_LAYOUT_UNDEFINED; image->compressed = false; UtilResetImageViews(image); memory->property = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; if (!VulkanCreateImage(ctx, &create, image, memory)) { EXIT("TextureCache: failed to create depth target, addr=0x%016" PRIx64 " extent=%ux%u format=%d\n", info.address, info.width, info.height, static_cast(info.format)); } image->memory = *memory; CreateDepthViews(ctx, image); return image; } void ValidateVideoOutInfo(GraphicContext* ctx, const VideoOutInfo& info) { const auto compression = ClassifyVideoOutCompression(info.compression != VideoOutCompression::Uncompressed, info.metadata_address, info.dcc_control, 0); const bool metadata_invalid = compression == VideoOutCompression::Dcc256_64_64 && (info.metadata_address >= TRACKER_ADDRESS_SIZE || (info.metadata_address >= info.address && info.metadata_address < info.address + info.size)); if (ctx == nullptr || 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.width > 16384 || info.height > 16384 || info.pitch < info.width || info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget) || info.bytes_per_element != 4 || compression == VideoOutCompression::Unsupported || compression != info.compression || metadata_invalid || !IsSupportedVideoOutFormat(info.guest_format, info.format)) { EXIT("TextureCache: unsupported video-out surface, ctx=%p addr=0x%016" PRIx64 " size=0x%016" PRIx64 " metadata=0x%016" PRIx64 " dcc=0x%08" PRIx32 " extent=%ux%u pitch=%u tile=%u guest_format=%u bpe=%u vk_format=%d\n", static_cast(ctx), info.address, info.size, info.metadata_address, info.dcc_control, info.width, info.height, info.pitch, info.tile_mode, info.guest_format, info.bytes_per_element, static_cast(info.format)); } TileSizeAlign exact {}; TileGetTextureTotalSize(info.guest_format, info.width, info.height, 1, info.pitch, 1, info.tile_mode, false, &exact); if (exact.align != 65536 || exact.size != info.size || TileGetTexturePitch(info.guest_format, info.width, 1, info.tile_mode) != info.pitch) { EXIT("TextureCache: video-out tile layout mismatch, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " expected_size=0x%016" PRIx64 " align=0x%016" PRIx64 " pitch=%u\n", info.address, info.size, exact.size, exact.align, info.pitch); } (void)RenderTargetUsage(ctx, info.format, 0); } VideoOutVulkanImage* CreateVideoOut(GraphicContext* ctx, const VideoOutInfo& info, VulkanMemory* memory) { auto* image = new VideoOutVulkanImage; image->extent.width = info.width; image->extent.height = info.height; image->format = info.format; image->layout = VK_IMAGE_LAYOUT_UNDEFINED; UtilResetImageViews(image); VkImageCreateInfo create {}; create.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; create.imageType = VK_IMAGE_TYPE_2D; create.extent = {info.width, info.height, 1}; create.mipLevels = 1; create.arrayLayers = 1; create.format = info.format; create.tiling = VK_IMAGE_TILING_OPTIMAL; create.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; create.usage = RenderTargetUsage(ctx, info.format, 0); create.sharingMode = VK_SHARING_MODE_EXCLUSIVE; create.samples = VK_SAMPLE_COUNT_1_BIT; memory->property = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; if (!VulkanCreateImage(ctx, &create, image, memory)) { EXIT("TextureCache: failed to create video-out image, addr=0x%016" PRIx64 " extent=%ux%u format=%d\n", info.address, info.width, info.height, static_cast(info.format)); } image->memory = *memory; CreateVideoOutViews(ctx, image); return image; } void UploadVideoOut(GraphicContext* ctx, VideoOutVulkanImage* image, const VideoOutInfo& info, bool refresh) { if (info.compression != VideoOutCompression::Uncompressed) { EXIT("TextureCache: compressed video-out guest upload is unsupported, " "addr=0x%016" PRIx64 " metadata=0x%016" PRIx64 " dcc=0x%08" PRIx32 "\n", info.address, info.metadata_address, info.dcc_control); } if (refresh) { VulkanDeviceWaitIdle(ctx); } image->layout = VK_IMAGE_LAYOUT_UNDEFINED; UtilScratchBuffer scratch(info.size); TileConvertTiledToLinearRenderTarget( scratch.Data(), reinterpret_cast(info.address), info.width, info.height, info.pitch, info.bytes_per_element, info.size); UtilFillImage(ctx, image, scratch.Data(), info.size, info.pitch, static_cast(VK_IMAGE_LAYOUT_GENERAL)); } } // namespace TextureCache::TextureCache(PageManager& page_manager, BufferCache& buffer_cache, ResourceMutex& resource_mutex) : 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_readback = std::make_unique(*this); } VkImageView TextureCache::GetRenderTargetAttachmentView(GraphicContext* ctx, RenderTextureVulkanImage* image, VkFormat format, uint32_t level, uint32_t base_layer, uint32_t layer_count) { if (ctx == nullptr || image == nullptr || image->image == nullptr || format == VK_FORMAT_UNDEFINED || level >= image->mip_levels || level >= 16 || layer_count == 0 || base_layer >= image->layers || layer_count > image->layers - base_layer) { EXIT("TextureCache: invalid render-target attachment view, image=%p format=%d" " level=%u image_levels=%u base_layer=%u layer_count=%u image_layers=%u\n", static_cast(image), static_cast(format), level, image != nullptr ? image->mip_levels : 0, base_layer, layer_count, image != nullptr ? image->layers : 0); } if (format == image->format && base_layer == 0 && layer_count == 1) { if (image->render_view[level] == nullptr) { EXIT("TextureCache: base render-target attachment view is missing, level=%u\n", level); } return image->render_view[level]; } if (format != image->format && !IsRgba8SrgbReinterpretation(image->format, format)) { EXIT("TextureCache: incompatible render-target attachment view, image_format=%d" " view_format=%d level=%u\n", static_cast(image->format), static_cast(format), level); } std::lock_guard lock(image->attachment_view_mutex); for (const auto& cached: image->attachment_views) { if (cached.format == format && cached.level == level && cached.base_layer == base_layer && cached.layer_count == layer_count) { return cached.view; } } VkImageViewUsageCreateInfo usage {}; usage.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO; usage.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; VkImageViewCreateInfo create {}; create.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; create.pNext = &usage; create.image = image->image; create.viewType = layer_count == 1 ? VK_IMAGE_VIEW_TYPE_2D : VK_IMAGE_VIEW_TYPE_2D_ARRAY; create.format = format; create.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; create.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; create.subresourceRange.baseMipLevel = level; create.subresourceRange.levelCount = 1; create.subresourceRange.baseArrayLayer = base_layer; create.subresourceRange.layerCount = layer_count; VkImageView view = nullptr; const auto result = vkCreateImageView(ctx->device, &create, nullptr, &view); if (result != VK_SUCCESS || view == nullptr) { EXIT("TextureCache: failed to create render-target attachment view," " result=%d image_format=%d view_format=%d level=%u base_layer=%u" " layer_count=%u\n", static_cast(result), static_cast(image->format), static_cast(format), level, base_layer, layer_count); } image->attachment_views.push_back({format, level, base_layer, layer_count, view}); LOGF("TextureCache: created render-target attachment view:" " image_format=%d view_format=%d level=%u layer=%u+%u extent=%ux%u\n", static_cast(image->format), static_cast(format), level, base_layer, layer_count, image->extent.width, image->extent.height); return view; } VkImageView TextureCache::GetDepthTargetAttachmentView(GraphicContext* ctx, DepthStencilVulkanImage* image, uint32_t base_layer, uint32_t layer_count) { if (ctx == nullptr || image == nullptr || image->image == nullptr || layer_count == 0 || base_layer >= image->layers || layer_count > image->layers - base_layer) { EXIT("TextureCache: invalid depth-target attachment view, image=%p base_layer=%u " "layer_count=%u image_layers=%u\n", static_cast(image), base_layer, layer_count, image != nullptr ? image->layers : 0); } if (base_layer == 0 && layer_count == 1) { return image->image_view[VulkanImage::VIEW_DEFAULT]; } std::lock_guard lock(image->attachment_view_mutex); for (const auto& cached: image->attachment_views) { if (cached.base_layer == base_layer && cached.layer_count == layer_count) { return cached.view; } } VkImageViewCreateInfo create {}; create.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; create.image = image->image; create.viewType = layer_count == 1 ? VK_IMAGE_VIEW_TYPE_2D : VK_IMAGE_VIEW_TYPE_2D_ARRAY; create.format = image->format; create.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; create.subresourceRange.aspectMask = DepthAspectMask(image->format); create.subresourceRange.baseMipLevel = 0; create.subresourceRange.levelCount = 1; create.subresourceRange.baseArrayLayer = base_layer; create.subresourceRange.layerCount = layer_count; VkImageView view = nullptr; const auto result = vkCreateImageView(ctx->device, &create, nullptr, &view); if (result != VK_SUCCESS || view == nullptr) { EXIT("TextureCache: failed to create depth-target attachment view, result=%d " "format=%d base_layer=%u layer_count=%u\n", static_cast(result), static_cast(image->format), base_layer, layer_count); } image->attachment_views.push_back({base_layer, layer_count, view}); return view; } VkImageView TextureCache::GetRenderTargetSampledView(GraphicContext* ctx, RenderTextureVulkanImage* image, VkFormat view_format, int variant, uint32_t base_level, uint32_t level_count, VkImageViewType type, uint32_t base_layer, uint32_t layer_count) { if (ctx == nullptr || image == nullptr || image->image == nullptr || base_level >= 16 || view_format == VK_FORMAT_UNDEFINED || level_count == 0 || base_level + level_count > image->mip_levels || layer_count == 0 || base_layer >= image->layers || layer_count > image->layers - base_layer || (type != VK_IMAGE_VIEW_TYPE_2D && type != VK_IMAGE_VIEW_TYPE_2D_ARRAY) || (type == VK_IMAGE_VIEW_TYPE_2D && layer_count != 1)) { EXIT("TextureCache: invalid render-target sampled view, image=%p variant=%d" " view_format=%d mip=%u+%u layer=%u+%u type=%d image_levels=%u image_layers=%u\n", static_cast(image), variant, static_cast(view_format), base_level, level_count, base_layer, layer_count, static_cast(type), image != nullptr ? image->mip_levels : 0, image != nullptr ? image->layers : 0); } const bool compatible_variant_format = view_format == image->format || (variant == VulkanImage::VIEW_BGRA_TO_RGBA && IsBgraToRgbaSampledView(image->format, view_format)); if (!compatible_variant_format) { EXIT("TextureCache: incompatible render-target sampled view format, image_format=%d" " view_format=%d variant=%d\n", static_cast(image->format), static_cast(view_format), variant); } const auto default_view_format = variant == VulkanImage::VIEW_BGRA_TO_RGBA ? BgraToRgbaSampledViewFormat(image->format) : image->format; int default_variant = variant; if (type == VK_IMAGE_VIEW_TYPE_2D_ARRAY) { switch (variant) { case VulkanImage::VIEW_DEFAULT: default_variant = VulkanImage::VIEW_DEFAULT_ARRAY; break; case VulkanImage::VIEW_BGRA: default_variant = VulkanImage::VIEW_BGRA_ARRAY; break; default: default_variant = VulkanImage::VIEW_MAX; break; } } const bool full_view = base_level == 0 && level_count == image->mip_levels && base_layer == 0 && layer_count == (type == VK_IMAGE_VIEW_TYPE_2D_ARRAY ? image->layers : 1u); if (view_format == default_view_format && full_view && default_variant >= 0 && default_variant < VulkanImage::VIEW_MAX && image->image_view[default_variant] != nullptr) { return image->image_view[default_variant]; } std::lock_guard lock(image->sampled_view_mutex); for (const auto& cached: image->sampled_views) { if (cached.format == view_format && cached.type == type && cached.variant == variant && cached.base_level == base_level && cached.level_count == level_count && cached.base_layer == base_layer && cached.layer_count == layer_count) { return cached.view; } } VkComponentMapping components {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; switch (variant) { case VulkanImage::VIEW_DEFAULT: break; case VulkanImage::VIEW_BGRA: components = {VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_IDENTITY}; break; case VulkanImage::VIEW_R001: components = {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ONE}; break; case VulkanImage::VIEW_RGB1: components = {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_ONE}; break; case VulkanImage::VIEW_R000: components = {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO}; break; case VulkanImage::VIEW_RG01: components = {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ONE}; break; case VulkanImage::VIEW_000R: components = {VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_ZERO, VK_COMPONENT_SWIZZLE_R}; break; case VulkanImage::VIEW_BGRA_TO_RGBA: if (!IsBgraToRgbaSampledView(image->format, view_format)) { EXIT("TextureCache: incompatible mutable render-target sampled view\n"); } components = {VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_A}; break; case VulkanImage::VIEW_ABGR: components = {VK_COMPONENT_SWIZZLE_A, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R}; break; default: EXIT("TextureCache: unsupported render-target sampled view variant: %d\n", variant); } VkImageViewCreateInfo create {}; VkImageViewUsageCreateInfo usage {}; usage.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO; usage.usage = VK_IMAGE_USAGE_SAMPLED_BIT; create.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; create.pNext = &usage; create.image = image->image; create.viewType = type; create.format = view_format; create.components = components; create.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; create.subresourceRange.baseMipLevel = base_level; create.subresourceRange.levelCount = level_count; create.subresourceRange.baseArrayLayer = base_layer; create.subresourceRange.layerCount = layer_count; VkImageView view = nullptr; const auto result = vkCreateImageView(ctx->device, &create, nullptr, &view); if (result != VK_SUCCESS || view == nullptr) { EXIT("TextureCache: failed to create render-target sampled subresource view," " result=%d view_format=%d variant=%d mip=%u+%u layer=%u+%u type=%d\n", static_cast(result), static_cast(view_format), variant, base_level, level_count, base_layer, layer_count, static_cast(type)); } image->sampled_views.push_back( {view_format, type, base_level, level_count, base_layer, layer_count, variant, view}); return view; } VkImageView TextureCache::GetRenderTargetStorageView(GraphicContext* ctx, RenderTextureVulkanImage* image, VkFormat view_format, uint32_t base_level, uint32_t level_count, VkImageViewType type, uint32_t base_layer, uint32_t layer_count) { if (ctx == nullptr || image == nullptr || image->image == nullptr || view_format == VK_FORMAT_UNDEFINED || level_count == 0 || base_level + level_count > image->mip_levels || layer_count == 0 || base_layer >= image->layers || layer_count > image->layers - base_layer || (type != VK_IMAGE_VIEW_TYPE_2D && type != VK_IMAGE_VIEW_TYPE_2D_ARRAY) || (type == VK_IMAGE_VIEW_TYPE_2D && layer_count != 1)) { EXIT("TextureCache: invalid render-target storage view, image=%p view_format=%d" " mip=%u+%u layer=%u+%u type=%d image_levels=%u image_layers=%u\n", static_cast(image), static_cast(view_format), base_level, level_count, base_layer, layer_count, static_cast(type), image != nullptr ? image->mip_levels : 0, image != nullptr ? image->layers : 0); } const bool exact = view_format == image->format; const bool compatible = view_format == BgraSrgbStorageViewFormat(image->format); if (!exact && !compatible) { EXIT("TextureCache: incompatible render-target storage view, image_format=%d" " view_format=%d base=%u count=%u\n", static_cast(image->format), static_cast(view_format), base_level, level_count); } if (exact) { const auto index = type == VK_IMAGE_VIEW_TYPE_2D_ARRAY ? VulkanImage::VIEW_STORAGE_ARRAY : VulkanImage::VIEW_STORAGE; const bool full_view = base_level == 0 && level_count == 1 && base_layer == 0 && layer_count == (type == VK_IMAGE_VIEW_TYPE_2D_ARRAY ? image->layers : 1u); if (full_view && image->image_view[index] != nullptr) { return image->image_view[index]; } } std::lock_guard lock(image->storage_view_mutex); for (const auto& cached: image->storage_views) { if (cached.format == view_format && cached.type == type && cached.base_level == base_level && cached.level_count == level_count && cached.base_layer == base_layer && cached.layer_count == layer_count) { return cached.view; } } if (compatible && !FormatSupportsStorage(ctx, view_format)) { EXIT("TextureCache: compatible render-target storage format lacks storage support," " image_format=%d view_format=%d base=%u count=%u\n", static_cast(image->format), static_cast(view_format), base_level, level_count); } VkImageViewUsageCreateInfo usage {}; usage.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO; usage.usage = VK_IMAGE_USAGE_STORAGE_BIT; VkImageViewCreateInfo create {}; create.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; create.pNext = &usage; create.image = image->image; create.viewType = type; create.format = view_format; create.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; create.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; create.subresourceRange.baseMipLevel = base_level; create.subresourceRange.levelCount = level_count; create.subresourceRange.baseArrayLayer = base_layer; create.subresourceRange.layerCount = layer_count; VkImageView view = nullptr; const auto result = vkCreateImageView(ctx->device, &create, nullptr, &view); if (result != VK_SUCCESS || view == nullptr) { EXIT("TextureCache: failed to create compatible render-target storage view," " result=%d image_format=%d view_format=%d base=%u count=%u\n", static_cast(result), static_cast(image->format), static_cast(view_format), base_level, level_count); } image->storage_views.push_back( {view_format, type, base_level, level_count, base_layer, layer_count, view}); LOGF("TextureCache: created compatible render-target storage view: image_format=%d" " view_format=%d base=%u count=%u extent=%ux%u\n", static_cast(image->format), static_cast(view_format), base_level, level_count, image->extent.width, image->extent.height); return view; } VkImageView TextureCache::GetStorageTextureSampledView(GraphicContext* ctx, StorageTextureVulkanImage* image, const ImageInfo& info) { const auto shape = SelectStorageSampledViewShape(info.type, info.depth, image != nullptr ? image->layers : 0); if (ctx == nullptr || image == nullptr || image->image == nullptr || shape == StorageSampledViewShape::Unsupported || info.base_array != 0 || info.levels != image->mip_levels || info.base_level >= info.levels || info.view_levels == 0 || info.base_level + info.view_levels > info.levels) { EXIT("TextureCache: invalid sampled view of storage texture, image=%p type=%u depth=%u" " base=%u levels=%u view_levels=%u image_levels=%u base_array=%u\n", static_cast(image), info.type, info.depth, info.base_level, info.levels, info.view_levels, image != nullptr ? image->mip_levels : 0, info.base_array); } const auto view_format = TextureGetFormat(info.format, TextureFormatUsage::Sampled); if (view_format != image->format && !IsRgba8SrgbReinterpretation(image->format, view_format)) { EXIT("TextureCache: incompatible sampled view of storage texture, image_format=%d" " view_format=%d swizzle=0x%03x\n", static_cast(image->format), static_cast(view_format), info.swizzle); } std::lock_guard lock(image->sampled_view_mutex); for (const auto& cached: image->sampled_views) { if (cached.format == view_format && cached.swizzle == info.swizzle && cached.type == info.type && cached.base_level == info.base_level && cached.level_count == info.view_levels) { return cached.view; } } VkImageViewUsageCreateInfo usage {}; usage.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO; usage.usage = VK_IMAGE_USAGE_SAMPLED_BIT; VkImageViewCreateInfo create {}; create.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; create.pNext = &usage; create.image = image->image; create.viewType = shape == StorageSampledViewShape::Image3D ? VK_IMAGE_VIEW_TYPE_3D : shape == StorageSampledViewShape::Image2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; create.format = view_format; create.components = TextureGetComponentMapping(info.swizzle); create.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; create.subresourceRange.baseMipLevel = info.base_level; create.subresourceRange.levelCount = info.view_levels; create.subresourceRange.baseArrayLayer = 0; create.subresourceRange.layerCount = shape == StorageSampledViewShape::Image2DArray ? info.depth : 1; VkImageView view = nullptr; const auto result = vkCreateImageView(ctx->device, &create, nullptr, &view); if (result != VK_SUCCESS || view == nullptr) { EXIT("TextureCache: failed to create sampled view of storage texture, result=%d" " image_format=%d view_format=%d swizzle=0x%03x type=%u base=%u count=%u\n", static_cast(result), static_cast(image->format), static_cast(view_format), info.swizzle, info.type, info.base_level, info.view_levels); } image->sampled_views.push_back( {view_format, info.swizzle, info.type, info.base_level, info.view_levels, view}); LOGF("TextureCache: created sampled view of storage texture: image_format=%d" " view_format=%d swizzle=0x%03x type=%u base=%u count=%u\n", static_cast(image->format), static_cast(view_format), info.swizzle, info.type, info.base_level, info.view_levels); return view; } TextureCache::CachedImage* TextureCache::FindGpuReadbackPageCandidateLocked(uint64_t vaddr, uint64_t size) const { CachedImage* selected = nullptr; for (const auto& cached: m_images) { 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.get())); } selected = cached.get(); } return selected; } void TextureCache::MarkSampledAliasesCpuDirtyLocked(uint64_t vaddr, uint64_t size) { for (auto& cached: m_images) { if (cached->kind != CachedImage::Kind::Texture) { continue; } if (cached->gpu_modified && ImagePageRangesOverlap(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); } } void TextureCache::RetireSampledTargetAliases(GraphicContext* ctx, const ImageInfo& requested) { std::vector retire; bool wait_idle = false; for (const auto& cached: m_images) { if (cached->kind != CachedImage::Kind::RenderTarget) { continue; } switch (ClassifySampledRenderTargetOverlap(requested, cached->target, cached->buffer_modified, cached->ctx == ctx)) { case RenderTargetOverlap::None: continue; case RenderTargetOverlap::RetireTarget: wait_idle |= cached->gpu_modified; retire.push_back(cached.get()); break; case RenderTargetOverlap::RetireSampled: 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 " same_context=%d gpu=%d buffer=%d\n", requested.address, requested.size, cached->target.address, cached->target.size, cached->ctx == ctx, cached->gpu_modified, cached->buffer_modified); } } if (retire.empty()) { return; } RequireRetirementIsolation(retire, "sampled target", requested.address, requested.size); if (wait_idle) { VulkanDeviceWaitIdle(ctx); } for (auto* cached: retire) { if (!cached->gpu_modified) { continue; } const auto transfer = m_readback->DownloadColorImage(*cached); m_memory_tracker.ForEachDownloadRange(transfer.address, transfer.size, [](uint64_t, uint64_t) noexcept {}); cached->gpu_modified = false; } RetireImages(retire); } void TextureCache::RetireStoragePageNeighbors(GraphicContext* ctx, const ImageInfo& requested) { std::vector retire; bool wait_idle = false; for (const auto& cached: m_images) { 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->ctx == ctx, cached->gpu_modified, cached->buffer_modified, tracker_gpu)) { case StorageImageOverlap::None: continue; case StorageImageOverlap::RetireSampled: retire.push_back(cached.get()); continue; case StorageImageOverlap::PageNeighbor: break; 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 same_context=%d gpu=%d/%d buffer=%d\n", requested.address, requested.size, cached->Address(), cached->Size(), static_cast(cached->kind), cached->ctx == ctx, cached->gpu_modified, tracker_gpu, cached->buffer_modified); } const bool tracker_cpu = m_memory_tracker.IsRegionCpuModified(cached->Address(), cached->Size()); if (cached->kind != CachedImage::Kind::StorageTexture || cached->ctx != ctx || cached->buffer_modified || cached->info.IsCpuDirty() || cached->gpu_modified != tracker_gpu || (tracker_gpu && tracker_cpu)) { EXIT("TextureCache: unsupported storage-image page neighbor, requested=0x%016" PRIx64 "+0x%016" PRIx64 " existing=0x%016" PRIx64 "+0x%016" PRIx64 " kind=%u same_context=%d gpu=%d/%d cpu=%d buffer=%d image_cpu=%d\n", requested.address, requested.size, cached->Address(), cached->Size(), static_cast(cached->kind), cached->ctx == ctx, cached->gpu_modified, tracker_gpu, tracker_cpu, cached->buffer_modified, cached->info.IsCpuDirty()); } wait_idle |= cached->gpu_modified; retire.push_back(cached.get()); } RequireRetirementIsolation(retire, "storage neighbor", requested.address, requested.size); if (wait_idle) { VulkanDeviceWaitIdle(ctx); } for (auto* cached: retire) { if (!cached->gpu_modified) { continue; } const auto transfer = m_readback->DownloadColorImage(*cached); m_memory_tracker.ForEachDownloadRange(transfer.address, transfer.size, [](uint64_t, uint64_t) noexcept {}); cached->gpu_modified = false; } RetireImages(retire); } TextureCache::~TextureCache() { m_readback.reset(); GraphicContext* ctx = nullptr; if (!m_images.empty()) { ctx = m_images.front()->ctx; } else { ctx = m_dummy_ctx; } if (ctx != nullptr) { VulkanDeviceWaitIdle(ctx); } m_images.clear(); for (size_t i = 0; i < m_dummy_sampled_textures.size(); i++) { if (m_dummy_sampled_textures[i] != nullptr) { DeleteGpuTexture(ctx, static_cast(m_dummy_sampled_textures[i]), m_dummy_sampled_memory[i]); delete m_dummy_sampled_memory[i]; } if (m_dummy_storage_textures[i] != nullptr) { DeleteGpuTexture(ctx, static_cast(m_dummy_storage_textures[i]), m_dummy_storage_memory[i]); delete m_dummy_storage_memory[i]; } } } VulkanImage* TextureCache::FindTexture(CommandBuffer* command, GraphicContext* ctx, const ImageInfo& info, bool metadata_read) { if (command == nullptr || ctx == nullptr || 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.view_levels == 0 || info.base_level + info.view_levels > info.levels) { EXIT("TextureCache: invalid sampled-image request, command=%p ctx=%p addr=0x%016" PRIx64 " size=0x%016" PRIx64 " extent=%ux%ux%u levels=%u\n", static_cast(command), static_cast(ctx), 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(ctx, 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 when necessary and otherwise // uses a CPU-current staging fallback through 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; const auto requested_view_format = TextureGetFormat(info.format, TextureFormatUsage::Sampled); for (auto& cached: m_images) { if (cached->kind != CachedImage::Kind::StorageTexture) { continue; } switch (ClassifyStorageSampledOverlap(info, cached->info, requested_view_format, cached->image->format, cached->gpu_modified, cached->info.IsCpuDirty(), cached->ctx == ctx)) { 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::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 cpu_dirty=%d same_context=%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->info.IsCpuDirty(), cached->ctx == ctx, 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) { 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 (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 || cached->ctx != ctx) { EXIT("TextureCache: invalid exact sampled-image cache match, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " duplicate=%d gpu_modified=%d same_context=%d\n", info.address, info.size, match != nullptr, cached->gpu_modified, cached->ctx == ctx); } match = cached; } if (match != nullptr) { 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(match->ctx, 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 shares pages with GPU target, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " target_kind=%u\n", info.address, info.size, static_cast(cached->kind)); } const auto overlap = ClassifySampledOverlap(info, cached->info, cached->gpu_modified, cached->ctx == ctx); 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 same_context=%d\n", info.address, info.size, cached->info.address, cached->info.size, cached->gpu_modified, cached->ctx == ctx); } } m_images.reserve(m_images.size() + 1); auto cached = std::make_shared(); cached->kind = CachedImage::Kind::Texture; cached->info = info; cached->ctx = ctx; cached->image = new TextureVulkanImage; const auto components = TextureCreateImage(ctx, cached->image, &cached->memory, MakeImageParams(info, false)); m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { m_tiler.DetileImage(cached->ctx, static_cast(cached->image), cached->info, source, false, false); }); TextureCreateImageViews(ctx, cached->image, components, info.type, info.base_array, info.base_level, info.view_levels, info.depth, true, TextureFormatUsage::Sampled); auto* image = cached->image; command->RetainResourceUntilFence(cached); m_images.push_back(std::move(cached)); return image; } StorageTextureVulkanImage* TextureCache::FindStorageTexture(CommandBuffer* command, GraphicContext* ctx, 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 (command == nullptr || ctx == nullptr || 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 != 1 || info.base_level != 0 || 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 ctx=%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), static_cast(ctx), 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); RequireNoMetaOverlapLocked(info.address, info.size); std::shared_ptr match; for (auto& cached: m_images) { if (cached->kind != CachedImage::Kind::StorageTexture || !Equal(info, cached->info)) { continue; } if (match != nullptr || cached->ctx != ctx || cached->info.IsCpuDirty()) { EXIT("TextureCache: invalid exact storage-image cache match, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " duplicate=%d same_context=%d cpu_dirty=%d\n", info.address, info.size, match != nullptr, cached->ctx == ctx, cached->info.IsCpuDirty()); } match = cached; } if (match != nullptr) { if (buffer_overlap) { EXIT("TextureCache: exact storage image aliases buffer pages, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", info.address, info.size); } const bool cpu_modified = m_memory_tracker.IsRegionCpuModified(info.address, info.size); const bool gpu_modified = m_memory_tracker.IsRegionGpuModified(info.address, info.size); if (match->gpu_modified != gpu_modified || (gpu_modified && cpu_modified)) { EXIT("TextureCache: storage-image ownership is inconsistent, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " cached_gpu_modified=%d tracker_gpu_modified=%d cpu_modified=%d\n", info.address, info.size, match->gpu_modified, gpu_modified, cpu_modified); } if (!gpu_modified) { if (cpu_modified) { m_memory_tracker.ForEachUploadRange( info.address, info.size, true, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { m_tiler.DetileImage( match->ctx, 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); } RetireStoragePageNeighbors(ctx, info); m_images.reserve(m_images.size() + 1); auto cached = std::make_shared(); cached->kind = CachedImage::Kind::StorageTexture; cached->info = info; cached->ctx = ctx; cached->image = new StorageTextureVulkanImage; const auto components = TextureCreateImage(ctx, cached->image, &cached->memory, MakeImageParams(info, true)); m_memory_tracker.ForEachUploadRange( info.address, info.size, true, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { m_tiler.DetileImage(cached->ctx, static_cast(cached->image), cached->info, source, false, true); }); cached->gpu_modified = true; TextureCreateImageViews(ctx, cached->image, components, info.type, 0, 0, 1, info.depth, false, TextureFormatUsage::Sampled | TextureFormatUsage::Storage); auto* image = static_cast(cached->image); command->RetainResourceUntilFence(cached); m_images.push_back(std::move(cached)); return image; } RenderTextureVulkanImage* TextureCache::FindRenderTarget(CommandBuffer* command, GraphicContext* ctx, const RenderTargetInfo& info) { const bool standard64 = IsSupportedStandard64RenderTarget(info); if (command == nullptr || ctx == nullptr || info.address == 0 || info.size == 0 || info.address >= TRACKER_ADDRESS_SIZE || info.size > TRACKER_ADDRESS_SIZE - info.address || info.format == VK_FORMAT_UNDEFINED || 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 || (info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kLinear) && info.tile_mode != Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget) && !standard64)) { EXIT("TextureCache: invalid render-target request, ctx=%p addr=0x%016" PRIx64 " size=0x%016" PRIx64 " extent=%ux%u pitch=%u bpe=%u tile=%u format=%d\n", static_cast(ctx), info.address, info.size, info.width, info.height, info.pitch, info.bytes_per_element, info.tile_mode, static_cast(info.format)); } 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 = 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}; if (m_buffer_cache.HasPageOverlap(info.address, info.size)) { // 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); RequireNoMetaOverlapLocked(info.address, info.size); std::shared_ptr match; for (auto& cached: m_images) { if (cached->kind != CachedImage::Kind::RenderTarget || cached->ctx != ctx || (!Equal(info, cached->target) && !IsCompatibleRenderTargetView(cached->target, info) && !IsCompatibleRenderTargetBacking(cached->target, info))) { continue; } match = cached; } if (match != nullptr) { 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 { UploadRenderTarget(ctx, static_cast(match->image), info, true); }); match->buffer_modified = false; } command->RetainResourceUntilFence(match); return static_cast(match->image); } std::vector retire; 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, cached.ctx == ctx, 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), cached.ctx == ctx, info); break; case CachedImage::Kind::RenderTarget: overlap = ClassifyRenderTargetOverlap(cached.target, cached.gpu_modified, cached.buffer_modified, cached.ctx == ctx, info); break; case CachedImage::Kind::DepthTarget: case CachedImage::Kind::VideoOut: break; } bool supported = false; switch (overlap) { case RenderTargetOverlap::RetireSampled: supported = cached.kind == CachedImage::Kind::Texture; 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; 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 same_context=%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.ctx == ctx, 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); RetireImages(retire, native_image_source.get()); auto cached = std::make_shared(); cached->kind = CachedImage::Kind::RenderTarget; cached->target = info; cached->ctx = ctx; cached->image = CreateRenderTarget(ctx, info, &cached->memory); 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 { UploadRenderTargetLayers(ctx, 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_IMAGE_ASPECT_COLOR_BIT); UtilImageToImage(command, regions, cached->image, static_cast(RENDER_COLOR_IMAGE_LAYOUT)); } else { m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { UploadRenderTarget(ctx, static_cast(cached->image), info, false); }); } cached->gpu_modified = preserve_native; if (preserve_native) { LOGF("TextureCache: preserved native image through render-target recreation, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " format=%d extent=%ux%u layers=%u\n", info.address, info.size, static_cast(info.format), info.width, info.height, info.layers); } auto* image = static_cast(cached->image); command->RetainResourceUntilFence(cached); m_images.push_back(std::move(cached)); return image; } DepthStencilVulkanImage* TextureCache::FindDepthTarget(CommandBuffer* command, GraphicContext* ctx, 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; if (command == nullptr || ctx == nullptr || 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 || info.size % info.layers != 0 || 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 || PageOverlaps(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 || PageOverlaps(info.address, info.size, info.htile_address, info.htile_size) || (has_stencil && PageOverlaps(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 ctx=%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 tile=%u format=%d guest_format=%u bpe=%u\n", static_cast(command), static_cast(ctx), info.address, info.size, info.stencil_address, info.stencil_size, info.htile_address, info.htile_size, info.width, info.height, info.pitch, info.tile_mode, static_cast(info.format), info.guest_format, info.bytes_per_element); } 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 || elements * info.bytes_per_element > UINT64_MAX / info.layers || info.size < elements * info.bytes_per_element * 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.layers || info.stencil_size < elements * 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); RequireNoMetaOverlapLocked(info.address, info.size); if (has_stencil) { RequireNoMetaOverlapLocked(info.stencil_address, info.stencil_size); } std::shared_ptr match; for (auto& cached: m_images) { if (cached->kind != CachedImage::Kind::DepthTarget || cached->ctx != ctx || (!Equal(info, cached->depth) && !IsCompatibleDepthTargetBacking(cached->depth, info))) { continue; } match = cached; } if (match != nullptr) { 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->gpu_modified && depth_source.cpu_dirty) { m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { m_tiler.DetileImage(ctx, 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(ctx, 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; 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: overlap = cached.ctx == ctx ? ClassifyDepthOverlap(cached.info, cached.gpu_modified, info) : DepthOverlap::Unsupported; break; case CachedImage::Kind::DepthTarget: overlap = ClassifyDepthTargetOverlap(cached.depth, cached.gpu_modified, cached.buffer_modified, cached.ctx == ctx, info); break; case CachedImage::Kind::StorageTexture: case CachedImage::Kind::RenderTarget: case CachedImage::Kind::VideoOut: break; } bool supported = false; switch (overlap) { case DepthOverlap::RetireSampled: supported = cached.kind == CachedImage::Kind::Texture; if (supported && !info.depth_load_clear && sampled_depth_source == nullptr) { sampled_depth_source = entry; } break; case DepthOverlap::ExpandTarget: supported = cached.kind == CachedImage::Kind::DepthTarget && native_depth_source == nullptr; if (supported) { native_depth_source = 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); RetireImages(retire, native_depth_source.get()); 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); } auto cached = std::make_shared(); cached->kind = CachedImage::Kind::DepthTarget; cached->depth = info; cached->ctx = ctx; cached->stencil_initialized = !has_stencil; cached->image = CreateDepthTarget(ctx, info, &cached->memory); 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(ctx, 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(ctx, 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_IMAGE_ASPECT_DEPTH_BIT); if (has_stencil) { AppendLayerCopies(regions, native_depth_source->image, VK_IMAGE_ASPECT_STENCIL_BIT); } UtilImageToImage(command, regions, cached->image, static_cast(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL)); cached->gpu_modified = true; LOGF("TextureCache: expanded native depth target, addr=0x%016" PRIx64 " size=0x%016" PRIx64 " layers=%u->%u\n", info.address, info.size, old.layers, info.layers); } else { if (sampled_depth_source != nullptr && (sampled_depth_source->image->type != VulkanImageType::Texture || sampled_depth_source->image->format != Prospero::SurfaceFormat(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(ctx, static_cast(cached->image), info, depth_source, false); break; case DepthTransitionSource::Native: command->RetainResourceUntilFence(sampled_depth_source); UtilCopyImageWithBuffer( command, ctx, sampled_depth_source->image, VK_IMAGE_ASPECT_COLOR_BIT, cached->image, VK_IMAGE_ASPECT_DEPTH_BIT, info.bytes_per_element, static_cast( VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL)); 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(ctx, static_cast(cached->image), info, stencil_source, false); } }); cached->stencil_initialized = true; } } auto* image = static_cast(cached->image); command->RetainResourceUntilFence(cached); m_images.push_back(std::move(cached)); return image; } std::vector TextureCache::RegisterVideoOutSurfaces(GraphicContext* ctx, const std::vector& infos) { if (infos.empty()) { EXIT("TextureCache: video-out registration requires surfaces\n"); } for (const auto& info: infos) { ValidateVideoOutInfo(ctx, info); } for (size_t i = 0; i < infos.size(); i++) { for (size_t j = i + 1; j < infos.size(); j++) { if (PageOverlaps(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) { RequireNoMetaOverlapLocked(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(); cached->kind = CachedImage::Kind::VideoOut; cached->video_out = info; cached->ctx = ctx; cached->image = CreateVideoOut(ctx, info, &cached->memory); if (info.compression == VideoOutCompression::Uncompressed) { m_memory_tracker.ForEachUploadRange( info.address, info.size, false, [](uint64_t, uint64_t) noexcept {}, [&]() noexcept { UploadVideoOut(ctx, 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)); } return result; } void TextureCache::RefreshVideoOut(VideoOutVulkanImage* image, bool render_target) { if (image == nullptr) { EXIT("TextureCache: invalid video-out refresh, image=%p\n", static_cast(image)); } 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 == 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_dirty = cached.buffer_modified || 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 { UploadVideoOut(cached.ctx, 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()); } GraphicContext* ctx = selected.front()->ctx; for (auto* cached: selected) { if (cached->ctx != ctx) { EXIT("TextureCache: video-out surfaces span graphics contexts, expected=%p actual=%p\n", static_cast(ctx), static_cast(cached->ctx)); } 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()); } } VulkanDeviceWaitIdle(ctx); for (auto* cached: selected) { if (cached->gpu_modified) { m_memory_tracker.UnmarkRegionAsGpuModified(cached->Address(), cached->Size()); cached->gpu_modified = false; } m_memory_tracker.UntrackMemory(cached->Address(), cached->Size()); } 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::ClearColorImageFromBuffer(CommandBuffer* command, uint64_t vaddr, uint64_t size, uint32_t packed_clear) { if (command == nullptr || 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); std::shared_ptr match; for (const auto& cached: m_images) { if (!cached->OverlapsRange(vaddr, size, true)) { continue; } const bool color = cached->kind == CachedImage::Kind::VideoOut || cached->kind == CachedImage::Kind::RenderTarget; const bool exact = cached->Address() == vaddr && cached->Size() == size; if (!color || !exact) { return false; } if (match != nullptr) { EXIT("TextureCache: compute image clear has ambiguous exact aliases, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " first=%p second=%p\n", vaddr, size, static_cast(match->image), static_cast(cached->image)); } match = cached; } if (match == nullptr) { 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) { // A prior buffer clear can leave one clean cached buffer after this range becomes a native // color target. Allow coexisting cache views only after the shared ownership // helper proves the stale source is coherent native-buffer or guest backing. 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); if (!buffer_source_valid || buffer_cpu_modified || buffer_gpu_modified || match->buffer_modified || image_cpu_modified) { 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); } VkClearColorValue clear {}; if (!DecodePackedColorClear(match->image->format, packed_clear, &clear)) { return false; } auto* vk_buffer = command->GetPool()->buffers[command->GetIndex()]; GraphicsRenderColorImageBarrier(vk_buffer, match->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); const VkImageSubresourceRange range {VK_IMAGE_ASPECT_COLOR_BIT, 0, VK_REMAINING_MIP_LEVELS, 0, match->image->layers}; vkCmdClearColorImage(vk_buffer, 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; } bool TextureCache::ClearDepthImageFromBuffer(CommandBuffer* command, uint64_t vaddr, uint64_t size, uint32_t packed_clear) { if (command == nullptr || command->IsInvalid() || vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid compute depth 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); std::shared_ptr match; for (const auto& cached: m_images) { if (!cached->OverlapsRange(vaddr, size, true)) { continue; } const bool exact_depth = cached->kind == CachedImage::Kind::DepthTarget && CanNativeClearDepthFromBuffer(cached->depth, vaddr, size); if (!exact_depth) { return false; } if (match != nullptr) { EXIT("TextureCache: compute depth clear has ambiguous exact aliases, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " first=%p second=%p\n", vaddr, size, static_cast(match->image), static_cast(cached->image)); } match = cached; } if (match == nullptr) { return false; } float depth_clear = 0.0f; if (!DecodePackedDepthClear(match->image->format, packed_clear, &depth_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); if (!match->gpu_modified || !buffer_source_valid || buffer_cpu_modified || buffer_gpu_modified || match->buffer_modified || image_cpu_modified) { EXIT("TextureCache: compute depth clear requires a GPU-owned depth image and a clean " "buffer 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", vaddr, size, match->gpu_modified, buffer_overlap, buffer_source_valid, buffer_cpu_modified, buffer_gpu_modified, match->buffer_modified, image_cpu_modified); } if (match->image->layout == VK_IMAGE_LAYOUT_UNDEFINED || match->image->layout == VK_IMAGE_LAYOUT_PREINITIALIZED) { EXIT("TextureCache: compute depth clear has invalid source layout %u\n", static_cast(match->image->layout)); } auto* vk_buffer = command->GetPool()->buffers[command->GetIndex()]; const auto old_layout = match->image->layout; GraphicsRenderDepthStencilImageBarrier(vk_buffer, match->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); const VkClearDepthStencilValue clear {depth_clear, 0}; const VkImageSubresourceRange range {VK_IMAGE_ASPECT_DEPTH_BIT, 0, VK_REMAINING_MIP_LEVELS, 0, match->image->layers}; vkCmdClearDepthStencilImage(vk_buffer, match->image->image, match->image->layout, &clear, 1, &range); GraphicsRenderDepthStencilImageBarrier(vk_buffer, match->image, old_layout); command->RetainResourceUntilFence(match); return true; } bool TextureCache::ClearStencilImageFromBuffer(CommandBuffer* command, uint64_t vaddr, uint64_t size, uint32_t packed_clear) { if (command == nullptr || command->IsInvalid() || vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid compute stencil clear, command=%p addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", static_cast(command), vaddr, size); } uint8_t stencil_clear = 0; if (!DecodePackedStencilClear(packed_clear, &stencil_clear)) { return false; } m_buffer_cache.ValidateGpuAccess(vaddr, size, false, true); std::lock_guard transaction(m_resource_mutex); FaultSafeTextureLock lock(this, m_lock); std::shared_ptr match; for (const auto& cached: m_images) { if (!cached->OverlapsRange(vaddr, size, true)) { continue; } const bool exact_stencil = cached->kind == CachedImage::Kind::DepthTarget && cached->RangeCount() == 2 && cached->Address(1) == vaddr && cached->Size(1) == size; if (!exact_stencil) { return false; } if (match != nullptr) { EXIT("TextureCache: compute stencil clear has ambiguous exact aliases, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " first=%p second=%p\n", vaddr, size, static_cast(match->image), static_cast(cached->image)); } match = cached; } if (match == nullptr) { 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); if (!match->gpu_modified || !buffer_source_valid || buffer_cpu_modified || buffer_gpu_modified || match->buffer_modified || image_cpu_modified) { EXIT("TextureCache: compute stencil clear requires a GPU-owned depth image and a clean " "stencil 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", vaddr, size, match->gpu_modified, buffer_overlap, buffer_source_valid, buffer_cpu_modified, buffer_gpu_modified, match->buffer_modified, image_cpu_modified); } if (match->image->layout == VK_IMAGE_LAYOUT_UNDEFINED || match->image->layout == VK_IMAGE_LAYOUT_PREINITIALIZED) { EXIT("TextureCache: compute stencil clear has invalid source layout %u\n", static_cast(match->image->layout)); } auto* vk_buffer = command->GetPool()->buffers[command->GetIndex()]; const auto old_layout = match->image->layout; GraphicsRenderDepthStencilImageBarrier(vk_buffer, match->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); const VkClearDepthStencilValue clear {0.0f, stencil_clear}; const VkImageSubresourceRange range {VK_IMAGE_ASPECT_STENCIL_BIT, 0, VK_REMAINING_MIP_LEVELS, 0, match->image->layers}; vkCmdClearDepthStencilImage(vk_buffer, match->image->image, match->image->layout, &clear, 1, &range); GraphicsRenderDepthStencilImageBarrier(vk_buffer, match->image, old_layout); match->stencil_initialized = true; command->RetainResourceUntilFence(match); return true; } void TextureCache::MarkGpuWritten(VulkanImage* image) { if (image == nullptr) { EXIT("TextureCache: invalid GPU-write notification, image=%p\n", static_cast(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++) { const bool host_refreshable = (cached->kind == CachedImage::Kind::Texture || cached->kind == CachedImage::Kind::RenderTarget) && !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::SynchronizeRenderTargetToBufferLocked(CachedImage& cached) { const auto& target = cached.target; const bool linear = target.tile_mode == Prospero::GpuEnumValue(Prospero::TileMode::kLinear); const bool tiled = IsTiledRenderTarget(target); const auto rows = static_cast(target.height - 1); TileSizeAlign exact {}; bool single_slice = false; if (IsSupportedStandard64RenderTarget(target)) { const auto format = RenderTargetTransferFormat(target.bytes_per_element); const auto expected_pitch = TileGetTexturePitch(format, target.width, target.levels, target.tile_mode); TileGetTextureSize(format, target.width, target.height, expected_pitch, target.levels, target.tile_mode, &exact, nullptr, nullptr); single_slice = target.pitch == expected_pitch && exact.align == 65536 && exact.size != 0; } else { single_slice = TileGetRenderTargetSize(target.width, target.height, target.pitch, target.bytes_per_element, &exact); } const bool layered_size = target.layers != 0 && single_slice && exact.size <= UINT64_MAX / target.layers && exact.size * target.layers == target.size; const bool exact_tiled = tiled && exact.align == 65536 && layered_size; const bool valid_cache = cached.kind == CachedImage::Kind::RenderTarget && cached.gpu_modified && !cached.buffer_modified && cached.ctx != nullptr && cached.image != nullptr; const bool valid_target = target.address != 0 && target.size != 0 && target.width != 0 && target.height != 0 && target.pitch >= target.width && target.bytes_per_element != 0 && target.layers != 0 && target.size % target.layers == 0 && target.size <= UINT32_MAX && target.levels == 1 && rows <= (UINT64_MAX - target.width) / target.pitch; const bool valid_image = cached.image != nullptr && cached.image->layers == target.layers; if (!valid_cache || !valid_target || (!linear && !exact_tiled) || !valid_image) { EXIT("TextureCache: unsupported render-target buffer synchronization, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " extent=%ux%u pitch=%u bpe=%u levels=%u layers=%u/%u tile=%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, cached.image == nullptr ? 0 : cached.image->layers, target.tile_mode, cached.gpu_modified, cached.buffer_modified); } const auto linear_elements = rows * target.pitch + target.width; if (linear_elements > UINT64_MAX / target.bytes_per_element) { EXIT("TextureCache: render-target buffer synchronization size overflow\n"); } const auto linear_size = linear_elements * target.bytes_per_element; const auto slice_size = target.size / target.layers; if (linear_size > slice_size || 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: render-target buffer synchronization storage mismatch, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 " linear=0x%016" PRIx64 "\n", target.address, target.size, linear_size); } // This is the CPU Tiler backend for the image-to-buffer synchronization seam. // The vectors and Vulkan staging allocation retain capacity; a future PS5 GPU tiler can replace // this block without changing alias classification or ownership transitions. VulkanDeviceWaitIdle(cached.ctx); m_buffer_transition_linear.resize(target.size); std::fill(m_buffer_transition_linear.begin(), m_buffer_transition_linear.end(), 0); const auto regions = MakeLayeredImageBufferCopies(target.layers, slice_size, target.pitch, target.width, target.height); UtilFillBuffer(cached.ctx, m_buffer_transition_linear.data(), target.size, regions, cached.image, cached.image->layout); if (tiled) { m_buffer_transition_guest.resize(target.size); m_tiler.TileImage(m_buffer_transition_guest.data(), m_buffer_transition_linear.data(), target); Libs::LibKernel::Memory::WriteBacking(target.address, m_buffer_transition_guest.data(), target.size); } else { Libs::LibKernel::Memory::WriteBacking(target.address, m_buffer_transition_linear.data(), target.size); } m_memory_tracker.ForEachDownloadRange(target.address, target.size, [](uint64_t, uint64_t) noexcept {}); if (m_memory_tracker.IsRegionGpuModified(target.address, target.size)) { EXIT("TextureCache: render-target buffer synchronization retained GPU ownership\n"); } m_buffer_cache.PublishImageBacking(target.address, target.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); 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: cached.buffer_modified = true; return true; case BufferImageWrite::SynchronizeRenderTarget: SynchronizeRenderTargetToBufferLocked(cached); return true; case BufferImageWrite::None: case BufferImageWrite::Unsupported: EXIT("TextureCache: invalid GPU invalidation action %u\n", static_cast(action)); } return false; } DepthStencilVulkanImage* TextureCache::FindDepthTargetByRange(uint64_t vaddr, uint64_t size) { 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); DepthStencilVulkanImage* found = nullptr; for (auto& cached: m_images) { if (cached->kind != CachedImage::Kind::DepthTarget || !cached->OverlapsRange(vaddr, size, false)) { continue; } if (!IsDepthTargetRangeCompatible(cached->depth, vaddr, size) || 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)); } 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 = static_cast(cached->image); } return found; } RenderTextureVulkanImage* TextureCache::FindRenderTargetByRange(CommandBuffer* command, uint64_t vaddr, uint64_t size) { if (command == nullptr || 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); std::shared_ptr found; for (auto& cached: m_images) { if (cached->kind != CachedImage::Kind::RenderTarget || !Overlaps(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.get())); } found = cached; } if (found == nullptr) { return nullptr; } command->RetainResourceUntilFence(found); return static_cast(found->image); } bool TextureCache::HasGpuTargetPageOverlap(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid GPU-target overlap query, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } FaultSafeTextureLock lock(this, m_lock); for (const auto& cached: m_images) { if (cached->kind != CachedImage::Kind::Texture && cached->OverlapsRange(vaddr, size, true)) { return true; } } return false; } bool TextureCache::HasPageOverlap(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid image page-overlap query, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } FaultSafeTextureLock lock(this, m_lock); for (const auto& cached: m_images) { if (cached->OverlapsRange(vaddr, size, true)) { return true; } } return false; } bool TextureCache::HasRangeOverlap(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid image-range-overlap query, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } FaultSafeTextureLock lock(this, m_lock); for (const auto& cached: m_images) { if (cached->OverlapsRange(vaddr, size, false)) { return true; } } return false; } bool TextureCache::HasGpuModifiedRangeOverlap(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid GPU-modified image-overlap query, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } FaultSafeTextureLock lock(this, m_lock); for (const auto& cached: m_images) { if (cached->gpu_modified && cached->OverlapsRange(vaddr, size, false)) { return true; } } return false; } 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 && PageOverlaps(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 auto overlap = ClassifyMetaImageOverlap(cached->kind == CachedImage::Kind::Texture, cached->kind == CachedImage::Kind::RenderTarget, cached->gpu_modified, cached->buffer_modified); 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::RetireTarget) { 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 CPU-current render target for metadata reuse, " "metadata=0x%016" PRIx64 "+0x%016" PRIx64 " target=0x%016" PRIx64 "+0x%016" PRIx64 "\n", range_vaddr, range_size, cached->Address(), cached->Size()); } 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::HasMetaRangeOverlap(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid metadata-range query, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } FaultSafeTextureLock lock(this, m_lock); return std::any_of(m_surface_metas.begin(), m_surface_metas.end(), [&](const auto& entry) { return Overlaps(vaddr, size, entry.first, entry.second.size); }); } bool TextureCache::HasMetaOverlap(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid metadata-overlap query, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } FaultSafeTextureLock lock(this, m_lock); for (const auto& [address, meta]: m_surface_metas) { if (PageOverlaps(vaddr, size, address, meta.size)) { return true; } } return false; } bool TextureCache::IsMetaGpuModified(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("TextureCache: invalid metadata ownership query, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } FaultSafeTextureLock lock(this, m_lock); for (const auto& [address, meta]: m_surface_metas) { if (Overlaps(vaddr, size, address, meta.size) && m_metadata_tracker.IsRegionGpuModified(vaddr, size)) { return true; } } return false; } bool TextureCache::HasMetaOverlapLocked(uint64_t vaddr, uint64_t size) const { for (const auto& [address, meta]: m_surface_metas) { if (PageOverlaps(vaddr, size, address, meta.size)) { return true; } } return false; } void TextureCache::RequireNoMetaOverlapLocked(uint64_t vaddr, uint64_t size) const { if (HasMetaOverlapLocked(vaddr, size)) { EXIT("TextureCache: image range overlaps virtual metadata, addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", 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()) { GraphicsRunFinishCommandProcessors(); } 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 (PageOverlaps(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 (PageOverlaps(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 = Overlaps(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); } GraphicContext* wait_ctx = nullptr; for (auto& cached: m_images) { if (cached->OverlapsRange(vaddr, size, false)) { if (wait_ctx != nullptr && wait_ctx != cached->ctx) { EXIT("TextureCache: unmap spans multiple graphics contexts, first=%p second=%p\n", static_cast(wait_ctx), static_cast(cached->ctx)); } wait_ctx = cached->ctx; } } if (wait_ctx != nullptr) { VulkanDeviceWaitIdle(wait_ctx); } 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& cached: m_images) { if (!cached->OverlapsRange(vaddr, size, false)) { continue; } for (uint32_t i = 0; i < cached->RangeCount(); i++) { m_memory_tracker.UntrackMemory(cached->Address(i), cached->Size(i)); } } for (auto it = m_surface_metas.begin(); it != m_surface_metas.end();) { const bool allocation_unmapped = Overlaps(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; } it = m_images.erase(it); } } static constexpr int DummyTextureIndex(bool uint_format, bool image_3d) { return (image_3d ? 2 : 0) + (uint_format ? 1 : 0); } static constexpr uint32_t DummyTextureSwizzle() { return Prospero::GpuEnumValue(Prospero::CompSwizzle::kRed) | (Prospero::GpuEnumValue(Prospero::CompSwizzle::kGreen) << 3u) | (Prospero::GpuEnumValue(Prospero::CompSwizzle::kBlue) << 6u) | (Prospero::GpuEnumValue(Prospero::CompSwizzle::kAlpha) << 9u); } static TextureImageCreateParams MakeDummyTextureParams(bool uint_format, bool image_3d, TextureFormatUsage usage, const char* owner) { TextureImageCreateParams params {}; params.fmt = static_cast( Prospero::GpuEnumValue(uint_format ? Prospero::BufferFormat::k8_8_8_8UInt : Prospero::BufferFormat::k8_8_8_8UNorm)); params.width = 1; params.height = 1; params.base_level = 0; params.levels = 1; params.depth = 1; params.type = Prospero::GpuEnumValue(image_3d ? Prospero::ImageType::kColor3D : Prospero::ImageType::kColor2D); params.swizzle = DummyTextureSwizzle(); params.format_usage = usage; params.required_format_usage = usage; params.view_usage = usage; params.image_layout = TextureUploadDestination::MipLevels; params.allow_cube_view = true; params.storage_swizzle_fallback = TextureHasFormatUsage(usage, TextureFormatUsage::Storage); params.owner = owner; return params; } static VulkanImage* CreateDummyTexture(GraphicContext* ctx, bool uint_format, bool image_3d, bool storage, VulkanMemory** memory) { if (memory == nullptr || *memory != nullptr) { EXIT("TextureCache: invalid dummy texture memory slot, slot=%p current=%p storage=%d\n", static_cast(memory), memory == nullptr ? nullptr : static_cast(*memory), storage); } auto* image = (storage ? static_cast(new StorageTextureVulkanImage) : new TextureVulkanImage); auto usage = (storage ? TextureFormatUsage::Storage : TextureFormatUsage::Sampled); auto layout = (storage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); auto owner = (storage ? "DummyStorageTexture" : "DummySampledTexture"); *memory = new VulkanMemory {}; auto params = MakeDummyTextureParams(uint_format, image_3d, usage, owner); auto components = TextureCreateImage(ctx, image, *memory, params); static constexpr uint32_t zero = 0; UtilFillImage(ctx, image, &zero, sizeof(zero), 1, static_cast(layout)); TextureCreateImageViews(ctx, image, components, params.type, 0, params.base_level, params.levels, params.depth, params.allow_cube_view, params.view_usage); return image; } void TextureCache::DeleteImageViews(GraphicContext* ctx, VulkanImage* image) { EXIT_IF(ctx == nullptr); EXIT_IF(image == nullptr); for (auto& view: image->image_view) { if (view != nullptr) { vkDestroyImageView(ctx->device, view, nullptr); view = nullptr; } } } void TextureCache::DeleteGpuTexture(GraphicContext* ctx, GpuTextureVulkanImage* image, VulkanMemory* mem) { KYTY_PROFILER_BLOCK("TextureCache::DeleteGpuTexture"); EXIT_IF(ctx == nullptr); EXIT_IF(image == nullptr); if (image->type == VulkanImageType::StorageTexture) { auto* storage = static_cast(image); for (auto& cached: storage->sampled_views) { if (cached.view != nullptr) { vkDestroyImageView(ctx->device, cached.view, nullptr); cached.view = nullptr; } } storage->sampled_views.clear(); } DeleteImageViews(ctx, image); VulkanDeleteImage(ctx, image, mem); switch (image->type) { case VulkanImageType::Texture: delete static_cast(image); break; case VulkanImageType::StorageTexture: delete static_cast(image); break; default: EXIT("unsupported gpu texture image type: %d\n", static_cast(image->type)); } } void TextureCache::DeleteRenderTexture(GraphicContext* ctx, RenderTextureVulkanImage* image, VulkanMemory* mem) { KYTY_PROFILER_BLOCK("TextureCache::DeleteRenderTexture"); EXIT_IF(ctx == nullptr); EXIT_IF(image == nullptr); g_render_ctx->GetFramebufferCache()->FreeFramebufferByColor(image); for (auto& view: image->render_view) { if (view != nullptr) { vkDestroyImageView(ctx->device, view, nullptr); view = nullptr; } } for (auto& cached: image->attachment_views) { if (cached.view != nullptr) { vkDestroyImageView(ctx->device, cached.view, nullptr); cached.view = nullptr; } } image->attachment_views.clear(); for (auto& cached: image->sampled_views) { if (cached.view != nullptr) { vkDestroyImageView(ctx->device, cached.view, nullptr); cached.view = nullptr; } } image->sampled_views.clear(); for (auto& cached: image->storage_views) { if (cached.view != nullptr) { vkDestroyImageView(ctx->device, cached.view, nullptr); cached.view = nullptr; } } image->storage_views.clear(); DeleteImageViews(ctx, image); VulkanDeleteImage(ctx, image, mem); delete image; } void TextureCache::DeleteDepthStencil(GraphicContext* ctx, DepthStencilVulkanImage* image, VulkanMemory* mem) { KYTY_PROFILER_BLOCK("TextureCache::DeleteDepthStencil"); EXIT_IF(ctx == nullptr); EXIT_IF(image == nullptr); g_render_ctx->GetFramebufferCache()->FreeFramebufferByDepth(image); for (auto& cached: image->attachment_views) { if (cached.view != nullptr) { vkDestroyImageView(ctx->device, cached.view, nullptr); cached.view = nullptr; } } image->attachment_views.clear(); DeleteImageViews(ctx, image); VulkanDeleteImage(ctx, image, mem); delete image; } void TextureCache::DeleteVideoOut(GraphicContext* ctx, VideoOutVulkanImage* image, VulkanMemory* mem) { KYTY_PROFILER_BLOCK("TextureCache::DeleteVideoOut"); EXIT_IF(ctx == nullptr); EXIT_IF(image == nullptr); g_render_ctx->GetFramebufferCache()->FreeFramebufferByColor(image); DeleteImageViews(ctx, image); VulkanDeleteImage(ctx, image, mem); delete image; } VulkanImage* TextureCache::GetDummySampledTexture(bool uint_format, bool image_3d) { KYTY_PROFILER_BLOCK("TextureCache::GetDummySampledTexture"); Common::LockGuard lock(m_dummy_mutex); auto* ctx = g_render_ctx->GetGraphicCtx(); if (m_dummy_ctx != nullptr && m_dummy_ctx != ctx) { EXIT("TextureCache: sampled dummy texture context changed, previous=%p current=%p\n", static_cast(m_dummy_ctx), static_cast(ctx)); } m_dummy_ctx = ctx; const auto index = DummyTextureIndex(uint_format, image_3d); auto*& image = m_dummy_sampled_textures[index]; if (image == nullptr) { image = CreateDummyTexture(ctx, uint_format, image_3d, false, &m_dummy_sampled_memory[index]); } return image; } VulkanImage* TextureCache::GetDummyStorageTexture(bool uint_format, bool image_3d) { KYTY_PROFILER_BLOCK("TextureCache::GetDummyStorageTexture"); Common::LockGuard lock(m_dummy_mutex); auto* ctx = g_render_ctx->GetGraphicCtx(); if (m_dummy_ctx != nullptr && m_dummy_ctx != ctx) { EXIT("TextureCache: storage dummy texture context changed, previous=%p current=%p\n", static_cast(m_dummy_ctx), static_cast(ctx)); } m_dummy_ctx = ctx; const auto index = DummyTextureIndex(uint_format, image_3d); auto*& image = m_dummy_storage_textures[index]; if (image == nullptr) { image = CreateDummyTexture(ctx, uint_format, image_3d, true, &m_dummy_storage_memory[index]); } return image; } } // namespace Libs::Graphics