From 88f56b480f23f50e7310dbc0a55cd44eccc828b4 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:10:20 +0100 Subject: [PATCH 1/7] fix(libSceAgcDriver): store image results to guest memory at draw time markLayersPending deferred its write-back, so a sampled-lookup of a surface read through a resident view fed the freshest GPU content back only at flip time. The menu composite loops its own trio as feedback each present, so the deferred view accumulated a pale frame until flip and whitened Bendy's opening menu against the (120, 87, 17) reference. Store a written image's results to guest memory at once, in recording order behind the draw that produced them, and keep the batched store behind APS5_DEFERRED_WRITEBACK=1 for comparison. --- core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp | 3 --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 0524592a6c5..06148417e80 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -180,7 +180,6 @@ class StorageTexture : public std::enable_shared_from_this { void RecordAttachmentProxyLoad(VkCommandBuffer commands, VkImageLayout attachmentLayout) const; void RecordAttachmentProxyStore(VkCommandBuffer commands, VkImageLayout attachmentLayout) const; void WriteBack(); - // Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead). void MarkDirty(); void MarkSliceDirty(std::uint32_t slice); void Flush(); @@ -435,8 +434,6 @@ class StorageTexture : public std::enable_shared_from_this { bool refreshProved(); void takeRefreshProof(bool aliased); bool otherPendingOverlaps() const; - // Marks `count` tracked layers from `first` pending and registers the image (MarkDirty's - // registration; APS5_EAGER_WRITEBACK=1 stores at once instead). void markLayersPending(std::uint32_t first, std::uint32_t count); // Registers or unregisters the image as its pending layers say (after a write-back). void reconcilePending(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 0ea0b616198..1cf72cf7cc2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3030,7 +3030,7 @@ void StorageTexture::refreshGeneration() { void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) { CaptureTrace::Log("image-write image=%llx first=%u count=%u generation=%llu", static_cast(descriptor.baseAddress), first, count, static_cast(generation)); - static const bool eager = std::getenv("APS5_EAGER_WRITEBACK") != nullptr || std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; + static const bool eager = std::getenv("APS5_DEFERRED_WRITEBACK") == nullptr; std::vector unsaved(trackedLayers, false); bool anyUnsaved = false; for (std::uint32_t layer = first; layer < first + count; ++layer) { From fc6d45768d86ff0d15a4288755806cbba56ed28c Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:17:11 +0100 Subject: [PATCH 2/7] test(libSceAgcDriver): run DCC pending test in deferred mode --- .../libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index e9668e78dc1..29175dd875f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -783,6 +783,11 @@ void RunSharedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { int main() { try { +#ifdef _WIN32 + Require(_putenv_s("APS5_DEFERRED_WRITEBACK", "1") == 0, "cannot enable deferred write-back for the pending-image test"); +#else + Require(setenv("APS5_DEFERRED_WRITEBACK", "1", 1) == 0, "cannot enable deferred write-back for the pending-image test"); +#endif std::optional block; std::optional watched; std::optional untracked; From cf43ef669112781e12f457c4279a2d4d2b93c9e1 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:29:55 +0100 Subject: [PATCH 3/7] fix(libSceAgcDriver): validate eager storage writeback behavior --- .../prx/libSceAgcDriver/Graphics/src/Texture.cpp | 1 + .../tests/execution/ArrayLayerTarget.cpp | 15 ++++++++++++--- .../tests/execution/DepthCompareCopy.cpp | 7 ++++++- .../tests/execution/ImageSrgbLoad.cpp | 4 +++- .../tests/execution/PendingExactSurface.cpp | 6 ++++++ .../tests/execution/StorageOneSliceAlias.cpp | 14 ++++++++++---- 6 files changed, 38 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 1cf72cf7cc2..5c366885052 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3055,6 +3055,7 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) if (superseded) source->reconcilePending(); } if (eager) { + ++version; WriteBack(); return; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index 18dcef360cc..4c452daf24d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -271,9 +272,17 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t sw std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); const auto drawn = std::bit_cast(Drawn); DrawSlice(device, surface, slice, swizzleMode, collapsed); - const auto pending = StorageTexture::FindPending(collapsed ? surface + slice * LayerBytes : surface, collapsed ? LayerBytes : SurfaceBytes); - if (collapsed) Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2D && pending->Descriptor().baseAddress == surface + slice * LayerBytes, "a collapsed slice did not render into its shifted surface"); - else Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array"); + const auto imageAddress = collapsed ? surface + slice * LayerBytes : surface; + const auto imageBytes = collapsed ? LayerBytes : SurfaceBytes; + const auto pending = StorageTexture::FindPending(imageAddress, imageBytes); + const bool deferred = std::getenv("APS5_DEFERRED_WRITEBACK") != nullptr; + if (!deferred) { + Require(pending == nullptr, "the eager draw left a pending image"); + } else if (collapsed) { + Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2D && pending->Descriptor().baseAddress == imageAddress, "a collapsed slice did not render into its shifted surface"); + } else { + Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice " + std::to_string(slice) + " did not render into the image of its array"); + } LoadLayer(device, surface, slice, swizzleMode); RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " read through the array after the draw"); for (std::uint32_t other = 0; other < Layers; ++other) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthCompareCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthCompareCopy.cpp index 1db56eaa873..248927baa09 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthCompareCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthCompareCopy.cpp @@ -214,7 +214,7 @@ std::uint64_t SurfaceBytes(std::uint8_t* texels, const Shape& shape) { std::uint64_t PendingVersion(std::uint8_t* texels, std::uint64_t bytes, const std::string& what) { const auto source = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), bytes); - Require(source != nullptr, what + ": no storage image holds the surface's results"); + Require(source != nullptr, what + ": no pending storage image holds the surface"); return source->Version(); } @@ -298,6 +298,11 @@ void LayersAndMipsCase(AgcDriver::VulkanDevice& device) { int main() { try { +#ifdef _WIN32 + Require(_putenv_s("APS5_DEFERRED_WRITEBACK", "1") == 0, "cannot enable deferred write-back for the pending depth-copy test"); +#else + Require(setenv("APS5_DEFERRED_WRITEBACK", "1", 1) == 0, "cannot enable deferred write-back for the pending depth-copy test"); +#endif const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; GuestBlock buffers; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp index ec759a3f7c6..e1e4cb80d38 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp @@ -203,7 +203,9 @@ void CheckStoredLoad(AgcDriver::VulkanDevice& device, GuestBlock& block, std::sp const auto bytes = Width * (format == Srgb8 ? 1u : 2u); std::fill(block.Data(), block.Data() + BlockBytes, std::uint8_t{0x5au}); Run(device, format == Srgb8 ? std::span(StoreX) : std::span(StoreXy), block.Data(), format == Srgb8 ? Unorm8 : Unorm8_8, SwizzleXY01, Groups); - Require(AgcDriver::Graphics::StorageTexture::FindPending(address, bytes) != nullptr, what + ": the image_store results are not pending in the storage image"); + const auto pending = AgcDriver::Graphics::StorageTexture::FindPending(address, bytes); + const bool deferred = std::getenv("APS5_DEFERRED_WRITEBACK") != nullptr; + Require(deferred ? pending != nullptr : pending == nullptr, what + (deferred ? ": the image_store results are not pending in the storage image" : ": eager write-back left a pending image")); Run(device, code, block.Data(), format, swizzle, Groups); Verify(dmask, format, swizzle, what); AgcDriver::Graphics::StorageTexture::FlushPending(address, BlockBytes, nullptr, "test"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PendingExactSurface.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PendingExactSurface.cpp index 9386584c9fe..b32b3469404 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PendingExactSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PendingExactSurface.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -87,6 +88,11 @@ std::string Describe(const std::shared_ptr& image) { int main() { try { +#ifdef _WIN32 + Require(_putenv_s("APS5_DEFERRED_WRITEBACK", "1") == 0, "cannot enable deferred write-back for the pending-surface test"); +#else + Require(setenv("APS5_DEFERRED_WRITEBACK", "1", 1) == 0, "cannot enable deferred write-back for the pending-surface test"); +#endif const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; std::vector storage((BlockBytes + 0x10000u) / 4u); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp index 78437ac99d4..95cc0fd270e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -169,17 +170,22 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, bool watched) { AgcDriver::GuestMemory::BumpCollectEpoch(); std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); Store(device, texels, 0x11111111u); - Require(StorageTexture::FindPending(surface, SurfaceBytes) != nullptr, "the 2D store left no pending image"); + const bool deferred = std::getenv("APS5_DEFERRED_WRITEBACK") != nullptr; + Require((StorageTexture::FindPending(surface, SurfaceBytes) != nullptr) == deferred, deferred ? "the 2D store left no pending image" : "the eager 2D store left a pending image"); + const auto live = StorageTexture::FindLive(surface, SurfaceBytes); + Require(live != nullptr, "the 2D storage image is not cached after its first store"); + const auto previousVersion = live->Version(); LoadArray(device, texels); RequireOutput(0x11111111u, "the first load through the one-slice array"); Store(device, texels, 0x22222222u); const auto written = StorageTexture::FindPending(surface, SurfaceBytes); - Require(written != nullptr && written->Descriptor().dimension == AgcDriver::Graphics::TextureDimension::k2D, "the second 2D store left no pending 2D image"); + Require(live->Version() > previousVersion, "the second 2D store did not advance the cached image version"); + Require(deferred ? written != nullptr && written->Descriptor().dimension == AgcDriver::Graphics::TextureDimension::k2D : written == nullptr, deferred ? "the second 2D store left no pending 2D image" : "the eager second 2D store left a pending image"); LoadArray(device, texels); RequireOutput(0x22222222u, "the second load through the one-slice array"); - if (watched && AgcDriver::GuestMemory::Watched(surface, SurfaceBytes)) { + if (deferred && watched && AgcDriver::GuestMemory::Watched(surface, SurfaceBytes)) { Require(StorageTexture::FindPending(surface, SurfaceBytes) == written, "the array image's refresh wrote the 2D image's results back instead of taking them on the device"); - } else { + } else if (deferred) { std::puts("the imported block is compared, not watched: taking the results on the device is not checked"); } StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); From 60a11b135da9c0e4d26218cd62b19879dcf58494 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:57:58 +0100 Subject: [PATCH 4/7] revert(libSceAgcDriver): restore deferred writeback and main execution tests The eager writeback default is reverted: Texture.hpp/Texture.cpp return to upstream/main, keeping deferred writeback the default. The execution tests (ArrayLayerTarget, DccKeysFirstWrite, DepthCompareCopy, ImageSrgbLoad, PendingExactSurface, StorageOneSliceAlias) return to their upstream/main state without deferred-mode branching. --- .../libSceAgcDriver/Graphics/include/Texture.hpp | 4 ++++ .../prx/libSceAgcDriver/Graphics/src/Texture.cpp | 9 +++++++-- .../tests/execution/ArrayLayerTarget.cpp | 15 +++------------ .../tests/execution/DccKeysFirstWrite.cpp | 5 ----- .../tests/execution/DepthCompareCopy.cpp | 7 +------ .../tests/execution/ImageSrgbLoad.cpp | 4 +--- .../tests/execution/PendingExactSurface.cpp | 6 ------ .../tests/execution/StorageOneSliceAlias.cpp | 14 ++++---------- 8 files changed, 20 insertions(+), 44 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 06148417e80..f9a0bdd6b69 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -180,6 +180,7 @@ class StorageTexture : public std::enable_shared_from_this { void RecordAttachmentProxyLoad(VkCommandBuffer commands, VkImageLayout attachmentLayout) const; void RecordAttachmentProxyStore(VkCommandBuffer commands, VkImageLayout attachmentLayout) const; void WriteBack(); + // Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead). void MarkDirty(); void MarkSliceDirty(std::uint32_t slice); void Flush(); @@ -196,6 +197,7 @@ class StorageTexture : public std::enable_shared_from_this { static bool AnyPending(); static void FlushDumpSelected(const char* reason); 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 // retile) moves it too. static void BumpPendingSerial(); @@ -434,6 +436,8 @@ class StorageTexture : public std::enable_shared_from_this { bool refreshProved(); void takeRefreshProof(bool aliased); bool otherPendingOverlaps() const; + // Marks `count` tracked layers from `first` pending and registers the image (MarkDirty's + // registration; APS5_EAGER_WRITEBACK=1 stores at once instead). void markLayersPending(std::uint32_t first, std::uint32_t count); // Registers or unregisters the image as its pending layers say (after a write-back). void reconcilePending(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 5c366885052..a57623bdb1c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3030,7 +3030,7 @@ void StorageTexture::refreshGeneration() { void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) { CaptureTrace::Log("image-write image=%llx first=%u count=%u generation=%llu", static_cast(descriptor.baseAddress), first, count, static_cast(generation)); - static const bool eager = std::getenv("APS5_DEFERRED_WRITEBACK") == nullptr; + static const bool eager = std::getenv("APS5_EAGER_WRITEBACK") != nullptr || std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; std::vector unsaved(trackedLayers, false); bool anyUnsaved = false; for (std::uint32_t layer = first; layer < first + count; ++layer) { @@ -3055,7 +3055,6 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) if (superseded) source->reconcilePending(); } if (eager) { - ++version; WriteBack(); return; } @@ -3336,6 +3335,12 @@ bool StorageTexture::StoreAtFlipRequested(const char* value) { throw std::runtime_error(std::string("APS5_STORE_AT_FLIP=") + value + ": expected 1"); } +bool StorageTexture::StoreAtLabelRequested(const char* value) { + if (value == nullptr) return false; + if (std::strcmp(value, "1") == 0) return true; + throw std::runtime_error(std::string("APS5_STORE_AT_LABEL=") + value + ": expected 1"); +} + std::shared_ptr StorageTexture::FindPending(std::uint64_t address, std::uint64_t bytes) { auto& pending = Pending(); std::lock_guard lock(pending.mutex); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index 4c452daf24d..18dcef360cc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -272,17 +271,9 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t sw std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); const auto drawn = std::bit_cast(Drawn); DrawSlice(device, surface, slice, swizzleMode, collapsed); - const auto imageAddress = collapsed ? surface + slice * LayerBytes : surface; - const auto imageBytes = collapsed ? LayerBytes : SurfaceBytes; - const auto pending = StorageTexture::FindPending(imageAddress, imageBytes); - const bool deferred = std::getenv("APS5_DEFERRED_WRITEBACK") != nullptr; - if (!deferred) { - Require(pending == nullptr, "the eager draw left a pending image"); - } else if (collapsed) { - Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2D && pending->Descriptor().baseAddress == imageAddress, "a collapsed slice did not render into its shifted surface"); - } else { - Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice " + std::to_string(slice) + " did not render into the image of its array"); - } + const auto pending = StorageTexture::FindPending(collapsed ? surface + slice * LayerBytes : surface, collapsed ? LayerBytes : SurfaceBytes); + if (collapsed) Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2D && pending->Descriptor().baseAddress == surface + slice * LayerBytes, "a collapsed slice did not render into its shifted surface"); + else Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array"); LoadLayer(device, surface, slice, swizzleMode); RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " read through the array after the draw"); for (std::uint32_t other = 0; other < Layers; ++other) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index 29175dd875f..e9668e78dc1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -783,11 +783,6 @@ void RunSharedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { int main() { try { -#ifdef _WIN32 - Require(_putenv_s("APS5_DEFERRED_WRITEBACK", "1") == 0, "cannot enable deferred write-back for the pending-image test"); -#else - Require(setenv("APS5_DEFERRED_WRITEBACK", "1", 1) == 0, "cannot enable deferred write-back for the pending-image test"); -#endif std::optional block; std::optional watched; std::optional untracked; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthCompareCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthCompareCopy.cpp index 248927baa09..1db56eaa873 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthCompareCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthCompareCopy.cpp @@ -214,7 +214,7 @@ std::uint64_t SurfaceBytes(std::uint8_t* texels, const Shape& shape) { std::uint64_t PendingVersion(std::uint8_t* texels, std::uint64_t bytes, const std::string& what) { const auto source = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), bytes); - Require(source != nullptr, what + ": no pending storage image holds the surface"); + Require(source != nullptr, what + ": no storage image holds the surface's results"); return source->Version(); } @@ -298,11 +298,6 @@ void LayersAndMipsCase(AgcDriver::VulkanDevice& device) { int main() { try { -#ifdef _WIN32 - Require(_putenv_s("APS5_DEFERRED_WRITEBACK", "1") == 0, "cannot enable deferred write-back for the pending depth-copy test"); -#else - Require(setenv("APS5_DEFERRED_WRITEBACK", "1", 1) == 0, "cannot enable deferred write-back for the pending depth-copy test"); -#endif const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; GuestBlock buffers; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp index e1e4cb80d38..ec759a3f7c6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp @@ -203,9 +203,7 @@ void CheckStoredLoad(AgcDriver::VulkanDevice& device, GuestBlock& block, std::sp const auto bytes = Width * (format == Srgb8 ? 1u : 2u); std::fill(block.Data(), block.Data() + BlockBytes, std::uint8_t{0x5au}); Run(device, format == Srgb8 ? std::span(StoreX) : std::span(StoreXy), block.Data(), format == Srgb8 ? Unorm8 : Unorm8_8, SwizzleXY01, Groups); - const auto pending = AgcDriver::Graphics::StorageTexture::FindPending(address, bytes); - const bool deferred = std::getenv("APS5_DEFERRED_WRITEBACK") != nullptr; - Require(deferred ? pending != nullptr : pending == nullptr, what + (deferred ? ": the image_store results are not pending in the storage image" : ": eager write-back left a pending image")); + Require(AgcDriver::Graphics::StorageTexture::FindPending(address, bytes) != nullptr, what + ": the image_store results are not pending in the storage image"); Run(device, code, block.Data(), format, swizzle, Groups); Verify(dmask, format, swizzle, what); AgcDriver::Graphics::StorageTexture::FlushPending(address, BlockBytes, nullptr, "test"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PendingExactSurface.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PendingExactSurface.cpp index b32b3469404..9386584c9fe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PendingExactSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PendingExactSurface.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -88,11 +87,6 @@ std::string Describe(const std::shared_ptr& image) { int main() { try { -#ifdef _WIN32 - Require(_putenv_s("APS5_DEFERRED_WRITEBACK", "1") == 0, "cannot enable deferred write-back for the pending-surface test"); -#else - Require(setenv("APS5_DEFERRED_WRITEBACK", "1", 1) == 0, "cannot enable deferred write-back for the pending-surface test"); -#endif const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; std::vector storage((BlockBytes + 0x10000u) / 4u); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp index 95cc0fd270e..78437ac99d4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageOneSliceAlias.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include #include #include @@ -170,22 +169,17 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, bool watched) { AgcDriver::GuestMemory::BumpCollectEpoch(); std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); Store(device, texels, 0x11111111u); - const bool deferred = std::getenv("APS5_DEFERRED_WRITEBACK") != nullptr; - Require((StorageTexture::FindPending(surface, SurfaceBytes) != nullptr) == deferred, deferred ? "the 2D store left no pending image" : "the eager 2D store left a pending image"); - const auto live = StorageTexture::FindLive(surface, SurfaceBytes); - Require(live != nullptr, "the 2D storage image is not cached after its first store"); - const auto previousVersion = live->Version(); + Require(StorageTexture::FindPending(surface, SurfaceBytes) != nullptr, "the 2D store left no pending image"); LoadArray(device, texels); RequireOutput(0x11111111u, "the first load through the one-slice array"); Store(device, texels, 0x22222222u); const auto written = StorageTexture::FindPending(surface, SurfaceBytes); - Require(live->Version() > previousVersion, "the second 2D store did not advance the cached image version"); - Require(deferred ? written != nullptr && written->Descriptor().dimension == AgcDriver::Graphics::TextureDimension::k2D : written == nullptr, deferred ? "the second 2D store left no pending 2D image" : "the eager second 2D store left a pending image"); + Require(written != nullptr && written->Descriptor().dimension == AgcDriver::Graphics::TextureDimension::k2D, "the second 2D store left no pending 2D image"); LoadArray(device, texels); RequireOutput(0x22222222u, "the second load through the one-slice array"); - if (deferred && watched && AgcDriver::GuestMemory::Watched(surface, SurfaceBytes)) { + if (watched && AgcDriver::GuestMemory::Watched(surface, SurfaceBytes)) { Require(StorageTexture::FindPending(surface, SurfaceBytes) == written, "the array image's refresh wrote the 2D image's results back instead of taking them on the device"); - } else if (deferred) { + } else { std::puts("the imported block is compared, not watched: taking the results on the device is not checked"); } StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); From c8159c564b67ee2e2c2a62cf77287ebe73dead91 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:58:10 +0100 Subject: [PATCH 5/7] fix(libSceAgcDriver): flush pending results at staged-clear sampled reads A title may stage DCC fast-clear keys while the draw results for the surface are still pending (Bendy stages its next frame clear behind the feedback loop). The cached-texture lookup only flushed pending results when no image was cached, so a read with staged clear keys served the clear over the results the pass had drawn. The lookup now flushes at that point for staged-clear keys, reads the keys uncompressed for that read, and keeps the flushed image out of the hit view. StagedClearSampledRead covers the case: it fails without the flush (the lookup serves without the pending results in guest memory) and passes with it. --- .../Graphics/src/ShaderResources.cpp | 15 +- .../execution/StagedClearSampledRead.cpp | 208 ++++++++++++++++++ 2 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index f09d2cbcb66..d4c8100530d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -616,10 +616,19 @@ std::shared_ptr cachedTexture(const Context& context, std::span cachedTexture(const Context& context, std::spansource) && (source != nullptr || *keys == DccKeys::Uncompressed) && StorageImageServesKeys(*it->source, resource.dccAddress)) { + if (!stagedFlushed && (source == nullptr || source == it->source) && (source != nullptr || *keys == DccKeys::Uncompressed) && StorageImageServesKeys(*it->source, resource.dccAddress)) { if (!GuestMemory::UnchangedSince(address, bytes, it->source->Generation())) it->source->Refresh(); if (it->texture->IsDepthCopy()) it->texture->RefreshDepthCopy(); it->keys = *keys; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp new file mode 100644 index 00000000000..065e13cfddb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp @@ -0,0 +1,208 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::DccKeys; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Side = 128; +constexpr std::uint32_t Format8888Unorm = 56; +constexpr std::uint32_t TileRenderTarget64KB = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t SurfaceBytes = 65536; +constexpr std::size_t KeyBytes = SurfaceBytes / 256; +constexpr std::size_t KeysOffset = SurfaceBytes; +constexpr std::size_t SampleTexelBytes = 4; +constexpr std::size_t UnalignedBlockBytes = 2 * 65536 + 2048; +constexpr std::size_t ArenaBytes = 3 * 65536; + +alignas(256) constexpr std::array WriteRowCode{ + 0x7e020281, 0x7e0402f2, 0x7e0602f2, 0x7e0802f2, 0x7e0a02f2, 0xf0201f08, 0x00010200, 0xbf810000, +}; + +alignas(256) constexpr std::array SampleSecondTexelCode{ + 0x34060084, 0x7e2802ff, 0x3c400000, 0x7e2a02ff, 0x3b800000, 0xf09c0f08, 0x00610a14, 0xbf8c3f70, + 0xe0781000, 0x80000a03, 0xbf810000, +}; + +constexpr std::array PointSampler{0x00000092u, 0x00fff000u, 0u, 0u}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(ArenaBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, ArenaBytes, PAGE_READWRITE, ArenaBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, ArenaBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + ArenaBytes != begin + ArenaBytes + alignment) munmap(reinterpret_cast(aligned + ArenaBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, UnalignedBlockBytes); + } +#endif + Require(block != nullptr, "staged clear sampled read: cannot allocate the guest block"); + Require(AgcDriver::GuestMemory::Watched(AddressOf(block), UnalignedBlockBytes), "staged clear sampled read: the guest block is not write-watched"); + GuestAllocations::Mutation().Add(block, UnalignedBlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, ArenaBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, ArenaBytes); +#else + munmap(block, ArenaBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, UnalignedBlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array TextureDescriptor(const std::uint8_t* texels, const std::uint8_t* keys) { + const auto address = AddressOf(texels); + const auto meta = AddressOf(keys) >> 8u; + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888Unorm << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileRenderTarget64KB << 20u) | (Type2D << 28u), + 0u, + 0u, + (1u << 20u) | (1u << 21u) | (static_cast(meta & 0xffu) << 24u), + static_cast(meta >> 8u), + }; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::span code, const std::vector& userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::size_t Count(const std::uint8_t* bytes, std::size_t size, std::uint8_t value) { + return static_cast(std::count(bytes, bytes + size, value)); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + auto* keys = texels + KeysOffset; + std::memset(texels, 0x00, SurfaceBytes); + const AgcDriver::Graphics::ColorTargetLayout layout(Side, Side, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + std::memset(texels + layout.Offset(1, 0), 0xff, SampleTexelBytes); + std::memset(keys, 0xff, KeyBytes); + const auto surface = AddressOf(texels); + const auto texture = TextureDescriptor(texels, keys); + const auto resource = AgcDriver::Graphics::DecodeTextureResource(texture); + AgcDriver::GuestMemory::BumpCollectEpoch(); + std::vector userData(24, 0u); + std::vector outputData(Threads * 4u, 0xdeadbeefu); + const auto results = BufferDescriptor(outputData.data(), Threads * 16u); + std::copy(results.begin(), results.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(PointSampler.begin(), PointSampler.end(), userData.begin() + 12); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + std::vector writeData(16, 0u); + std::copy(texture.begin(), texture.end(), writeData.begin() + 4); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Dispatch(*device, WriteRowCode, writeData); + const auto image = StorageTexture::FindPending(surface, SurfaceBytes); + Require(image != nullptr, "the pass left no pending results at the surface"); + Require(image->UploadedKeys() == DccKeys::Uncompressed, "the pass did not draw under the settled keys"); + std::memset(keys, 0x00, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(AgcDriver::Graphics::TextureClearKeys(resource, SurfaceBytes) == DccKeys::Clear0000, "the key bytes do not read a staged 0000 clear"); + Dispatch(*device, SampleSecondTexelCode, userData); + Require(StorageTexture::FindPending(surface, SurfaceBytes) == nullptr, "the sampled read did not store the pending results: the lookup serves without them in guest memory"); + const auto written = Count(texels, SurfaceBytes, 0xff); + const auto cleared = Count(texels, SurfaceBytes, 0x00); + char message[320]; + std::snprintf(message, sizeof(message), "the sampled read stored %zu written and %zu cleared bytes of %zu, expected %u written", written, cleared, SurfaceBytes, Threads * 4u + SampleTexelBytes); + Require(written == Threads * 4u + SampleTexelBytes && cleared == SurfaceBytes - written, message); + for (std::uint32_t index = 0; index < Threads * 4u; ++index) { + std::snprintf(message, sizeof(message), "component %u of thread %u is 0x%08x, expected the drawn 0x3f800000 the staged clear does not cover", index % 4u, index / 4u, outputData[index]); + Require(outputData[index] == 0x3f800000u, message); + } + } + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::FlushCachedTextures(device->Device()); + } + device->WaitIdle(); + std::puts("staged clear sampled read tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 5921095b65f682dfc1d040bda0d5d866e1afad48 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:48:25 +0100 Subject: [PATCH 6/7] test(libSceAgcDriver): page-align staged-clear write-watch registration on POSIX The POSIX watch registration passed UnalignedBlockBytes (2*65536+2048), whose 2048 remainder is rejected by UFFDIO_REGISTER with EINVAL, so the test failed at the not-write-watched Require on Linux. Register and unregister ArenaBytes (3*65536), which is page-aligned; the Watched Require and Mutation().Add keep UnalignedBlockBytes so the Windows host-import refusal that preserves the write-watch is unchanged. --- .../tests/execution/StagedClearSampledRead.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp index 065e13cfddb..8997e9418ad 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp @@ -80,7 +80,7 @@ class GuestBlock { if (aligned != begin) munmap(mapped, aligned - begin); if (aligned + ArenaBytes != begin + ArenaBytes + alignment) munmap(reinterpret_cast(aligned + ArenaBytes), begin + alignment - aligned); block = reinterpret_cast(aligned); - GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, UnalignedBlockBytes); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, ArenaBytes); } #endif Require(block != nullptr, "staged clear sampled read: cannot allocate the guest block"); @@ -95,7 +95,7 @@ class GuestBlock { GuestArena::GuestArenaRelease_nid_postfix(block, ArenaBytes); #else munmap(block, ArenaBytes); - GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, UnalignedBlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, ArenaBytes); #endif } From bc316c73414c4d4b7b0aded7581ec1ee40336645 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:43:54 +0100 Subject: [PATCH 7/7] revert(libSceAgcDriver): drop the unused StoreAtLabelRequested helper The eager-writeback commits added StorageTexture::StoreAtLabelRequested (APS5_STORE_AT_LABEL). Nothing reads that variable, so the helper is dead code; the deferred-writeback revert kept it by accident. Remove it so the PR changes only the staged-clear sampled-read flush and its test. --- core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp | 1 - core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 6 ------ 2 files changed, 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index f9a0bdd6b69..0524592a6c5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -197,7 +197,6 @@ class StorageTexture : public std::enable_shared_from_this { static bool AnyPending(); static void FlushDumpSelected(const char* reason); 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 // retile) moves it too. static void BumpPendingSerial(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a57623bdb1c..0ea0b616198 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3335,12 +3335,6 @@ bool StorageTexture::StoreAtFlipRequested(const char* value) { throw std::runtime_error(std::string("APS5_STORE_AT_FLIP=") + value + ": expected 1"); } -bool StorageTexture::StoreAtLabelRequested(const char* value) { - if (value == nullptr) return false; - if (std::strcmp(value, "1") == 0) return true; - throw std::runtime_error(std::string("APS5_STORE_AT_LABEL=") + value + ": expected 1"); -} - std::shared_ptr StorageTexture::FindPending(std::uint64_t address, std::uint64_t bytes) { auto& pending = Pending(); std::lock_guard lock(pending.mutex);