From dad900c61d6c7ff8aefc9e666ca7c3de4bbf4176 Mon Sep 17 00:00:00 2001 From: nmzik Date: Mon, 12 Oct 2026 04:51:02 +0200 Subject: [PATCH] Fix BC alias writes and GC --- src/graphics/guest_gpu/graphicsRun.cpp | 13 +- .../host_gpu/renderer/cache/textureCache.cpp | 70 +++++++++- .../host_gpu/renderer/cache/textureCache.h | 10 ++ tests/ShaderRecompilerComputeTests.cpp | 130 ++++++++++++++++++ 4 files changed, 211 insertions(+), 12 deletions(-) diff --git a/src/graphics/guest_gpu/graphicsRun.cpp b/src/graphics/guest_gpu/graphicsRun.cpp index 50389810..e2b06883 100644 --- a/src/graphics/guest_gpu/graphicsRun.cpp +++ b/src/graphics/guest_gpu/graphicsRun.cpp @@ -576,12 +576,7 @@ bool GuestGpu::Process(Submission& submission) { } } if (progressed) { - if (complete) { - m_renderer.RunGarbageCollector(); - } cp.BufferFlush(); - } else if (complete) { - m_renderer.RunGarbageCollector(); } break; } @@ -602,20 +597,16 @@ bool GuestGpu::Process(Submission& submission) { complete = cp.Process(submission.command_execution, submission.commands) == Pm4ProcessResult::Complete; if (submission.command_execution.MadeProgress()) { - if (complete) { - m_renderer.RunGarbageCollector(); - } cp.BufferFlush(); - } else if (complete) { - m_renderer.RunGarbageCollector(); } break; } case SubmissionType::FlipPreparation: - m_renderer.RunGarbageCollector(); cp.PrepareCpuFlip(submission.flip_request_id); break; case SubmissionType::SuspendPoint: + // Age caches at the frame boundary, not once per command list. + m_renderer.RunGarbageCollector(); cp.EmitGlobalBarrier(); m_renderer.GetCommandScheduler().DeferPriorityOperation( [ready = m_suspend_point_ready] { ready->release(); }); diff --git a/src/graphics/host_gpu/renderer/cache/textureCache.cpp b/src/graphics/host_gpu/renderer/cache/textureCache.cpp index 9cbbdfda..5dab5ffd 100644 --- a/src/graphics/host_gpu/renderer/cache/textureCache.cpp +++ b/src/graphics/host_gpu/renderer/cache/textureCache.cpp @@ -633,6 +633,9 @@ void TextureCache::CopyImage(ImageId destination_id, ImageId source_id) { void TextureCache::CopyImageMip(ImageId destination_id, ImageId source_id, uint32_t mip, uint32_t layer) { RefreshCopySource(source_id); + if (m_slot_images[destination_id].IsBufferModified()) { + RefreshImage(destination_id); + } auto& destination = m_slot_images[destination_id]; auto& source = m_slot_images[source_id]; TrackImage(destination_id); @@ -753,11 +756,63 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp return replacement_id; } +namespace BcAlias { + +bool Matches(const ImageInfo& requested, const ImageInfo& cached) { + return requested.guest_format != cached.guest_format && + requested.IsBlock() != cached.IsBlock() && + requested.tile_mode == cached.tile_mode && + requested.bytes_per_block == cached.bytes_per_block && + requested.samples == 1 && cached.samples == 1 && + (requested.data != cached.data || requested.resources != cached.resources || + requested.BlockExtent() != cached.BlockExtent()) && + !TextureCache::SameBacking(cached, requested, false); +} + +bool Publish(TextureCache& cache, Image& image) { + if (!cache.CanDownload(image)) { + return false; + } + // ObtainBuffer preserves padding and records ownership before the GPU-only transfer. + auto [buffer, offset] = + cache.m_buffer_cache.ObtainBuffer(image.info.data.address, image.info.data.size, true); + cache.DownloadImage(image, *buffer, offset, image.info.data.size, image.info.resources.levels); + image.ClearGpuModified(); + image.MarkBufferModified(); + return true; +} + +void Resolve(TextureCache& cache, ImageId cached_id, ImageId merged_id) { + auto& cached = cache.m_slot_images[cached_id]; + auto* merged = merged_id ? &cache.m_slot_images[merged_id] : nullptr; + // PS5 raw block images can span several packed BC mips. Transfer their complete + // guest bytes, including tail padding, before retiring the incompatible backing. + if (cached.SafeToDownload()) { + if (merged && merged->SafeToDownload() && !Publish(cache, *merged)) { + return; + } + if (!Publish(cache, cached)) { + return; + } + if (merged) { + merged->MarkBufferModified(); + } + } + cached.binding.needs_rebind |= cached.binding.is_bound || cached.binding.is_target; + cache.FreeImage(cached_id); +} + +} // namespace BcAlias + TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& requested, BindingType binding, ImageId cached_id, ImageId merged_id) { auto owner = m_slot_images.try_get(cached_id); - if (owner == nullptr) { + if (owner == nullptr || !owner->registered) { + return {merged_id}; + } + if (BcAlias::Matches(requested, owner->info) && !owner->IsCpuDirty()) { + BcAlias::Resolve(*this, cached_id, merged_id); return {merged_id}; } auto& cached = *owner; @@ -1175,6 +1230,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) { std::scoped_lock lock {m_lock}; const auto candidates = FindImagesInRegion(desc.info.data.address, desc.info.data.size, false); + bool block_alias = false; for (const auto id: candidates) { const auto& image = m_slot_images[id]; @@ -1193,6 +1249,9 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) { } if (SameBacking(image.info, desc.info, exact_format)) { result = id; + } else if (image.SafeToDownload() && + BcAlias::Matches(desc.info, image.info)) { + block_alias = true; } } @@ -1210,6 +1269,15 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) { view_layer = overlap.layer; } } + } else if (block_alias) { + for (const auto candidate: candidates) { + const auto* owner = m_slot_images.try_get(candidate); + if (candidate != result && owner != nullptr && owner->registered && + !owner->IsCpuDirty() && + BcAlias::Matches(m_slot_images[result].info, owner->info)) { + BcAlias::Resolve(*this, candidate, result); + } + } } if (result && !m_slot_images[result].depth_id) { diff --git a/src/graphics/host_gpu/renderer/cache/textureCache.h b/src/graphics/host_gpu/renderer/cache/textureCache.h index 7f426a0d..6d9b8864 100644 --- a/src/graphics/host_gpu/renderer/cache/textureCache.h +++ b/src/graphics/host_gpu/renderer/cache/textureCache.h @@ -26,8 +26,15 @@ class BufferCache; class CommandBuffer; class CommandScheduler; class RenderExecutor; +class TextureCache; struct TextureCacheTestAccess; +namespace BcAlias { +[[nodiscard]] bool Matches(const ImageInfo& requested, const ImageInfo& cached); +[[nodiscard]] bool Publish(TextureCache& cache, Image& image); +void Resolve(TextureCache& cache, ImageId cached, ImageId merged); +} // namespace BcAlias + class TextureCache { public: enum class BindingType : uint8_t { Texture, Storage, RenderTarget, DepthTarget, VideoOut }; @@ -181,6 +188,9 @@ private: friend struct TextureCacheTestAccess; friend class BufferCache; friend class RenderExecutor; + friend bool BcAlias::Matches(const ImageInfo&, const ImageInfo&); + friend bool BcAlias::Publish(TextureCache&, Image&); + friend void BcAlias::Resolve(TextureCache&, ImageId, ImageId); }; } // namespace Libs::Graphics diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 14653903..c0a961d8 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -159,6 +159,8 @@ struct BufferCacheTestAccess { static_assert(std::same_as>); + static uint64_t GcTick(const BufferCache &cache) { return cache.m_gc_tick; } + static void SetGarbageCollectionThresholds(BufferCache &cache, uint64_t trigger, uint64_t critical) { @@ -224,6 +226,8 @@ struct TextureCacheTestAccess { static_assert(TextureCache::ImagePageTable::kAddressSpaceBits == 44); static_assert(TextureCache::ImagePageTable::kFirstLevelBits == 14); + static uint64_t GcTick(const TextureCache &cache) { return cache.m_gc_tick; } + static std::unique_lock Lock(TextureCache &cache) { return std::unique_lock(cache.m_lock); } @@ -2614,6 +2618,31 @@ public: packet[4] = value; }; + const auto cache_ticks = [&] { + std::pair ticks; + gpu.SendCommandSync([&] { + ticks = {TextureCacheTestAccess::GcTick(context.GetTextureCache()), + BufferCacheTestAccess::GcTick(context.GetBufferCache())}; + }); + return ticks; + }; + const auto initial_cache_ticks = cache_ticks(); + uint32_t graphics_value = 0; + uint32_t async_value = 0; + std::array graphics_list{}, compute_list{}; + write(graphics_list.data(), &graphics_value, 17); + write(compute_list.data(), &async_value, 29); + // A frame can contain many lists; completing them must not age either cache. + for (uint32_t i = 0; i < 32; ++i) { + gpu.Submit(graphics_list, {}); + gpu.SubmitCompute(0x20, compute_list); + } + finish(); + Require(name, "command-list cache lifetime", + graphics_value == 17 && async_value == 29 && + cache_ticks() == initial_cache_ticks, + "individual graphics or compute lists aged the caches within a frame"); + gpu.SendCommandSync([&] { setup.BufferInit(); SubmitInfo blocked; @@ -2648,6 +2677,10 @@ public: parser.join(); Require(name, "marker insertion", parser_nonblocking, "inserting a suspend point blocked the GPU command thread"); + Require(name, "frame cache maintenance", + cache_ticks() == std::pair{initial_cache_ticks.first + 1, + initial_cache_ticks.second + 1}, + "a suspend point did not age both caches exactly once"); std::binary_semaphore second_entered{0}; std::binary_semaphore second_returned{0}; @@ -2685,6 +2718,10 @@ public: "or the host command lane"); Require(name, "frame count", gpu.GetFrameNum() == 2, "suspend-point insertion did not advance the frame counter once"); + Require(name, "async cache lifetime", + cache_ticks() == std::pair{initial_cache_ticks.first + 2, + initial_cache_ticks.second + 2}, + "asynchronous work aged the caches between frame boundaries"); // A marker must stay behind a stalled graphics stream, then clear its // predicate before the next stream. Async compute releases that stream. @@ -6741,6 +6778,99 @@ public: "to the image"); texture_cache.MarkGpuWritten(mirror_refresh); + // Unity copies BC mip regions and packed mip tails through integer images. + // The CPU allocation remains white; later array sampling must see GPU writes. + for (uint32_t format_index = 0; format_index < 3; ++format_index) { + constexpr std::array formats{vk::Format::eBc4UnormBlock, + vk::Format::eBc5UnormBlock, + vk::Format::eBc7UnormBlock}; + constexpr std::array guest_formats{Prospero::BufferFormat::kBc4UNorm, + Prospero::BufferFormat::kBc5UNorm, + Prospero::BufferFormat::kBc7UNorm}; + constexpr uint32_t width = 128, levels = 8, layers = 3; + const uint32_t block_bytes = format_index == 0 ? 8 : 16; + const uint64_t address = base + 0x1900000 + format_index * 0x40000; + auto array = MakeLinearDesc(address, 0, formats[format_index], + guest_formats[format_index], Prospero::ImageType::kColor2D, + {width, width, 1}, layers, block_bytes, 1); + array.info.resources.levels = levels; + array.info.tile_mode = Prospero::TileMode::kStandard4KB; + array.info.UpdateSize(); + array.view_info.level_count = levels; + std::memset(memory + address - base, 0xff, array.info.data.size); + auto value_for = [&](uint32_t layer, uint64_t offset) { + return 0x7c000000u | (format_index << 16) | (layer << 8) | + static_cast(offset / 4096); + }; + auto write_raw = [&](uint32_t layer, uint32_t level, uint32_t value) { + const auto &mip = array.info.mip_layout[level]; + auto raw = MakeLinearDesc( + address + layer * array.info.tiled_slice_stride + mip.offset, 0, + block_bytes == 8 ? vk::Format::eR32G32Uint + : vk::Format::eR32G32B32A32Uint, + block_bytes == 8 ? Prospero::BufferFormat::k32_32UInt + : Prospero::BufferFormat::k32_32_32_32UInt, + Prospero::ImageType::kColor2D, {mip.pitch, mip.height, 1}, + 1, block_bytes, 1); + raw.type = BindingType::Storage; + raw.info.tile_mode = array.info.tile_mode; + raw.info.UpdateSize(); + raw.view_info.usage = vk::ImageUsageFlagBits::eStorage; + const auto producer = texture_cache.FindImage(raw); + (void)texture_cache.FindTexture(producer, raw); + vk::ClearValue clear{}; + std::fill_n(clear.color.uint32.data(), 4, value); + TextureCacheTestAccess::ClearImage(texture_cache, scheduler.Current(), + producer, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, clear); + }; + for (const uint32_t layer : {0u, 2u}) { + uint64_t previous_offset = UINT64_MAX; + for (uint32_t level = 0; level < levels; ++level) { + const auto offset = array.info.mip_layout[level].offset; + if (offset == previous_offset) continue; + previous_offset = offset; + write_raw(layer, level, value_for(layer, offset)); + } + } + std::vector probes; + std::vector expected; + for (uint32_t layer = 0; layer < layers; ++layer) { + for (uint32_t level = 0; level < levels; ++level) { + vk::BufferImageCopy copy{}; + copy.bufferOffset = expected.size() * sizeof(uint32_t); + copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, level, layer, 1}; + copy.imageExtent = {std::min(width >> level, 4u), + std::min(width >> level, 4u), 1}; + probes.push_back(copy); + expected.insert(expected.end(), block_bytes / sizeof(uint32_t), + layer == 1 ? UINT32_MAX + : value_for(layer, array.info.mip_layout[level].offset)); + } + } + auto verify_array = [&] { + const auto consumer = texture_cache.FindImage(array); + (void)texture_cache.FindTexture(consumer, array); + auto readback = CreateHostBuffer(name, expected.size() * sizeof(uint32_t), + vk::BufferUsageFlagBits::eTransferDst, std::vector(expected.size())); + texture_cache.GetImage(consumer).Download(probes, readback.buffer, 0, readback.size); + HostReadBarrier(readback.buffer, readback.size, vk::PipelineStageFlagBits::eTransfer, + vk::AccessFlagBits::eTransferWrite); + scheduler.Finish(); + Require(name, "raw image producers populate BC array", + ReadBuffer(name, readback, expected.size()) == expected, + "BC array lost GPU mip/tail writes or overwrote an untouched layer"); + DestroyBuffer(&readback); + }; + verify_array(); + constexpr uint32_t replacement = 0x13579bdfu; + write_raw(2, array.info.first_tail_level, replacement); + for (uint32_t level = array.info.first_tail_level; level < levels; ++level) { + const auto word = (2 * levels + level) * block_bytes / sizeof(uint32_t); + std::fill_n(expected.begin() + word, block_bytes / sizeof(uint32_t), replacement); + } + verify_array(); + } + // A write through a separate buffer must not replace untouched image // texels with stale backing bytes. Exact-base writes still refresh it. constexpr uint64_t buffer_alias_offset = 0x1a00000;