From 6d4c595634f85376e0600f67162347426e74944d Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Sun, 1 Nov 2026 08:43:42 -0500 Subject: vulkan: reuse ASTC transcode decode images The RGBA8 image an ASTC texture is decoded into before BC1/BC3 compression was made for each texture (263 us) and freed once the GPU was past it. Once the GPU is done with one it now decodes the next texture of the same shape: title 1's title screen decodes dozens of textures a frame, mostly of a few shapes. Free ones are kept up to 256 MiB and destroyed after 60 unused frames. Taking them from VMA's shared blocks instead was faster (90 us), but freeing dozens a frame there left ~1 GB of those blocks unused. Against that, Mario Kart World's title screen over 10 minutes: VRAM in use 6.34 -> 5.90 GB on average, peak 6.99 -> 6.45 GB. --- .../renderer_vulkan/vk_texture_cache.cpp | 90 ++++++++++++++++------ src/video_core/renderer_vulkan/vk_texture_cache.h | 9 ++- 2 files changed, 73 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 5ba36feee9..6daf394039 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -2060,10 +2060,33 @@ void TextureCacheRuntime::ReleaseMsaaScratchImage(VkImage image) { } void TextureCacheRuntime::TickFrame() { + static constexpr u32 MAX_UNUSED_SCRATCH_FRAMES = 60; + // Finished decode images wait for the next texture of their shape, up to this much memory. + static constexpr u64 MaxFreeTranscodeBytes = u64{256} << 20; + const auto decode_bytes = [](const PendingTranscode& t) { + u64 bytes{}; + for (u32 level = 0; level < t.levels; ++level) { + bytes += u64{(std::max)(t.width >> level, 1U)} * (std::max)(t.height >> level, 1U) * 4; + } + return bytes * t.layers; + }; + u64 free_bytes{}; + for (const PendingTranscode& t : free_transcodes) { + free_bytes += decode_bytes(t); + } while (!pending_transcodes.empty() && scheduler.IsFree(pending_transcodes.front().tick)) { + PendingTranscode& done = pending_transcodes.front(); + const u64 bytes = decode_bytes(done); + if (free_bytes + bytes <= MaxFreeTranscodeBytes) { + done.unused_frames = 0; + free_bytes += bytes; + free_transcodes.push_back(std::move(done)); + } pending_transcodes.pop_front(); } - static constexpr u32 MAX_UNUSED_SCRATCH_FRAMES = 60; + std::erase_if(free_transcodes, [](PendingTranscode& t) { + return ++t.unused_frames > MAX_UNUSED_SCRATCH_FRAMES; + }); std::erase_if(msaa_scratch_images, [this](MsaaScratchImage& scratch) { if (!scheduler.IsFree(scratch.tick)) { scratch.unused_frames = 0; @@ -3660,30 +3683,47 @@ void TextureCacheRuntime::TranscodeAstcToBcn( const u32 levels = static_cast(info.resources.levels); const u32 layers = static_cast(info.resources.layers); constexpr VkFormat DecodeFormat = VK_FORMAT_A8B8G8R8_UNORM_PACK32; - PendingTranscode pending{ - .tick = 0, - .image = memory_allocator.CreateImage(VkImageCreateInfo{ - .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .imageType = VK_IMAGE_TYPE_2D, - .format = DecodeFormat, - .extent = {width, height, 1}, - .mipLevels = levels, - .arrayLayers = layers, - .samples = VK_SAMPLE_COUNT_1_BIT, - .tiling = VK_IMAGE_TILING_OPTIMAL, - .usage = VK_IMAGE_USAGE_STORAGE_BIT, - .sharingMode = VK_SHARING_MODE_EXCLUSIVE, - .queueFamilyIndexCount = 0, - .pQueueFamilyIndices = nullptr, - .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, - }, true), - .views = {}, - }; - for (u32 level = 0; level < levels; ++level) { - pending.views.push_back( - MakeStorageView(device.GetLogical(), level, *pending.image, DecodeFormat)); + // A decode image of the same shape the GPU is done with: title 1's title screen decodes + // dozens of textures a frame, mostly of a few shapes. Making one took 263 us; from VMA's + // shared blocks 90 us, but freeing them there left ~1 GB of those blocks unused. + const auto reusable = std::ranges::find_if(free_transcodes, [&](const PendingTranscode& t) { + return t.width == width && t.height == height && t.levels == levels && t.layers == layers; + }); + PendingTranscode pending; + if (reusable != free_transcodes.end()) { + pending = std::move(*reusable); + free_transcodes.erase(reusable); + } else { + pending = PendingTranscode{ + .tick = 0, + .image = memory_allocator.CreateImage(VkImageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .imageType = VK_IMAGE_TYPE_2D, + .format = DecodeFormat, + .extent = {width, height, 1}, + .mipLevels = levels, + .arrayLayers = layers, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = VK_IMAGE_USAGE_STORAGE_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }, true), + .views = {}, + .width = width, + .height = height, + .levels = levels, + .layers = layers, + .unused_frames = 0, + }; + for (u32 level = 0; level < levels; ++level) { + pending.views.push_back( + MakeStorageView(device.GetLogical(), level, *pending.image, DecodeFormat)); + } } astc_decoder_pass->AssembleInto( info, 0, image.guest_size_bytes, diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 3944e183fd..1b99e690ce 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -177,13 +177,20 @@ public: void TranscodeAstcToBcn(Image& image, const StagingBufferRef& map, std::span swizzles); - /// Temporary decode images, freed once the GPU is past `tick`. + /// Temporary decode images, reusable once the GPU is past `tick`. struct PendingTranscode { u64 tick; vk::Image image; std::vector views; + u32 width; + u32 height; + u32 levels; + u32 layers; + u32 unused_frames; }; std::deque pending_transcodes; + /// Decode images the GPU is done with, for the next texture of the same shape. + std::vector free_transcodes; std::optional bl3d_unswizzle_pass; const Settings::ResolutionScalingInfo& resolution; -- cgit v1.2.3