From 21cdb8f0572440c40d0b6be75c141530c66d4e41 Mon Sep 17 00:00:00 2001 From: ps54all Date: Thu, 15 Oct 2026 13:39:14 +0200 Subject: [PATCH] fix(agc): store an image-copied buffer copy's destination before later labels A buffer copy whose source is exactly a live storage image becomes a device copy between images (AliasCopy). Its results stayed pending in the destination image and reached guest memory only at a later flush, so a guest that waits for a label recorded after the copy and reads the destination on the CPU (a readback) read the bytes from before the copy. Store the destination's results in the same batch, behind the image copy. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/VulkanDevice.cpp | 4 +- .../tests/execution/AliasCopyLabel.cpp | 148 ++++++++++++++++++ 2 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/AliasCopyLabel.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bb39fa7beb..7f69d3c563 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2478,12 +2478,10 @@ VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, st } outcome.syncMs = std::chrono::duration(std::chrono::steady_clock::now() - syncStart).count(); } - // A whole surface copied into another: a device copy between their images, whose results reach - // the destination's memory through the image's deferred write-back (nothing crosses to host - // memory for the copy itself). if (AliasCopy(context, destination, source, bytes)) { noteWriter({}, 0); recorder.NotePendingWrite(destination, bytes); + Graphics::StorageTexture::FlushPending(destination, bytes, nullptr, "buffer copy destination", Graphics::PublishScope::Whole); outcome.path = 3; return outcome; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AliasCopyLabel.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AliasCopyLabel.cpp new file mode 100644 index 0000000000..6db8ed0908 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AliasCopyLabel.cpp @@ -0,0 +1,148 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 8; +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t SurfaceBytes = 0x40000; +constexpr std::size_t LabelOffset = 2 * SurfaceBytes; +constexpr std::size_t BlockBytes = LabelOffset + 0x10000; +constexpr std::uint32_t LabelValue = 0x600dcafeu; + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, 0x12345678u, 0xf0200108, 0x00020102, 0xbf810000, +}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::array TextureDescriptor(const std::uint8_t* texels) { + const auto address = AddressOf(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Store(AgcDriver::VulkanDevice& device, const std::uint8_t* texels) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + const std::span code(StoreCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector storage(BlockBytes + 0x10000u); + auto* block = reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}); + auto* source = block; + auto* destination = block + SurfaceBytes; + auto* label = reinterpret_cast(block + LabelOffset); + for (std::size_t at = 0; at < SurfaceBytes; ++at) source[at] = static_cast(0x31u + at % 67u); + std::memset(destination, 0xa5, SurfaceBytes); + *label = 0; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, BlockBytes, true, true, true); + } + const auto surfaceBytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(source))).guestBytes; + Require(surfaceBytes == SurfaceBytes, "the 256x256 32-bit surface must cover exactly its 256 KiB block"); + + Store(*device, source); + Require(StorageTexture::FindPending(AddressOf(source), SurfaceBytes) != nullptr, "the compute store left no pending image over the source"); + + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto outcome = device->CopyBuffer(AddressOf(destination), AddressOf(source), SurfaceBytes, 0, std::numeric_limits::max(), 0, 0, 0, [](std::span, std::uint64_t) {}); + if (outcome.path != 3) { + std::printf("skipped, the copy took path %d instead of the image copy\n", outcome.path); + return VulkanTestSkipped; + } + const std::uint32_t value = LabelValue; + const int reason = device->WriteLabelOnGpu(AddressOf(const_cast(label)), std::as_bytes(std::span(&value, 1)), 1, 0); + if (reason != 0) { + std::printf("skipped, the label was not recorded on the GPU (reason %d)\n", reason); + return VulkanTestSkipped; + } + device->SubmitRecorded(true); + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (*label != LabelValue) { + Require(std::chrono::steady_clock::now() < deadline, "the label after the image copy never landed"); + std::this_thread::yield(); + } + std::vector seen(destination, destination + SurfaceBytes); + + std::vector expected(SurfaceBytes); + AgcDriver::GuestMemory::Read(AddressOf(source), expected); + std::size_t differences = 0; + for (std::size_t at = 0; at < SurfaceBytes; ++at) differences += static_cast(seen[at]) != expected[at]; + Require(differences == 0, "a CPU read after the label that followed the image copy found " + std::to_string(differences) + " of " + std::to_string(SurfaceBytes) + " destination bytes still unwritten"); + + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::FlushCachedTextures(device->Device()); + } + device->WaitIdle(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + std::puts("alias copy label tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +}