Keep huge ASTC arrays from a lower mip on the host (title 1 terrain)

Title 1's terrain virtual textures are 8192x8192x68 ASTC 8x8 sRGB arrays with
12 levels: 1.5 GB in guest memory (fully backed) but 23 GB each as the RGBA8
Eden decodes ASTC to, so they were created unbacked and the near terrain
sampled black. A GPU-decoded ASTC 2D array over 1 GiB decoded now keeps only
the levels from the first one that fits in 512 MiB (here 3: 1024x1024, 380
MiB); host mip 0 is that guest level. Uploads read each layer's tail of the
mip chain into a compact buffer and decode only those levels, views shift
their level range, and these images are never downloaded. Normalized
coordinates and derivative LOD address the smaller image correctly; the
nearest terrain is lower resolution than on the console.

Also logs how unbacked images are used (views, render targets, blits, DMA).
This commit is contained in:
Takoroka 2026-10-28 15:37:34 -04:00
commit b8a07d6d5b
5 changed files with 132 additions and 9 deletions

View file

@ -31,6 +31,7 @@
#include "video_core/renderer_vulkan/vk_update_descriptor.h"
#include "video_core/texture_cache/accelerated_swizzle.h"
#include "video_core/texture_cache/types.h"
#include "video_core/texture_cache/util.h"
#include "video_core/textures/decoders.h"
#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
@ -558,6 +559,15 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, image_barrier);
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, vk_pipeline);
});
// With trimmed host levels the input holds each layer's mip chain from host_level_offset on
// (see TextureCache::UploadImageContents), so its layer stride is that tail's size.
const u32 skip = image.host_level_offset;
const u32 input_layer_stride =
skip != 0 ? image.info.layer_stride -
static_cast<u32>(VideoCommon::FullUploadSwizzles(image.info)[skip].buffer_offset)
: image.info.layer_stride;
const size_t input_size = skip != 0 ? size_t{input_layer_stride} * image.info.resources.layers
: image.guest_size_bytes;
for (const VideoCommon::SwizzleParameters& swizzle : swizzles) {
const size_t input_offset = swizzle.buffer_offset + map.offset;
const u32 num_dispatches_x = Common::DivCeil(swizzle.num_tiles.width, 8U);
@ -566,12 +576,14 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map,
compute_pass_descriptor_queue.Acquire(scheduler, 2);
compute_pass_descriptor_queue.AddBuffer(map.buffer, input_offset,
image.guest_size_bytes - swizzle.buffer_offset);
compute_pass_descriptor_queue.AddImage(image.StorageImageView(swizzle.level));
input_size - swizzle.buffer_offset);
compute_pass_descriptor_queue.AddImage(
image.StorageImageView(swizzle.level - static_cast<s32>(skip)));
const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()};
// To unswizzle the ASTC data
const auto params = MakeBlockLinearSwizzle2DParams(swizzle, image.info);
auto params = MakeBlockLinearSwizzle2DParams(swizzle, image.info);
params.layer_stride = input_layer_stride;
ASSERT(params.origin == (std::array<u32, 3>{0, 0, 0}));
ASSERT(params.destination == (std::array<s32, 3>{0, 0, 0}));
ASSERT(params.bytes_per_block_log2 == 4);

View file

