aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-28 17:08:43 -0400
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-28 17:08:43 -0400
commit55d315c644fdf36405daaf71f75942553a267a6e (patch)
tree5c68632583f18db89e15352ee825a8dc73a57e2a
parent104381f025772909bb0032d58c47aeeab17a6ef6 (diff)
ASTC: re-compress to BC3 except huge arrays, which stay RGBA8 and trimmed
Title 1 has ~700 ASTC 8x8 textures (~280 MB as stored); decoded to RGBA8 they took ~4.5 GB, pushing Eden past the 8 GB of an RTX 4070 Laptop (8.4-9.8 GB in use) and the texture cache into permanent aggressive collection. With astc_recompression=Bc3 (CPU, asynchronous) they fit (4.6-5.6 GB). The two 8192x8192x68 terrain arrays would still not fit as BC3, so ASTC 2D arrays over 1 GiB as RGBA8 always use the GPU decoder with trimmed levels (RGBA8 host image, UNORM/sRGB views), and the decoder pass now exists whenever the GPU lacks native ASTC. Also logs every 600 frames: device memory against the collector thresholds, the largest images, totals by format, and buffer cache totals. Title screen 65-150 s: 10.1 -> 16.0 fps.
-rw-r--r--src/video_core/buffer_cache/buffer_cache.h16
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp51
-rw-r--r--src/video_core/texture_cache/texture_cache.h40
3 files changed, 104 insertions, 3 deletions
diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h
index 126e73c9b6..4df855c0fe 100644
--- a/src/video_core/buffer_cache/buffer_cache.h
+++ b/src/video_core/buffer_cache/buffer_cache.h
@@ -10,6 +10,8 @@
#include <memory>
#include <numeric>
#include <stdexcept>
+#include <vector>
+#include <fmt/ranges.h>
#include "common/range_sets.inc"
#include "video_core/buffer_cache/buffer_cache_base.h"
@@ -79,6 +81,20 @@ void BufferCache<P>::TickFrame() {
return;
}
runtime.TickFrame(slot_buffers);
+ if (static u64 frames{}; ++frames % 600 == 0) {
+ u64 total{};
+ size_t count{};
+ std::vector<std::pair<u64, VAddr>> largest;
+ for (const auto [buffer_id, buffer] : slot_buffers) {
+ total += buffer->SizeBytes();
+ ++count;
+ largest.emplace_back(buffer->SizeBytes(), buffer->CpuAddr());
+ }
+ std::partial_sort(largest.begin(), largest.begin() + std::min<size_t>(6, largest.size()),
+ largest.end(), std::greater<>{});
+ LOG_INFO(HW_GPU, "Buffer cache: {} buffers {} MiB; largest {}", count, total >> 20,
+ fmt::join(largest.begin(), largest.begin() + std::min<size_t>(6, largest.size()), ", "));
+ }
// Calculate hits and shots and move hit bits to the right
const u32 hits = std::reduce(channel_state->uniform_cache_hits.begin(),
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index d3f255f05d..12d985fb12 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -148,10 +148,41 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
return usage;
}
+// An ASTC 2D array whose RGBA8 decode exceeds 1 GiB (title 1's 8192x8192x68 terrain textures).
+// These stay on the GPU decoder as RGBA8 with trimmed levels (TrimmedHostInfo) even when other
+// ASTC images are re-compressed to BC1/BC3 on the CPU: re-compressing them would not fit either.
+[[nodiscard]] bool IsHugeAstcArray(const Device& device, const ImageInfo& info) {
+ if (!IsPixelFormatASTC(info.format) || device.IsOptimalAstcSupported() ||
+ info.type != ImageType::e2D || info.size.depth != 1) {
+ return false;
+ }
+ u64 bytes = 0;
+ for (s32 level = 0; level < info.resources.levels; ++level) {
+ bytes += u64{(std::max)(info.size.width >> level, 1U)} *
+ (std::max)(info.size.height >> level, 1U) * 4;
+ }
+ return bytes * info.resources.layers > (u64{1} << 30);
+}
+
+// The host format of a huge ASTC array: the GPU decoder's RGBA8 (sRGB kept for sampling).
+[[nodiscard]] std::optional<VkFormat> HugeAstcHostFormat(const Device& device, const ImageInfo& info) {
+ if (!IsHugeAstcArray(device, info)) {
+ return std::nullopt;
+ }
+ return VideoCore::Surface::IsPixelFormatSRGB(info.format) ? VK_FORMAT_A8B8G8R8_SRGB_PACK32
+ : VK_FORMAT_A8B8G8R8_UNORM_PACK32;
+}
+
+constexpr std::array<VkFormat, 2> HugeAstcViewFormats{VK_FORMAT_A8B8G8R8_UNORM_PACK32,
+ VK_FORMAT_A8B8G8R8_SRGB_PACK32};
+
[[nodiscard]] bool WillUseAcceleratedAstcDecode(const Device& device, const ImageInfo& info) {
if (!IsPixelFormatASTC(info.format) || device.IsOptimalAstcSupported()) {
return false;
}
+ if (IsHugeAstcArray(device, info)) {
+ return true;
+ }
if (Settings::values.accelerate_astc.GetValue() != Settings::AstcDecodeMode::Gpu) {
return false;
}
@@ -990,7 +1021,9 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched
: device{device_}, scheduler{scheduler_}, memory_allocator{memory_allocator_},
staging_buffer_pool{staging_buffer_pool_}, blit_image_helper{blit_image_helper_},
render_pass_cache{render_pass_cache_}, resolution{Settings::values.resolution_info} {
- if (Settings::values.accelerate_astc.GetValue() == Settings::AstcDecodeMode::Gpu) {
+ // Huge ASTC arrays always decode on the GPU (IsHugeAstcArray), whatever the decode setting.
+ if (Settings::values.accelerate_astc.GetValue() == Settings::AstcDecodeMode::Gpu ||
+ !device.IsOptimalAstcSupported()) {
astc_decoder_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool,
compute_pass_descriptor_queue, memory_allocator);
}
@@ -1926,7 +1959,10 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
runtime{&runtime_},
original_image(MakeImage(runtime_.device, runtime_.memory_allocator,
TrimmedHostInfo(runtime_.device, info, host_level_offset),
- runtime->ViewFormats(info.format))),
+ IsHugeAstcArray(runtime_.device, info)
+ ? std::span<const VkFormat>{HugeAstcViewFormats}
+ : runtime->ViewFormats(info.format),
+ HugeAstcHostFormat(runtime_.device, info))),
aspect_mask(ImageAspectMask(info.format)) {
if (original_image.IsUnbacked()) {
flags |= VideoCommon::ImageFlagBits::Unbacked;
@@ -1934,7 +1970,11 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
if (host_level_offset != 0) {
flags |= VideoCommon::ImageFlagBits::TrimmedLevels;
}
- if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported()) {
+ if (IsHugeAstcArray(runtime->device, info)) {
+ flags |= VideoCommon::ImageFlagBits::AcceleratedUpload;
+ flags |= VideoCommon::ImageFlagBits::Converted;
+ flags |= VideoCommon::ImageFlagBits::CostlyLoad;
+ } else if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported()) {
switch (Settings::values.accelerate_astc.GetValue()) {
case Settings::AstcDecodeMode::Gpu:
if (WillUseAcceleratedAstcDecode(runtime->device, info)) {
@@ -2580,6 +2620,11 @@ ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::ImageViewI
}
}
auto format_info = MaxwellToVK::SurfaceFormat(*device, FormatType::Optimal, true, format);
+ if (IsHugeAstcArray(*device, image.info)) {
+ format_info.format = VideoCore::Surface::IsPixelFormatSRGB(format)
+ ? VK_FORMAT_A8B8G8R8_SRGB_PACK32
+ : VK_FORMAT_A8B8G8R8_UNORM_PACK32;
+ }
if (device->ApiVersion() >= VK_API_VERSION_1_3) {
const VkFormatProperties3 properties3 =
device->GetPhysical().GetFormatProperties3(format_info.format);
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index e1cb493697..11de37eb6b 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -6,7 +6,10 @@
#pragma once
+#include <algorithm>
#include <limits>
+#include <map>
+#include <vector>
#include <optional>
#include <bit>
#include "common/container/unordered_map.h"
@@ -173,6 +176,43 @@ void TextureCache<P>::TickFrame() {
if (runtime.CanReportMemoryUsage()) {
total_used_memory = runtime.GetDeviceMemoryUsage();
}
+ // Every 600 frames: device memory in use against the collector's thresholds, and what the
+ // images themselves account for (VRAM pressure makes the collector thrash).
+ if (static u64 frames{}; ++frames % 600 == 0) {
+ u64 image_bytes{};
+ size_t images{};
+ std::vector<std::pair<u64, const Image*>> largest;
+ std::map<u32, std::pair<u64, u32>> by_format;
+ for (const auto [image_id, image] : slot_images) {
+ const u64 bytes = GetScaledImageSizeBytes(*image);
+ image_bytes += bytes;
+ ++images;
+ largest.emplace_back(bytes, image);
+ auto& format = by_format[static_cast<u32>(image->info.format)];
+ format.first += bytes;
+ ++format.second;
+ }
+ LOG_INFO(HW_GPU, "Texture cache: device memory {} MiB (expected {} MiB, critical {} MiB), {} images {} MiB",
+ total_used_memory >> 20, expected_memory >> 20, critical_memory >> 20, images,
+ image_bytes >> 20);
+ std::partial_sort(largest.begin(), largest.begin() + std::min<size_t>(12, largest.size()),
+ largest.end(), [](const auto& a, const auto& b) { return a.first > b.first; });
+ for (size_t i = 0; i < std::min<size_t>(12, largest.size()); ++i) {
+ const Image& image = *largest[i].second;
+ LOG_INFO(HW_GPU, " {} MiB: {:#x} {}x{}x{} layers {} levels {} format {} type {} flags {:#x}",
+ largest[i].first >> 20, image.gpu_addr, image.info.size.width, image.info.size.height,
+ image.info.size.depth, image.info.resources.layers, image.info.resources.levels,
+ static_cast<u32>(image.info.format), static_cast<u32>(image.info.type),
+ static_cast<u32>(image.flags));
+ }
+ std::vector<std::pair<u64, u32>> formats;
+ for (const auto& [format, total] : by_format) formats.emplace_back(total.first, format);
+ std::sort(formats.rbegin(), formats.rend());
+ for (size_t i = 0; i < std::min<size_t>(8, formats.size()); ++i) {
+ LOG_INFO(HW_GPU, " format {}: {} MiB in {} images", formats[i].second, formats[i].first >> 20,
+ by_format[formats[i].second].second);
+ }
+ }
if (total_used_memory > minimum_memory) {
RunGarbageCollector();
}