From 9bcf80bf672834f5b39fc00568dba9099fc4f374 Mon Sep 17 00:00:00 2001 From: nmzik Date: Tue, 13 Oct 2026 00:28:17 +0200 Subject: [PATCH] graphics: share temporary staging for oversized image uploads --- .../host_gpu/renderer/cache/bufferCache.cpp | 54 +++++++----- tests/ShaderRecompilerComputeTests.cpp | 85 +++++++++++++++++++ 2 files changed, 116 insertions(+), 23 deletions(-) diff --git a/src/graphics/host_gpu/renderer/cache/bufferCache.cpp b/src/graphics/host_gpu/renderer/cache/bufferCache.cpp index 165e32e37..da3c8d75a 100644 --- a/src/graphics/host_gpu/renderer/cache/bufferCache.cpp +++ b/src/graphics/host_gpu/renderer/cache/bufferCache.cpp @@ -27,6 +27,26 @@ namespace { constexpr uint64_t MiB = 1024 * 1024; constexpr uint64_t GdsBufferSize = 64 * 1024; +std::pair UploadMemory(GraphicContext& graphics, CommandScheduler& scheduler, + StreamBuffer& staging, uint64_t size, uint64_t alignment, + vk::BufferUsageFlags usage, const auto& write) { + const auto [mapped, offset] = staging.Map(size, alignment); + if (mapped != nullptr) { + write(mapped, offset); + staging.Commit(); + return {&staging, offset}; + } + + // Large transfers use the same one-time host allocation as ordinary buffer uploads. + auto temporary = std::make_unique(graphics, scheduler, MemoryUsage::Upload, 0, + usage, size); + write(temporary->Mapped().data(), 0); + temporary->Flush(0, size); + auto* buffer = temporary.get(); + scheduler.DeferOperation([owner = std::move(temporary)]() mutable { owner.reset(); }); + return {buffer, 0}; +} + } // namespace void BufferCache::WriteDataBuffer(Buffer& buffer, uint64_t address, const void* source, @@ -447,28 +467,15 @@ vk::Buffer BufferCache::UploadCopies(Buffer& buffer, std::span c return nullptr; } - auto [mapped, base_offset] = m_staging_buffer.Map(total_size, 4); - if (mapped != nullptr) { + const auto source = UploadMemory(m_graphics, m_scheduler, m_staging_buffer, total_size, 4, + vk::BufferUsageFlagBits::eTransferSrc, [&](uint8_t* mapped, uint64_t base_offset) { for (auto& copy: copies) { const auto address = buffer.CpuAddress() + copy.dstOffset; std::memcpy(mapped + copy.srcOffset, reinterpret_cast(address), copy.size); copy.srcOffset += base_offset; } - m_staging_buffer.Commit(); - return m_staging_buffer.Handle(); - } - - auto temporary = std::make_unique(m_graphics, m_scheduler, MemoryUsage::Upload, 0, - vk::BufferUsageFlagBits::eTransferSrc, total_size); - for (const auto& copy: copies) { - const auto address = buffer.CpuAddress() + copy.dstOffset; - std::memcpy(temporary->Mapped().data() + copy.srcOffset, - reinterpret_cast(address), copy.size); - } - temporary->Flush(0, total_size); - const auto handle = temporary->Handle(); - m_scheduler.DeferOperation([owner = std::move(temporary)]() mutable { owner.reset(); }); - return handle; + }); + return source.first->Handle(); } std::pair BufferCache::ObtainBuffer(uint64_t vaddr, uint64_t size, @@ -521,12 +528,13 @@ std::pair BufferCache::ObtainBufferForImage(uint64_t vaddr, u return ObtainBuffer(vaddr, size, false, false); } - auto [staging, stage_offset] = m_staging_buffer.Map(size, 16); - if (staging == nullptr || !Libs::LibKernel::Memory::TryReadSparseBacking(vaddr, staging, size)) { - EXIT("BufferCache: failed to read mapped guest image backing\n"); - } - m_staging_buffer.Commit(); - return {&m_staging_buffer, stage_offset}; + return UploadMemory(m_graphics, m_scheduler, m_staging_buffer, size, 16, + vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eStorageBuffer, + [&](uint8_t* mapped, uint64_t) { + if (!Libs::LibKernel::Memory::TryReadSparseBacking(vaddr, mapped, size)) { + EXIT("BufferCache: failed to read mapped guest image backing\n"); + } + }); } void BufferCache::FillBuffer(uint64_t vaddr, uint64_t size, uint32_t value, bool is_gds) { diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 2650df023..ecced8559 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -5261,6 +5261,85 @@ public: std::printf("[gpu] %-32s ok\n", name); } + void CheckLargeSparseImageUpload() { + constexpr const char *name = "LargeSparseImageUpload"; + constexpr uint64_t size = 576ull << 20, ring_size = 512ull << 20, page = 0x10000; + constexpr uint64_t base = 0x0000000250000000ull; + constexpr std::array mapped_offsets{0, ring_size, size - page}; + EnsureRuntimeContext(); + RenderContext context(m_runtime_context); + auto &scheduler = context.GetCommandScheduler(); + HW::Context registers{}; + HW::UserConfig user_config{}; + HW::Shader shaders{}; + scheduler.Begin(registers, user_config, shaders); + context.InitializeGpu(nullptr); + void *reservation = reinterpret_cast(base); + Require(name, "sparse reservation", LibKernel::Memory::KernelReserveVirtualRange( + &reservation, size, 0x10, page) == 0, "large sparse reservation failed"); + int64_t direct = -1; + Require(name, "physical pages", LibKernel::Memory::KernelAllocateDirectMemory( + 0, LibKernel::Memory::KernelGetDirectMemorySize(), 3u * page, page, 0, &direct) == 0, + "sparse physical allocation failed"); + for (u32 index = 0; index < mapped_offsets.size(); ++index) { + void *mapped = reinterpret_cast(base + mapped_offsets[index]); + Require(name, "map sparse page", LibKernel::Memory::KernelMapDirectMemory( + &mapped, page, 0x3, 0x10, direct + index * page, page) == 0, + "sparse page mapping failed"); + auto *words = static_cast(mapped); + std::fill_n(words, page / sizeof(u32), 0u); + words[0] = 0x00020001u + index * 0x100u; + words[1] = 0x00040003u + index * 0x100u; + words[page / sizeof(u32) - 1u] = 0xabcdef01u; + context.MapMemory(base + mapped_offsets[index], page); + } + auto &cache = context.GetBufferCache(); + // Exercise the ordinary ring before requesting the captured 576-MiB footprint. + const auto small = cache.ObtainBufferForImage(base, page); + auto readback = CreateHostBuffer(name, 8u * sizeof(u32), + vk::BufferUsageFlagBits::eTransferDst, {}); + const vk::BufferCopy small_copy{small.second, 0, sizeof(u32)}; + scheduler.Current().Handle().copyBuffer(small.first->Handle(), readback.buffer, 1, &small_copy); + const auto large = cache.ObtainBufferForImage(base, size); + Require(name, "temporary image source", large.first != nullptr && large.first != small.first && + large.first->Size() == size && small.first->Size() == ring_size, + "oversized image upload resized the ring or truncated its source"); + constexpr std::array probes{0, page, ring_size - 4u, ring_size, size - 4u}; + std::array copies{}; + for (u32 index = 0; index < probes.size(); ++index) + copies[index] = {large.second + probes[index], (index + 1u) * sizeof(u32), sizeof(u32)}; + scheduler.Current().Handle().copyBuffer(large.first->Handle(), readback.buffer, + copies.size(), copies.data()); + // A compute consumer requires the same storage-buffer capability as the detiler. + const auto converted = TextureCacheTestAccess::Tiler(context.GetTextureCache()).TransformColor( + {large.first->Handle(), large.second + ring_size, 8u}, ColorTransform::SwapBgra16, true); + const vk::BufferCopy converted_copy{converted.offset, 6u * sizeof(u32), 8u}; + scheduler.Current().Handle().copyBuffer(converted.buffer, readback.buffer, 1, &converted_copy); + vk::BufferMemoryBarrier barrier{}; + barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + barrier.dstAccessMask = vk::AccessFlagBits::eHostRead; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.buffer = readback.buffer; + barrier.size = readback.size; + scheduler.Current().Handle().pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, + vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &barrier, 0, nullptr); + scheduler.Finish(); + Require(name, "sparse upload and deferred lifetime", + ReadBuffer(name, readback, 8) == std::vector{0x00020001u, 0x00020001u, 0u, 0u, + 0x00020101u, 0xabcdef01u, 0x00020103u, 0x00040101u}, + "upload lost sparse holes, boundary bytes, compute access, or in-flight lifetime"); + DestroyBuffer(&readback); + for (const auto offset : mapped_offsets) context.UnmapMemory(base + offset, page); + scheduler.Finish(); + context.ShutdownGpu(); + Require(name, "release mappings", LibKernel::Memory::KernelMunmap(base, size) == 0, + "sparse mappings release failed"); + Require(name, "release physical pages", + LibKernel::Memory::KernelReleaseDirectMemory(direct, 3u * page) == 0, + "sparse physical release failed"); + std::printf("[gpu] %-32s ok\n", name); + } + void CheckUnifiedTextureCacheFlow() { constexpr const char *name = "UnifiedTextureCacheFlow"; constexpr uintptr_t base = 0x0000000200600000ull; @@ -44889,6 +44968,11 @@ int main(int argc, char **argv) { CheckPm4RewindResume(vulkan.RuntimeRenderer()); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--image-upload-only") == 0) { + VulkanHarness vulkan; + vulkan.CheckLargeSparseImageUpload(); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--image-overlap-only") == 0) { CheckDepthAttachmentWrites(); CheckDepthFeedbackAspects(); @@ -45186,6 +45270,7 @@ int main(int argc, char **argv) { vulkan.CheckNativeIndirectDispatch(); CheckComputeLdsLimit(vulkan); vulkan.CheckBdaPageTableUploads(); + vulkan.CheckLargeSparseImageUpload(); vulkan.CheckUnifiedTextureCacheFlow(); vulkan.CheckUnifiedImageViewCache(); vulkan.CheckPackedTextureComponents();