From 043d25a4a158ee4d43878d67d402b4de3917f20e Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 14 Oct 2026 08:21:22 +0000 Subject: [PATCH 01/31] perf(agc): return freed memory when shader preparation drains A shipping title ends its first run, with no resource plans in the shader cache, with about 4 GB more resident memory than a run with a warm cache. The threads of the registration prepare pool free what they allocate to prepare a shader, but glibc keeps the freed memory in their arenas: at 110 s, 4.6 GB of the 6.5 GB the arenas held was free. When the pool has no task left, the worker that finished last calls malloc_trim, at most once a second, which gives the free pages of every arena back to the system. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit f4f69bb717db535012a77d83c32fe9e6d554009b) --- .../src/Driver/Shaders/ShaderRegistry.cpp | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 4caaf9d4102d..d7c2f8c46f96 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -13,9 +13,11 @@ #include "Optimization/ResourceProgram.hpp" #include #include +#include #include #include #include +#include #include #include #include @@ -25,6 +27,9 @@ #include #include #include +#ifdef __GLIBC__ +#include +#endif #include #include #include @@ -697,6 +702,16 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap return plan != nullptr ? PrepareRegistered(*plan) : std::vector{}; } +#ifdef __GLIBC__ +void trimFreedMemory() { + static std::atomic last{0}; + const auto now = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + auto previous = last.load(std::memory_order_relaxed); + if (now - previous < 1000 || !last.compare_exchange_strong(previous, now, std::memory_order_relaxed)) return; + malloc_trim(0); +} +#endif + class RegistrationPreparePool { public: static RegistrationPreparePool& Instance() { @@ -752,11 +767,16 @@ class RegistrationPreparePool { } task(); task = nullptr; + bool drained = false; { std::lock_guard lock(pool.mutex); --pool.running; - if (pool.tasks.empty() && pool.running == 0) pool.idle.notify_all(); + drained = pool.tasks.empty() && pool.running == 0; + if (drained) pool.idle.notify_all(); } +#ifdef __GLIBC__ + if (drained) trimFreedMemory(); +#endif } return nullptr; } From e46216bdb32586f37443bbb3f337deb38ee93be8 Mon Sep 17 00:00:00 2001 From: Semir Kabir Date: Wed, 14 Oct 2026 04:51:41 +0000 Subject: [PATCH 02/31] fix(agc): take an unregistered compute shader's float mode from the live COMPUTE_PGM_RSRC1 (cherry picked from commit ee1e341976a0df29ae2089c348c4fcbde116d5d1) --- .../include/Driver/Shaders/ShaderRegistry.hpp | 1 + .../src/Driver/Dispatch/Dispatch.cpp | 2 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 9 +++++++++ .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 20 +++++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 2a17808ec7b3..fece337dca52 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -100,6 +100,7 @@ std::shared_ptr PrepareShaderWithDiagnosti std::uint64_t NullPixelProgramAddress(); ShaderSnapshot PrepareNullPixelProgram(const VulkanDevice& device); std::optional RegisteredFloatMode(const ShaderSnapshot& snapshot); +std::optional DispatchFloatMode(const ShaderSnapshot& snapshot, const Registers& live); std::shared_ptr RegisteredHeader(const ShaderRegistry& registry, const Shader* shader); // Inline so the Metal driver, which does not link ShaderRegistry.cpp, resolves programs identically. inline std::shared_ptr RegisteredProgram(const ShaderRegistry& registry, std::uint64_t address, std::initializer_list types) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 6b3bfb26f2e2..7f8b6ab860d7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -94,7 +94,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, if (partialLastGroup) compute.partialThreads = partialGroups; ShaderRecompiler::RecompileRequest request{ {ShaderRecompiler::ShaderStage::Compute, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header}, - {decoded.waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory, RegisteredFloatMode(snapshot)}, + {decoded.waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory, DispatchFloatMode(snapshot, queue.shader)}, localDevice->ComputeTarget(decoded.waveSize), {0, 0, 0, 128} }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index d7c2f8c46f96..bf494c9e039c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -519,6 +519,15 @@ std::optional RegisteredFloatMode(const Shade return DecodeFloatMode(*reg, found->second); } +std::optional DispatchFloatMode(const ShaderSnapshot& snapshot, const Registers& live) { + if (snapshot.registeredState != nullptr) return RegisteredFloatMode(snapshot); + const auto reg = FloatModeRegisterFor(snapshot.type); + if (!reg) return std::nullopt; + const auto found = live.find(reg->offset); + if (found == live.end()) return std::nullopt; + return DecodeFloatMode(*reg, found->second); +} + namespace { template diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 7b977b45d3af..bfa7a20809ad 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -590,6 +590,25 @@ void testRegisteredFloatMode() { } } +void testDispatchFloatMode() { + using AgcDriver::DriverDetail::DispatchFloatMode; + using AgcDriver::DriverDetail::ShaderSnapshot; + using AgcDriver::DriverDetail::RegisteredShaderState; + const AgcDriver::Registers live{{0x212, (0xc0u << 12u) | (1u << 21u) | (1u << 26u)}}; + ShaderSnapshot raw{0x20000, 0, 0, {}, {}}; + const auto fromLive = DispatchFloatMode(raw, live); + check(fromLive.has_value() && fromLive->floatMode == 0xc0u && fromLive->dx10Clamp && !fromLive->ieeeMode && fromLive->fp16Overflow, "an unregistered compute shader does not take its float mode from the live COMPUTE_PGM_RSRC1"); + check(!DispatchFloatMode(raw, AgcDriver::Registers{}).has_value(), "an unregistered compute shader without a live COMPUTE_PGM_RSRC1 has a float mode"); + auto state = std::make_shared(); + state->shader.emplace(0x212, 1u << 23u); + ShaderSnapshot registered{0x20000, 0, 0, {}, {}}; + registered.registeredState = state; + const auto fromRegistered = DispatchFloatMode(registered, live); + check(fromRegistered.has_value() && fromRegistered->ieeeMode && fromRegistered->floatMode == 0u, "a registered compute shader takes its float mode from the live registers"); + registered.registeredState = std::make_shared(); + check(!DispatchFloatMode(registered, live).has_value(), "a registered compute shader without RSRC1 takes its float mode from the live registers"); +} + void testProgramSnapshots() { alignas(256) static std::array registered{}; registered.fill(0xbf800000); @@ -759,6 +778,7 @@ int main(int argc, char** argv) { testHeaderWithoutProgramAddress(); testCopyIntoHostMemory(); testRegisteredFloatMode(); + testDispatchFloatMode(); testReadOnlyResummarize(); testWorkerFailure(); check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("required shader register") != std::string::npos, "shutdown lost worker failure"); From 055089ddfeeaa8625089a119d11c9eb0c79a190b Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Mon, 12 Oct 2026 17:43:43 -0400 Subject: [PATCH 03/31] fix(agc): store small pending storage results at each flip (cherry picked from commit 0b5a4aae32faaaf82dbad1e91ca84cbe7f9a81c6) --- .../src/Driver/Packets/PacketMemory.cpp | 6 + .../Graphics/include/Texture.hpp | 3 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 26 +++ .../execution/FlipStoresSmallResults.cpp | 155 ++++++++++++++++++ 4 files changed, 190 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index c7b1198a17b9..dcaa56914de2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -174,6 +174,12 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } } + if (header == FlipPacketHeader) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + static_cast(Graphics::StorageTexture::StoreSmallPending("flip")); + } + static const bool syncFlip = std::getenv("APS5_SYNC_FLIP") != nullptr; const bool drains = drainAll ? ((Pm4::AccessesMemory(header) && opcode != 0x16) || opcode == 0x42 || opcode == 0x46 || opcode == 0x58 || header == FlipPacketHeader) : (!wroteOnGpu && !orderedAlready && (opcode == 0x49 || opcode == 0x37 || opcode == 0x40 || opcode == 0x45 || opcode == 0x50 || opcode == 0x83 || sampleDump || (drawPacket && drawDrain) || (header == FlipPacketHeader && syncFlip))); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 16cddc780663..24f139571041 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -182,6 +182,9 @@ class StorageTexture : public std::enable_shared_from_this { // overlapping pending image, as before). static bool FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr, const char* reason = "memory access", PublishScope scope = PublishScope::Whole, bool* published = nullptr); static void FlushAllPending(const char* reason); + static constexpr std::uint64_t SmallStoreBytes = 64 * 1024; + static std::size_t StoreSmallPending(const char* reason); + static bool AnyPending(); static bool StoreAtFlipRequested(const char* value); static bool StoreAtLabelRequested(const char* value); // See PendingSerial: a change of a surface's source outside the registry (a unit shadow diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 2a6964a7735a..4c2a13114a9b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3036,6 +3036,32 @@ void StorageTexture::FlushAllPending(const char* reason) { static_cast(FlushPending(0, std::numeric_limits::max(), nullptr, reason)); } +std::size_t StorageTexture::StoreSmallPending(const char* reason) { + std::vector> small; + { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (auto* texture : pending.textures) { + if (texture->guestBytes > SmallStoreBytes) continue; + if (auto alive = texture->weak_from_this().lock()) small.push_back(std::move(alive)); + } + } + std::size_t stored = 0; + for (const auto& texture : small) { + const auto address = texture->descriptor.baseAddress; + const auto bytes = static_cast(texture->guestBytes); + if (!HostImportCovers(texture->context, address, bytes)) continue; + if (FlushPending(address, bytes, nullptr, reason)) ++stored; + } + return stored; +} + +bool StorageTexture::AnyPending() { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + return pending.textures.begin() != pending.textures.end(); +} + bool StorageTexture::StoreAtFlipRequested(const char* value) { if (value == nullptr) return false; if (std::strcmp(value, "1") == 0) return true; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp new file mode 100644 index 000000000000..bbac636c998a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp @@ -0,0 +1,155 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +constexpr std::uint32_t Threads = 8; +constexpr std::uint32_t Side = 16; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t OutputHandle = 7; +constexpr std::size_t BlockBytes = 0x20000; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::uint32_t Written = 0x12345678u; + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Written, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(4) std::atomic Gate = 0; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::uint32_t Low(const void* data) { + return static_cast(AddressOf(data)); +} + +std::uint32_t High(const void* data) { + return static_cast(AddressOf(data) >> 32u); +} + +std::array TextureDescriptor(const std::uint32_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, + }; +} + +std::vector Frame(const std::uint32_t* texels) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::vector words; + const auto registers = [&](const std::uint32_t first, const std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{Threads, 1, 1}; + registers(0x207, shape); + const std::array program{static_cast(AddressOf(StoreCode.data()) >> 8u), static_cast(AddressOf(StoreCode.data()) >> 40u)}; + registers(0x20c, program); + const std::array resource{static_cast(userData.size()) << 1u}; + registers(0x213, resource); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + words.insert(words.end(), {AgcDriver::FlipPacketHeader, OutputHandle, 0xfffffffeu, 1, 0, 0}); + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + return words; +} + +class Request final : public AgcDriver::IFlipRequest { +public: + explicit Request(std::atomic& ready) : ready(ready) {} + void GpuReady(const std::shared_ptr&) override { ++ready; } + void Fail(std::exception_ptr) noexcept override {} +private: + std::atomic& ready; +}; + +class Output final : public AgcDriver::IVideoOutput { +public: + std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return std::make_shared(ready); } + void Fail(std::exception_ptr) noexcept override {} + std::atomic ready = 0; +}; + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + std::vector storage((BlockBytes + 0x10000u) / 4u); + auto* texels = reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}); + std::fill(texels, texels + BlockBytes / 4u, Untouched); + { + GuestAllocations::Mutation mutation; + mutation.Add(texels, BlockBytes, true, true, true); + } + const auto bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes; + Require(bytes <= AgcDriver::Graphics::StorageTexture::SmallStoreBytes, "the test image is larger than an image the flip stores"); + const volatile std::uint32_t* cpu = texels; + const auto stored = [&] { + for (std::size_t index = 0; index < bytes / 4u; ++index) { + if (cpu[index] == Written) return true; + } + return false; + }; + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(OutputHandle, output); + + auto words = Frame(texels); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while ((output->ready.load() == 0 || !stored()) && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool flipped = output->ready.load() == 1; + const bool seen = stored(); + Gate.store(1, std::memory_order_release); + + AgcDriverWaitIdle_nid_postfix(); + AgcDriverUnregisterVideoOutput_nid_postfix(OutputHandle, output); + AgcDriverShutdown_nid_postfix(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + } + Require(flipped, "the flip did not reach the GPU"); + Require(seen, "a small image written before a flip did not reach guest memory while the queue kept working"); + std::puts("flip stores small results tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From d6d41ede2d6e31e830ec602139cec29405d12efd Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:36:22 -0400 Subject: [PATCH 04/31] fix(agc): advance stored layers when a write-back keeps CPU-written blocks On a host-imported surface a storage write-back keeps the 64 KiB blocks the CPU wrote since their tracked layer's generation, stores the rest and stamps the stored ranges with MarkWritten. Settling advanced the stored layers' generations past those stamps only when no layer kept a block (or when an adjacent pending surface needed the collect). When one tracked layer kept a block, every other stored layer kept its old generation, so the write-back's own stamps read as CPU writes: the next write-back kept those layers as well and did not store the GPU's newer results over them. A tracked layer is one 64 KiB unit when the surface has block units: tiled (not linear), 64 KiB-aligned base, not thick, with layer and block tracking on. Otherwise it is an array layer (64 KiB-aligned layer slices, layer tracking on) or the whole surface. Settling now collects and advances each stored layer that kept no block; a layer that kept a block keeps its old generation and re-uploads at its next use, as before. When no stored layer advances (nothing stored, or every selected layer kept a block, such as a single tracked layer that kept one), no adjacent pending surface needs the collect and no unit was retiled into the unit shadow, settling still skips the collect, as main did. Units retiled into the unit shadow are now marked at the settle collect's generation also when a block was kept; main marked them at TrackerGeneration() there without collecting. The two differ only for a CPU write to a stored unit made after the keep decision: main left it to a later walk, which made the retiled unit stale so the CPU's bytes won; now the walk stamps it at the marked generation and the stored results win, as they already did when no block was kept. agc_storage_kept_unit_settle uses a 1024x512 RGBA8 surface in 64 KiB tiling (32 block units): it writes one byte of unit 0 from the CPU, stores a red clear, stores a blue clear, and requires units 1-31 to hold blue and unit 0 to keep the CPU's byte after both stores. On main the GPU-mapped direct memory case fails with unit 1 still red. The watched-memory case (host imports off: the write-back that stores through the CPU) passes on main; it guards the kept layer's old generation and fails when settling also advances a layer that kept a block. The test returns 77, naming the reason, when the direct-memory case cannot run: no write watching, no host imports, direct memory not write-watched, or its host import refused. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit dc39e051b90963cb4cbd88362f32b823b603865b) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 20 ++- .../prx/libSceAgcDriver/tests/Recorder.cpp | 135 ++++++++++++++++++ 3 files changed, 150 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 5c788df25cba..bd35c2efb313 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -892,6 +892,12 @@ if(BUILD_TESTING) set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_storage_kept_unit_settle COMMAND agc_driver_recorder_tests --kept-unit-settle-only) + set_tests_properties(agc_storage_kept_unit_settle PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_storage_kept_unit_settle PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_storage_baseline_settled COMMAND agc_driver_recorder_tests --settled-baseline-only) add_test(NAME agc_storage_integer_clear COMMAND agc_driver_recorder_tests --integer-clear-only) set_tests_properties(agc_storage_integer_clear PROPERTIES SKIP_RETURN_CODE 77) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 4c2a13114a9b..c0eb43541462 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -4136,18 +4136,16 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (layers[layer]) layerPending[layer] = false; } - const bool allKept = std::none_of(skippedLayer.begin(), skippedLayer.end(), [](bool kept) { return kept; }); - if (allKept || !adjacent.empty()) { - const auto now = memoizedCollect ? GuestMemory::CollectWrites(firstStored, static_cast(lastStored - firstStored)) : GuestMemory::CollectWritesUncached(firstStored, static_cast(lastStored - firstStored)); - for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = now; - } - refreshGeneration(); - advanceAdjacent(adjacent, now, firstBlock, lastBlock); - if (!shadowed.empty()) MarkShadowed(*shadowImport, shadowed, now); - } else if (!shadowed.empty()) { - MarkShadowed(*shadowImport, shadowed, GuestMemory::TrackerGeneration()); + bool advances = false; + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) advances = advances || (layers[layer] && !skippedLayer[layer]); + if (!advances && adjacent.empty() && shadowed.empty()) return; + const auto now = memoizedCollect ? GuestMemory::CollectWrites(firstStored, static_cast(lastStored - firstStored)) : GuestMemory::CollectWritesUncached(firstStored, static_cast(lastStored - firstStored)); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = now; } + refreshGeneration(); + advanceAdjacent(adjacent, now, firstBlock, lastBlock); + if (!shadowed.empty()) MarkShadowed(*shadowImport, shadowed, now); }; if (keep.empty()) { // Every selected block was written by the CPU since the layer's generation: nothing to diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index a4f978336384..acf87904e907 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3463,6 +3463,137 @@ void unchangedCpuStampTests(const Device& device, Recorder& recorder) { std::cout << "unchanged CPU stamps: ok\n"; } +bool keptUnitSettleTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + constexpr std::uint32_t width = 1024; + constexpr std::uint32_t height = 512; + constexpr std::size_t surfaceBytes = std::size_t{width} * height * 4; + constexpr std::size_t unitBytes = 65536; + constexpr std::array red{255, 0, 0, 255}; + constexpr std::array blue{0, 0, 255, 255}; + bool directTested = false; + for (const bool direct : {false, true}) { + const std::string what = direct ? "GPU-mapped direct memory" : "watched memory"; + if (!AgcDriver::GuestMemory::WriteWatched()) { + std::cout << "no write watching: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + if (direct && base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + struct Backing { + Recorder& recorder; + bool direct; + void* block = nullptr; + std::int64_t physical = -1; + ~Backing() { + try { + recorder.Sync(); + } catch (const std::exception& error) { + std::cerr << "(k) the recorded work did not finish, the test memory stays mapped: " << error.what() << '\n'; + return; + } + if (direct) { + if (block != nullptr) sceKernelMunmap(block, surfaceBytes); + if (physical >= 0) sceKernelReleaseDirectMemory(physical, surfaceBytes); + return; + } + if (block == nullptr) return; + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + ReleaseWatched(block, surfaceBytes); + } + } backing{recorder, direct}; + if (direct) { + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, surfaceBytes, unitBytes, 0, &backing.physical) == 0, "(k) allocate the direct memory"); + Require(sceKernelMapDirectMemory(&backing.block, surfaceBytes, 0x33, 0, backing.physical, unitBytes) == 0, "(k) map the direct memory for the GPU"); + } else { + backing.block = AllocateWatched(surfaceBytes, unitBytes); + Require(backing.block != nullptr, "(k) no watched block although write watching is on"); + GuestAllocations::Mutation mutation; + mutation.Add(backing.block, surfaceBytes, true, true, true); + } + auto* memory = static_cast(backing.block); + std::memset(memory, 0x5a, surfaceBytes); + const auto address = reinterpret_cast(backing.block); + AgcDriver::GuestMemory::CollectWritesUncached(address, surfaceBytes); + TextureDetiler detiler(base); + auto context = base; + if (!direct) context.hostImportAlignment = 0; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = width; + resource.height = height; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == surfaceBytes, "(k) unexpected surface size"); + if (direct && !AgcDriver::GuestMemory::Watched(address, surfaceBytes)) { + std::cout << "GPU-mapped direct memory is not write-watched here: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + Require(AgcDriver::GuestMemory::Watched(address, surfaceBytes), "(k) " + what + " is not watched"); + if (direct && HostImportFor(context, address, surfaceBytes) == nullptr) { + std::cout << "host import of the direct memory refused: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + Require(direct || HostImportFor(context, address, surfaceBytes) == nullptr, "(k) watched memory was imported although host imports are off for it"); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto clear = [&](VkClearColorValue value) { + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + }; + const auto store = [&] { + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + }; + const auto unitHolds = [&](std::size_t unit, const std::array& texel) { + std::vector read(unitBytes); + AgcDriver::GuestMemory::Read(address + unit * unitBytes, read); + for (std::size_t i = 0; i < unitBytes; ++i) { + if (std::to_integer(read[i]) != texel[i % 4]) return false; + } + return true; + }; + clear({{1.0f, 0.0f, 0.0f, 1.0f}}); + memory[16] = 0x77; + AgcDriver::GuestMemory::CollectWritesUncached(address, surfaceBytes); + store(); + std::vector head(unitBytes); + AgcDriver::GuestMemory::Read(address, head); + Require(std::to_integer(head[16]) == 0x77, "(k) " + what + ": the CPU store into unit 0 was overwritten by results it was newer than"); + for (std::size_t unit = 1; unit < surfaceBytes / unitBytes; ++unit) Require(unitHolds(unit, red), "(k) " + what + ": the first write-back did not store unit " + std::to_string(unit)); + clear({{0.0f, 0.0f, 1.0f, 1.0f}}); + store(); + for (std::size_t unit = 1; unit < surfaceBytes / unitBytes; ++unit) { + Require(unitHolds(unit, blue), "(k) " + what + ": unit " + std::to_string(unit) + " kept the results of the earlier write-back; that write-back's own store was taken for a CPU write because a neighbouring unit was kept for the CPU"); + } + AgcDriver::GuestMemory::Read(address, head); + Require(std::to_integer(head[16]) == 0x77, "(k) " + what + ": the second write-back stored over unit 0, which the CPU wrote after that unit's generation"); + } + std::cout << "units stored next to a unit kept for the CPU in " << what << ": ok\n"; + if (direct) directTested = true; + } + return directTested; +} + void settledBaselineTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); #ifdef _WIN32 @@ -5420,6 +5551,9 @@ int main(int argc, char** argv) { unchangedCpuStampTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--kept-unit-settle-only") { + return keptUnitSettleTests(device, recorder) ? 0 : 77; + } if (argc == 2 && std::string_view(argv[1]) == "--integer-clear-only") { integerClearTests(device); return 0; @@ -5519,6 +5653,7 @@ int main(int argc, char** argv) { singlePassTests(device, recorder, false); singlePassTests(device, recorder, true); unchangedCpuStampTests(device, recorder); + keptUnitSettleTests(device, recorder); settledBaselineTests(device, recorder); importWatchTests(device); staleGenerationTests(device, recorder); From 606b2f0ef3c57653c8fbe062e49f30028d54dde1 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:45:19 -0400 Subject: [PATCH 05/31] test(agc): skip the kept-unit settle direct case when its import is compared agc_storage_kept_unit_settle checked that the GPU-mapped direct memory was write-watched before it made the host import. On Linux, when host imports resolve write protection and dma-buf imports keep it, the host-pointer import is tried first in a window that does not stay watched, and dma-buf only when it fails (importAllocation in GuestBufferMemory.cpp). Where the host-pointer import of shared memory succeeds, as on Intel's ANV, the import takes the direct memory out of the write watch, so the case ran on the compared path: the upload saves the guest bytes, the first write-back keeps unit 0 by comparison and drops the saved bytes of every unit it selected, and the second write-back has nothing to compare. Before e5449936f it stored every unit whole, over the CPU's byte in unit 0, which is the failure reported on Linux; on main it throws (technical debt: results over memory outside the write watch without saved bytes). Neither depends on the settle change. On Windows, importing watched private memory (compared there) reproduces both: on 7e181fcc3, with or without this branch's Texture.cpp change, the second check fails with that message; on main the second write-back throws. The case now checks the write watch again after the import and returns 77, naming the reason, when the import took the memory out of it. Where the import keeps the memory watched (the Windows alias import, a Linux dma-buf import) it runs as before. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 7bfeeb9774abf1311fdbaace4b414cbb1e5a3be3) --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index acf87904e907..263d35d3ad60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3546,6 +3546,10 @@ bool keptUnitSettleTests(const Device& device, Recorder& recorder) { std::cout << "host import of the direct memory refused: units stored next to a unit kept for the CPU in " << what << " not tested\n"; continue; } + if (direct && !AgcDriver::GuestMemory::Watched(address, surfaceBytes)) { + std::cout << "the host import of the direct memory took it out of the write watch (its imports are compared): units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } Require(direct || HostImportFor(context, address, surfaceBytes) == nullptr, "(k) watched memory was imported although host imports are off for it"); { auto image = StorageTexture::Create(context, detiler, resource, 0); From 8f373545281ec21a259d6563b1651870171b1699 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:48:28 -0400 Subject: [PATCH 06/31] test(agc): keep watched settle coverage when direct imports are unavailable Co-Authored-By: Codex (cherry picked from commit ca73d0fb321b1239ff0aaa76f8d8276689dbfbe5) --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 263d35d3ad60..7b5e7bc654b8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3463,7 +3463,8 @@ void unchangedCpuStampTests(const Device& device, Recorder& recorder) { std::cout << "unchanged CPU stamps: ok\n"; } -bool keptUnitSettleTests(const Device& device, Recorder& recorder) { +void keptUnitSettleTests(const Device& device, Recorder& recorder) { + Require(AgcDriver::GuestMemory::WriteWatched(), "(k) kept-unit settle tests require host write watching"); const auto& base = device.GetContext(); constexpr std::uint32_t width = 1024; constexpr std::uint32_t height = 512; @@ -3471,13 +3472,8 @@ bool keptUnitSettleTests(const Device& device, Recorder& recorder) { constexpr std::size_t unitBytes = 65536; constexpr std::array red{255, 0, 0, 255}; constexpr std::array blue{0, 0, 255, 255}; - bool directTested = false; for (const bool direct : {false, true}) { const std::string what = direct ? "GPU-mapped direct memory" : "watched memory"; - if (!AgcDriver::GuestMemory::WriteWatched()) { - std::cout << "no write watching: units stored next to a unit kept for the CPU in " << what << " not tested\n"; - continue; - } if (direct && base.hostImportAlignment == 0) { std::cout << "host imports unavailable: units stored next to a unit kept for the CPU in " << what << " not tested\n"; continue; @@ -3593,9 +3589,7 @@ bool keptUnitSettleTests(const Device& device, Recorder& recorder) { Require(std::to_integer(head[16]) == 0x77, "(k) " + what + ": the second write-back stored over unit 0, which the CPU wrote after that unit's generation"); } std::cout << "units stored next to a unit kept for the CPU in " << what << ": ok\n"; - if (direct) directTested = true; } - return directTested; } void settledBaselineTests(const Device& device, Recorder& recorder) { @@ -5556,7 +5550,8 @@ int main(int argc, char** argv) { return 0; } if (argc == 2 && std::string_view(argv[1]) == "--kept-unit-settle-only") { - return keptUnitSettleTests(device, recorder) ? 0 : 77; + keptUnitSettleTests(device, recorder); + return 0; } if (argc == 2 && std::string_view(argv[1]) == "--integer-clear-only") { integerClearTests(device); From 8534b9e6872ff381f727ef8ff9e0701524c94aa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 14 Oct 2026 19:14:08 +0200 Subject: [PATCH 07/31] docs(debt): record type-0 texture descriptors bound as null images Co-Authored-By: Claude (cherry picked from commit 58b2245027222137fe5a26dbc11267a0c942c98e) --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 8548f1ff5bb4..1c138240efe1 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -27,6 +27,7 @@ ### Functional - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. A title that enables stencil tests without writing the control register motivated this. +- [Type-0 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 in dword 3) with a nonzero base are bound as null images, like a zero base. The behaviour comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault); it was not measured on the console's gfx1013. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. Matching depth/stencil storage access still throws. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. From c217e2ff7982a9d527f4fd348260fdc57e569290 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:53:19 -0400 Subject: [PATCH 08/31] fix(agc): settle registration preparations before a preparation scope locks sceAgcCreateInterpolantMapping resolved the pixel shader's ABI, which takes the process-wide shader preparation lock until the scope ends, and then waited under that lock for the vertex shader's background registration preparation. sceAgcCreatePrimState and sceAgcLinkShaders could wait the same way. Every other shader registration and ABI resolution queued behind that one compile: on a shipping title the lock was held 8 to 10 s of every 10 s, almost all of it waiting for registration preparations, while its loader threads waited 13 to 45 s per 10 s to take it. A preparation scope now names the shaders it resolves and waits for their registration preparations before it can take the lock. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 11ef8e6c837fe09116757d104332d6f4fcc01c6a) --- .../Shader/src/InterpolantMapping.cpp | 2 +- .../prx/libSceAgc/Shader/src/LinkShaders.cpp | 2 +- .../prx/libSceAgc/Shader/src/PrimState.cpp | 3 + core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../Execution/include/Driver/Driver.hpp | 1 + .../include/ShaderPreparationScope.hpp | 9 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 19 ++- .../execution/PreparationScopeSettle.cpp | 150 ++++++++++++++++++ 8 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index b3b0da8f4212..ec7d9a092a16 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -61,7 +61,7 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* throw std::runtime_error(std::string(fn) + ": input semantic count exceeds 32"); } - AgcDriver::ShaderPreparationScope transaction; + AgcDriver::ShaderPreparationScope transaction({ps != nullptr ? gs : nullptr, ps}); std::array values{}; auto* output = regs; regs = values.data(); diff --git a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp index 1044b410f440..59a43354c8ba 100644 --- a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp @@ -22,7 +22,7 @@ extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister* context, ShaderRegist // The simple vertex/pixel pipeline combines two primitive context registers // and 32 interpolant registers. Publish only after both helpers succeed. - AgcDriver::ShaderPreparationScope transaction; + AgcDriver::ShaderPreparationScope transaction({vertex, pixel}); std::array contextValues{}; std::array primitiveValues{}; sceAgcCreatePrimState(contextValues.data(), primitiveValues.data(), nullptr, vertex, primitiveType); diff --git a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp index 9963ae2db9b5..a1e77d402b11 100644 --- a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp @@ -8,6 +8,7 @@ #include "SceShaders.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp" #include "prx/libSceAgc/Shader/include/ShaderUtils.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" @@ -48,7 +49,9 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_ const std::array stages{hs, gs}; const bool patch = prim_type == static_cast(ShaderRegs::PrimitiveType::Patch); if (patch == ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_HS_BIT) != 0)) { + AgcDriver::ShaderPreparationScope transaction({hs, gs}); AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span(stages).subspan(hs == nullptr ? 1u : 0u), contextValues, primitiveValues); + transaction.Commit(); } if (cx_regs != nullptr) std::copy(contextValues.begin(), contextValues.end(), cx_regs); if (uc_regs != nullptr) std::copy(primitiveValues.begin(), primitiveValues.end(), uc_regs); diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bd35c2efb313..2b67d7f8a615 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1098,6 +1098,8 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) list(APPEND agcExecutionTests ${agcExecutionTest}) endforeach() +target_link_libraries(agc_driver_preparation_scope_settle_tests PRIVATE libSceAgc) + target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $) target_include_directories(agc_driver_dual_source_blend_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index ed41072f11fd..d3c692258da2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -60,6 +60,7 @@ class Driver { void ResolveShaderAbi(const Shader* shader, std::span context, std::span primitive); void ResolveGraphicsStagesAbi(std::span stages, std::span context, std::span primitive); void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType); + void SettleShaderPreparations(std::span stages); static void FoldDrawOffsets(const ShaderRecompiler::RecompileResult& result, const DrawProgram& program, Pm4::DrawParameters& parameters); static std::optional ClassifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect); static std::string dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp index 0c314803071a..30fcf5b8f433 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp @@ -1,11 +1,16 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP +#include +#include + +struct Shader; + namespace AgcDriver::DriverDetail { class ShaderPreparationTransaction; } -extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(); +extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(const Shader* const* shaders, std::size_t count); extern "C" void AgcDriverCommitShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction); extern "C" void AgcDriverEndShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction) noexcept; @@ -13,7 +18,7 @@ namespace AgcDriver { class ShaderPreparationScope { public: - ShaderPreparationScope() : transaction(AgcDriverBeginShaderPreparation_nid_postfix()) {} + explicit ShaderPreparationScope(std::initializer_list shaders) : transaction(AgcDriverBeginShaderPreparation_nid_postfix(shaders.begin(), shaders.size())) {} ~ShaderPreparationScope() { AgcDriverEndShaderPreparation_nid_postfix(transaction); } ShaderPreparationScope(const ShaderPreparationScope&) = delete; ShaderPreparationScope& operator=(const ShaderPreparationScope&) = delete; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index bf494c9e039c..3d7b1ee6d66c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1048,6 +1048,22 @@ void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std:: transaction.Commit(); } +void Driver::SettleShaderPreparations(std::span stages) { + std::vector> snapshots; + { + std::lock_guard lock(mutex); + if (shaders == nullptr) return; + for (const auto* shader : stages) { + if (shader == nullptr) continue; + if (auto snapshot = RegisteredHeader(*shaders, shader)) snapshots.push_back(std::move(snapshot)); + } + } + for (const auto& snapshot : snapshots) { + std::unique_lock lock(snapshot->prepared->mutex); + AwaitRegisteredPreparation(*snapshot->prepared, lock); + } +} + void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target) { ShaderPreparationTransaction transaction; require(fragment.get() != &front, "rectangle stages refer to the same shader"); @@ -1206,7 +1222,8 @@ void Driver::RegisterShader(const Shader* shader) { } -extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix() { +extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(const Shader* const* shaders, std::size_t count) { + AgcDriver::DriverDetail::Driver::Get().SettleShaderPreparations({shaders, count}); return new AgcDriver::DriverDetail::ShaderPreparationTransaction(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp new file mode 100644 index 000000000000..2233d80ba37b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp @@ -0,0 +1,150 @@ +#include "VulkanTestDevice.hpp" +#include "SceShaders.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps); + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) const std::array VertexCode{0x7e0002ffu, 0, 0x7e0202ffu, 0, 0x7e0402ffu, 0, 0x7e0602ffu, 0x3f800000u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; +alignas(256) const std::array PixelCode{0xbf810000u}; + +struct VertexHeader { + Shader shader{}; + std::array registers{}; + ShaderSpecialRegs specials{}; + ShaderUserData users{}; + std::array context{{{0x2d5, 0x2000}}}; + + VertexHeader() { + const auto address = reinterpret_cast(VertexCode.data()); + shader.file_header = 0x34333231u; + shader.version = 0x18; + shader.header_size = sizeof(VertexHeader); + shader.shader_size = sizeof(VertexCode); + shader.code = VertexCode.data(); + shader.type = 2; + registers = {{{0xc8, static_cast(address >> 8u)}, {0xc9, static_cast(address >> 40u)}, {0x8b, 0}, {0x8a, 0}, {0xca, 0}, {0xcb, 0}, {0xcc, 0}}}; + shader.sh_registers = registers.data(); + shader.num_sh_registers = registers.size(); + shader.cx_registers = context.data(); + shader.num_cx_registers = context.size(); + shader.specials = &specials; + shader.user_data = &users; + specials.dispatch_modifier = 0x8000; + } +}; + +struct PixelHeader { + Shader shader{}; + std::array registers{}; + std::array context{{{0x1b3, 2}, {0x1b4, 2}, {0x1b6, 1}}}; + + PixelHeader() { + const auto address = reinterpret_cast(PixelCode.data()); + shader.file_header = 0x34333231u; + shader.version = 0x18; + shader.header_size = sizeof(PixelHeader); + shader.shader_size = sizeof(PixelCode); + shader.code = PixelCode.data(); + shader.type = 1; + registers = {{{0x008, static_cast(address >> 8u)}, {0x009, static_cast(address >> 40u)}, {0x00a, 0}, {0x00b, 0}}}; + shader.sh_registers = registers.data(); + shader.num_sh_registers = registers.size(); + shader.cx_registers = context.data(); + shader.num_cx_registers = context.size(); + } +}; + +class HeldPreparationThreads { +public: + HeldPreparationThreads() { + for (unsigned index = 0; index < std::thread::hardware_concurrency() + 2u; ++index) { + AgcDriver::DriverDetail::SubmitRegistrationPreparation([state = state] { + std::unique_lock lock(state->mutex); + state->changed.wait(lock, [&] { return state->released; }); + }); + } + } + + ~HeldPreparationThreads() { + Release(); + } + + void Release() { + { + std::lock_guard lock(state->mutex); + state->released = true; + } + state->changed.notify_all(); + } + +private: + struct State { + std::mutex mutex; + std::condition_variable changed; + bool released = false; + }; + std::shared_ptr state = std::make_shared(); +}; + +void RegistrationBesidePendingLink() { + VertexHeader vertex; + VertexHeader other; + PixelHeader pixel; + AgcDriverRegisterShader_nid_postfix(&pixel.shader); + std::future link; + std::future registration; + bool registeredBeside = false; + std::array mapping{}; + { + HeldPreparationThreads held; + AgcDriverRegisterShader_nid_postfix(&vertex.shader); + link = std::async(std::launch::async, [&] { return sceAgcCreateInterpolantMapping_0100(mapping.data(), &vertex.shader, &pixel.shader); }); + std::this_thread::sleep_for(std::chrono::seconds(1)); + Require(link.wait_for(std::chrono::seconds(0)) == std::future_status::timeout, "the link did not wait for the vertex shader's registration preparation"); + registration = std::async(std::launch::async, [&] { AgcDriverRegisterShader_nid_postfix(&other.shader); }); + registeredBeside = registration.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + held.Release(); + } + registration.get(); + Require(link.get() == 0, "the interpolant mapping failed"); + Require(registeredBeside, "a link waiting for a registration preparation blocked the registration of another shader"); + for (std::uint32_t index = 0; index < mapping.size(); ++index) Require(mapping[index].offset == 0x191u + index, "the interpolant mapping wrote another register"); +} + +} + +int main() { + try { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + RegistrationBesidePendingLink(); + AgcDriverShutdown_nid_postfix(); + std::cout << "preparation scope settle tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} From 5ba53dd09250a8333582baa3391e977f6f7d851f Mon Sep 17 00:00:00 2001 From: "john.maia" Date: Tue, 13 Oct 2026 11:49:12 +0100 Subject: [PATCH 09/31] fix(agc): store a storage image whose last block row runs past its allocation A storage image whose surface runs past the end of its registered allocation had every write-back dropped as "memory no longer registered". Titles can size a single-level surface's allocation as pitch x height. A tiled mip's 64 KiB blocks are stored row-major, so that size ends inside the last block row, and the bytes after it are the tail of that row: they hold texels with y < height, which the GPU's own store writes into the next allocation. When the allocation ends inside the last block row and registered readable memory continues to the surface's end, the write-back now stores the whole surface, as the GPU does. Padding slots keep their bytes, since the store writes only bytes that changed. A shipping title streams its lightmaps into an 8160x8160 BC3 pool (SW_64KB_S, 2048 block rows) through a 2040x2040 RGBA32UI alias written by compute. The game allocates 2040 rows, so all its write-backs were dropped and the walls sampled leftover memory as speckled noise. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 6ad9372fdde615ef8530f527a118211d0dad325f) --- .../libSceAgcDriver/Graphics/src/Texture.cpp | 17 ++ .../execution/StorageWriteBackOverhang.cpp | 170 ++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index c0eb43541462..3f48c75d4271 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -4288,7 +4288,24 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // the round trip through the CPU below would store stale texels over whatever the memory // holds now. Its selected units are dropped instead. APS5_NO_UNREGISTERED_DROP=1 stores. static const bool unregisteredDrop = std::getenv("APS5_NO_UNREGISTERED_DROP") == nullptr; + bool overhang = false; if (unregisteredDrop && !RegisteredReadableCovers(descriptor.baseAddress, static_cast(guestBytes))) { + const auto allocationEnd = RegisteredReadableEnd(descriptor.baseAddress); + const auto& level = mips.front(); + const auto rowBytes = std::uint64_t{level.height} * level.pitchBytes; + const auto lastRowTail = mips.size() == 1 && arrayLayers == 1 && !level.tail && level.tiledSize > rowBytes ? level.tiledSize - rowBytes : 0; + const auto end = descriptor.baseAddress + guestBytes; + if (allocationEnd > descriptor.baseAddress && end - allocationEnd <= lastRowTail) { + auto covered = allocationEnd; + while (covered < end) { + const auto next = RegisteredReadableEnd(covered); + if (next <= covered) break; + covered = next; + } + overhang = covered >= end; + } + } + if (unregisteredDrop && !overhang && !RegisteredReadableCovers(descriptor.baseAddress, static_cast(guestBytes))) { static std::atomic reports{0}; if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), keep.size()); unregisteredDropped.fetch_add(1, std::memory_order_relaxed); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp new file mode 100644 index 000000000000..4f5f7f8c0bde --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp @@ -0,0 +1,170 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.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 "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; + +constexpr std::uint32_t Width = 128; +constexpr std::uint32_t Height = 120; +constexpr std::uint32_t FormatRgba16Float = 71; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Standard64KB = 0x09; +constexpr std::size_t SurfaceBytes = 0x20000; +constexpr std::size_t AllocationBytes = std::size_t{Width} * Height * 8u; +constexpr std::uint8_t Sentinel = 0xa5; + +alignas(256) constexpr std::array WriteCode{0xd7460004u, 0x0401060eu, 0xd7460005u, 0x0405060fu, 0x7e100d04u, 0x7e120d05u, 0x7e140c0du, 0x7e1602f2u, 0xf0200f08u, 0x00000804u, 0xbf810000u}; + +std::array TextureDescriptor(const void* texels) { + const auto address = reinterpret_cast(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (FormatRgba16Float << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (Standard64KB << 20u) | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +void Write(std::uint8_t* texels) { + std::vector data; + const auto texture = TextureDescriptor(texels); + data.insert(data.end(), texture.begin(), texture.end()); + data.resize(14, 0u); + data[13] = 7u; + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{8, 8, 1}; + registers(0x207, shape); + const auto code = reinterpret_cast(WriteCode.data()); + const std::array program{static_cast(code >> 8u), static_cast(code >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (7u << 7u) | (1u << 11u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, Width / 8u, Height / 8u, 1u, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::Graphics::StorageTexture::FlushAllPending("test"); + AgcDriverWaitIdle_nid_postfix(); +} + +class GuestSurface { +public: + explicit GuestSurface(bool split) { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, Alignment)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + 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) & ~(static_cast(Alignment) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + Alignment) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + Alignment - aligned); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + } +#endif + Require(block != nullptr, "storage write-back overhang: cannot allocate the guest surface"); + std::memset(block, 0, AllocationBytes); + std::memset(block + AllocationBytes, Sentinel, SurfaceBytes - AllocationBytes); + GuestAllocations::Mutation mutation; + if (split) { + mutation.Add(block, AllocationBytes, true, true, true); + mutation.Add(block + AllocationBytes, SurfaceBytes - AllocationBytes, true, true, true); + } else { + mutation.Add(block, SurfaceBytes, true, true, true); + } + this->split = split; + } + + ~GuestSurface() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + if (split) mutation.Remove(block + AllocationBytes); + } +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); +#else + munmap(block, SurfaceBytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes); +#endif + } + + GuestSurface(const GuestSurface&) = delete; + GuestSurface& operator=(const GuestSurface&) = delete; + + std::uint8_t* Data() { return block; } + +private: + static constexpr std::size_t Alignment = 65536; + std::uint8_t* block = nullptr; + bool split = false; +}; + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + GuestSurface whole(false); + GuestSurface split(true); + Write(whole.Data()); + Write(split.Data()); + const auto* expected = whole.Data(); + const auto* stored = split.Data(); + const auto texel = [](const std::uint8_t* at) { return !std::all_of(at, at + 8, [](std::uint8_t byte) { return byte == 0 || byte == Sentinel; }); }; + std::size_t written = 0; + std::size_t past = 0; + for (std::size_t at = 0; at < SurfaceBytes; at += 8) { + written += texel(expected + at); + past += at >= AllocationBytes && texel(expected + at); + } + Require(written != 0, "storage write-back overhang: the fully registered surface received no texels"); + Require(past != 0, "storage write-back overhang: no texel of the fully registered surface lies past pitch x height"); + const auto differs = std::mismatch(expected, expected + SurfaceBytes, stored); + Require(differs.first == expected + SurfaceBytes, "storage write-back overhang: byte 0x" + [&] { char text[32]; std::snprintf(text, sizeof(text), "%zx", static_cast(differs.first - expected)); return std::string(text); }() + " differs from the fully registered surface (" + std::to_string(written) + " texels, " + std::to_string(past) + " past pitch x height)"); + Require(std::any_of(stored + AllocationBytes, stored + SurfaceBytes, [](std::uint8_t byte) { return byte == Sentinel; }), "storage write-back overhang: the padding past the image's rows was overwritten"); + AgcDriverShutdown_nid_postfix(); + std::puts("storage write-back overhang tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 494359e906a309609a9877036c4dbb14c904da57 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Tue, 13 Oct 2026 15:29:43 +0200 Subject: [PATCH 10/31] test(agc): compile each v_cmp denormals program once for its eight input groups The test recompiled and rebuilt the same program for every group of 32 input pairs. Only the input buffer changes between groups, so compile once per compare, EXEC/VCC capture and float mode (96 programs instead of 768) and dispatch it for each group. On lavapipe the test drops from about 11.5 s to 1.3 s; it is the slowest test of the Linux CI run (14.5 s). Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 59235d9cb0de5c27dcba2160a75188da9b585bf3) --- .../tests/execution/VCmpF32Denormals.cpp | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp index 54e132b6c6e8..22568a2f9ade 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp @@ -95,17 +95,9 @@ std::string Hex(std::uint32_t value) { return text; } -std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, std::uint32_t compare, std::uint32_t capture, std::uint32_t group) { +ShaderRecompiler::RecompileResult Compile(const AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, std::uint32_t compare, std::uint32_t capture) { Code[6] = compare; Code[7] = capture; - for (std::uint32_t lane = 0; lane < Threads; ++lane) { - const std::uint32_t pair = group * Threads + lane; - Input[lane * Inputs] = Values[pair / ValueCount]; - Input[lane * Inputs + 1u] = Values[pair % ValueCount]; - Input[lane * Inputs + 2u] = 0u; - Input[lane * Inputs + 3u] = 0u; - } - Output.fill(0xdeadbeefu); std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); @@ -122,8 +114,19 @@ std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloat }; request.useCache = false; request.context.floatMode = mode; - const auto result = ShaderRecompiler::Recompile(request); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + return ShaderRecompiler::Recompile(request); +} + +std::uint32_t Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& program, std::uint32_t group) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const std::uint32_t pair = group * Threads + lane; + Input[lane * Inputs] = Values[pair / ValueCount]; + Input[lane * Inputs + 1u] = Values[pair % ValueCount]; + Input[lane * Inputs + 2u] = 0u; + Input[lane * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(Code.data())); device.WaitIdle(); std::uint32_t mask = 0u; for (std::uint32_t lane = 0; lane < Threads; ++lane) { @@ -135,8 +138,9 @@ std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloat void Check(AgcDriver::VulkanDevice& device, const char* modeName, ShaderRecompiler::ShaderFloatMode mode, const Row& row, bool exec, const std::uint32_t* expected) { const std::string name = std::string(exec ? "v_cmpx_" : "v_cmp_") + row.condition + "_f32"; + const auto program = Compile(device, mode, exec ? row.cmpx : row.cmp, exec ? CaptureExec : CaptureVcc); for (std::uint32_t group = 0; group < Groups; ++group) { - const std::uint32_t actual = Run(device, mode, exec ? row.cmpx : row.cmp, exec ? CaptureExec : CaptureVcc, group); + const std::uint32_t actual = Run(device, program, group); if (actual == expected[group]) continue; const std::uint32_t diff = actual ^ expected[group]; for (std::uint32_t lane = 0; lane < Threads; ++lane) { From c80c2ab363edcf64ab54bab13e30892edc870fe2 Mon Sep 17 00:00:00 2001 From: Maz Date: Wed, 14 Oct 2026 11:38:52 +0400 Subject: [PATCH 11/31] fix(agc): return 0 for out-of-range sceAgcInit without table lookup Pointer-shaped calls match the #4808 console probe (success, no writes); selecting table 13 for those values was unsupported. (cherry picked from commit eb1b9eca96162629566803b50d3e87188a8aed66) --- core/libs/prx/libSceAgc/DcbState/src/Init.cpp | 2 +- core/libs/prx/libSceAgc/tests/Commands.cpp | 6 +++--- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp index e73f00218950..f3acef34c525 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp @@ -10,7 +10,7 @@ extern "C" { int APS5_VABI sceAgcInit(std::uint32_t version) { - if (version >= 14) version = 13; + if (version >= 14) return 0; Agc::Command::GetRegisterDefaults(version, false, __func__); Agc::Command::GetRegisterDefaults(version, true, __func__); return 0; diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 88257532bbc2..702e062b6100 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -541,9 +541,9 @@ void testDefaults() { check(sceAgcInit_0090(&state, 14) == 0 && state == 0x12345678, "out-of-range AGC initialization version changed caller state"); check(sceAgcInit(8) == 0, "AGC version initialization failed"); check(sceAgcInit(13) == 0, "AGC version 13 initialization failed"); - check(sceAgcInit(14) == 0, "out-of-range AGC version did not fall back"); - check(sceAgcInit(0xffffffffu) == 0, "maximum AGC version did not fall back"); - check(sceAgcInit(0xffffc278u) == 0, "pointer-valued AGC argument did not fall back"); + check(sceAgcInit(14) == 0, "out-of-range AGC version did not return success"); + check(sceAgcInit(0xffffffffu) == 0, "maximum AGC version did not return success"); + check(sceAgcInit(0xffffc278u) == 0, "pointer-valued AGC argument did not return success"); for (std::uint32_t version = 0; version < 14; ++version) { for (const bool internal : {false, true}) { auto* first = Agc::Command::GetRegisterDefaults(version, internal, __func__); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 259ecc59eb2f..8e9e58742ee1 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -135,6 +135,7 @@ Throughout the project, every function at every stage either **does exactly what - [Command buffer cursors](../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceAgcGetRegisterDefaultsInternal](../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it +- [sceAgcInit](../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - versions 0–13 select their register-default tables and return 0; pointer-shaped or otherwise out-of-range values (>=14) return 0 without a register-default lookup. Console probe on firmware 13.60 ([#4808](https://github.com/boykopovar/AnyPS5/issues/4808#issuecomment-6149448503)): `sceAgcInit(&buf, 8)` (NID `kW3GLb7QfPg`) returns 0 and writes nothing; scalar versions 2–13 return `0x8A6C0004` there, while AnyPS5 still returns 0 for those scalars. The probe does not show that table 13 is selected for pointer-shaped calls. [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns `0x80890001` above 12 for `sceAgcInit`, and falls back to version 11 in `sceAgcGetRegisterDefaults2`. Public homebrew, [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan) and [prospero-win](https://github.com/mpereiraesaa/prospero-win) call that NID with a pointer-shaped first argument. `sceAgcInit_0090` (NID `23LRUSvYu1M`) checks the state pointer and then calls `sceAgcInit`, as OpenAGC does - [sceVideoOutAddBufferAttributeOption](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented - [sceVideoOutIsOutputSupported](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - only the default mode (1) is reported as supported. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels From c27ab1f185329d9b7f6c813aad59ebb0180cee7f Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Tue, 13 Oct 2026 16:59:50 +0200 Subject: [PATCH 12/31] feat(agc): copy HLE for the 16-byte stream copy kernel A 16-word compute kernel copies a buffer in 16-byte elements: 64 threads a group, thread t of group g loads elements g*128+t and g*128+64+t from the index-addressed buffer in s[0:3] and stores them at the same indices of s[4:7]. Run as a shader against host imports it costs 11.8 ms of GPU per present in a shipping title's main menu on an RTX 4060. Matched like the existing copy kernel, by its code hash and its descriptors (16-byte stride and no swizzle on both, the same word 3), it becomes one copy through the existing copy HLE path: the elements the kernel stores, min of groups * 128 and the destination's record count, when the source's record count covers them all. APS5_NO_COPY_HLE turns it off with the other kernel. (cherry picked from commit fc24253487fa6a407a9093ccd5f3cb0970af4ddf) --- .../Execution/include/Driver/Driver.hpp | 1 + .../src/Driver/Dispatch/Dispatch.cpp | 2 +- .../src/Driver/Memory/BufferCopy.cpp | 64 +++++++++++++------ 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index d3c692258da2..fd71dbd5a03a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -150,6 +150,7 @@ class Driver { static void fillClearCount(const Graphics::StorageTexture::FillCoverage& coverage, std::size_t bytes, std::span pattern, std::size_t discarded, bool cleared, const char* refusal); bool fillBuffer(QueueState& queue, std::uint32_t queueId, std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr& localDevice); static bool matchesCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); + static bool matchesStreamCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); static void countCopy(int path, std::size_t bytes, std::chrono::steady_clock::time_point started, std::chrono::steady_clock::time_point locked, const VulkanDevice::CopyOutcome* outcome = nullptr); void noteCopyWriter(std::uint64_t program, std::uint64_t begin, std::uint64_t end, std::uint32_t queue, std::span value = {}, std::uint64_t generation = 0); static bool copyKnownValues(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 7f8b6ab860d7..b75a04cdbca4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -79,7 +79,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, const bool partialLastGroup = partialGroups != std::array{}; if (resolving) { if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; - if (!partialLastGroup && indirectArguments == 0 && matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; + if (!partialLastGroup && indirectArguments == 0 && (matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) || matchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute))) return; } else { if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) Graphics::NoteHtileDepthClear(htile); if (!partialLastGroup && fillBuffer(queue, submission.queue, packet, std::span(snapshot.code).subspan(codeOffset), userData, compute, localDevice)) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp index dc96d1039159..5ae6f160203b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp @@ -27,6 +27,23 @@ bool Driver::matchesCopyKernel(std::span code, const std::v return userData[3] == 0x00014004u && userData[7] == 0x00014004u && userData[11] == 0x0004dfacu && stride(1) == 4 && stride(5) == 4 && stride(9) == 16 && !swizzled(1) && !swizzled(5) && !swizzled(9) && userData[10] >= 1; } +bool Driver::matchesStreamCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { + static const bool enabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; + if (!enabled || userData.size() != 8 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1 || !compute.groupIdEnable[0]) return false; + static constexpr std::size_t kernelWords = 16; + static constexpr std::uint64_t kernelHash = 0x08bcbb6f503a7f87ull; + if (code.size() < kernelWords || code[kernelWords - 1] != 0xbf810000u) return false; + std::uint64_t hash = 0xcbf29ce484222325ull; + for (const auto word : code.first(kernelWords)) { + hash ^= word; + hash *= 0x100000001b3ull; + } + if (hash != kernelHash) return false; + const auto stride = [&](std::size_t word) { return (userData[word] >> 16u) & 0x3fffu; }; + const auto swizzled = [&](std::size_t word) { return ((userData[word] >> 31u) & 1u) != 0; }; + return stride(1) == 16 && stride(5) == 16 && !swizzled(1) && !swizzled(5) && userData[3] == userData[7]; +} + void Driver::countCopy(int path, std::size_t bytes, std::chrono::steady_clock::time_point started, std::chrono::steady_clock::time_point locked, const VulkanDevice::CopyOutcome* outcome) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile || path < 0 || path >= CopyPaths) return; @@ -92,7 +109,8 @@ bool Driver::knownValueVerify() { } bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr& localDevice, std::uint64_t programAddress) { - if (!matchesCopyKernel(code, userData, compute)) return false; + const bool stream = !matchesCopyKernel(code, userData, compute); + if (stream && !matchesStreamCopyKernel(code, userData, compute)) return false; const auto started = std::chrono::steady_clock::now(); static const std::size_t cpuMax = [] { if (std::getenv("APS5_NO_CPU_COPY") != nullptr) return std::size_t{0}; @@ -106,7 +124,7 @@ bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span(userData[word + 1] & 0xffffu) << 32u); }; const auto source = base(0); const auto destination = base(4); - const auto record = base(8); + const auto record = stream ? std::uint64_t{0} : base(8); std::array groups{packet[1], packet[2], packet[3]}; if ((packet[4] & 0x20u) != 0) { for (std::uint32_t axis = 0; axis < 3; ++axis) { @@ -132,22 +150,32 @@ bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span(record), 8); - - bool published = false; - if (!recordAccessible || Graphics::Recorder::SnapshotWriteOverlaps(record, 8) || Graphics::StorageTexture::FlushPending(record, 8, nullptr, "buffer copy count", Graphics::PublishScope::Whole, &published) || published || labelPending(record, 8)) { - if (traceCopy()) traceCopyPending(recordAccessible ? "count record" : "inaccessible count record", record, 8, source, destination, 0, localDevice); - countCopy(CopyRecordPending, 0, started, locked); - return false; - } - std::uint32_t count = 0, period = 0; - std::memcpy(&count, reinterpret_cast(record), 4); - std::memcpy(&period, reinterpret_cast(record + 4), 4); - const auto bytes = static_cast(count) * 4u; - - if (count == 0 || period < count || groups[0] != (count + 63u) / 64u || count > userData[2] || count > userData[6] || bytes > (16u << 20u) || source % 4 != 0 || destination % 4 != 0 || (record < destination + bytes && destination < record + 8)) { - countCopy(CopyShape, bytes, started, locked); - return false; + std::size_t bytes = 0; + if (stream) { + const auto elements = std::min(std::uint64_t{groups[0]} * 128u, userData[6]); + bytes = static_cast(elements) * 16u; + if (bytes == 0 || bytes > (16u << 20u) || userData[2] < elements || source % 16 != 0 || destination % 16 != 0) { + countCopy(CopyShape, bytes, started, locked); + return false; + } + } else { + const bool recordAccessible = GuestMemory::Accessible(reinterpret_cast(record), 8); + + bool published = false; + if (!recordAccessible || Graphics::Recorder::SnapshotWriteOverlaps(record, 8) || Graphics::StorageTexture::FlushPending(record, 8, nullptr, "buffer copy count", Graphics::PublishScope::Whole, &published) || published || labelPending(record, 8)) { + if (traceCopy()) traceCopyPending(recordAccessible ? "count record" : "inaccessible count record", record, 8, source, destination, 0, localDevice); + countCopy(CopyRecordPending, 0, started, locked); + return false; + } + std::uint32_t count = 0, period = 0; + std::memcpy(&count, reinterpret_cast(record), 4); + std::memcpy(&period, reinterpret_cast(record + 4), 4); + bytes = static_cast(count) * 4u; + + if (count == 0 || period < count || groups[0] != (count + 63u) / 64u || count > userData[2] || count > userData[6] || bytes > (16u << 20u) || source % 4 != 0 || destination % 4 != 0 || (record < destination + bytes && destination < record + 8)) { + countCopy(CopyShape, bytes, started, locked); + return false; + } } if (source < destination + bytes && destination < source + bytes) { countCopy(CopyOverlapping, bytes, started, locked); From c2ebf03137aa6dba68bbe8e796614676684786fe Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Wed, 14 Oct 2026 13:46:41 +0200 Subject: [PATCH 13/31] test(agc): stream copy kernel match, refusals and HLE path The stream copy kernel is matched by its 16 code words instead of a hash, and the match and its range checks become free functions (MatchesStreamCopyKernel, StreamCopyRange) so a test can reach them. Its loads and stores are format instructions, so the match also requires the descriptor the pattern fill takes: 32_32_32_32_UINT, XYZW selects, no add-tid, out-of-bounds mode 0, type 0. Copy paths are counted in copyPathCounts whether or not APS5_PROFILE_DRAW is set. agc_driver_stream_copy checks the match against every word with a bit flipped, the descriptor and dispatch refusals, the range clamp and refusals, and runs nine dispatches through the driver: four must take the HLE path and copy elements g*128+t and g*128+64+t, five must be refused for their shape and leave the shader's result. (cherry picked from commit fa4fc23471852654318d0c37f21ecfd952970188) --- .../Execution/include/Driver/Driver.hpp | 1 - .../include/Driver/Memory/BufferCopy.hpp | 16 ++ .../src/Driver/Dispatch/Dispatch.cpp | 3 +- .../src/Driver/Memory/BufferCopy.cpp | 44 +++-- .../tests/execution/StreamCopy.cpp | 177 ++++++++++++++++++ 5 files changed, 222 insertions(+), 19 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/StreamCopy.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index fd71dbd5a03a..d3c692258da2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -150,7 +150,6 @@ class Driver { static void fillClearCount(const Graphics::StorageTexture::FillCoverage& coverage, std::size_t bytes, std::span pattern, std::size_t discarded, bool cleared, const char* refusal); bool fillBuffer(QueueState& queue, std::uint32_t queueId, std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr& localDevice); static bool matchesCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); - static bool matchesStreamCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); static void countCopy(int path, std::size_t bytes, std::chrono::steady_clock::time_point started, std::chrono::steady_clock::time_point locked, const VulkanDevice::CopyOutcome* outcome = nullptr); void noteCopyWriter(std::uint64_t program, std::uint64_t begin, std::uint64_t end, std::uint32_t queue, std::span value = {}, std::uint64_t generation = 0); static bool copyKnownValues(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp index ad4da0a76822..0c72dfd1f1d1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp @@ -1,9 +1,14 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_BUFFERCOPY_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_BUFFERCOPY_HPP +#include "Recompiler.hpp" +#include #include #include #include +#include +#include +#include namespace AgcDriver::DriverDetail { @@ -13,6 +18,17 @@ extern std::atomic knownValueEntries, knownValueReads, knownValue enum CopyPath { CopyCpu = 0, CopyGpu = 1, CopyNotImported = 2, CopyAliased = 3, CopyRecordPending = 4, CopyShape = 5, CopyOverlapping = 6, CopyInaccessible = 7, CopyOverGpuMax = 8, CopyPaths = 9 }; +extern std::array, CopyPaths> copyPathCounts; + +struct StreamCopy { + std::uint64_t source = 0; + std::uint64_t destination = 0; + std::size_t bytes = 0; +}; + +bool MatchesStreamCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); +std::optional StreamCopyRange(const std::vector& userData, std::uint32_t groups); + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index b75a04cdbca4..56ab2ad3b134 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" @@ -79,7 +80,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, const bool partialLastGroup = partialGroups != std::array{}; if (resolving) { if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; - if (!partialLastGroup && indirectArguments == 0 && (matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) || matchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute))) return; + if (!partialLastGroup && indirectArguments == 0 && (matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) || MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute))) return; } else { if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) Graphics::NoteHtileDepthClear(htile); if (!partialLastGroup && fillBuffer(queue, submission.queue, packet, std::span(snapshot.code).subspan(codeOffset), userData, compute, localDevice)) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp index 5ae6f160203b..6a7686763525 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include #include #include #include @@ -9,6 +10,7 @@ namespace AgcDriver::DriverDetail { std::atomic knownValueEntries{0}, knownValueReads{0}, knownValueVerified{0}, knownValueMismatches{0}; +std::array, CopyPaths> copyPathCounts{}; bool Driver::matchesCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { static const bool enabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; @@ -27,26 +29,34 @@ bool Driver::matchesCopyKernel(std::span code, const std::v return userData[3] == 0x00014004u && userData[7] == 0x00014004u && userData[11] == 0x0004dfacu && stride(1) == 4 && stride(5) == 4 && stride(9) == 16 && !swizzled(1) && !swizzled(5) && !swizzled(9) && userData[10] >= 1; } -bool Driver::matchesStreamCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { +bool MatchesStreamCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { static const bool enabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; if (!enabled || userData.size() != 8 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1 || !compute.groupIdEnable[0]) return false; - static constexpr std::size_t kernelWords = 16; - static constexpr std::uint64_t kernelHash = 0x08bcbb6f503a7f87ull; - if (code.size() < kernelWords || code[kernelWords - 1] != 0xbf810000u) return false; - std::uint64_t hash = 0xcbf29ce484222325ull; - for (const auto word : code.first(kernelWords)) { - hash ^= word; - hash *= 0x100000001b3ull; - } - if (hash != kernelHash) return false; + static constexpr std::array kernel{0xd7460008u, 0x04010e08u, 0x8f6a8708u, 0xd76d0009u, 0x0401806au, 0xe00c2000u, 0x80000008u, 0xe00c2000u, 0x80000409u, 0xbf8c3f71u, 0xe01c2000u, 0x80010008u, 0xbf8c3f70u, 0xe01c2000u, 0x80010409u, 0xbf810000u}; + if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return false; const auto stride = [&](std::size_t word) { return (userData[word] >> 16u) & 0x3fffu; }; const auto swizzled = [&](std::size_t word) { return ((userData[word] >> 31u) & 1u) != 0; }; - return stride(1) == 16 && stride(5) == 16 && !swizzled(1) && !swizzled(5) && userData[3] == userData[7]; + const auto dstSel = userData[3] & 0xfffu; + const bool addTid = ((userData[3] >> 23u) & 1u) != 0; + const auto outOfBounds = (userData[3] >> 28u) & 3u; + const auto type = userData[3] >> 30u; + const auto format = (userData[3] >> 12u) & 0x7fu; + return stride(1) == 16 && stride(5) == 16 && !swizzled(1) && !swizzled(5) && userData[3] == userData[7] && type == 0 && !addTid && outOfBounds == 0 && dstSel == 0xfacu && format == 0x4bu; +} + +std::optional StreamCopyRange(const std::vector& userData, std::uint32_t groups) { + const auto base = [&](std::size_t word) { return userData[word] | (static_cast(userData[word + 1] & 0xffffu) << 32u); }; + const auto elements = std::min(std::uint64_t{groups} * 128u, userData[6]); + const StreamCopy copy{base(0), base(4), static_cast(elements) * 16u}; + if (copy.bytes == 0 || copy.bytes > (16u << 20u) || userData[2] < elements || copy.source % 16 != 0 || copy.destination % 16 != 0) return std::nullopt; + return copy; } void Driver::countCopy(int path, std::size_t bytes, std::chrono::steady_clock::time_point started, std::chrono::steady_clock::time_point locked, const VulkanDevice::CopyOutcome* outcome) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - if (!profile || path < 0 || path >= CopyPaths) return; + if (path < 0 || path >= CopyPaths) return; + copyPathCounts[path].fetch_add(1, std::memory_order_relaxed); + if (!profile) return; using Outcome = VulkanDevice::CopyOutcome; static std::mutex countsMutex; static std::uint64_t counts[CopyPaths] = {}; @@ -110,7 +120,7 @@ bool Driver::knownValueVerify() { bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr& localDevice, std::uint64_t programAddress) { const bool stream = !matchesCopyKernel(code, userData, compute); - if (stream && !matchesStreamCopyKernel(code, userData, compute)) return false; + if (stream && !MatchesStreamCopyKernel(code, userData, compute)) return false; const auto started = std::chrono::steady_clock::now(); static const std::size_t cpuMax = [] { if (std::getenv("APS5_NO_CPU_COPY") != nullptr) return std::size_t{0}; @@ -152,12 +162,12 @@ bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span(std::uint64_t{groups[0]} * 128u, userData[6]); - bytes = static_cast(elements) * 16u; - if (bytes == 0 || bytes > (16u << 20u) || userData[2] < elements || source % 16 != 0 || destination % 16 != 0) { - countCopy(CopyShape, bytes, started, locked); + const auto range = StreamCopyRange(userData, groups[0]); + if (!range) { + countCopy(CopyShape, 0, started, locked); return false; } + bytes = range->bytes; } else { const bool recordAccessible = GuestMemory::Accessible(reinterpret_cast(record), 8); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StreamCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StreamCopy.cpp new file mode 100644 index 000000000000..65bbde489031 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StreamCopy.cpp @@ -0,0 +1,177 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::MatchesStreamCopyKernel; +using AgcDriver::DriverDetail::StreamCopyRange; + +alignas(256) constexpr std::array Code{0xd7460008u, 0x04010e08u, 0x8f6a8708u, 0xd76d0009u, 0x0401806au, 0xe00c2000u, 0x80000008u, 0xe00c2000u, 0x80000409u, 0xbf8c3f71u, 0xe01c2000u, 0x80010008u, 0xbf8c3f70u, 0xe01c2000u, 0x80010409u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Format = 0x0004bfacu; +constexpr std::uint32_t LargeGroups = 8193; +constexpr std::size_t Words = (std::size_t{LargeGroups} * 128u + 64u) * 4u; +alignas(256) std::array Source{}; +alignas(256) std::array Destination{}; + +std::vector UserData(const std::uint32_t* source, std::uint32_t sourceRecords, const std::uint32_t* destination, std::uint32_t destinationRecords, std::uint32_t sourceStride = 16, std::uint32_t destinationStride = 16) { + const auto from = reinterpret_cast(source); + const auto to = reinterpret_cast(destination); + return {static_cast(from), static_cast((from >> 32u) & 0xffffu) | (sourceStride << 16u), sourceRecords, Format, + static_cast(to), static_cast((to >> 32u) & 0xffffu) | (destinationStride << 16u), destinationRecords, Format}; +} + +void MatchTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + const auto data = UserData(Source.data(), 512, Destination.data(), 512); + Require(MatchesStreamCopyKernel(Code, data, compute), "the stream copy kernel was not recognized"); + for (std::size_t word = 0; word < Code.size(); ++word) { + auto altered = Code; + altered[word] ^= 1u; + Require(!MatchesStreamCopyKernel(altered, data, compute), "a different shader matched the stream copy kernel (word " + std::to_string(word) + ")"); + } + Require(!MatchesStreamCopyKernel(std::span(Code).first(15), data, compute), "a truncated shader matched"); + Require(!MatchesStreamCopyKernel(Code, UserData(Source.data(), 512, Destination.data(), 512, 4), compute), "a source stride other than 16 matched"); + Require(!MatchesStreamCopyKernel(Code, UserData(Source.data(), 512, Destination.data(), 512, 16, 4), compute), "a destination stride other than 16 matched"); + auto swizzled = data; + swizzled[5] |= 0x80000000u; + Require(!MatchesStreamCopyKernel(Code, swizzled, compute), "a swizzled destination matched"); + auto formats = data; + formats[7] ^= 0x1000u; + Require(!MatchesStreamCopyKernel(Code, formats, compute), "descriptors of different formats matched"); + const auto both = [&](std::uint32_t word3) { + auto changed = data; + changed[3] = changed[7] = word3; + return MatchesStreamCopyKernel(Code, changed, compute); + }; + Require(!both((Format & ~0x7f000u) | (0x14u << 12u)), "a 32-bit format matched a copy of 16-byte elements"); + Require(!both((Format & ~0xfffu) | 0xc6cu), "a swapped destination select matched"); + Require(!both(Format | 0x00800000u), "an add-tid descriptor matched"); + Require(!both(Format | 0x30000000u), "a raw out-of-bounds mode matched"); + Require(!both(Format | 0x40000000u), "a descriptor of another type matched"); + auto extra = data; + extra.push_back(0); + Require(!MatchesStreamCopyKernel(Code, extra, compute), "a different user SGPR count matched (the group id moves)"); + auto wave32 = compute; + wave32.numThreads[0] = 32; + Require(!MatchesStreamCopyKernel(Code, data, wave32), "a group of 32 threads matched"); + auto noGroup = compute; + noGroup.groupIdEnable[0] = false; + Require(!MatchesStreamCopyKernel(Code, data, noGroup), "a dispatch without the group id matched"); + + const auto source = reinterpret_cast(Source.data()); + const auto destination = reinterpret_cast(Destination.data()); + const auto copy = StreamCopyRange(data, 4); + Require(copy && copy->source == source && copy->destination == destination && copy->bytes == 8192, "4 groups were not taken as a copy of 512 elements"); + const auto clamped = StreamCopyRange(UserData(Source.data(), 512, Destination.data(), 300), 4); + Require(clamped && clamped->bytes == 300u * 16u, "the copy was not clamped to the destination's records"); + const auto launched = StreamCopyRange(UserData(Source.data(), 600, Destination.data(), 600), 4); + Require(launched && launched->bytes == 8192, "the copy exceeded the launched threads"); + Require(!StreamCopyRange(UserData(Source.data(), 400, Destination.data(), 512), 4), "a source with fewer records than the copy was taken"); + Require(!StreamCopyRange(UserData(Source.data(), 512, Destination.data(), 0), 4), "a copy of 0 bytes was taken"); + Require(!StreamCopyRange(data, 0), "a copy of 0 groups was taken"); + Require(StreamCopyRange(UserData(Source.data(), 8192u * 128u, Destination.data(), 8192u * 128u), 8192).has_value(), "a copy of 16 MiB was refused"); + Require(!StreamCopyRange(UserData(Source.data(), LargeGroups * 128u, Destination.data(), LargeGroups * 128u), LargeGroups), "a copy above 16 MiB was taken"); + Require(!StreamCopyRange(UserData(Source.data() + 1, 512, Destination.data(), 512), 4), "a source not aligned to 16 bytes was taken"); + Require(!StreamCopyRange(UserData(Source.data(), 512, Destination.data() + 1, 512), 4), "a destination not aligned to 16 bytes was taken"); +} + +std::vector Commands(const std::vector& data, std::uint32_t groups) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (1u << 7u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + return words; +} + +std::uint64_t Taken() { + using namespace AgcDriver::DriverDetail; + return copyPathCounts[CopyCpu].load() + copyPathCounts[CopyGpu].load() + copyPathCounts[CopyAliased].load(); +} + +void ExecuteTests() { + using AgcDriver::DriverDetail::copyPathCounts; + using AgcDriver::DriverDetail::CopyShape; + struct Case { + const char* name; + std::uint32_t groups, sourceRecords, destinationRecords, sourceOffset, destinationOffset; + bool hle; + }; + constexpr std::array cases{{ + {"4 groups", 4, 512, 512, 0, 0, true}, + {"16 groups", 16, 2048, 2048, 0, 0, true}, + {"destination records below the launch", 4, 512, 300, 0, 0, true}, + {"records above the launch", 4, 600, 600, 0, 0, true}, + {"source records below the copy", 4, 400, 512, 0, 0, false}, + {"0 destination records", 4, 512, 0, 0, 0, false}, + {"above 16 MiB", LargeGroups, LargeGroups * 128u, LargeGroups * 128u, 0, 0, false}, + {"source not aligned to 16 bytes", 4, 512, 512, 1, 0, false}, + {"destination not aligned to 16 bytes", 4, 512, 512, 0, 1, false}, + }}; + const bool hleEnabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; + for (const auto& item : cases) { + const bool hle = item.hle && hleEnabled; + for (std::size_t i = 0; i < Source.size(); ++i) Source[i] = static_cast(i) * 2654435761u + 1u; + Destination.fill(Untouched); + const auto taken = Taken(); + const auto refused = copyPathCounts[CopyShape].load(); + const auto words = Commands(UserData(Source.data() + item.sourceOffset, item.sourceRecords, Destination.data() + item.destinationOffset, item.destinationRecords), item.groups); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Destination.data()), sizeof(Destination)); + const std::string where = std::string(" (") + item.name + ")"; + Require(Taken() - taken == (hle ? 1u : 0u), (hle ? "the copy did not take the HLE path" : "the copy took the HLE path") + where); + Require(copyPathCounts[CopyShape].load() - refused == (hle || !hleEnabled ? 0u : 1u), (hle ? "the copy was refused for its shape" : "the copy was not refused for its shape") + where); + const auto elements = std::min(item.groups * 128u, item.destinationRecords); + for (std::size_t i = 0; i < Destination.size(); ++i) { + const bool inside = i >= item.destinationOffset && (i - item.destinationOffset) / 4u < elements; + const auto element = (i - item.destinationOffset) / 4u; + const auto expected = !inside ? Untouched : element < item.sourceRecords ? Source[item.sourceOffset + i - item.destinationOffset] : 0u; + Require(Destination[i] == expected, "stream copy mismatch at word " + std::to_string(i) + where + ": got " + std::to_string(Destination[i]) + ", expected " + std::to_string(expected)); + } + } +} + +} + +int main() { + try { + if (std::getenv("APS5_NO_COPY_HLE") == nullptr) MatchTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("stream copy tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 3a18017cd1ebef57aede3a5cb9a73764deaa1a1d Mon Sep 17 00:00:00 2001 From: Sasha Papo Date: Tue, 13 Oct 2026 10:21:10 -0400 Subject: [PATCH 14/31] fix(agc): count adjacent registered allocations as covering a storage-image write-back Co-Authored-By: Claude Opus 5.5 (cherry picked from commit db1ddb96ce300a4eb60ee76f8755898a43b688f5) --- .../Graphics/src/GuestBufferMemory.cpp | 8 ++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index d093b86ec415..8e4f599154ee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2034,7 +2034,13 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& bool RegisteredReadableCovers(std::uint64_t address, std::size_t bytes) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); - return containingRange(lease, address, address + bytes) != nullptr; + const auto end = address + bytes; + for (auto at = address; at < end;) { + const auto* range = containingRange(lease, at, at + 1); + if (range == nullptr) return false; + at = range->address + range->bytes; + } + return true; } std::uint64_t RegisteredReadableEnd(std::uint64_t address) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 13af9823a601..76d8ad221163 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3575,6 +3575,40 @@ void barycentricEmulationTests() { expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, limits)); }, "not exported by the vertex shader"); } +void registeredReadableCoverTests() { + using namespace AgcDriver::Graphics; + + constexpr std::size_t block = 65536; + auto* memory = static_cast(::operator new(4 * block, std::align_val_t{block})); + const auto base = reinterpret_cast(memory); + const auto add = [&](std::size_t index, bool readable) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, memory + index * block, block, readable, readable, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto remove = [&](std::size_t index) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, memory + index * block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + + add(0, true); + add(1, true); + Require(RegisteredReadableCovers(base, block), "a registered allocation must be covered"); + Require(RegisteredReadableCovers(base + block / 2, block), "a range across two adjacent registered allocations must be covered"); + Require(RegisteredReadableCovers(base, 2 * block), "two adjacent registered allocations must be covered together"); + Require(!RegisteredReadableCovers(base + block, 2 * block), "a range past the last registered allocation must not be covered"); + add(3, true); + Require(!RegisteredReadableCovers(base, 4 * block), "a range over a gap between registered allocations must not be covered"); + add(2, false); + Require(!RegisteredReadableCovers(base + block, 2 * block), "a range over an unreadable registered allocation must not be covered"); + Require(RegisteredReadableCovers(base + 3 * block, block), "the readable allocation after an unreadable one must be covered"); + Require(!RegisteredReadableCovers(base, 0), "an empty range must not be covered"); + for (std::size_t index = 0; index < 4; ++index) remove(index); + Require(!RegisteredReadableCovers(base, block), "a removed allocation must not be covered"); + ::operator delete(memory, std::align_val_t{block}); +} + void rectListTests() { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; @@ -5133,6 +5167,7 @@ int main(int argc, char** argv) { rectListTests(); floatControlsModeTests(); barycentricEmulationTests(); + registeredReadableCoverTests(); mock = MockVulkan{}; auto bdaContext = mockContext(); bdaContext.bufferDeviceAddress = true; From 0053d892f89c1894a6a3f41fddd83255cec6f411 Mon Sep 17 00:00:00 2001 From: SaloQT <123214229+SaloQT@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:41:11 +1100 Subject: [PATCH 15/31] test(recompiler): cover prebuilt shader pack loading Build packs from EncodeEntry output and check the hit and miss paths, an entry stored under another key, a truncated entry, a damaged index, an out-of-range record, a foreign compiler source version and an unknown format. A child process covers pack discovery on a real disk load, the ANYPS5_NO_PREBUILT_SHADERS opt out and fall-through to the writable cache. ARCHITECTURE.md describes where the prebuilt lookup sits. (cherry picked from commit 93ab137194fa45d98220db6bf826cd23c5d258dc) --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 169 ++++++++++++++++++ docs/dev/ARCHITECTURE.md | 2 +- 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 301ab0476a98..8ddc29f98e15 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1,5 +1,6 @@ #include "ShaderDiskCache.hpp" #include "CacheKey.hpp" +#include "ShaderCachePack.hpp" #include "VertexInputSpecialization.hpp" #include "SpirvBackend/SpirvSpecialization.hpp" #include "Optimization/ResourceMaterializer.hpp" @@ -611,6 +612,171 @@ void verifyPlanRoundTrip() { require(!ShaderDiskCache::DecodePlan(badBytes, refused), "a plan with an unknown sampler source decodes"); } +std::vector packBytes(const std::vector, std::vector>>& entries) { + using namespace ShaderRecompiler::ShaderDiskCache; + std::vector records; + std::uint64_t offset = sizeof(PackHeader) + entries.size() * sizeof(PackRecord); + for (const auto& [key, entry] : entries) { + records.push_back({PackKeyHash(key), offset, entry.size()}); + offset += entry.size(); + } + std::sort(records.begin(), records.end(), [](const PackRecord& left, const PackRecord& right) { return left.hash < right.hash; }); + PackHeader header; + header.sourceVersion = SourceVersion(); + header.fileBytes = offset; + header.entryCount = entries.size(); + header.indexHash = ShaderRecompiler::HashBytes(std::as_bytes(std::span(records))); + std::vector bytes(offset); + std::memcpy(bytes.data(), &header, sizeof(header)); + std::memcpy(bytes.data() + sizeof(header), records.data(), records.size() * sizeof(PackRecord)); + std::size_t at = sizeof(header) + records.size() * sizeof(PackRecord); + for (const auto& [key, entry] : entries) { + std::memcpy(bytes.data() + at, entry.data(), entry.size()); + at += entry.size(); + } + return bytes; +} + +void refreshIndexHash(std::vector& pack, std::size_t entries) { + using namespace ShaderRecompiler::ShaderDiskCache; + const auto index = std::span(pack).subspan(sizeof(PackHeader), entries * sizeof(PackRecord)); + const auto hash = ShaderRecompiler::HashBytes(index); + std::memcpy(pack.data() + offsetof(PackHeader, indexHash), &hash, sizeof(hash)); +} + +void verifyPrebuiltPack(const std::filesystem::path& directory) { + using namespace ShaderRecompiler::ShaderDiskCache; + SampleRequest sample; + const auto key = sample.Key(); + const auto variant = sampleVariant(); + const auto file = EncodeEntry(key, variant); + auto otherKey = key; + otherKey.back() ^= std::byte{1}; + const auto otherFile = EncodeEntry(otherKey, variant); + std::filesystem::create_directories(directory); + const auto writePack = [&](const char* name, const std::vector& bytes) { + const auto path = directory / name; + require(WriteFileAtomically(path, bytes), "cannot write the test pack"); + return path; + }; + PackLoadTimings timings; + CompiledVariant decoded; + { + CachePack pack(writePack("valid.pack", packBytes({{key, file}, {otherKey, otherFile}}))); + require(pack.Valid(), "a valid pack is rejected: " + pack.Error()); + require(pack.Count() == 2, "the pack does not hold both entries"); + require(pack.Load(key, decoded, timings) == LoadStatus::Loaded, "a pack entry does not load"); + requireSameVariant(variant, decoded, "pack round trip"); + auto missingKey = key; + missingKey.front() ^= std::byte{1}; + require(pack.Load(missingKey, decoded, timings) == LoadStatus::Absent, "an absent key loads from the pack"); + } + { + std::vector, std::vector>> mismatched; + mismatched.emplace_back(otherKey, file); + CachePack pack(writePack("mismatch.pack", packBytes(mismatched))); + require(pack.Valid(), "the key mismatch pack is rejected: " + pack.Error()); + require(pack.Load(otherKey, decoded, timings) != LoadStatus::Loaded, "an entry stored under another key loads"); + } + { + auto bytes = packBytes({{key, file}}); + const auto truncated = static_cast(file.size() / 2); + std::memcpy(bytes.data() + sizeof(PackHeader) + offsetof(PackRecord, size), &truncated, sizeof(truncated)); + refreshIndexHash(bytes, 1); + CachePack pack(writePack("truncated.pack", bytes)); + require(pack.Valid(), "the truncated-entry pack is rejected: " + pack.Error()); + require(pack.Load(key, decoded, timings) == LoadStatus::Rejected, "a truncated pack entry loads"); + } + { + auto bytes = packBytes({{key, file}}); + bytes[sizeof(PackHeader)] ^= std::byte{0x10}; + CachePack pack(writePack("corrupt-index.pack", bytes)); + require(!pack.Valid(), "a pack with a damaged index is accepted"); + } + { + auto bytes = packBytes({{key, file}}); + const auto outside = static_cast(bytes.size()) + 16; + std::memcpy(bytes.data() + sizeof(PackHeader) + offsetof(PackRecord, offset), &outside, sizeof(outside)); + refreshIndexHash(bytes, 1); + CachePack pack(writePack("corrupt-record.pack", bytes)); + require(!pack.Valid(), "a pack with an out-of-range record is accepted"); + } + { + auto bytes = packBytes({{key, file}}); + const auto wrong = SourceVersion() + 1; + std::memcpy(bytes.data() + offsetof(PackHeader, sourceVersion), &wrong, sizeof(wrong)); + CachePack pack(writePack("wrong-version.pack", bytes)); + require(!pack.Valid(), "a pack from another compiler source version is accepted"); + } + { + auto bytes = packBytes({{key, file}}); + bytes.front() ^= std::byte{0x10}; + CachePack pack(writePack("unknown-format.pack", bytes)); + require(!pack.Valid(), "a pack with an unknown format is accepted"); + } +} + +SampleRequest prebuiltSample() { + SampleRequest sample; + sample.code[3] ^= 0x5a5a5a5au; + return sample; +} + +int runPrebuiltLoadingProcess() { + using namespace ShaderRecompiler::ShaderDiskCache; + SampleRequest sample = prebuiltSample(); + const auto key = sample.Key(); + const auto expected = sampleVariant(); + CompiledVariant loaded; + require(Load(key, loaded), "the prebuilt pack entry does not load"); + requireSameVariant(expected, loaded, "prebuilt load"); + require(Totals().prebuiltHits == 1, "the prebuilt load was not counted"); + const auto afterPack = Totals(); + require(afterPack.prebuiltBytesRead > 0 && afterPack.prebuiltLoadNanoseconds > 0, "the prebuilt load was not timed"); + require(afterPack.hits == 0 && afterPack.loadFailures == 0 && afterPack.bytesRead == 0 && afterPack.loadNanoseconds == 0, "the prebuilt load reached the writable totals"); + auto looseKey = key; + looseKey.back() ^= std::byte{1}; + CompiledVariant loose; + require(Load(looseKey, loose), "the loose prebuilt entry does not load"); + requireSameVariant(expected, loose, "loose prebuilt load"); + require(Totals().prebuiltHits == 2, "the loose prebuilt load was not counted"); + require(Totals().hits == 0 && Totals().loadFailures == 0 && Totals().bytesRead == 0, "the loose prebuilt load reached the writable totals"); + auto corruptKey = looseKey; + corruptKey.front() ^= std::byte{1}; + CompiledVariant corrupt; + require(!Load(corruptKey, corrupt), "a corrupt loose prebuilt entry loads"); + require(Totals().prebuiltRejected >= 1, "the corrupt loose prebuilt entry was not counted as rejected"); + require(Totals().loadFailures == 0, "the corrupt loose prebuilt entry reached the writable failures"); + auto otherKey = looseKey; + otherKey.front() ^= std::byte{1}; + CompiledVariant other; + require(!Load(otherKey, other), "a key absent from prebuilt storage loads"); + Store(otherKey, std::make_shared(sampleVariant())); + Flush(); + require(Load(otherKey, other), "the writable cache fall-through does not load"); + std::cout << "prebuilt process loaded from the pack and fell through to the writable cache\n"; + return 0; +} + +void verifyPrebuiltAcrossProcesses(const char* self, const std::filesystem::path& directory) { + using namespace ShaderRecompiler::ShaderDiskCache; + SampleRequest sample = prebuiltSample(); + const auto prebuilt = directory / "prebuilt"; + std::filesystem::create_directories(prebuilt); + const auto bytes = packBytes({{sample.Key(), EncodeEntry(sample.Key(), sampleVariant())}}); + require(WriteFileAtomically(prebuilt / "test.pack", bytes), "cannot write the prebuilt pack"); + auto looseKey = sample.Key(); + looseKey.back() ^= std::byte{1}; + require(WriteFileAtomically(prebuilt / EntryName(looseKey), EncodeEntry(looseKey, sampleVariant())), "cannot write the loose prebuilt entry"); + auto corruptKey = looseKey; + corruptKey.front() ^= std::byte{1}; + auto corruptEntry = EncodeEntry(corruptKey, sampleVariant()); + corruptEntry.resize(corruptEntry.size() / 2); + require(WriteFileAtomically(prebuilt / EntryName(corruptKey), corruptEntry), "cannot write the corrupt loose prebuilt entry"); + const std::string command = "\"" + std::string(self) + "\" --load-prebuilt"; + require(std::system(command.c_str()) == 0, "the prebuilt loading process failed"); +} + void verifyArtifactStorageIsolation() { SampleRequest sample; const auto key = sample.Key(); @@ -1371,6 +1537,7 @@ void verifyCacheDirectory(const char* self) { int main(int argc, char** argv) { try { if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); + if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo(); const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}())); std::filesystem::remove_all(directory); @@ -1380,11 +1547,13 @@ int main(int argc, char** argv) { verifyResultRoundTrip(); verifyEntryRoundTrip(); verifyPlanRoundTrip(); + verifyPrebuiltPack(directory / "packs"); verifyArtifactStorageIsolation(); verifyKeySensitivity(); verifyFloatModeSerialization(); verifyStore(); verifyAcrossProcesses(argv[0]); + verifyPrebuiltAcrossProcesses(argv[0], directory); verifyEmissionFailureMemo(); verifyFailureMemoSwitch(argv[0]); verifyInvocationIsolation(); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 815f18029c4d..f215ad249d25 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -86,7 +86,7 @@ flowchart LR ``` - [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools. -- `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. +- `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. Before the writable cache, a miss is also looked up in read-only prebuilt packs and loose entry folders under `shader_cache/prebuilt`; pack entries are ordinary encoded cache entries behind a sorted key-hash index, validated by magic, compiler source version and index checksum when mapped, and a miss or rejected entry falls through to the writable cache and the compiler. - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. From a2922d4618a6cb2bacbd530f640e0eacf2a516bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 14 Oct 2026 22:11:52 +0200 Subject: [PATCH 16/31] docs(debt): record COPY_DATA from a register and the dispatch registers it reads Co-Authored-By: Claude (cherry picked from commit 5d9def64e4da8697dff23ba823c3c298b1917cab) --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 1c138240efe1..3223d8a88ae9 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -88,6 +88,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. +- [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [16-bit RGBA color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support the standard and the alternate (`COMP_SWAP` 1) component order. Alternate swaps red and blue in shader exports, write masks and constant-color blend factors, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c). The reverse orders (`COMP_SWAP` 2 and 3) keep throwing when blending, and simultaneous targets requiring conflicting blend constants throw. Console equivalence has not been measured on hardware. - [UINT16_ABGR color exports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - export format 7 is accepted only into an unsigned integer target, and each 16-bit value reaches the target unchanged; a value beyond a narrower target's range (`R8_UINT`) is undefined in Vulkan, and whether the console clamps or truncates it is not confirmed. SINT16_ABGR (8) throws - [VGT_TF_PARAM](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - DISTRIBUTION_MODE is taken as scheduling only; the host tessellator decides the order of a patch's triangles. From f40e074c607ea3860a67908460639306000703bc Mon Sep 17 00:00:00 2001 From: ee6aba54 <340958984+ee6aba54@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:42:56 +0200 Subject: [PATCH 17/31] fix(agc-driver): keep a live-backed snapshot mismatch from failing the dispatch A captured word that no longer matches a live import or mirror only means the capture raced a guest write; the GPU reads the live bytes either way, as the hardware would. The compare now counts such mismatches and reports them with the snapshot, the region and the first differing offset at most every 10 s instead of throwing; APS5_SNAPSHOT_CHECK_FATAL=1 restores the throw, and a snapshot-backed region still fails, now naming the snapshot, the region and the first differing offset. an Unreal Engine 5 title hit the throw within 65 to 91 s of reaching its title screen. (cherry picked from commit 67973cd89a957a0e39b8751a1e684414e5a8c041) --- .../Graphics/src/GuestBufferMemory.cpp | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 8e4f599154ee..f63a05585f41 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2515,7 +2515,25 @@ void GuestBufferMemory::AddSnapshot(const GuestMemorySnapshot& snapshot) { } const auto offset = static_cast(snapshot.address - owner->begin); const auto* source = live ? reinterpret_cast(snapshot.address) : owner->snapshot.data() + offset; - Require(std::memcmp(source, snapshot.bytes.data(), snapshot.bytes.size()) == 0, "guest snapshot differs from registered memory"); + if (std::memcmp(source, snapshot.bytes.data(), snapshot.bytes.size()) != 0) { + std::size_t first = 0; + while (first < snapshot.bytes.size() && source[first] == snapshot.bytes[first]) ++first; + char text[256]; + std::snprintf(text, sizeof(text), "guest snapshot differs from registered memory: snapshot 0x%llx+0x%zx (%s region 0x%llx+0x%llx%s%s%s) differs first at +0x%zx", static_cast(snapshot.address), snapshot.bytes.size(), live ? "live" : "snapshot-backed", static_cast(owner->begin), static_cast(owner->end - owner->begin), owner->writable ? ", writable" : "", owner->hostBacked ? ", host-backed" : "", owner->mirror != nullptr ? ", mirrored" : "", first); + static const bool fatal = std::getenv("APS5_SNAPSHOT_CHECK_FATAL") != nullptr; + if (!live || fatal) throw std::runtime_error(text); + static std::mutex reportMutex; + static std::uint64_t mismatches = 0; + static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); + std::lock_guard lock(reportMutex); + ++mismatches; + const auto now = std::chrono::steady_clock::now(); + if (now - lastReport >= std::chrono::seconds(10)) { + lastReport = now; + std::fprintf(stderr, "[gpu] %s; the live region serves the GPU (%llu mismatches so far; APS5_SNAPSHOT_CHECK_FATAL=1 fails the dispatch instead)\n", text, static_cast(mismatches)); + } + return; + } if (profile) Snapshots().checked.fetch_add(1, std::memory_order_relaxed); return; } From 36874ab0b2752297d682078fe1133c1a73bf0c4f Mon Sep 17 00:00:00 2001 From: Semir Kabir Date: Tue, 13 Oct 2026 06:17:24 +0000 Subject: [PATCH 18/31] fix(agc): print a GPU queue worker failure when it is first stored (cherry picked from commit ea13f6a4b5af94a58612448675eaf803f5a9ecfd) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../Execution/src/Driver/Lifecycle.cpp | 21 ++++++- core/libs/prx/libSceAgcDriver/tests/Flip.cpp | 57 +++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 2b67d7f8a615..ace4b75e05c5 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1248,10 +1248,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_pm4_priority_refused PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_THREAD_PRIORITY=high;DBUS_SYSTEM_BUS_ADDRESS=unix:path=/nonexistent/aps5-no-rtkit" SKIP_RETURN_CODE 77) endif() + add_test(NAME agc_driver_worker_failure_log COMMAND agc_driver_flip_tests failure-log) + set_tests_properties(agc_driver_worker_failure_log PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 20) add_test(NAME agc_driver_flip_compute COMMAND agc_driver_flip_tests compute) set_tests_properties(agc_driver_flip_compute PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_driver_flip_compute PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_flip_compute agc_driver_worker_failure_log PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_draw_cache_reuse_off COMMAND agc_driver_draw_cache_reuse_tests) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index fa645dd135c7..080030d62af5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -7,9 +7,25 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include +#include namespace AgcDriver::DriverDetail { +namespace { + +void PrintFailure(std::exception_ptr error) { + try { + std::rethrow_exception(error); + } catch (const std::exception& cause) { + std::fprintf(stderr, "[gpu] driver failure: %s\n", cause.what()); + } catch (...) { + std::fprintf(stderr, "[gpu] driver failure with a non-standard exception\n"); + } + std::fflush(stderr); +} + +} + Driver& Driver::Get() { static Driver driver; return driver; @@ -96,9 +112,11 @@ void Driver::CheckFailure() { void Driver::ReportFailure(std::exception_ptr error) { require(error != nullptr, "null asynchronous failure"); + bool first = false; { std::lock_guard lock(mutex); - if (!failure) failure = error; + first = !failure; + if (first) failure = error; failed.store(true, std::memory_order_release); for (const auto& [handle, output] : outputs) output->Fail(failure); for (auto& [queue, worker] : workers) { @@ -110,6 +128,7 @@ void Driver::ReportFailure(std::exception_ptr error) { worker.queued.store(0, std::memory_order_release); } } + if (first) PrintFailure(error); changed.notify_all(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index 8b3823728555..473619b8f577 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -8,12 +8,28 @@ #include #include #include +#include +#include +#include #include #include #include #include #include #include +#ifdef _WIN32 +#include +#define APS5_DUP _dup +#define APS5_DUP2 _dup2 +#define APS5_CLOSE _close +#define APS5_FILENO _fileno +#else +#include +#define APS5_DUP dup +#define APS5_DUP2 dup2 +#define APS5_CLOSE close +#define APS5_FILENO fileno +#endif namespace { @@ -322,6 +338,40 @@ void testFailure() { AgcDriverUnregisterVideoOutput_nid_postfix(7, output); } +void testFailureLog() { + const auto path = std::filesystem::temp_directory_path() / ("aps5_worker_failure_log_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + std::fflush(stderr); + const int saved = APS5_DUP(APS5_FILENO(stderr)); + check(saved >= 0, "stderr could not be saved"); + check(std::freopen(path.string().c_str(), "w", stderr) != nullptr, "stderr could not be redirected"); + std::string message; + std::string error; + try { + auto output = std::make_shared(); + output->state->fail = true; + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + submitFlip(); + message = expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }); + check(expectFailure([] { submitFlip(); }) == message, "the stored failure was not rethrown on the next driver call"); + AgcDriverUnregisterVideoOutput_nid_postfix(7, output); + } catch (const std::exception& caught) { + error = caught.what(); + } + std::fflush(stderr); + APS5_DUP2(saved, APS5_FILENO(stderr)); + APS5_CLOSE(saved); + std::string captured; + { + std::ifstream file(path, std::ios::binary); + captured.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + std::filesystem::remove(path); + if (!error.empty()) throw std::runtime_error(error); + check(message == "intentional flip failure", "the worker failure was not rethrown"); + check(captured.find("[gpu] driver failure: intentional flip failure\n") != std::string::npos, "the worker failure was not printed"); + check(captured.find("driver failure") == captured.rfind("driver failure"), "the worker failure was printed more than once"); +} + void testReset(bool compute) { std::array registers{0xc0027600, 0x20c, 1, 0}; Packet packet{registers.data(), 4, 0, {}}; @@ -340,6 +390,13 @@ void testReset(bool compute) { int main(int argc, char** argv) { try { + if (argc == 2 && std::string(argv[1]) == "failure-log") { + testFailureLog(); + const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); }); + check(shutdown == "intentional flip failure", "shutdown lost worker failure"); + std::puts("AGC worker failure log test passed"); + return 0; + } if (argc == 2) testReset(std::string(argv[1]) == "compute"); else { testFlipAndBoundary(); testFlipHold(); testComputeRenderingWait(); testFailure(); } const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); }); From 44301a60fda8601c4ea4c3c3b9267913bb44b1e0 Mon Sep 17 00:00:00 2001 From: Semir Kabir Date: Wed, 14 Oct 2026 19:24:49 +0000 Subject: [PATCH 19/31] fix(agc): print the stored worker failure before WaitIdle can return (cherry picked from commit 6e3152f0e61e0df9962657b2d3ebd825fc7e6436) --- .../libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Flip.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 080030d62af5..0e87648fdfbd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -127,8 +127,8 @@ void Driver::ReportFailure(std::exception_ptr error) { worker.unfinishedWrites.clear(); worker.queued.store(0, std::memory_order_release); } + if (first) PrintFailure(error); } - if (first) PrintFailure(error); changed.notify_all(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index 473619b8f577..8df560a2a5a9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -343,7 +343,7 @@ void testFailureLog() { std::fflush(stderr); const int saved = APS5_DUP(APS5_FILENO(stderr)); check(saved >= 0, "stderr could not be saved"); - check(std::freopen(path.string().c_str(), "w", stderr) != nullptr, "stderr could not be redirected"); + check(std::freopen(path.string().c_str(), "wb", stderr) != nullptr, "stderr could not be redirected"); std::string message; std::string error; try { From a10dff08012f7005f64ad2c71d5423ebbeb6c636 Mon Sep 17 00:00:00 2001 From: mugurc Date: Mon, 12 Oct 2026 15:17:44 +0300 Subject: [PATCH 20/31] fix(libs): build with libc++ Apple clang ships libc++, which differs from libstdc++ where the sources relied on it: - std::atomic> is not provided. AtomicSharedPtr is that type where the library has it and a mutex-guarded pointer with the same member functions elsewhere; the AGC driver uses it. - std::atomic_ref is not provided; the page span reads go through a non-const reference. - std::filesystem::file_time_type::clock::to_sys is not provided; std::chrono::file_clock::to_sys is. - std::max and std::min do not deduce across the integer types of the guest range arithmetic, which is spelled with an explicit type. - and are included where the sources and tests use them. (cherry picked from commit facc03ae90e3612f848f40ecc6569c1a113a9178) --- .../Execution/include/Driver/DeviceAccess.hpp | 3 +- .../include/Driver/Dispatch/DispatchCache.hpp | 3 +- .../include/Driver/Draw/DrawCache.hpp | 3 +- .../Execution/src/GuestMemory.cpp | 6 +-- .../Graphics/src/GuestBufferMemory.cpp | 3 +- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 3 +- .../libs/prx/libSceSaveData.native/Export.cpp | 2 +- .../libc/include/general/AtomicSharedPtr.hpp | 50 +++++++++++++++++++ core/libs/prx/libc/src/GuestAllocations.cpp | 8 +-- core/libs/tests/GuestDup.cpp | 1 + core/libs/tests/GuestPthreadMutexattr.cpp | 1 + core/libs/tests/GuestSchedPriority.cpp | 1 + core/libs/tests/GuestSignals.cpp | 1 + 13 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libc/include/general/AtomicSharedPtr.hpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp index 910f6ba77d3a..3bd84a70db6d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp @@ -7,6 +7,7 @@ #include #include #include +#include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::DriverDetail { @@ -21,7 +22,7 @@ class DevicePointer { bool operator==(std::nullptr_t) const; private: - std::atomic> pointer; + AtomicSharedPtr pointer; }; class DeviceUseGate { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp index 1591221fdff4..110894795c8e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp @@ -17,6 +17,7 @@ #include #include #include +#include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::DriverDetail { @@ -31,7 +32,7 @@ struct DispatchVariant { std::shared_ptr shader; - std::atomic> recipe; + AtomicSharedPtr recipe; static constexpr std::uint32_t NoFlatBinding = std::numeric_limits::max(); std::vector dataPositions; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index ac06e979ff46..31f580396430 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -11,6 +11,7 @@ #include #include #include +#include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::DriverDetail { @@ -54,7 +55,7 @@ struct DrawEntry { std::vector>> stages; - std::atomic>> recipes; + AtomicSharedPtr> recipes; std::uint64_t touched = 0; std::list::iterator order; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 674be0cfae66..1b6a569b3e57 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -375,7 +375,7 @@ struct PageSpan { } std::uint8_t load(std::uintptr_t address) const { - return std::atomic_ref(pages[(address - base) / PageBytes]).load(std::memory_order_relaxed); + return std::atomic_ref(const_cast(pages[(address - base) / PageBytes])).load(std::memory_order_relaxed); } void store(std::uintptr_t address, std::uint8_t value) { @@ -388,11 +388,11 @@ struct PageSpan { const auto stop = std::min((std::min(limit, base + size) - base + PageBytes - 1) / PageBytes, size / PageBytes); const std::uint64_t wide = 0x0101010101010101ull * value; while (index < stop) { - if (index % 8 == 0 && index + 8 <= stop && std::atomic_ref(*reinterpret_cast(pages + index)).load(std::memory_order_relaxed) == wide) { + if (index % 8 == 0 && index + 8 <= stop && std::atomic_ref(*reinterpret_cast(const_cast(pages + index))).load(std::memory_order_relaxed) == wide) { index += 8; continue; } - if (std::atomic_ref(pages[index]).load(std::memory_order_relaxed) != value) break; + if (std::atomic_ref(const_cast(pages[index])).load(std::memory_order_relaxed) != value) break; ++index; } return std::min(limit, base + index * PageBytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index f63a05585f41..80389341b1b5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -10,6 +10,7 @@ #include "prx/libc/include/CpuTopology.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libc/include/general/AtomicSharedPtr.hpp" #ifdef _WIN32 #include #else @@ -115,7 +116,7 @@ struct GuestBufferMemory::AddressSpace { namespace { struct AddressSpaceCache { - std::atomic> current; + AtomicSharedPtr current; std::atomic serials{0}; // Set by the waiter's drop, cleared by the next publish: the rebuild's reason. std::atomic droppedByWaiter{false}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index c9ef2a20d43f..3831caedfe4b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -29,6 +29,7 @@ #include #include #include +#include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::Graphics { @@ -633,7 +634,7 @@ using WriteRanges = Recorder::WriteRanges; // work. A range leaves the snapshot only after its batch's completions (CPU write-backs) ran, so a // reader that sees no overlap either precedes the note (the queues are unordered then, as on the // GPU) or follows the write-back. -std::atomic> pendingWrites; +AtomicSharedPtr pendingWrites; bool HookSnapshotEnabled() { // Debug aid: APS5_NO_HOOK_SNAPSHOT=1 takes the GpuMutex on every access as before. diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index bb58612ea7bd..3a75d7875f20 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -145,7 +145,7 @@ SaveDataParam load_param(const std::string& real_path) { std::error_code ec; const auto written = std::filesystem::last_write_time(real_path, ec); if (!ec) { - const auto system = std::filesystem::file_time_type::clock::to_sys(written); + const auto system = std::chrono::file_clock::to_sys(written); param.mtime = std::chrono::duration_cast(system.time_since_epoch()).count(); } } diff --git a/core/libs/prx/libc/include/general/AtomicSharedPtr.hpp b/core/libs/prx/libc/include/general/AtomicSharedPtr.hpp new file mode 100644 index 000000000000..30054c527e9b --- /dev/null +++ b/core/libs/prx/libc/include/general/AtomicSharedPtr.hpp @@ -0,0 +1,50 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_ATOMICSHAREDPTR_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_ATOMICSHAREDPTR_HPP + +#include +#include +#include +#include + +#if defined(__cpp_lib_atomic_shared_ptr) +template +using AtomicSharedPtr = std::atomic>; +#else +template +class AtomicSharedPtr { +public: + AtomicSharedPtr() noexcept = default; + AtomicSharedPtr(std::shared_ptr desired) noexcept : value(std::move(desired)) {} + AtomicSharedPtr(const AtomicSharedPtr&) = delete; + AtomicSharedPtr& operator=(const AtomicSharedPtr&) = delete; + + std::shared_ptr load(std::memory_order = std::memory_order_seq_cst) const noexcept { + std::lock_guard lock(mutex); + return value; + } + void store(std::shared_ptr desired, std::memory_order order = std::memory_order_seq_cst) noexcept { + exchange(std::move(desired), order); + } + std::shared_ptr exchange(std::shared_ptr desired, std::memory_order = std::memory_order_seq_cst) noexcept { + std::lock_guard lock(mutex); + return std::exchange(value, std::move(desired)); + } + bool compare_exchange_strong(std::shared_ptr& expected, std::shared_ptr desired, std::memory_order = std::memory_order_seq_cst) noexcept { + std::lock_guard lock(mutex); + if (value == expected) { + value = std::move(desired); + return true; + } + expected = value; + return false; + } + operator std::shared_ptr() const noexcept { return load(); } + void operator=(std::shared_ptr desired) noexcept { store(std::move(desired)); } + +private: + mutable std::mutex mutex; + std::shared_ptr value; +}; +#endif + +#endif diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index 9090140e4b22..dae64b7c3512 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -506,10 +506,10 @@ RangePatch replaceRange(const void* pointer, std::size_t bytes, bool remove, boo const auto insert = [&](std::uint64_t first, std::uint64_t last, bool canRead, bool canWrite, bool gpuMapped) { if (first < last) patch.inserted.emplace_back(first, std::make_shared(Range{first, static_cast(last - first), canRead, canWrite, range.allocationAddress, range.allocationBytes, range.releasable, gpuMapped})); }; - insert(base, std::max(base, address), range.readable, range.writable, range.gpu); - if (!remove) insert(std::max(base, address), std::min(finish, end), readable, writable, gpu); - insert(std::min(finish, end), finish, range.readable, range.writable, range.gpu); - cursor = std::min(finish, end); + insert(base, std::max(base, address), range.readable, range.writable, range.gpu); + if (!remove) insert(std::max(base, address), std::min(finish, end), readable, writable, gpu); + insert(std::min(finish, end), finish, range.readable, range.writable, range.gpu); + cursor = std::min(finish, end); } require(cursor == end, "guest protection or unmap range is not registered"); return patch; diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index 04614f8062ab..4e0451ae85f6 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index 410aa02ba00f..f3a7257808b0 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include #include +#include #include extern "C" { diff --git a/core/libs/tests/GuestSchedPriority.cpp b/core/libs/tests/GuestSchedPriority.cpp index a83bebd23102..25393d9bcd2e 100644 --- a/core/libs/tests/GuestSchedPriority.cpp +++ b/core/libs/tests/GuestSchedPriority.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" { diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 913fbe1ef111..d2fe283b8a31 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include From 0d972e85fad05f099e0f9cc73ed3f9e162fd5752 Mon Sep 17 00:00:00 2001 From: ee6aba54 <340958984+ee6aba54@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:07:09 +0200 Subject: [PATCH 21/31] fix(agc-driver): size the storage image cache from the device's reported memory budget The storage image cache kept a quarter of the device-local heap (4 GiB on a 16 GiB card) regardless of how much of the heap the driver reports as usable. With a desktop and other allocations the reported budget is about 10 GiB, the cache stayed full at 4 GiB, and after minutes of a cutscene device buffer allocations failed with VK_ERROR_OUT_OF_DEVICE_MEMORY. The cache budget is now the smaller of that quarter of the heap and a quarter of the reported budget, at least 512 MiB, read once per second; APS5_TEXTURE_CACHE_MIB still overrides it. The budget report names the storage budget in use. A test covers the limits. (cherry picked from commit 64ffeb8c9f906a1a9ebd88a9fb60e14d1e3ea600) --- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 28 +++++++++++++++++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 28 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index d666c83d6386..e677b8529a8e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -28,6 +28,7 @@ class Recorder; void FlushCachedTextures(VkDevice device); void ClearCachedTextures(VkDevice device); std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory); +std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes); bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport); struct TextureCacheUse { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 919f50de218c..7a4722661df3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -482,7 +482,25 @@ std::uint64_t forcedCacheBudget() { std::uint64_t storageBudget(const Context& context) { const auto forced = forcedCacheBudget(); - return forced != 0 ? forced : TextureCacheBudget(context.memory); + if (forced != 0) return forced; + if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return TextureCacheBudget(context.memory); + static struct { + std::mutex mutex; + VkPhysicalDevice device{VK_NULL_HANDLE}; + std::chrono::steady_clock::time_point read{}; + std::uint64_t budget{0}; + } cached; + const auto now = std::chrono::steady_clock::now(); + std::lock_guard lock(cached.mutex); + if (cached.device == context.physical && now - cached.read < std::chrono::seconds(1)) return cached.budget; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; + properties.pNext = &reported; + context.memoryProperties2(context.physical, &properties); + cached.device = context.physical; + cached.read = now; + cached.budget = StorageTextureBudget(context.memory, &reported); + return cached.budget; } std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { @@ -501,7 +519,7 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) { char was[48] = ""; if (!fresh) std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast(cache.reportedBudget >> 20u)); - std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(TextureCacheBudget(context.memory) >> 20u)); + std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(storageBudget(context) >> 20u)); cache.reportedBudget = budget; cache.reportedAt = now; } @@ -903,6 +921,12 @@ std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory) return std::max(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u); } +std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) { + const auto heap = largestDeviceLocalHeap(memory); + if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); + return std::max(512ull << 20u, std::min(TextureCacheBudget(memory), reported->heapBudget[*heap] / 4u)); +} + std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes) { const auto heap = largestDeviceLocalHeap(memory); if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 76d8ad221163..949cc4e3371b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3206,6 +3206,33 @@ void sampledTextureBudgetTests() { Require(SampledTextureBudget(memory, &reported, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of sampled textures"); } +void storageTextureBudgetTests() { + using AgcDriver::Graphics::StorageTextureBudget; + constexpr std::uint64_t GiB = 1ull << 30u; + constexpr std::uint64_t MiB = 1ull << 20u; + VkPhysicalDeviceMemoryProperties memory{}; + memory.memoryHeapCount = 3; + memory.memoryHeaps[0] = {256 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + memory.memoryHeaps[1] = {32 * GiB, 0}; + memory.memoryHeaps[2] = {16 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + Require(StorageTextureBudget(memory, nullptr) == 4 * GiB, "without VK_EXT_memory_budget the storage image cache does not keep a quarter of the 16 GiB heap"); + reported.heapBudget[0] = 100 * GiB; + reported.heapBudget[1] = 100 * GiB; + Require(StorageTextureBudget(memory, &reported) == 4 * GiB, "a zero budget for the largest device-local heap did not fall back to a quarter of the heap"); + reported.heapBudget[2] = 10 * GiB; + Require(StorageTextureBudget(memory, &reported) == 2560 * MiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); + reported.heapBudget[2] = 6 * GiB; + Require(StorageTextureBudget(memory, &reported) == 1536 * MiB, "a 6 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); + reported.heapBudget[2] = 1 * GiB; + Require(StorageTextureBudget(memory, &reported) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor"); + reported.heapBudget[2] = 40 * GiB; + Require(StorageTextureBudget(memory, &reported) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap"); + memory.memoryHeaps[0].flags = 0; + memory.memoryHeaps[2].flags = 0; + Require(StorageTextureBudget(memory, &reported) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images"); +} + void sampledBudgetReportTests() { using AgcDriver::Graphics::SampledBudgetReportDue; using std::chrono::seconds; @@ -5150,6 +5177,7 @@ int main(int argc, char** argv) { debugBranchTests(); textureCacheBudgetTests(); sampledTextureBudgetTests(); + storageTextureBudgetTests(); sampledBudgetReportTests(); meshArgumentTests(); meshIndexBufferTests(); From 33ba278ecf93821d20615b30289b721e5e157e78 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 14 Oct 2026 18:00:37 +0300 Subject: [PATCH 22/31] fix(agc): name the shader when a draw stage fails to capture or materialize (cherry picked from commit 9960503f6ad13e9fb3f37cde30932ae563e1755e) --- .../Execution/src/Driver/Draw/DrawCapture.cpp | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index 7485fc31f65a..fc033b0e1f77 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -8,6 +8,8 @@ #include "Optimization/ResourceProgram.hpp" #include #include +#include +#include namespace AgcDriver::DriverDetail { @@ -24,9 +26,27 @@ std::shared_ptr Driver::materializeDraw auto& stageCapture = stageCaptures[i]; stageCapture.forgetSerial = GuestMemory::ForgetSerial(); stageCapture.pushOffset = pushOffset; + const auto named = [&](const std::exception& error) { + static const bool dumpShaders = std::getenv("APS5_DUMP_SHADERS") != nullptr; + std::string dump; + if (dumpShaders) { + try { + dump = dumpRequest(program.binary.codeAddress, request); + } catch (...) {} + } + std::ostringstream where; + where << "shader 0x" << std::hex << program.binary.codeAddress << " stage " << std::dec << static_cast(program.binary.stage); + if (!dump.empty()) where << " (" << dump << ")"; + where << ": "; + return std::runtime_error(where.str() + error.what()); + }; const auto capture = [&] { const SampledReadScope sampling(evidenceReads); - return shaderMemory.Capture(invocation); + try { + return shaderMemory.Capture(invocation); + } catch (const std::exception& error) { + throw named(error); + } }(); stageCapture.regions = shaderMemory.TakeRecentRegions(); @@ -70,7 +90,11 @@ std::shared_ptr Driver::materializeDraw phaseTiming.Phase(DrawRowCapture); timing.Mark("capture_resources"); - stageCapture.compiled = invocation.Materialize(*capture); + try { + stageCapture.compiled = invocation.Materialize(*capture); + } catch (const std::exception& error) { + throw named(error); + } timing.Mark("materialize"); phaseTiming.Phase(DrawRowRecompile); return stageCapture.compiled; From 58c00452b31ec78b7f5fb25d1ae359cc50ba610f Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 14 Oct 2026 18:00:37 +0300 Subject: [PATCH 23/31] test(agc): check that a draw refused for a buffer type names its shader and V# (cherry picked from commit 7af296130895b4c9ae573622b33fa2bc0f5e44d3) --- .../execution/DrawUnsupportedBufferType.cpp | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp new file mode 100644 index 000000000000..2c4ef6537ee3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp @@ -0,0 +1,112 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +alignas(256) std::array Pixels{}; +alignas(256) std::array Source{}; +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{ + 0x7e020280u, 0xe0301000u, 0x80000201u, 0xbf8c3f70u, 0x7e000280u, 0x7e0602f2u, 0xf800180fu, 0x03010002u, 0xbf810000u, +}; + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +void Draw(const std::array& descriptor) { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 0u}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 4u << 1u}, + {0x00c, descriptor[0]}, {0x00d, descriptor[1]}, {0x00e, descriptor[2]}, {0x00f, descriptor[3]}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + const auto color = reinterpret_cast(Pixels.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x2d, {3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "unsupported buffer type draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +void NamesShaderAndDescriptor() { + const auto address = reinterpret_cast(Source.data()); + const std::array descriptor{static_cast(address), static_cast((address >> 32u) & 0xffffu), sizeof(Source), 0xa1b00facu}; + try { + Draw(descriptor); + } catch (const std::exception& error) { + const std::string message = error.what(); + char shader[64]; + std::snprintf(shader, sizeof(shader), "shader 0x%llx stage %u", static_cast(reinterpret_cast(PixelCode.data())), static_cast(ShaderRecompiler::ShaderStage::Fragment)); + char words[64]; + std::snprintf(words, sizeof(words), "(buffer 0 V# %08x %08x %08x %08x)", descriptor[0], descriptor[1], descriptor[2], descriptor[3]); + Require(message.find("buffer descriptor uses an unsupported type") != std::string::npos, "draw failed for another reason: " + message); + Require(message.find(shader) != std::string::npos, std::string("draw error does not name ") + shader + ": " + message); + Require(message.find(words) != std::string::npos, std::string("draw error does not name ") + words + ": " + message); + return; + } + throw std::runtime_error("a pixel shader read a buffer through a descriptor of type 2"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + NamesShaderAndDescriptor(); + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + std::puts("draw unsupported buffer type tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + return 1; + } +} From 70b43ff3778fb79602fbfa013733003466f2a8cc Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Thu, 15 Oct 2026 10:33:50 +1300 Subject: [PATCH 24/31] docs(agc): record the accepted VGT_SHADER_STAGES_EN 0x02002008 as debt (cherry picked from commit d16745be14852059b01c2fe028dba7ebfcd827de) --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3223d8a88ae9..802d9df2854b 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -11,6 +11,7 @@ - [User data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw - [NGG vertex programs on the mesh path](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - an unregistered vertex program with `PRIMGEN_EN` that uses LDS or `s_barrier` runs on the geometry (mesh) path. Each input primitive is assumed to yield at most one output primitive, the subgroup is bounded by `GE_MAX_OUTPUT_PER_SUBGROUP` as a vertex budget, and the GS/ES VGPR count checks are skipped, because the merged inputs keep their positions. Taken from the code Mesa's ACO emits for NGG culling and from PPSA59322 and PPSA99010 draws, not measured on hardware. Registered programs keep the vertex path +- [VGT_SHADER_STAGES_EN 0x02002008](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - this one value, `ES_EN` = 1 without `HS_EN` on a non-patch draw, is drawn as the vertex path. AMD's CIK register reference calls `ES_EN` = 1 `ES_STAGE_DS` (the ES stage runs the tessellation evaluation shader), which does not explain it on a non-patch draw. Seen in one title only and not verified on the console; other vertex-path values with `ES_EN` set still throw. - [Strided buffer overhang](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. - [Zero-stride vertex fetch range](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks. - [Resource registration](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws From 45d03847d19ddb435f4e4b508eb199493936911c Mon Sep 17 00:00:00 2001 From: umang Date: Wed, 14 Oct 2026 07:53:53 +0530 Subject: [PATCH 25/31] fix(agc): skip unused pixel stages with retained export formats (cherry picked from commit 843f7e963e0d1e0f2d17f391c82067e9765ca0aa) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 9 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 75 ++++++++++++++++- .../tests/execution/DrawCacheReuse.cpp | 35 ++++++++ .../tests/execution/PreparedGraphics.cpp | 82 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 6 files changed, 198 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ace4b75e05c5..ef1470a35dce 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1258,6 +1258,7 @@ if(BUILD_TESTING) endif() add_test(NAME agc_driver_draw_cache_reuse_off COMMAND agc_driver_draw_cache_reuse_tests) set_tests_properties(agc_driver_draw_cache_reuse_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_GPL=1" SKIP_RETURN_CODE 77) + set_property(TEST agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off APPEND PROPERTY ENVIRONMENT "APS5_VERIFY_DRAW_RECIPE=1" "APS5_VERIFY_REGISTER_KEYS=1") if(WIN32) set_tests_properties(agc_driver_draw_cache_reuse_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 7b0d8f720f2e..5d00d5e7066a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -128,7 +128,7 @@ constexpr std::uint32_t DepthControlMask = ~0x007007f0u; // EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader // to run, which it always does here. constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u | 0x00010000u); -constexpr std::uint32_t PixelStageRunsMask = 0x00020747u; +constexpr std::uint32_t PixelStageRunsMask = 0x000207c7u; constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u; constexpr std::uint32_t AlphaToCoverageUndithered = 0x0000aa01u; constexpr std::uint32_t ScanModeMask = ~0x22u; @@ -1380,10 +1380,11 @@ bool PixelProgramSkipped(const QueueState& queue) { const auto& cx = queue.context; const auto targetMask = find(cx, 0x8e); const auto shaderMask = find(cx, 0x8f); - const auto zFormat = find(cx, 0x1c4); const auto shaderControl = find(cx, 0x203); - if (targetMask == cx.end() || shaderMask == cx.end() || zFormat == cx.end() || shaderControl == cx.end()) return false; - return (targetMask->second & shaderMask->second) == 0 && zFormat->second == 0 && (shaderControl->second & PixelStageRunsMask) == 0; + if (targetMask == cx.end() || shaderMask == cx.end() || shaderControl == cx.end()) return false; + const auto alphaToMask = find(cx, 0x2dc); + if (alphaToMask != cx.end() && (alphaToMask->second & 1u) != 0) return false; + return (targetMask->second & shaderMask->second) == 0 && (shaderControl->second & PixelStageRunsMask) == 0; } std::string NullPixelProgramRejection(const QueueState& queue) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 949cc4e3371b..4c4f150a334c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -183,6 +183,48 @@ void inactiveVertexPointSizeTests() { rejected(queue); } +void retainedPixelStageTests() { + using namespace AgcDriver::Graphics; + auto queue = makeState(); + queue.context[0x8e] = queue.context[0x8f] = queue.context[0x203] = 0; + for (const auto format : {0u, 1u, 2u, 3u, 9u}) { + queue.context[0x1c4] = format; + Require(PixelProgramSkipped(queue), "a retained export format forced an unused pixel program to run"); + Require(DrawRejection(queue, true).empty(), "an unused pixel program with a retained export format was rejected"); + for (const auto control : {1u, 2u, 4u, 0x40u, 0x80u, 0x100u, 0x200u, 0x400u, 0x20000u}) { + queue.context[0x203] = control; + Require(!PixelProgramSkipped(queue), "an enabled export or execution flag was skipped"); + } + queue.context[0x203] = 0; + queue.context[0x8e] = 0xf0000000u; + queue.context[0x8f] = 0x0f000000u; + Require(PixelProgramSkipped(queue), "disjoint color masks ran an unused pixel program"); + queue.context[0x8f] = 0xf0000000u; + Require(!PixelProgramSkipped(queue), "an enabled high render target lost its pixel program"); + queue.context[0x8e] = queue.context[0x8f] = 0; + } + queue.context[0x1b3] = queue.context[0x1b4] = 2; + for (const auto [format, control] : {std::pair{1u, 1u}, std::pair{2u, 2u}, std::pair{9u, 0x100u}, std::pair{9u, 0x103u}}) { + queue.context[0x1c4] = format; + queue.context[0x203] = control; + const auto state = DecodeState(queue); + Require(DrawRejection(queue, true).empty(), "a valid pixel export combination was rejected"); + const auto pixel = DecodePixelStageInfo(queue.context, ExportMappings(state), PixelProgramSkipped(queue)); + Require(pixel.depthExportEnable == ((control & 1u) != 0) && pixel.stencilRefExportEnable == ((control & 2u) != 0) && pixel.sampleMaskExportEnable == ((control & 0x100u) != 0), "decoding lost an enabled pixel export"); + } + queue.context[0x203] = 0; + for (const auto alpha : {0xaa00u, 0xaa01u}) { + queue.context[0x2dc] = alpha; + Require(PixelProgramSkipped(queue) == ((alpha & 1u) == 0), "alpha-to-coverage enable was not respected"); + } + queue.context[0x2dc] = 0xaa00u; + for (const auto offset : {0x8eu, 0x8fu, 0x203u}) { + auto missing = queue; + missing.context.erase(offset); + Require(!PixelProgramSkipped(missing), "incomplete execution state skipped a bound pixel program"); + } +} + void hdrComponentSwapTests() { using namespace AgcDriver::Graphics; for (const auto number : {0u, 1u, 4u, 7u}) { @@ -455,7 +497,18 @@ void stateTests() { Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program with Z_EXPORT_ENABLE was skipped"); queue.context[0x203] = 0; queue.context[0x1c4] = 1; - Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program exporting depth was skipped"); + Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "a retained Z export format ran a pixel program with depth exports disabled"); + Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a depth-only draw with a retained Z export format was rejected"); + for (const auto control : {1u, 2u, 4u, 0x40u, 0x80u, 0x100u, 0x200u, 0x400u, 0x20000u}) { + queue.context[0x203] = control; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "an enabled pixel export or execution flag was ignored with a retained Z format"); + } + queue.context[0x203] = 0; + queue.context[0x2dc] = 0xaa01; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "alpha-to-coverage skipped the pixel program"); + queue.context[0x203] = 0x800; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "the Vulkan alpha-to-coverage state lost its pixel program with DB's override set"); + queue.context[0x203] = queue.context[0x2dc] = 0; queue.context[0x1c4] = 0; queue.context[0x8e] = 0xf; Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program writing color was skipped"); @@ -1425,6 +1478,25 @@ void DepthStencilTests() { const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); Require(rejection.empty(), "precheck rejected a stencil draw with a surface: " + rejection); Require(state.depth && state.depth->address == 0x10000 && state.depth->stencilAddress == 0x20000 && state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT && state.depth->clearStencil == 7 && state.depth->htileAddress == 0x10000030000ull && !state.depth->htileStencil, "depth surface decode changed"); + for (const auto clear : {0u, 1u, 2u, 3u}) { + auto retained = queue; + retained.context[0x8e] = retained.context[0x8f] = retained.context[0x203] = 0; + retained.context[0x000] = clear; + retained.context[0x200] = 0x00700777u; + auto unbound = retained; + unbound.shader[0x008] = unbound.shader[0x009] = 0; + const auto expected = AgcDriver::Graphics::DecodeState(unbound); + for (const auto format : {1u, 2u, 3u, 9u}) { + retained.context[0x1c4] = format; + Require(AgcDriver::Graphics::PixelProgramSkipped(retained), "a retained format ran the pixel program during a depth/stencil pass"); + const auto actual = AgcDriver::Graphics::DecodeState(retained); + Require(actual.depth && expected.depth && actual.depth->address == expected.depth->address && actual.depth->stencilAddress == expected.depth->stencilAddress && actual.depth->format == expected.depth->format && actual.depth->clearDepth == expected.depth->clearDepth && actual.depth->clearStencil == expected.depth->clearStencil, "null substitution changed the depth/stencil surface"); + Require(actual.depthTest == expected.depthTest && actual.depthWrite == expected.depthWrite && actual.depthCompare == expected.depthCompare && actual.depthBoundsTest == expected.depthBoundsTest && actual.stencilTest == expected.stencilTest, "null substitution changed fixed-function depth/stencil tests"); + Require(std::memcmp(&actual.stencilFront, &expected.stencilFront, sizeof(VkStencilOpState)) == 0 && std::memcmp(&actual.stencilBack, &expected.stencilBack, sizeof(VkStencilOpState)) == 0, "null substitution changed stencil operations"); + Require(actual.viewport.minDepth == expected.viewport.minDepth && actual.viewport.maxDepth == expected.viewport.maxDepth && actual.renderExtent.width == expected.renderExtent.width && actual.renderExtent.height == expected.renderExtent.height, "null substitution changed depth coverage"); + Require(AgcDriver::Graphics::DrawRejection(retained, true).empty(), "a retained-format depth/stencil pass was rejected"); + } + } { queue.context[0x011] &= ~(1u << 29u); const auto stencilTiled = AgcDriver::Graphics::DecodeState(queue); @@ -5122,6 +5194,7 @@ int main(int argc, char** argv) { RunGuestLeaseWaitTests(); ResourceContentKeyTests(false); stateTests(); + retainedPixelStageTests(); hdrComponentSwapTests(); depthMaintenanceTests(); hardwareScreenOffsetTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp index 23fbd3392b20..7def898cf593 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp @@ -104,6 +104,40 @@ void ThirdDrawHitsTheCache(bool libraries) { Require(counters.hits == 1, mode + ": the repeated draw did not reuse the cached draw (" + std::to_string(counters.hits) + " hits)"); } +void RetainedPixelCacheTransitions() { + auto commands = Draws(32.0f, VertexCode.data(), 1, PixelCode.data(), 1, {}); + Append(commands, 0x76, {0x00c, std::bit_cast(1.0f)}); + const auto draw = [&] { + for (unsigned i = 0; i < 3; ++i) Append(commands, 0x2d, {3, 2}); + }; + const auto skipped = [&] { + Append(commands, 0x69, {0x8e, 0, 0}); + Append(commands, 0x69, {0x203, 0}); + Append(commands, 0x69, {0x2dc, 0xaa00}); + draw(); + }; + Append(commands, 0x69, {0x1c4, 1}); + skipped(); + Append(commands, 0x69, {0x8e, 0xf, 0xf}); + draw(); + skipped(); + Append(commands, 0x69, {0x203, 0x400}); + draw(); + skipped(); + Append(commands, 0x69, {0x8e, 0xf, 0xf}); + Append(commands, 0x69, {0x2dc, 0xaa01}); + draw(); + skipped(); + const auto before = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "retained pixel cache transitions failed to submit"); + AgcDriverWaitIdle_nid_postfix(); + const auto after = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(after.lookups - before.lookups == 21 && after.absent - before.absent == 8 && after.inserts - before.inserts == 4 && after.hits - before.hits == 13, "color, execution or alpha-to-coverage transitions reused the wrong cached draw"); + if (std::getenv("APS5_VERIFY_DRAW_RECIPE") != nullptr) Require(after.verifyDecodes > before.verifyDecodes, "pixel cache transitions did not verify any cached decode"); + Require(after.verifyDecodeMismatches == before.verifyDecodeMismatches && after.facadeMismatches == before.facadeMismatches, "cached pixel transitions disagreed with fresh decoding"); +} + void SightingsKeepCollidingKeys() { DrawKeySightings sightings(DrawKeySightings::Ways); Require(!sightings.SeenBefore(0) && sightings.SeenBefore(0), "draw key 0 was not admitted at its second sight"); @@ -127,6 +161,7 @@ int main() { } if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; ThirdDrawHitsTheCache(libraries); + RetainedPixelCacheTransitions(); AgcDriverShutdown_nid_postfix(); std::cout << "A repeated draw reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp index 5d456afdf625..4f28b65446f2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp @@ -284,6 +284,87 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path Reject([&] { DecodeGraphicsPrograms(invalid, queue, registry, true, true); }, "reserved graphics program address"); } +void RetainedPixelMatchesNullProgram(AgcDriver::VulkanDevice& device) { + using namespace AgcDriver::DriverDetail; + Fixture front; + Fixture retained; + front.Initialize(2u); + retained.Initialize(1u); + const auto null = std::make_shared(PrepareNullPixelProgram(device)); + ShaderRegistry registry; + for (const auto& snapshot : {front.snapshot, retained.snapshot, null}) registry.emplace(snapshot->codeAddress, std::vector>{snapshot}); + AgcDriver::QueueState queue{}; + queue.context[0x8e] = queue.context[0x8f] = queue.context[0x203] = 0; + queue.context[0x2dc] = 0xaa00; + queue.context[0x1c4] = 1; + front.Bind(queue, 0xc8u, 0x8bu); + queue.shader[0x8bu] = 0; + retained.Bind(queue, 0x008u, 0x00bu); + queue.shader[0x00bu] = (31u << 1u) | (1u << 27u); + for (std::uint32_t i = 0; i < 30; ++i) queue.shader[0x00cu + i] = 0xdead0000u + i; + queue.context[0x1b6] = queue.context[0x1b3] = queue.context[0x1b4] = queue.context[0x1c5] = 0xffffffffu; + auto captured = queue; + captured.shader[0x00bu] = 20u << 1u; + DrawDecode capturedDraw{}; + capturedDraw.state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + DecodeGraphicsPrograms(capturedDraw, captured, registry, false, true); + Require(capturedDraw.programs.back().snapshot == null && capturedDraw.programs.back().userData.empty(), "a retained pixel program with a valid 20-word resource count consumed unrelated userdata"); + for (const auto wave : {64u, 32u}) { + DrawDecode draw{}; + draw.state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + draw.state.stages.vertexWaveSize = 32u; + draw.state.stages.fragmentWaveSize = wave; + std::vector reads; + AgcDriver::Graphics::RegisterReadLog() = &reads; + try { + DecodeGraphicsPrograms(draw, queue, registry, false, true); + draw.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, {}, AgcDriver::Graphics::PixelProgramSkipped(queue)); + } catch (...) { + AgcDriver::Graphics::RegisterReadLog() = nullptr; + throw; + } + AgcDriver::Graphics::RegisterReadLog() = nullptr; + Require(draw.roles.size() == 2 && draw.roles.back() == ShaderRecompiler::ProgramRole::Fragment, "null substitution changed stage linking"); + Require(draw.programs.back().snapshot == null && draw.programs.back().userData.empty(), "an unused retained pixel program consumed unrelated userdata"); + Require(draw.pixel.interpolatorCount == 0 && !draw.pixel.depthExportEnable && !draw.pixel.stencilRefExportEnable && !draw.pixel.sampleMaskExportEnable && !draw.pixel.pixelKillEnable, "the substituted pixel program inherited stale inputs or exports"); + for (const auto mode : draw.pixel.targetOutputMode) Require(mode == 0, "the substituted pixel program inherited stale color formats"); + for (const auto read : reads) { + Require(AgcDriver::Graphics::DrawKeyCovers(read), "a pixel substitution dependency is absent from the draw key"); + Require(read.bank != AgcDriver::Graphics::RegisterBank::Shader || read.offset < 0x00bu || read.offset > 0x02bu, "the substituted program read fragment resources or userdata"); + } + const auto stages = PrepareGraphicsStages(draw, device.Target()); + Require(stages.size() == 2 && stages.back().entry.handle != nullptr, "a retained-format null program was not prepared"); + std::vector linked; + std::vector memory; + for (std::size_t i = 0; i < draw.programs.size(); ++i) { + const auto& program = draw.programs[i]; + linked.push_back({draw.roles[i], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); + memory.insert(memory.end(), program.memory.begin(), program.memory.end()); + } + const auto& fragment = draw.programs.back(); + const auto offset = stages.back().pushOffset; + const auto room = AgcDriver::Graphics::PipelinePushSlotBytes - offset % AgcDriver::Graphics::PipelinePushSlotBytes; + ShaderRecompiler::RecompileRequest request{fragment.binary, {wave, 0, {}, {}, draw.pixel, {}, memory, RegisteredFloatMode(*null)}, device.Target(), {0, 0, offset, room}, ShaderRecompiler::GraphicsCompileContext{0, linked, {}, {}, {}}}; + static_cast(InvocationFor(*null, fragment.codeOffset, request)); + const auto handle = SourceHandleFor(*null, fragment.codeOffset, request); + const auto entries = null->prepared->entries.size(); + static_cast(InvocationFor(*null, fragment.codeOffset, request)); + Require(null->prepared->entries.size() == entries && SourceHandleFor(*null, fragment.codeOffset, request) == handle, "a repeated null program did not reuse its prepared variant"); + } + queue.shader[0x00bu] = 2; + queue.shader[0x00cu] = 0x12345678u; + for (const auto trigger : {0u, 1u, 2u}) { + auto active = queue; + if (trigger == 0) active.context[0x8e] = active.context[0x8f] = 0xf; + if (trigger == 1) active.context[0x203] = 0x400; + if (trigger == 2) active.context[0x2dc] = 0xaa01; + DrawDecode draw{}; + draw.state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + DecodeGraphicsPrograms(draw, active, registry, false, true); + Require(draw.programs.back().snapshot == retained.snapshot && draw.programs.back().userData == std::vector{0x12345678u}, "an enabled consumer did not restore the retained pixel program"); + } +} + void NullPixelMatchesRegistration(AgcDriver::VulkanDevice& device) { using namespace AgcDriver::DriverDetail; Fixture front; @@ -330,6 +411,7 @@ int main(int argc, char** argv) { if (!device) return VulkanTestSkipped; Require(argc <= 2, "invalid test arguments"); const auto dump = argc == 2 ? std::filesystem::path(argv[1]) : std::filesystem::path{}; + RetainedPixelMatchesNullProgram(*device); NullPixelMatchesRegistration(*device); Check(*device, AgcDriver::Graphics::ShaderPath::Vertex, dump); Check(*device, AgcDriver::Graphics::ShaderPath::Geometry, dump); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 802d9df2854b..baf3ae5e90e8 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -37,6 +37,7 @@ - [Partial rect-list primitives](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): a rect-list draw whose vertex or index count is not a multiple of 3 draws its complete rectangles and leaves out the 1 or 2 vertices after them, because rect lists are drawn as Vulkan patch lists of 3 control points, which generate [⌊vertexCount/3⌋ patches](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#drawing-patch-lists). What the console draws for the incomplete rectangle is not measured; Palworld (PPSA23126) draws 4 vertices × 9 instances as a rect list when its character creation opens. - [Separate alpha blend into 8_8 SWAP_ALT targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver): the second channel of a two-channel target with `COMP_SWAP` 1 holds the alpha export, but Vulkan blends it with the color equation of the R8G8 attachment, so a draw whose alpha equation differs from its color equation is recorded twice, the first channel with the color equation and the second with the alpha factors and operation moved into the color equation. The vertex stages run twice, so a vertex shader that writes memory writes twice, and occlusion counts include both passes. Draws that write depth or stencil, pixel shaders that write memory and factors of the destination alpha throw. That the hardware blends the second channel with the alpha equation is taken from Mesa's `si_translate_colorswap` and radeonsi's use of it for luminance-alpha formats, not measured on hardware. - [Unexported pixel inputs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver): a pixel input whose `SPI_PS_INPUT_CNTL` parameter the previous stage does not export is programmed as a default-value input (`OFFSET` bit 5, `DEFAULT_VAL` 0, FP16 high half too) and reads (0, 0, 0, 0), as radeonsi's `SI_PS_INPUT_CNTL_UNUSED` does and as rect-list draws already do. The console's value for an unwritten parameter cache entry is not measured on hardware. +- [Unused pixel programs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a bound pixel program is skipped when color masks have no intersection and DB export and execution enables are clear, regardless of retained SPI_SHADER_Z_FORMAT. This follows [AMD PAL DbShaderControl](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9AbiToPipelineRegisters.h) and its [GFX10 register definitions](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h), not a hardware measurement. Enabled DB_ALPHA_TO_MASK conservatively retains the program even with ALPHA_TO_MASK_DISABLE, because DecodeState currently enables Vulkan alpha-to-coverage without applying that override. Console behavior for these combinations has not been measured. - [Covered-centroid raster state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) accepts PA_SC_AA_CONFIG.COVERED_CENTROID_IS_CENTER only when the pixel program is skipped or SPI_PS_INPUT_ENA and SPI_PS_INPUT_ADDR have no active perspective or linear centroid input. Bit 29 follows [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json); [RADV](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/radv_cmd_buffer.c) sets it on GFX10.3 and later. Active centroid interpolation and unspecified input registers still throw. Console covered-pixel centroid results were not measured. - [Compute LDS beyond the device's shared-memory limit](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. From 056d2d33b3884ccfbaee03bb51b63a7dc27eaf94 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sat, 3 Oct 2026 11:07:44 +0200 Subject: [PATCH 26/31] feat(agc): single-sample sample masks as a per-quad-pixel discard With one sample, PA_SC_AA_MASK leaves one coverage bit per pixel of the 2x2 quad, so a draw can cover only some pixels of every quad (a shipping title renders a target one quarter of the pixels at a time this way). The pixel stage info carries that 4-bit mask; a fragment shader whose mask is not full starts by killing the invocation unless its gl_FragCoord quad position is covered, and runs without early fragment tests. The mask is part of the recompile cache key and the serialized request (fork request version 26). Sample masks of multisampled draws stay refused. (cherry picked from commit 72518f64fbc3fc9a22491a4bcaeea34f5033d0f7) --- .../Graphics/src/ShaderInputState.cpp | 6 +- .../libSceAgcDriver/Graphics/src/State.cpp | 4 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 16 +++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 23 +++- .../tests/execution/QuadPixelMask.cpp | 115 ++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +- .../Optimization/ShaderStageInputInfo.hpp | 1 + .../Optimization/src/ShaderInfoCollector.cpp | 2 +- core/shader/recompiler/Recompiler.hpp | 1 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 2 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 17 +++ .../src/ShaderInputInfoBuilder.cpp | 1 + 13 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 4d2bf30bbdd6..91a994dbbb7b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -122,6 +122,9 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con // behavior for that combination is unmeasured. const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0 && !depthExportEnable; const auto zOrder = (shaderControl >> 4u) & 0x3u; + const auto aaMask = read(context, 0x30e, RegisterBank::Context); + const auto aaMaskBottom = read(context, 0x30f, RegisterBank::Context); + const auto quadPixelMask = static_cast((aaMask & 1u) | (((aaMask >> 16u) & 1u) << 1u) | ((aaMaskBottom & 1u) << 2u) | (((aaMaskBottom >> 16u) & 1u) << 3u)); const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; }; return ShaderRecompiler::ShaderPixelStageInfo{ .interpolatorCount = inputNum, @@ -148,7 +151,8 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .conservativeZExport = static_cast(conservativeZExport), .orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0, .targetOutputMode = targetOutputMode, - .targetExportMapping = nullProgram ? std::array{} : exportMappings + .targetExportMapping = nullProgram ? std::array{} : exportMappings, + .quadPixelMask = quadPixelMask }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 5d00d5e7066a..737018a675e2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -818,7 +818,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(read(cx, 0x83) == 0xffffu, "clip rectangles are unsupported"); Require((read(cx, 0x8c) & 0xfu) == 0xau, "nonstandard triangle edge rules are unsupported"); Require(read(cx, 0x2f9) == 0x2du, "nonstandard pixel center or vertex quantization is unsupported"); - Require(read(cx, 0x30e) == 0xffffffffu && read(cx, 0x30f) == 0xffffffffu, "sample masks are unsupported"); + Require((read(cx, 0x2f8) & 7u) == 0 || (read(cx, 0x30e) == 0xffffffffu && read(cx, 0x30f) == 0xffffffffu), "sample masks of multisampled draws are unsupported"); const auto viewportControl = read(cx, 0x206); if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl)); zero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); @@ -1357,7 +1357,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(cx, 0x2f9, word) && word != 0x2du) return require(false, "nonstandard pixel center or vertex quantization is unsupported"); if (value(cx, 0x313, word) && (word & ~0x001f6000u) != 0 && word != 0x6001u) return conservativeMessage(word); std::uint32_t other = 0; - if (value(cx, 0x30e, word) && value(cx, 0x30f, other) && (word != 0xffffffffu || other != 0xffffffffu)) return require(false, "sample masks are unsupported"); + if (value(cx, 0x2f8, word) && (word & 7u) != 0 && value(cx, 0x30e, word) && value(cx, 0x30f, other) && (word != 0xffffffffu || other != 0xffffffffu)) return require(false, "sample masks of multisampled draws are unsupported"); if (value(cx, 0x206, word) && word != 0x43fu) return vteMessage(word); if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason; std::uint32_t targetMask = 0, shaderMask = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 4c4f150a334c..e71734216f7c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2215,6 +2215,21 @@ void uint16ExportTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 8"); } +void quadPixelMaskTests() { + auto queue = makeState(); + queue.context[0x30e] = 0xffff0000u; + queue.context[0x30f] = 0u; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + static_cast(AgcDriver::Graphics::DecodeState(queue)); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a single-sample quad pixel mask was refused by the precheck"); + Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, {}).quadPixelMask == 0x2u, "PA_SC_AA_MASK did not decode to the covered quad pixel"); + queue = makeState(); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, {}).quadPixelMask == 0xfu, "full sample masks did not cover the whole quad"); +} + void DepthClipTests() { auto queue = makeState(); const auto direct = AgcDriver::Graphics::DecodeState(queue); @@ -5227,6 +5242,7 @@ int main(int argc, char** argv) { uint8x4TargetTests(); MsaaEnableSingleSampleTests(); UnwrittenSampleLocationTests(); + quadPixelMaskTests(); ShaderStageTests(); NggVertexWorkgroupTests(); inactiveVertexPointSizeTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 586f7f01bb35..9823ed369684 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2039,6 +2039,22 @@ void verifyMeshConfiguration() { request.graphics = GraphicsCompileContext{0u, {}, mesh, std::nullopt, {}}; const auto replay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(request)); require(replay.request.graphics.has_value() && replay.request.graphics->mesh.has_value() && replay.request.graphics->mesh->esgsItemSize == 12u && replay.request.graphics->mesh->primitivesPerGroup == 21u, "mesh configuration was lost in serialization"); + { + RecompileRequest fragment{}; + fragment.shader = {ShaderStage::Fragment, 0x10000u, code, 0, {}}; + fragment.context.waveSize = 64; + ShaderPixelStageInfo info{}; + info.quadPixelMask = 0x2u; + fragment.context.pixel = info; + const auto back = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(fragment)); + require(back.request.context.pixel.has_value() && back.request.context.pixel->quadPixelMask == 0x2u, "the quad pixel mask was lost in serialization"); + std::vector masked; + RecompileCacheKey::Build(fragment, masked); + fragment.context.pixel->quadPixelMask = 0xfu; + std::vector full; + RecompileCacheKey::Build(fragment, full); + require(masked != full, "the quad pixel mask is not part of the recompile cache key"); + } std::vector key; RecompileCacheKey::Build(request, key); const auto first = key; @@ -2185,7 +2201,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel = ShaderPixelStageInfo{}; minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRkAAAA=", "new requests did not use serialization version 25"); + require(requestPrefix(encoded, 8u) == "NVNQQRoAAAA=", "new requests did not use serialization version 26"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -2194,10 +2210,10 @@ void verifyPixelRequestSerialization() { require(serializer.Deserialize(encoded).request.context.linkedCallFallbacks.empty(), "a request without linked call fallbacks gained some"); require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "the pixel primitive class did not round-trip"); constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; - for (std::size_t bytes = 0; bytes < 20u; ++bytes) { + for (std::size_t bytes = 0; bytes < 21u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag or primitive class was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRoAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRsAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -2301,6 +2317,7 @@ void verifyLegacyPixelRequests() { require(pixel.inputAddr == 2u && pixel.hasPerspectiveCenterVgpr && pixel.targetOutputMode[0] == 9u, "legacy pixel layout was misread"); require(pixel.conservativeZExport == (version >= 7u ? ConservativeZExport::GreaterThanZ : ConservativeZExport::AnyZ), "legacy conservative Z layout was misread"); require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader"); + require(pixel.quadPixelMask == 0xfu, "a legacy request masked quad pixels"); require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned"); require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned"); require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve, "legacy request trailer was misread"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp new file mode 100644 index 000000000000..73c7e137de7f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp @@ -0,0 +1,115 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +alignas(256) std::array Pixels{}; +alignas(256) std::array, 3> Triangle{{{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}}}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void Draw(AgcDriver::VulkanDevice& device, std::uint8_t quadPixelMask) { + Pixels.fill(std::byte{0}); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {64, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + pixel.quadPixelMask = quadPixelMask; + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {64, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(std::uint8_t quadPixelMask) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const bool expected = ((quadPixelMask >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0; + const bool written = std::to_integer(Pixels[(y * Width + x) * 4u]) == 255u; + Require(written == expected, "quad pixel mask 0x" + std::to_string(quadPixelMask) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + (written ? "written" : "not written")); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const std::uint8_t mask : {std::uint8_t{0xf}, std::uint8_t{0x2}, std::uint8_t{0x9}}) { + Draw(*device, mask); + Check(mask); + } + std::puts("quad pixel mask tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index a6b3848c4d3a..92e7c6fea7de 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -186,6 +186,7 @@ class RecompileCacheKey { append(key, value.orderedPixelShader); append(key, value.alphaToCoverage); append(key, value.stencilRefExportEnable); + append(key, value.quadPixelMask); append(key, value.targetOutputMode); } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 68fafd123a6b..432aaa99ea39 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -340,6 +340,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteBool(info.layerExported); writer.WriteBool(info.frontFaceAllBits); writer.WriteU32(info.layerBase); + writer.WriteU8(info.quadPixelMask); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -393,6 +394,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { if (version >= 23u) info.layerExported = reader.ReadBool(); if (version >= 24u) info.frontFaceAllBits = reader.ReadBool(); if (version >= 24u) info.layerBase = reader.ReadU32(); + if (version >= 26u) info.quadPixelMask = reader.ReadU8(); if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -756,7 +758,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(25u); + writer.WriteU32(26u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -789,7 +791,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 25u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 26u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 44a8916ef58e..15014879222e 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -175,6 +175,7 @@ struct ShaderPixelInputInfo { ConservativeZExport psConservativeZExport = ConservativeZExport::AnyZ; bool psOrderedPixelShader = false; bool psAlphaToCoverage = false; + std::uint8_t quadPixelMask = 0xf; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 40097adec4c1..8c1eb2e44db3 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -237,7 +237,7 @@ bool IsPixelParameterFlat(const ShaderPixelInputInfo& pixel, std::uint32_t input } void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pixel, ShaderInfo& info) { - if (pixel->HasPositionInput()) { + if (pixel->HasPositionInput() || pixel->quadPixelMask != 0xf) { AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); } if (pixel->psFrontFace) { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 0197c4ffb259..596ff0d91d57 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -153,6 +153,7 @@ struct ShaderPixelStageInfo { RasterPrimitive rasterPrimitive = RasterPrimitive::Triangle; bool layerExported = false; std::uint32_t layerBase = 0; + std::uint8_t quadPixelMask = 0xf; }; struct ShaderVertexBufferResource { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 61ab8168625e..573940a02d55 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -188,7 +188,7 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitExtension("SPV_EXT_shader_stencil_export"); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); } - if (pixel.psEarlyZ && !pixel.psAlphaToCoverage) { + if (pixel.psEarlyZ && !pixel.psAlphaToCoverage && pixel.quadPixelMask == 0xf) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } if (pixel.psOrderedPixelShader) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index c44861accbe1..059f8ea34f63 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1096,6 +1096,23 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpBeginInvocationInterlockEXT); } EmitMemoryOffsets(state); + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->quadPixelMask != 0xfu) { + const auto coordinate = [&](std::uint32_t component) { + const auto value = Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component)); + return EmitBinaryU32(state, spv::OpBitwiseAnd, Unary(state, spv::OpConvertFToU, TypeU32(state), value), ConstantU32(state, 1u)); + }; + const auto quad = EmitBinaryU32(state, spv::OpBitwiseOr, coordinate(0u), EmitBinaryU32(state, spv::OpShiftLeftLogical, coordinate(1u), ConstantU32(state, 1u))); + const auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, ConstantU32(state, state.inputInfo.pixel->quadPixelMask), quad), ConstantU32(state, 1u)); + const auto uncovered = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), uncovered, bit, ConstantU32(state, 0u)); + const auto killLabel = state.module.AllocateId(); + const auto mergeLabel = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, mergeLabel, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, uncovered, killLabel, mergeLabel); + EmitLabel(state, killLabel); + state.module.AddFunction(spv::OpKill); + EmitLabel(state, mergeLabel); + } if (program.BlockOrder().empty()) { if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 1e9b8d7477ec..8ba299d48b4d 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -147,6 +147,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psConservativeZExport = pixel.conservativeZExport; pixelStorage.psOrderedPixelShader = pixel.orderedPixelShader; pixelStorage.psAlphaToCoverage = pixel.alphaToCoverage; + pixelStorage.quadPixelMask = pixel.quadPixelMask; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; From c951c96879b55f2319cf96a12cf17e9c2d0f8bac Mon Sep 17 00:00:00 2001 From: RINNECODER <93304526+RINNECODER@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:06:35 -0500 Subject: [PATCH 27/31] fix(agc): take the stages in the Metal shader preparation stub 11ef8e6c8 passes the stages a preparation scope covers to AgcDriverBeginShaderPreparation_nid_postfix, so the Vulkan driver can settle their queued preparations. The Metal build's empty stub takes the same arguments and still keeps no prepared state. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DgW4n3qYgi5jAAohEmXRQj --- .../prx/libSceAgcDriver/Graphics/Metal/MetalHostExports.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/Metal/MetalHostExports.cpp b/core/libs/prx/libSceAgcDriver/Graphics/Metal/MetalHostExports.cpp index 788125a006bc..26c19de775d1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/Metal/MetalHostExports.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/Metal/MetalHostExports.cpp @@ -9,7 +9,9 @@ // Ahead-of-time shader preparation: the Vulkan driver records prepared artifacts inside these // transactions (Driver/Shaders/ShaderRegistry.cpp). The Metal backend compiles its pipelines when // a draw or dispatch first uses them, so it keeps no prepared state and the scope is empty. -extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix() { +extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(const Shader* const* shaders, std::size_t count) { + static_cast(shaders); + static_cast(count); return nullptr; } From 3ccac5ccc96a9eba1ae216239ce3920afa0b3a67 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sun, 4 Oct 2026 10:31:29 +0200 Subject: [PATCH 28/31] fix(agc): demote quad-mask pixels to helpers instead of killing them With PA_SC_AA_MASK covering only some pixels of a quad, the uncovered pixels run on the hardware as helper lanes, so derivatives of the covered pixels stay defined. OpKill ends those invocations; OpDemoteToHelperInvocation keeps them as helpers. The device enables VK_EXT_shader_demote_to_helper_invocation when available and passes the DemoteToHelperInvocation capability and its SPIR-V extension to the recompiler target and the graphics shader validation. A demoted pixel also reads as a helper, so its initial EXEC bit is clear as on the hardware. Without demote, OpKill stays for pixel programs that read no neighbouring lanes; implicit-LOD sampling, LOD queries and cross-lane reads are refused. The quad pixel mask execution test now also samples a mipmapped texture with implicit LOD under the mask. (cherry picked from commit 90b20afac818681f93c796e80e867f1924087ad4) --- .../Execution/src/VulkanDevice.cpp | 19 ++++ .../Graphics/include/Context.hpp | 1 + .../Graphics/include/Pipeline.hpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- .../Graphics/src/ShaderValidation.cpp | 11 ++- .../tests/execution/QuadPixelMask.cpp | 98 ++++++++++++++++--- .../include/SpirvBackend/SpirvAnalysis.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvBackend/src/SpirvAnalysis.cpp | 22 +++++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 25 ++++- 10 files changed, 161 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index d4c2acb525f6..8e5ee4a3f486 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -216,6 +216,7 @@ struct VulkanDevice::State { bool shaderClock = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; + bool demoteToHelperInvocation = false; // VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; @@ -872,6 +873,14 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->narrowSubgroupClock = NarrowSubgroupClock(driver.driverID, state->properties.deviceID, state->properties.deviceName); APS5_LOG_OUT("Shader clock driver=%d narrowSubgroupClock=%d", static_cast(driver.driverID), state->narrowSubgroupClock ? 1 : 0); } + VkPhysicalDeviceShaderDemoteToHelperInvocationFeaturesEXT demoteFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DEMOTE_TO_HELPER_INVOCATION_FEATURES_EXT}; + if (hasExtension(VK_EXT_SHADER_DEMOTE_TO_HELPER_INVOCATION_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &demoteFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + state->demoteToHelperInvocation = demoteFeatures.shaderDemoteToHelperInvocation == VK_TRUE; + } + demoteFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DEMOTE_TO_HELPER_INVOCATION_FEATURES_EXT}; + demoteFeatures.shaderDemoteToHelperInvocation = VK_TRUE; std::vector deviceExtensions; if (window != nullptr) deviceExtensions.assign(presentationExtensions.begin(), presentationExtensions.end()); if (state->fragmentShaderBarycentric) { @@ -914,6 +923,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->imageInt64Atomics = true; state->spirvExtensions.push_back("SPV_EXT_shader_image_int64"); } + if (state->demoteToHelperInvocation) { + deviceExtensions.push_back(VK_EXT_SHADER_DEMOTE_TO_HELPER_INVOCATION_EXTENSION_NAME); + state->capabilities.push_back(spv::CapabilityDemoteToHelperInvocation); + state->spirvExtensions.push_back("SPV_EXT_demote_to_helper_invocation"); + } deviceExtensions.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilitySignedZeroInfNanPreserve); state->spirvExtensions.push_back("SPV_KHR_float_controls"); @@ -1231,6 +1245,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u imageAtomicInt64Features.pNext = byteFeatures.pNext; byteFeatures.pNext = &imageAtomicInt64Features; } + if (state->demoteToHelperInvocation) { + demoteFeatures.pNext = byteFeatures.pNext; + byteFeatures.pNext = &demoteFeatures; + } VkPhysicalDeviceImageRobustnessFeaturesEXT imageRobustnessFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_ROBUSTNESS_FEATURES_EXT, nullptr, VK_TRUE}; if (imageRobustness) { imageRobustnessFeatures.pNext = byteFeatures.pNext; @@ -2793,6 +2811,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors(); context.bufferInt64Atomics = state->bufferInt64Atomics; context.placeholderImages = state->placeholderImages.get(); + context.demoteToHelperInvocation = state->demoteToHelperInvocation; context.primitiveListRestart = state->primitiveListRestart; context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index d0aea04e82a2..ac2b4bbfb32d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -163,6 +163,7 @@ struct Context { bool nullDescriptors = false; bool bufferInt64Atomics = false; const PlaceholderImages* placeholderImages = nullptr; + bool demoteToHelperInvocation = false; bool primitiveListRestart = false; bool imageViewMinLod = false; bool pipelineExecutableInfo = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 5258bc026b65..e5ba9770ef6b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -109,7 +109,7 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); bool ShadersWriteMemory(std::span shaders); // Returns the color attachment locations the pixel shader writes. -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false); +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool demoteToHelperInvocation = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index c0d66da5906b..13daf9829f4e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -731,7 +731,7 @@ std::set CachedFragmentOutputs(const Context& context, std::span< } std::set outputs; try { - outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics); + outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.demoteToHelperInvocation); } catch (const std::exception& error) { if (keyed) { std::lock_guard lock(validationMutex()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 2a05a98e6f39..1fe7067a7fbb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -136,7 +136,7 @@ struct Module { } }; -Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics) { +Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool demoteToHelperInvocation) { using Stage = ShaderRecompiler::ShaderStage; Require(compiled.program != nullptr, "missing compiled shader"); const auto& shader = *compiled.program; @@ -247,7 +247,8 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityStorageBuffer8BitAccess || capability == spv::CapabilityPhysicalStorageBufferAddresses || capability == spv::CapabilitySampledImageArrayDynamicIndexing || - capability == spv::CapabilityStorageImageArrayDynamicIndexing; + capability == spv::CapabilityStorageImageArrayDynamicIndexing || + (fragment && demoteToHelperInvocation && capability == spv::CapabilityDemoteToHelperInvocation); // Bindless image tables index their slots non-uniformly in graphics stages // (VK_EXT_descriptor_indexing, enabled by the device setup when available). @@ -290,7 +291,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader"); break; } - Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); + Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (fragment && demoteToHelperInvocation && extension == "SPV_EXT_demote_to_helper_invocation") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); break; } case spv::OpDecorateId: @@ -547,7 +548,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics) { +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool demoteToHelperInvocation) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; const bool mesh = state.stages.path == ShaderPath::Geometry; @@ -568,7 +569,7 @@ std::set ValidateShaders(std::span shaders, Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == 0, "graphics resource uses a descriptor set other than zero"); - const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, demoteToHelperInvocation); if (i != 0) { for (const auto& [location, signature] : current.inputs) { const auto output = previous.outputs.find(location); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp index 73c7e137de7f..613daa2001ba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp @@ -1,10 +1,14 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" +#include #include #include +#include #include +#include #include #include @@ -15,23 +19,66 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Width = 64; constexpr std::uint32_t Height = 32; +constexpr std::uint32_t TextureWidth = Width * 4u; +constexpr std::uint32_t TextureHeight = Height * 4u; +constexpr std::uint32_t TextureLevels = 9; +constexpr std::uint32_t SampledLevel = 2; +constexpr std::uint32_t Format8888Unorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t DemoteToHelperInvocation = 5379; alignas(256) std::array Pixels{}; +alignas(4096) std::array Texels{}; alignas(256) std::array, 3> Triangle{{{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}}}; alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, }; -alignas(256) constexpr std::array PixelCode{ +alignas(256) constexpr std::array ConstantPixelCode{ 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, }; +alignas(256) constexpr std::array SampledPixelCode{ + 0xbe8c047e, 0xbefe0a7e, 0x100000ff, 0x3c800000, 0x100202ff, 0x3d000000, 0xf0800f08, 0x00400200, + 0xbf8c3f70, 0x87fe0c7e, 0xf800180f, 0x05040302, 0xbf810000, +}; + +std::array LevelColor(std::uint32_t level) { + return {static_cast(32u * (level + 1u)), static_cast(255u - 16u * level), static_cast(0x40u + level), 255u}; +} + std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { const auto address = reinterpret_cast(data); return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; } -void Draw(AgcDriver::VulkanDevice& device, std::uint8_t quadPixelMask) { +std::array TextureDescriptor() { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888Unorm << 20u) | (((TextureWidth - 1u) & 3u) << 30u), + ((TextureWidth - 1u) >> 2u) | ((TextureHeight - 1u) << 14u), + 0xfacu | ((TextureLevels - 1u) << 16u) | (Type2D << 28u), + 0u, (TextureLevels - 1u) << 4u, 0u, 0u, + }; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void FillTexture() { + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format8888Unorm, TextureWidth, TextureHeight, TextureLevels); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "the mipmapped test texture does not fit its storage"); + for (std::uint32_t level = 0; level < TextureLevels; ++level) { + const auto color = LevelColor(level); + for (std::uint64_t offset = mips[level].tiledOffset; offset < mips[level].tiledOffset + mips[level].tiledSize; offset += 4u) { + std::memcpy(Texels.data() + offset, color.data(), color.size()); + } + } +} + +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint8_t quadPixelMask) { Pixels.fill(std::byte{0}); const auto target = device.Target(); std::vector vertexUserData(4, 0u); @@ -49,13 +96,21 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint8_t quadPixelMask) { const auto vertexPush = static_cast(vertexResult.pushConstants.size()); ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; pixel.targetOutputMode[0] = 9; pixel.targetExportMapping.fill(0xe4u); pixel.quadPixelMask = quadPixelMask; - const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + std::vector pixelUserData(12, 0u); + const auto texture = TextureDescriptor(); + const auto sampler = SamplerDescriptor(); + std::copy(texture.begin(), texture.end(), pixelUserData.begin()); + std::copy(sampler.begin(), sampler.end(), pixelUserData.begin() + 8); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; ShaderRecompiler::RecompileRequest fragment{ - {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, - {64, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory}, + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, PixelPushLayout(vertexPush, target) }; @@ -86,12 +141,16 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint8_t quadPixelMask) { device.WaitIdle(); } -void Check(std::uint8_t quadPixelMask) { +void Check(const std::string& what, std::uint8_t quadPixelMask, std::array color) { for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < Width; ++x) { - const bool expected = ((quadPixelMask >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0; - const bool written = std::to_integer(Pixels[(y * Width + x) * 4u]) == 255u; - Require(written == expected, "quad pixel mask 0x" + std::to_string(quadPixelMask) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + (written ? "written" : "not written")); + const bool covered = ((quadPixelMask >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0; + const auto* written = &Pixels[(y * Width + x) * 4u]; + for (std::uint32_t channel = 0; channel < 4u; ++channel) { + const auto expected = covered ? color[channel] : 0u; + const auto value = std::to_integer(written[channel]); + Require(value == expected, what + " with quad pixel mask " + std::to_string(quadPixelMask) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(value) + ", expected " + std::to_string(expected)); + } } } } @@ -102,11 +161,26 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + const auto capabilities = device->Target().supportedCapabilities; + const bool demote = std::find(capabilities.begin(), capabilities.end(), DemoteToHelperInvocation) != capabilities.end(); + FillTexture(); for (const std::uint8_t mask : {std::uint8_t{0xf}, std::uint8_t{0x2}, std::uint8_t{0x9}}) { - Draw(*device, mask); - Check(mask); + Draw(*device, ConstantPixelCode, mask); + Check("constant color", mask, {255u, 255u, 255u, 255u}); + if (mask == 0xf || demote) { + Draw(*device, SampledPixelCode, mask); + Check("implicit-LOD sample", mask, LevelColor(SampledLevel)); + continue; + } + try { + Draw(*device, SampledPixelCode, mask); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("shaderDemoteToHelperInvocation") != std::string::npos, std::string("an implicit-LOD sample under a quad pixel mask failed for another reason: ") + error.what()); + continue; + } + Require(false, "an implicit-LOD sample under quad pixel mask " + std::to_string(mask) + " was drawn without demote to helper invocation"); } - std::puts("quad pixel mask tests passed"); + std::printf("quad pixel mask tests passed (%s)\n", demote ? "demote to helper invocation" : "kill, derivatives refused"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index 5c63224c6397..1b1c53242baf 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -15,6 +15,7 @@ struct SpirvRequirements { bool subgroupShuffle = false; bool subgroupLocalInvocationId = false; bool computeDerivatives = false; + bool neighbourLaneReads = false; bool imageGatherExtended = false; bool functionLds = false; bool ldsLock = false; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 1530c932cd90..7fda7b64daa9 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -173,6 +173,7 @@ struct SpirvEmitterState { std::uint32_t currentLabel = 0; const IrBlock* currentBlock = nullptr; std::uint32_t pixelValidMaskVariable = 0; + std::uint32_t quadPixelUncovered = 0; std::uint32_t subgroupLocalInvocationIdVariable = 0; std::uint32_t perVertexVariable = 0; std::uint32_t pointSizeVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 2c3e675b6ceb..8d1bebbf22a3 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvAnalysis.hpp" +#include "RdnaDecoder/RdnaInstruction.hpp" #include #include #include @@ -9,6 +10,26 @@ namespace ShaderRecompiler { namespace { +bool ReadsNeighbourLanes(const IrProgram& program, const IrValue& inst) { + switch (inst.Opcode()) { + case IrOpcode::ImageQueryLod: + case IrOpcode::DppMoveU32: + case IrOpcode::DppUpdateU32: + case IrOpcode::SwizzleU32: + case IrOpcode::BpermuteU32: + case IrOpcode::Permlane16U32: + case IrOpcode::ReadLane: + return true; + case IrOpcode::ImageSampleImplicitLod: + case IrOpcode::ImageSampleRaw: { + const auto flags = program.Resources().memoryInfo.at(inst.Flags().index).imageSampleFlags; + return (flags & (RdnaImageSampleFlagDerivative | RdnaImageSampleFlagLod | RdnaImageSampleFlagLevelZero)) == 0u; + } + default: + return false; + } +} + bool UniformSource(const IrValue& value) { switch (value.Opcode()) { case IrOpcode::Ballot: @@ -377,6 +398,7 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { requirements.subgroupLocalInvocationId = true; } } + requirements.neighbourLaneReads = requirements.neighbourLaneReads || ReadsNeighbourLanes(program, *inst); switch (inst->Opcode()) { case IrOpcode::Ballot: requirements.subgroupBallot = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 059f8ea34f63..f83527b7fa29 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -84,9 +84,14 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: return bits; } if (kind == StageInputKind::HelperInvocation) { - const auto value = state.module.AllocateId(); + auto value = state.module.AllocateId(); const auto bits = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeBool(state), value, variable); + if (state.quadPixelUncovered != 0u) { + const auto helper = state.module.AllocateId(); + state.module.AddFunction(spv::OpLogicalOr, TypeBool(state), helper, value, state.quadPixelUncovered); + value = helper; + } state.module.AddFunction(spv::OpSelect, TypeU32(state), bits, value, ConstantU32(state, 1u), ConstantU32(state, 0u)); return bits; } @@ -1103,14 +1108,30 @@ void EmitProgram(SpirvEmitterState& state) { }; const auto quad = EmitBinaryU32(state, spv::OpBitwiseOr, coordinate(0u), EmitBinaryU32(state, spv::OpShiftLeftLogical, coordinate(1u), ConstantU32(state, 1u))); const auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, ConstantU32(state, state.inputInfo.pixel->quadPixelMask), quad), ConstantU32(state, 1u)); + const auto mask = static_cast(state.inputInfo.pixel->quadPixelMask); + const bool demoteCapability = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityDemoteToHelperInvocation)) != state.supportedCapabilities.end(); + const bool demoteCore = state.spirvVersion >= 0x00010600u; + const bool demoteExtension = std::find(state.supportedExtensions.begin(), state.supportedExtensions.end(), "SPV_EXT_demote_to_helper_invocation") != state.supportedExtensions.end(); + const bool demote = demoteCapability && (demoteCore || demoteExtension); + if (!demote && state.requirements.neighbourLaneReads) { + throw std::runtime_error("the PA_SC_AA_MASK quad pixel mask " + std::to_string(mask) + " leaves helper pixels whose lanes the pixel program reads (implicit-LOD sampling or cross-lane reads), which needs the device's shaderDemoteToHelperInvocation"); + } const auto uncovered = state.module.AllocateId(); state.module.AddFunction(spv::OpIEqual, TypeBool(state), uncovered, bit, ConstantU32(state, 0u)); + state.quadPixelUncovered = uncovered; const auto killLabel = state.module.AllocateId(); const auto mergeLabel = state.module.AllocateId(); state.module.AddFunction(spv::OpSelectionMerge, mergeLabel, spv::SelectionControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, uncovered, killLabel, mergeLabel); EmitLabel(state, killLabel); - state.module.AddFunction(spv::OpKill); + if (demote) { + state.module.EmitCapability(spv::CapabilityDemoteToHelperInvocation); + if (!demoteCore) state.module.EmitExtension("SPV_EXT_demote_to_helper_invocation"); + state.module.AddFunction(spv::OpDemoteToHelperInvocation); + state.module.AddFunction(spv::OpBranch, mergeLabel); + } else { + state.module.AddFunction(spv::OpKill); + } EmitLabel(state, mergeLabel); } if (program.BlockOrder().empty()) { From 28a19810eaf89ca96ccee018e466129d958e773c Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Mon, 5 Oct 2026 17:19:04 +0200 Subject: [PATCH 29/31] fix(agc): count whole-quad mode as a neighbour-lane read for a quad mask without demote A program that enables whole-quad mode (s_wqm_b64) and then reads a ballot or readfirstlane sees the helper lanes, which OpKill removes. (cherry picked from commit 90783f2748470a9de8191178c0602765e87ef5ec) --- core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp | 1 + core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 8d1bebbf22a3..212841591e53 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -19,6 +19,7 @@ bool ReadsNeighbourLanes(const IrProgram& program, const IrValue& inst) { case IrOpcode::BpermuteU32: case IrOpcode::Permlane16U32: case IrOpcode::ReadLane: + case IrOpcode::WqmU64: return true; case IrOpcode::ImageSampleImplicitLod: case IrOpcode::ImageSampleRaw: { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index f83527b7fa29..25fa81687a33 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1114,7 +1114,7 @@ void EmitProgram(SpirvEmitterState& state) { const bool demoteExtension = std::find(state.supportedExtensions.begin(), state.supportedExtensions.end(), "SPV_EXT_demote_to_helper_invocation") != state.supportedExtensions.end(); const bool demote = demoteCapability && (demoteCore || demoteExtension); if (!demote && state.requirements.neighbourLaneReads) { - throw std::runtime_error("the PA_SC_AA_MASK quad pixel mask " + std::to_string(mask) + " leaves helper pixels whose lanes the pixel program reads (implicit-LOD sampling or cross-lane reads), which needs the device's shaderDemoteToHelperInvocation"); + throw std::runtime_error("the PA_SC_AA_MASK quad pixel mask " + std::to_string(mask) + " leaves helper pixels whose lanes the pixel program reads (implicit-LOD sampling, whole-quad mode or cross-lane reads), which needs the device's shaderDemoteToHelperInvocation"); } const auto uncovered = state.module.AllocateId(); state.module.AddFunction(spv::OpIEqual, TypeBool(state), uncovered, bit, ConstantU32(state, 0u)); From b9dbbbaed43273a9fbd6dc2beb63159e297958f5 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:57:28 -0400 Subject: [PATCH 30/31] fix(agc): require demote for masked subgroup reads ReadFirstLane, Ballot and PermuteU32 can consume uncovered helper lanes after a fragment explicitly enables EXEC. Classify those operations as neighbour reads so a partial quad mask refuses their kill fallback when demote is unavailable. ReadFirstLane also stays protected when a constant whole-quad instruction folds away. The CPU regression covers explicit EXEC, constant WQM, a comparison ballot and ds_permute. Unmasked programs still compile, supported masked programs retain demote and their subgroup operation, and constant colour keeps the kill fallback. The recovered source fails the readfirstlane and ballot checks; the corrected source passes both the focused check and shader-memory suite. Co-Authored-By: Codex (cherry picked from commit dbbe282c14ba2ddec9fa422afd7dbe461c7d05a2) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 59 +++++++++++++++++++ .../SpirvBackend/src/SpirvAnalysis.cpp | 3 + 3 files changed, 67 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ef1470a35dce..c781768d2e07 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1161,6 +1161,11 @@ if(BUILD_TESTING) add_test(NAME agc_shader_type_zero_image_materialization COMMAND agc_shader_memory_tests --type-zero-image-materialization) set_tests_properties(agc_shader_type_zero_image_materialization PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_frontend_pair COMMAND agc_shader_memory_tests --frontend-pair) + add_test(NAME agc_shader_quad_mask_no_demote COMMAND agc_shader_memory_tests --quad-mask-no-demote) + set_tests_properties(agc_shader_quad_mask_no_demote PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_shader_quad_mask_no_demote PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_shader_frontend_pair_off COMMAND agc_shader_memory_tests --frontend-pair) set_tests_properties(agc_shader_frontend_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_FRONTEND_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 9823ed369684..ff6f15ee51d4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3981,6 +3981,59 @@ void verifyGuardedNullPointers() { #endif } +void verifyQuadMaskNeighbourReads() { + using namespace ShaderRecompiler; + constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + constexpr std::array demoteCapabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityDemoteToHelperInvocation}; + constexpr std::array demoteExtensions{"SPV_EXT_demote_to_helper_invocation"}; + constexpr std::array constant{0x7e0202f2u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array explicitExec{0xbefe04c1u, 0x7e000500u, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array constantWqm{0xbefe0ac1u, 0x7e000500u, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array permute{0xbefe04c1u, 0x7e040284u, 0xdac80000u, 0x01000002u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array comparisonBallot{0xbe8003ffu, 0x3f000000u, 0xbefe04c1u, 0x7d840000u, 0xbe80046au, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + const auto compile = [&](std::span code, bool masked, bool demote) { + RecompileRequest request{}; + request.shader = {ShaderStage::Fragment, 0x58000u, code, 0, {}}; + request.context.waveSize = 32u; + request.context.pixel = ShaderPixelStageInfo{}; + request.context.pixel->inputAddr = PixelInputBit(PixelInput::PositionX); + request.context.pixel->posX = true; + request.context.pixel->quadPixelMask = masked ? 0x2u : 0xfu; + request.context.pixel->targetOutputMode[0] = 9u; + request.context.pixel->targetExportMapping.fill(0xe4u); + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32u; + request.target.supportedCapabilities = demote ? std::span(demoteCapabilities) : std::span(capabilities); + if (demote) request.target.supportedExtensions = demoteExtensions; + request.layout.pushConstantSizeBytes = 128u; + request.useCache = false; + return Recompile(request); + }; + const auto declares = [](const RecompileResult& result, spv::Op opcode) { + const auto& words = result.spirv.Words(); + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + require(count != 0 && count <= words.size() - cursor, "quad mask: truncated SPIR-V instruction"); + if ((words[cursor] & 0xffffu) == static_cast(opcode)) return true; + cursor += count; + } + return false; + }; + require(declares(compile(constant, true, false), spv::OpKill), "a quad mask without neighbour reads did not retain its kill fallback"); + for (const auto code : {std::span(explicitExec), std::span(constantWqm), std::span(permute)}) { + const auto unmasked = compile(code, false, false); + require(declares(unmasked, spv::OpGroupNonUniformShuffle), "the neighbour-read test did not retain its cross-lane shuffle"); + expectFailure([&] { static_cast(compile(code, true, false)); }, "shaderDemoteToHelperInvocation", "a quad mask killed helper lanes before a cross-lane read with EXEC enabled for those lanes"); + const auto masked = compile(code, true, true); + require(declares(masked, spv::OpDemoteToHelperInvocation) && declares(masked, spv::OpGroupNonUniformShuffle), "a quad mask with demote did not preserve its cross-lane shuffle path"); + } + require(declares(compile(comparisonBallot, false, false), spv::OpGroupNonUniformBallot), "the comparison test did not retain its subgroup ballot"); + expectFailure([&] { static_cast(compile(comparisonBallot, true, false)); }, "shaderDemoteToHelperInvocation", "a quad mask killed helper lanes before a ballot with EXEC enabled for those lanes"); + const auto maskedBallot = compile(comparisonBallot, true, true); + require(declares(maskedBallot, spv::OpDemoteToHelperInvocation) && declares(maskedBallot, spv::OpGroupNonUniformBallot), "a quad mask with demote did not preserve its ballot path"); +} + void verifyCubeDepthCompareMode() { using namespace ShaderRecompiler; ImageResource image{}; @@ -4018,6 +4071,11 @@ void verifyVolumeArrayLoadMode() { int main(int argc, char** argv) { try { using namespace ShaderRecompiler; + if (argc == 2 && std::string_view(argv[1]) == "--quad-mask-no-demote") { + verifyQuadMaskNeighbourReads(); + std::cout << "Quad masks preserve neighbour-lane helpers or refuse without demote\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--frontend-pair") { verifyFrontendPair(); std::cout << "Prepared frontend reuse preserves full and partial-thread programs\n"; @@ -4080,6 +4138,7 @@ int main(int argc, char** argv) { verifyGuardedNullPointers(); verifyCubeDepthCompareMode(); verifyVolumeArrayLoadMode(); + verifyQuadMaskNeighbourReads(); #if ANYPS5_ENABLE_SPIRV_TOOLS const std::vector minimalSpirv{ 0x07230203u, 0x00010000u, 0u, 5u, 0u, diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 212841591e53..84a3248633ed 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -17,8 +17,11 @@ bool ReadsNeighbourLanes(const IrProgram& program, const IrValue& inst) { case IrOpcode::DppUpdateU32: case IrOpcode::SwizzleU32: case IrOpcode::BpermuteU32: + case IrOpcode::PermuteU32: case IrOpcode::Permlane16U32: case IrOpcode::ReadLane: + case IrOpcode::ReadFirstLane: + case IrOpcode::Ballot: case IrOpcode::WqmU64: return true; case IrOpcode::ImageSampleImplicitLod: From 1ca8a7932f9b6d204a54e47dd6ad6c3c99fb4967 Mon Sep 17 00:00:00 2001 From: RINNECODER <93304526+RINNECODER@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:13:20 -0500 Subject: [PATCH 31/31] test(agc): give the Metal early-depth decode fixture full sample masks 72518f64f reads PA_SC_AA_MASK (0x30e, 0x30f) when it decodes the pixel stage, for the quad pixel mask. The Metal render-state test built its registers without them, so the decode threw. The fixture now sets both masks to cover every sample, as CLEAR_STATE does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DgW4n3qYgi5jAAohEmXRQj --- core/libs/prx/libSceAgcDriver/tests/metal/RenderState.mm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/metal/RenderState.mm b/core/libs/prx/libSceAgcDriver/tests/metal/RenderState.mm index 515ec49accc2..6078d4e01aa5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/metal/RenderState.mm +++ b/core/libs/prx/libSceAgcDriver/tests/metal/RenderState.mm @@ -665,6 +665,9 @@ void CoveredCentroid() { // [[early_fragment_tests]]). void EarlyDepthDecode() { Registers cx; + // PA_SC_AA_MASK_X0Y0_X1Y0 / X0Y1_X1Y1: every sample of the quad covered (the pixel stage reads the quad mask). + cx[0x30e] = 0xffffffffu; + cx[0x30f] = 0xffffffffu; cx[0x1b3] = 2u; cx[0x1b4] = 2u; cx[0x1b6] = 0u;