From 247e3f5eb2e38745c27d00f21891fe8dc11938c8 Mon Sep 17 00:00:00 2001 From: Nahuel Genchi <65604505+NahuelGenchi@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:46:22 -0300 Subject: [PATCH 1/2] perf(agc): take a 2D storage image's results for a one-slice array on the device A 2D surface and a one-slice 2D array over the same memory are separate storage images, since the storage key holds the dimension. pendingAlias only took a pending image of the same dimension, so a refresh of one view wrote the other view's results back and uploaded them again. Sonic Origins Plus (PPSA05324) switches between the two views of the same surfaces about 50 times a frame in its opening animation. pendingAlias now counts a 2D surface and a one-slice 2D array as the same dimension. The rest of the shape test (size, array range, mips, tiling, element size, image layers and depth, tracked units) and the conditions of the device copy (uncompressed keys, a host import, the surface's write stamps) are unchanged. agc_driver_storage_one_slice_alias stores through the 2D view twice and loads through the array view after each store. It checks the texels read through the array, that the 2D image is still pending after the array image's refresh (where the imported block is write-watched), and the guest memory after the final flush. Without the change the pending check fails on lavapipe. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/Texture.cpp | 4 +- .../tests/execution/StorageOneSliceAlias.cpp | 207 ++++++++++++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 2e4b692a9..43e3eb315 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3628,7 +3628,9 @@ std::shared_ptr StorageTexture::pendingAlias() const { const auto& theirs = candidate->descriptor; // The same texels under another format of the same size: the device copy moves them bit // for bit, as the guest bytes would. - const bool sameShape = mine.width == theirs.width && mine.height == theirs.height && mine.depthOrLastArray == theirs.depthOrLastArray && mine.baseArray == theirs.baseArray && mine.mipCount == theirs.mipCount && mine.tileMode == theirs.tileMode && mine.dimension == theirs.dimension && BytesPerElement(mine.format) == BytesPerElement(theirs.format) && geometry.imageLayers == candidate->geometry.imageLayers && geometry.imageDepth == candidate->geometry.imageDepth && trackedLayers == candidate->trackedLayers; + const auto oneSlice = [](const GuestTextureResource& resource) { return resource.dimension == TextureDimension::k2D || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray == 0); }; + const bool sameDimension = mine.dimension == theirs.dimension || (oneSlice(mine) && oneSlice(theirs)); + const bool sameShape = mine.width == theirs.width && mine.height == theirs.height && mine.depthOrLastArray == theirs.depthOrLastArray && mine.baseArray == theirs.baseArray && mine.mipCount == theirs.mipCount && mine.tileMode == theirs.tileMode && sameDimension && BytesPerElement(mine.format) == BytesPerElement(theirs.format) && geometry.imageLayers == candidate->geometry.imageLayers && geometry.imageDepth == candidate->geometry.imageDepth && trackedLayers == candidate->trackedLayers; if (sameShape && candidate->Cached() && candidate->uploadedKeys == DccKeys::Uncompressed) return candidate; } return nullptr; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp new file mode 100644 index 000000000..ccfa31bf3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp @@ -0,0 +1,207 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#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 = 32; +constexpr std::uint32_t Side = 128; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::size_t SurfaceBytes = 65536; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Initial = 0x5a5a5a5au; + +alignas(256) constexpr std::array StoreCode{ + 0x7e020280, 0x7e04020c, 0xf0201108, 0x00010200, 0xbf810000, +}; + +alignas(256) constexpr std::array LoadArrayCode{ + 0x7e020280, 0x7e040280, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000, +}; + +alignas(256) std::array Output{}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +class GuestBlock { +public: + explicit GuestBlock(bool watched) { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); + static_cast(watched); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + if (watched) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "one-slice alias: cannot allocate the guest block"); + Require(!watched || AgcDriver::GuestMemory::Watched(AddressOf(block), BlockBytes), "one-slice alias: the guest block is not write-watched"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array TextureDescriptor(const std::uint8_t* texels, std::uint32_t type) { + 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) | (type << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::span code, const std::vector& userData) { + 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(); +} + +void Store(AgcDriver::VulkanDevice& device, const std::uint8_t* texels, std::uint32_t value) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, Type2D); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = value; + Dispatch(device, StoreCode, userData); +} + +void LoadArray(AgcDriver::VulkanDevice& device, const std::uint8_t* texels) { + Output.fill(0xdeadbeefu); + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + const auto texture = TextureDescriptor(texels, Type2DArray); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + Dispatch(device, LoadArrayCode, userData); +} + +void RequireOutput(std::uint32_t expected, const std::string& what) { + for (std::uint32_t x = 0; x < Threads; ++x) { + Require(Output[x] == expected, what + ": texel (" + std::to_string(x) + ", 0) reads " + Hex(Output[x]) + ", expected " + Hex(expected)); + } +} + +void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, bool watched) { + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Store(device, texels, 0x11111111u); + Require(StorageTexture::FindPending(surface, SurfaceBytes) != nullptr, "the 2D store left no pending image"); + LoadArray(device, texels); + RequireOutput(0x11111111u, "the first load through the one-slice array"); + Store(device, texels, 0x22222222u); + const auto written = StorageTexture::FindPending(surface, SurfaceBytes); + Require(written != nullptr && written->Descriptor().dimension == AgcDriver::Graphics::TextureDimension::k2D, "the second 2D store left no pending 2D image"); + LoadArray(device, texels); + RequireOutput(0x22222222u, "the second load through the one-slice array"); + if (watched && AgcDriver::GuestMemory::Watched(surface, SurfaceBytes)) { + Require(StorageTexture::FindPending(surface, SurfaceBytes) == written, "the array image's refresh wrote the 2D image's results back instead of taking them on the device"); + } else { + std::puts("the imported block is compared, not watched: taking the results on the device is not checked"); + } + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); + Require(std::count(reinterpret_cast(texels), reinterpret_cast(texels) + SurfaceBytes / 4u, 0x22222222u) == static_cast(Threads), "guest memory does not hold the 32 texels of the second store after the flush"); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + +} + +int main() { + try { + const bool watched = AgcDriver::GuestMemory::WriteWatched(); + GuestBlock block(watched); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!watched) std::puts("guest memory has no write watch: only the texels read through the array are checked"); + Run(*device, block.Data(), watched); + std::puts("storage one-slice alias tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From aa91d9144c4ce1e8b73086468d3c3c75f90546da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 05:41:07 +0200 Subject: [PATCH 2/2] test(agc): pass the GPU flag to GuestAllocations Add in the one-slice alias test --- .../libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp index ccfa31bf3..78437ac99 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp @@ -81,7 +81,7 @@ public: #endif Require(block != nullptr, "one-slice alias: cannot allocate the guest block"); Require(!watched || AgcDriver::GuestMemory::Watched(AddressOf(block), BlockBytes), "one-slice alias: the guest block is not write-watched"); - GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); } ~GuestBlock() {