From 130c255ed91e2bf8f726b38101cc5fc2abcf3935 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:21:38 -0400 Subject: vulkan: share one zeroed block between empty sparse blocks on AMD AMD's drivers can't sample images with unbound memory, so large sparse images are backed whole there, Mario Kart World's nine sparse masks included (about 2.9 GB, re-uploaded whole for every tile the game maps). Where the driver supports sparseResidencyAliased, SparseTiles images are now created with VK_IMAGE_CREATE_SPARSE_ALIASED_BIT and every block is bound to one zeroed 64 KiB block when the image is created (the image is cleared once); a block gets memory of its own when data arrives, as on other drivers. Nothing is ever left unbound. If the GPU renders into such an image, every block gets memory of its own first, cleared, and the data is loaded again, so the shared block stays zero. EDEN_NX2_SPARSE_ALIASED=1 uses the shared block wherever it is supported, =0 never. Radeon 780M (proprietary driver 32.0.31041), Mario Kart World title screen, 200 s: no hang or device loss, world lit, shared memory 12.7 GB at the end growing 1.8 GB/min, against 18.7 GB and 5.1 GB/min with the masks backed whole. With every sparse image unbound instead (no AMD fallback at all) the static world renders dark there, so the other large sparse images stay backed whole on AMD. --- src/video_core/texture_cache/texture_cache.h | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'src/video_core/texture_cache/texture_cache.h') diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 4b93cd135b..5f655e5b60 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -1465,6 +1465,20 @@ bool TextureCache

::BindSparseTiles(Image& image, std::span r } } +template +bool TextureCache

::BackSparseTilesWhole(Image& image) { + if constexpr (requires { &Image::BackTilesWhole; }) { + const u64 bound_bytes = ImageMemoryBytes(image); + const bool backed = image.BackTilesWhole(); + if (True(image.flags & ImageFlagBits::Registered)) { + total_used_memory += ImageMemoryBytes(image) - bound_bytes; + } + return backed; + } else { + return false; + } +} + template bool TextureCache

::UploadWrittenLayers(Image& image) { if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) { @@ -3090,6 +3104,18 @@ void TextureCache

::PrepareImage(ImageId image_id, bool is_modification, bool } } if (is_modification) { + if constexpr (requires { &Image::BackTilesWhole; }) { + if (True(image.flags & ImageFlagBits::SparseTiles) && image.SparseTilesShareZeroBlock()) { + // The render would write the zeroed block all empty blocks share: each gets memory + // of its own first, and the parts that hold data are loaded again. + LOG_INFO(HW_GPU, "GPU write into sparse tiled image {:#x}; backing it whole", + image.gpu_addr); + if (BackSparseTilesWhole(image)) { + image.dirty_ranges.clear(); + RefreshSparseTiles(image); + } + } + } if constexpr (requires { &Image::BindLayers; }) { if (True(image.flags & ImageFlagBits::SparseLayers)) { std::vector layers(static_cast(image.info.resources.layers)); -- cgit v1.2.3