From d7f5ffb043d61c9d1ed40469f0622089ba9d04d9 Mon Sep 17 00:00:00 2001 From: Miguel Silva Date: Wed, 14 Oct 2026 23:07:29 -0300 Subject: [PATCH] perf(agc): import storage surfaces that cross the import window through a joined import HostImportFor keeps a run inside one 8 MiB import window, so a storage surface that crosses a window boundary had no import and every upload of it took the CPU path: read guest memory, copy it into a staging buffer, detile in a batch of its own and wait for it. UFC 6 (PPSA23566) builds its transient render targets from a pool of 8 MiB pages and packs surfaces across page boundaries, so most of its storage uploads took that path: 1,700 CPU uploads (6.9 GiB) per 10 s at the title screen, half of the queue worker's time. HostImportSpanning falls back to a joined import over the contiguous registered ranges the surface covers, as buffers already do through importJoined; the storage texture paths use it. HostImportFor keeps its per-window contract. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/GuestBufferMemory.hpp | 1 + .../Graphics/src/GuestBufferMemory.cpp | 14 ++++++++++ .../libSceAgcDriver/Graphics/src/Texture.cpp | 12 ++++----- .../libSceAgcDriver/tests/BdaResources.cpp | 27 +++++++++++++++++++ 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index e5180f62135..093b109737d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -63,6 +63,7 @@ void SetImportWatch(const Context& context, ImportWatch watch); // (alignment and budget permitting), or null. Bytes at `address` are at `address - import->base` in // the import's buffer. const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); +const HostImport* HostImportSpanning(const Context& context, std::uint64_t address, std::size_t bytes); void SetHostImportWindow(std::uint64_t bytes); // Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one). void SetHostImportRangeWindow(std::uint64_t bytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index c04d2bdf937..aae3b4bd017 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2407,6 +2407,20 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s return importAccess(context, state, address, address + bytes, lease); } +const HostImport* HostImportSpanning(const Context& context, std::uint64_t address, std::size_t bytes) { + if (const auto* import = HostImportFor(context, address, bytes)) return import; + if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return nullptr; + auto& state = Imports(); + std::lock_guard lock(state.mutex); + if (const auto* found = findImport(state, address, address + bytes)) return found; + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + refreshImports(context, state, lease); + const auto run = crossedRun(lease, address, address + bytes); + if (run.ranges.empty()) return nullptr; + const auto* joined = importJoined(context, state, run.base, run.base + run.bytes, lease); + return joined != nullptr && findImport(state, address, address + bytes) == joined ? joined : nullptr; +} + bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) { if (context.hostImportAlignment == 0) return true; ensurePinWaiter(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ebd2feb5ad2..d7c03af1101 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1741,7 +1741,7 @@ bool StorageTexture::Refresh() { // stamped); an untracked unit is stored whole by that write-back, and a unit pending in both // images takes the old order (store, then this image re-uploads it). auto alias = pendingAlias(); - if (alias != nullptr && !(ProvedClearKeys(descriptor, guestBytes, keyProof) == DccKeys::Uncompressed && HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) != nullptr)) alias = nullptr; + if (alias != nullptr && !(ProvedClearKeys(descriptor, guestBytes, keyProof) == DccKeys::Uncompressed && HostImportSpanning(context, descriptor.baseAddress, static_cast(guestBytes)) != nullptr)) alias = nullptr; if (alias != nullptr) { std::vector aliasStamped(trackedLayers); if (!GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), alias->layerGeneration, aliasStamped)) alias = nullptr; @@ -1856,7 +1856,7 @@ bool StorageTexture::Refresh() { } // Only the direct path uploads the selected layers alone (see upload); the others replace // the whole image, so every pending layer's results are stored first. - direct = keys == DccKeys::Uncompressed && HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) != nullptr; + direct = keys == DccKeys::Uncompressed && HostImportSpanning(context, descriptor.baseAddress, static_cast(guestBytes)) != nullptr; if (alias != nullptr && direct) { // The alias's pending units that nothing stamped since its generation (a CPU store or a // driver store there wins over its results, as at its own write-back) are taken from @@ -2183,7 +2183,7 @@ void StorageTexture::upload(const std::vector* layers) { // The clear is recorded into the open batch like a direct upload (the image kept by it): a // batch of its own submitted the recorder's work first and waited for all of it, 25-40 ms // under the GPU mutex at the movie stage. APS5_NO_RECORDED_CLEAR=1 waits as before. - static_cast(HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes))); + static_cast(HostImportSpanning(context, descriptor.baseAddress, static_cast(guestBytes))); if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) { layerOriginal.assign(trackedLayers, false); cancelStoredSaves(); @@ -2236,7 +2236,7 @@ void StorageTexture::upload(const std::vector* layers) { if (profile) LookupOutcomes::Add(LookupOutcomes::UploadClear, start); return; } - if (const auto* import = uploadedKeys == DccKeys::Uncompressed ? HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) : nullptr) { + if (const auto* import = uploadedKeys == DccKeys::Uncompressed ? HostImportSpanning(context, descriptor.baseAddress, static_cast(guestBytes)) : nullptr) { if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) { layerOriginal.assign(trackedLayers, false); cancelStoredSaves(); @@ -4211,7 +4211,7 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { refusal = "released"; return false; } - if (HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) == nullptr) { + if (HostImportSpanning(context, descriptor.baseAddress, static_cast(guestBytes)) == nullptr) { refusal = "not imported"; return false; } @@ -4670,7 +4670,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { settle(true); return; } - if (const auto* import = HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes))) segments.push_back({import, descriptor.baseAddress, descriptor.baseAddress + guestBytes}); + if (const auto* import = HostImportSpanning(context, descriptor.baseAddress, static_cast(guestBytes))) segments.push_back({import, descriptor.baseAddress, descriptor.baseAddress + guestBytes}); else segments = importSegments(); if (!segments.empty()) { Require(!unsavedOutsideWatch, "storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented"); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 2fe20f100c6..9cc4d41753c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -468,6 +468,32 @@ void importWindowTests(const Context& context) { ::operator delete(block, std::align_val_t{window}); } +void importSpanningTests(const Context& context) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + void* block = ::operator new(4 * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto first = reinterpret_cast(block); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (std::size_t index = 0; index < 4; ++index) change(guest + index * piece, true); + Require(HostImportFor(importing, first + window - 16, 32) == nullptr, "a view crossing the import window was imported"); + const auto* spanning = HostImportSpanning(importing, first + window - 16, 32); + Require(spanning != nullptr && spanning->base <= first + window - 16 && spanning->base + spanning->bytes >= first + window + 16, "a surface crossing the import window was not imported through a joined import"); + const auto* inside = HostImportSpanning(importing, first + window, piece); + Require(inside != nullptr && inside->base <= first + window && inside->base + inside->bytes >= first + window + piece, "a surface inside one window was not imported"); + for (std::size_t index = 0; index < 4; ++index) change(guest + index * piece, false); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + void importJoinTests(const Context& context) { constexpr std::size_t piece = 65536; constexpr std::size_t window = 2 * piece; @@ -1783,6 +1809,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { spaceWritableWritesTests(context); spaceReadListTests(context); importWindowTests(context); + importSpanningTests(context); spaceReadAcrossImportsTests(context, access); spaceReadAcrossImportsWrittenTests(context); importJoinTests(context);