aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-28 15:37:34 -0400
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-28 15:37:34 -0400
commit934ad3b97c12e1d40cf4e1f6094c756873b6af82 (patch)
tree5e867981c552d4b74898db22a90230dbd4fda546
parent8393e2912616ce475619900be8cb29e551ff2997 (diff)
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).
-rw-r--r--src/video_core/renderer_vulkan/vk_compute_pass.cpp18
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp61
-rw-r--r--src/video_core/texture_cache/image_base.h4
-rw-r--r--src/video_core/texture_cache/texture_cache.h51
-rw-r--r--src/video_core/texture_cache/util.cpp7
5 files changed, 132 insertions, 9 deletions
diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.cpp b/src/video_core/renderer_vulkan/vk_compute_pass.cpp
index 2eac00103e..65e0d5f145 100644
--- a/src/video_core/renderer_vulkan/vk_compute_pass.cpp
+++ b/src/video_core/renderer_vulkan/vk_compute_pass.cpp
@@ -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);
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index 7e2420200e..d3f255f05d 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -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};
diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h
index 846a6f7fc1..d0af53631d 100644
--- a/src/video_core/texture_cache/image_base.h
+++ b/src/video_core/texture_cache/image_base.h
@@ -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;
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index 75005f0731..e1cb493697 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -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);
}
diff --git a/src/video_core/texture_cache/util.cpp b/src/video_core/texture_cache/util.cpp
index 21070b2d25..aae2c7e4fd 100644
--- a/src/video_core/texture_cache/util.cpp
+++ b/src/video_core/texture_cache/util.cpp
@@ -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)) {