@ -160,6 +160,40 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
info.size.depth == 1;
}
// The host shape of an image whose decoded copy would not fit in VRAM: a GPU-decoded ASTC array
// over 1 GiB as RGBA8 (title 1's 8192x8192x68 ASTC 8x8 virtual textures are 24 GB each) keeps only
// its mip levels from `level_offset` on, so host mip 0 is guest mip `level_offset`. Normalized
// coordinates and derivative LOD still address it correctly; explicit LODs sample one level lower.
[[nodiscard]] ImageInfo TrimmedHostInfo(const Device& device, const ImageInfo& info, u32& level_offset) {
level_offset = 0;
if (!WillUseAcceleratedAstcDecode(device, info) || info.type != ImageType::e2D) {
return info;
}
const auto decoded_bytes = [&](u32 skip) {
u64 bytes = 0;
for (s32 level = static_cast<s32>(skip); 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;
};
if (decoded_bytes(0) <= (u64{1} << 30)) {
return info;
}
while (level_offset + 1 < static_cast<u32>(info.resources.levels) &&
decoded_bytes(level_offset) > (u64{512} << 20)) {
++level_offset;
}
ImageInfo host = info;
host.size.width = (std::max)(info.size.width >> level_offset, 1U);
host.size.height = (std::max)(info.size.height >> level_offset, 1U);
host.resources.levels -= static_cast<s32>(level_offset);
LOG_WARNING(Render_Vulkan, "{}x{}x{} ASTC image would be {} MiB decoded; keeping mip levels {}+ ({}x{})",
info.size.width, info.size.height, info.resources.layers, decoded_bytes(0) >> 20,
level_offset, host.size.width, host.size.height);
return host;
}
[[nodiscard]] VkImageCreateInfo MakeImageCreateInfo(const Device& device, const ImageInfo& info,
std::optional<VkFormat> format_override = {}) {
auto format_info =
@ -527,6 +561,20 @@ TransformBufferCopies(std::span<const VideoCommon::BufferCopy> copies, size_t bu
};
}
// A guest view's levels on a host image that keeps only levels >= host_level_offset.
[[nodiscard]] SubresourceRange HostRange(const Image& image, SubresourceRange range) {
const s32 skip = static_cast<s32>(image.host_level_offset);
if (skip == 0) {
return range;
}
const s32 host_levels = image.info.resources.levels - skip;
const s32 base = std::clamp(range.base.level - skip, 0, host_levels - 1);
const s32 end = std::clamp(range.base.level + range.extent.levels - skip, base + 1, host_levels);
range.base.level = base;
range.extent.levels = end - base;
return range;
}
[[nodiscard]] VkImageSubresourceRange MakeSubresourceRange(const ImageView* image_view) {
SubresourceRange range = image_view->range;
if (True(image_view->flags & VideoCommon::ImageViewFlagBits::Slice)) {
@ -1876,12 +1924,16 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
VAddr cpu_addr_)
: VideoCommon::ImageBase(info_, gpu_addr_, cpu_addr_), scheduler{&runtime_.scheduler},
runtime{&runtime_},
original_image(MakeImage(runtime_.device, runtime_.memory_allocator, info,
original_image(MakeImage(runtime_.device, runtime_.memory_allocator,
TrimmedHostInfo(runtime_.device, info, host_level_offset),
runtime->ViewFormats(info.format))),
aspect_mask(ImageAspectMask(info.format)) {
if (original_image.IsUnbacked()) {
flags |= VideoCommon::ImageFlagBits::Unbacked;
}
if (host_level_offset != 0) {
flags |= VideoCommon::ImageFlagBits::TrimmedLevels;
}
if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported()) {
switch (Settings::values.accelerate_astc.GetValue()) {
case Settings::AstcDecodeMode::Gpu:
@ -1906,11 +1958,12 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
original_image.SetObjectNameEXT(VideoCommon::Name(*this).c_str());
}
current_image = &Image::original_image;
storage_image_views.resize(info.resources.levels);
const s32 host_levels = info.resources.levels - static_cast<s32>(host_level_offset);
storage_image_views.resize(host_levels);
if (WillUseAcceleratedAstcDecode(runtime->device, info)) {
const auto& device = runtime->device.GetLogical();
const VkFormat storage_format = VK_FORMAT_A8B8G8R8_UNORM_PACK32;
for (s32 level = 0; level < info.resources.levels; ++level) {
for (s32 level = 0; level < host_levels; ++level) {
storage_image_views[level] =
MakeStorageView(device, level, *original_image, storage_format);
}
@ -2575,7 +2628,7 @@ ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::ImageViewI
.viewType = VkImageViewType{},
.format = format_info.format,
.components = swizzle_mapping,
.subresourceRange = MakeSubresourceRange(aspect_mask, info.range),
.subresourceRange = MakeSubresourceRange(aspect_mask, HostRange(image, info.range)),
};
const auto create = [&](TextureType tex_type, std::optional<u32> num_layers) {
VkImageViewCreateInfo ci{create_info};

View file

@ -46,6 +46,9 @@ enum class ImageFlagBits : u32 {
AsynchronousDecode = 1 << 16,
IsDecoding = 1 << 17, ///< Is currently being decoded asynchronously.
Unbacked = 1 << 18, ///< Host image has no memory (too big to back); never upload/download.
/// The host image keeps only mip levels >= host_level_offset (its decoded copy would not fit
/// in VRAM); uploads skip the rest, downloads are never done.
TrimmedLevels = 1 << 19,
};
DECLARE_ENUM_FLAG_OPERATORS(ImageFlagBits)
@ -94,6 +97,7 @@ struct ImageBase {
u32 unswizzled_size_bytes = 0;
u32 converted_size_bytes = 0;
u32 scale_rating = 0;
u32 host_level_offset = 0; ///< Guest mip level held by host mip 0 (see TrimmedLevels).
u64 scale_tick = 0;
bool has_scaled = false;

View file

@ -597,7 +597,7 @@ void TextureCache<P>::WriteMemory(DAddr cpu_addr, size_t size) {
template <class P>
bool TextureCache<P>::IsDownloadable(const ImageBase& image) const noexcept {
if (!image.IsSafeGpuCopy() || True(image.flags & ImageFlagBits::Unbacked)) {
if (!image.IsSafeGpuCopy() || True(image.flags & (ImageFlagBits::Unbacked | ImageFlagBits::TrimmedLevels))) {
return false;
}
if (image.info.num_samples == 1) {
@ -714,6 +714,14 @@ bool TextureCache<P>::BlitImage(const Tegra::Engines::Fermi2D::Surface& dst,
Image& dst_image = slot_images[dst_id];
Image& src_image = slot_images[src_id];
if (True(dst_image.flags & ImageFlagBits::Unbacked)) {
static u32 logged{};
if (logged++ < 64) {
LOG_INFO(HW_GPU, "2D blit into unbacked image {:#x}: dst {:#x} {}x{} from {:#x} {}x{}",
dst_image.gpu_addr, dst.Address(), dst.width, dst.height, src.Address(),
src.width, src.height);
}
}
bool is_src_rescaled = True(src_image.flags & ImageFlagBits::Rescaled);
bool is_dst_rescaled = True(dst_image.flags & ImageFlagBits::Rescaled);
@ -1186,6 +1194,24 @@ void TextureCache<P>::UploadImageContents(Image& image, StagingBuffer& staging)
const std::span<u8> mapped_span = staging.mapped_span;
const GPUVAddr gpu_addr = image.gpu_addr;
if (True(image.flags & ImageFlagBits::AcceleratedUpload) && image.host_level_offset != 0) {
// Only levels >= host_level_offset exist on the host: read each layer's tail of the mip
// chain into a compact buffer (layer stride = that tail's size) and decode those levels.
auto uploads = FullUploadSwizzles(image.info);
const size_t level_offset = uploads[image.host_level_offset].buffer_offset;
const size_t tail = image.info.layer_stride - level_offset;
for (s32 layer = 0; layer < image.info.resources.layers; ++layer) {
gpu_memory->ReadBlock(gpu_addr + layer * image.info.layer_stride + level_offset,
mapped_span.data() + layer * tail, tail,
VideoCommon::CacheType::NoTextureCache);
}
uploads.erase(uploads.begin(), uploads.begin() + image.host_level_offset);
for (auto& upload : uploads) {
upload.buffer_offset -= level_offset;
}
runtime.AccelerateImageUpload(image, staging, uploads, 0, 0);
return;
}
if (True(image.flags & ImageFlagBits::AcceleratedUpload)) {
gpu_memory->ReadBlock(gpu_addr, mapped_span.data(), mapped_span.size_bytes(),
VideoCommon::CacheType::NoTextureCache);
@ -1908,6 +1934,13 @@ ImageId TextureCache<P>::FindDMAImage(const ImageInfo& info, GPUVAddr gpu_addr)
return true;
}
} else if (IsSubCopy(info, existing_image, gpu_addr)) {
if (True(existing_image.flags & ImageFlagBits::Unbacked)) {
static u32 logged{};
if (logged++ < 64) {
LOG_INFO(HW_GPU, "DMA copy at {:#x} ({}x{}) into unbacked image {:#x}", gpu_addr,
info.size.width, info.size.height, existing_image.gpu_addr);
}
}
image_id = existing_image_id;
image_ids.push_back(existing_image_id);
return true;
@ -2003,6 +2036,13 @@ ImageViewId TextureCache<P>::FindRenderTargetView(const ImageInfo& info, GPUVAdd
base = image.TryFindBase(gpu_addr).value();
}
const s32 layers = image.info.type == ImageType::e3D ? info.size.depth : info.resources.layers;
if (True(image.flags & ImageFlagBits::Unbacked)) {
static u32 logged{};
if (logged++ < 64) {
LOG_INFO(HW_GPU, "Render target in unbacked image {:#x} at {:#x}: {}x{} level {} layer {}",
image.gpu_addr, gpu_addr, info.size.width, info.size.height, base.level, base.layer);
}
}
const SubresourceRange range{
.base = base,
.extent = {.levels = 1, .layers = layers},
@ -2185,6 +2225,13 @@ ImageViewId TextureCache<P>::FindOrEmplaceImageView(ImageId image_id, const Imag
if (const ImageViewId image_view_id = image.FindView(info); image_view_id) {
return image_view_id;
}
if (True(image.flags & ImageFlagBits::Unbacked)) {
// How the game uses an image too large to back (e.g. a sparse virtual texture).
LOG_INFO(HW_GPU, "View of unbacked image {:#x}: type {} format {} levels {}+{} layers {}+{}",
image.gpu_addr, static_cast<u32>(info.type), static_cast<u32>(info.format),
info.range.base.level, info.range.extent.levels, info.range.base.layer,
info.range.extent.layers);
}
const ImageViewId image_view_id = slot_image_views.insert(runtime, info, image_id, image, slot_images);
image.InsertView(info, image_view_id);
return image_view_id;
@ -2682,7 +2729,7 @@ void TextureCache<P>::BindRenderTarget(ImageViewId* old_id, ImageViewId new_id)
if (new_id) {
const ImageViewBase& old_view = slot_image_views[new_id];
if (True(old_view.flags & ImageViewFlagBits::PreemtiveDownload) &&
False(slot_images[old_view.image_id].flags & ImageFlagBits::Unbacked)) {
False(slot_images[old_view.image_id].flags & (ImageFlagBits::Unbacked | ImageFlagBits::TrimmedLevels))) {
const PendingDownload new_download{true, 0, old_view.image_id};
uncommitted_downloads.emplace_back(new_download);
}

View file

@ -1298,6 +1298,13 @@ void DeduceBlitImages(ImageInfo& dst_info, ImageInfo& src_info, const ImageBase*
}
u32 MapSizeBytes(const ImageBase& image) {
if (True(image.flags & ImageFlagBits::AcceleratedUpload) && image.host_level_offset != 0) {
// Each layer's mip chain from host_level_offset on (see UploadImageContents).
const auto uploads = FullUploadSwizzles(image.info);
const u32 tail = image.info.layer_stride -
static_cast<u32>(uploads[image.host_level_offset].buffer_offset);
return tail * image.info.resources.layers;
}
if (True(image.flags & ImageFlagBits::AcceleratedUpload)) {
return image.guest_size_bytes;
} else if (True(image.flags & ImageFlagBits::Converted)) {