diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index a688a413b..1ee949927 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include #include #include @@ -153,6 +154,7 @@ public: } Context context; + std::shared_ptr> active = std::make_shared>(true); ColorTarget target; VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; @@ -175,6 +177,7 @@ struct GuardedRange { std::uint64_t address; std::uint64_t bytes; std::uint32_t samples; + std::shared_ptr> active; }; class PrimedSharedMutex : public std::shared_mutex { @@ -204,7 +207,17 @@ void guardReads(std::uint64_t address, std::size_t bytes) { RequireNoMultisampleTarget(address, bytes, "a CPU read"); } -void guard(const Context& context, const ColorTarget& target) { +void retireUnmapped(std::uintptr_t address, std::size_t bytes) { + std::unique_lock lock(guardedMutex()); + std::erase_if(guarded(), [&](const auto& range) { + if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false; + range.active->store(false, std::memory_order_release); + return true; + }); + anyGuarded().store(!guarded().empty(), std::memory_order_release); +} + +void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active) { auto bytes = std::max(target.bytes * target.samples, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); if (target.tileMode == ColorTileMode::RenderTarget && std::has_single_bit(target.elementBytes) && std::has_single_bit(target.samples)) { const auto log2Samples = std::countr_zero(target.samples); @@ -216,10 +229,13 @@ void guard(const Context& context, const ColorTarget& target) { bytes = align(target.extent.width, blockWidth) * align(target.extent.height, blockHeight) * target.elementBytes * target.samples; } std::unique_lock lock(guardedMutex()); - guarded().push_back({context.device, target.address, bytes, target.samples}); + guarded().push_back({context.device, target.address, bytes, target.samples, active}); anyGuarded().store(true, std::memory_order_release); static std::once_flag installed; - std::call_once(installed, [] { GuestMemory::SetReadGuard(&guardReads); }); + std::call_once(installed, [] { + GuestMemory::SetReadGuard(&guardReads); + }); + GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(&retireUnmapped); } std::mutex& targetsMutex() { @@ -234,14 +250,15 @@ std::vector>& targets() { MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { for (const auto& target : targets()) { + if (!target->active->load(std::memory_order_acquire)) continue; if (target->context.device != context.device || !sameTarget(target->target, use)) continue; target->cmask.CheckAddress(use.cmaskAddress); target->ApplyFastClear(use); return *target; } targets().push_back(std::make_unique(context, use)); - guard(context, use); auto& created = *targets().back(); + guard(context, use, created.active); if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); created.ApplyFastClear(use); return created; @@ -388,6 +405,7 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { std::lock_guard lock(targetsMutex()); for (const auto& target : targets()) { + if (!target->active->load(std::memory_order_acquire)) continue; target->colorFill.NoteFill(address, bytes, pattern); const auto before = target->cmask.Keys(); target->cmask.NoteFill(address, bytes, pattern); @@ -403,6 +421,7 @@ void ClearMultisampleTargets(VkDevice device) { std::unique_lock guardLock(guardedMutex()); std::erase_if(guarded(), [&](const auto& range) { return range.device == device; }); anyGuarded().store(!guarded().empty(), std::memory_order_release); + if (guarded().empty()) GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(nullptr); } void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 12dcfeb77..dd0b50ba6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5055,6 +5055,12 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { context.nullDescriptors = true; TextureCache cache(context); context.textureCache = &cache; + const auto clearTargets = [&] { + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + ClearMultisampleTargets(context.device); + }; const auto refused = [](const std::function& access, const std::string& expected, const std::string& what) { std::string message; try { @@ -5111,7 +5117,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { }, "a texture read", "a texture read"); refused([&] { AgcDriver::GuestMemory::Read(address + 3u * 65536u + 256u, texels); }, "a CPU read", "a CPU read in the padding of a later sample plane"); AgcDriver::GuestMemory::Read(address + 4u * 65536u, texels); - ClearMultisampleTargets(context.device); + clearTargets(); target.tileMode = ColorTileMode::RenderTarget; { std::lock_guard gpu(GpuMutex()); @@ -5122,7 +5128,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } refused([&] { AgcDriver::GuestMemory::Read(address + targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the tiled multisampled target"); AgcDriver::GuestMemory::Read(address + targetBytes, texels); - ClearMultisampleTargets(context.device); + clearTargets(); if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_2_BIT) != 0) { target.extent = {128, 64}; target.surfaceExtent = target.extent; @@ -5136,7 +5142,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 2x tile-height padding"); AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels); - ClearMultisampleTargets(context.device); + clearTargets(); } if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) != 0) { target.extent = {64, 32}; @@ -5151,9 +5157,48 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 8x tile-height padding"); AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels); - ClearMultisampleTargets(context.device); + clearTargets(); } AgcDriver::GuestMemory::Read(address + 256u, texels); + target.extent = {side, side}; + target.surfaceExtent = target.extent; + target.samples = 4; + VkImageView previous = VK_NULL_HANDLE; + for (const bool removeAllocation : {false, true}) { + VkImageView view = VK_NULL_HANDLE; + { + std::lock_guard gpu(GpuMutex()); + view = MultisampleTargetView(context, target); + Require(view != VK_NULL_HANDLE && view != previous, "a remapped allocation reused its retired multisampled image"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + { + GuestAllocations::Mutation mutation; + mutation.Protect(block, bytes, true, true, true, [] {}); + } + refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after protection changed"); + bool failed = false; + try { + GuestAllocations::Mutation mutation; + mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) { throw std::runtime_error("unmap failed"); }); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed, "the failed host unmap did not throw"); + refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after a failed unmap"); + { + GuestAllocations::Mutation mutation; + if (removeAllocation) mutation.Remove(block); + else mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) {}); + mutation.Add(block, bytes, true, true, true); + } + RequireNoMultisampleTarget(address, bytes, "a texture read"); + AgcDriver::GuestMemory::Read(address + 256u, texels); + previous = view; + } + clearTargets(); std::cout << "Multisampled target guest access tests passed\n"; } diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index 231826bb9..ce42a0be6 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -83,6 +83,7 @@ void GuestAllocationsInvalidate_nid_postfix(std::uintptr_t address, std::size_t // until the lease is dropped by another thread. void GuestAllocationsSetPinWaiter_nid_postfix(bool (*callback)(std::uintptr_t address, std::size_t bytes)); void GuestAllocationsSetGpuMapObserver_nid_postfix(void (*callback)(const Mapped& ranges, std::uint64_t generation)); +void GuestAllocationsSetUnmapObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes)); void GuestAllocationsNoteGpuMapping_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); } diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index 1faac8313..c7b286af8 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -49,6 +49,7 @@ std::atomic generation{1}; std::atomic invalidator{nullptr}; std::atomic pinWaiter{nullptr}; std::atomic gpuMapObserver{nullptr}; +std::atomic unmapObserver{nullptr}; std::atomic gpuMapObserverCalls{0}; std::chrono::milliseconds pinWait() { @@ -421,9 +422,10 @@ void GuestAllocationsRemove_nid_postfix(void* mutation, const void* pointer) { const auto exact = ranges.find(range.address); if (exact != ranges.end() && exact->second->allocationBytes == exact->second->bytes) { ranges.erase(exact); - return; + } else { + std::erase_if(ranges, [&](const auto& entry) { return entry.second->allocationAddress == range.address; }); } - std::erase_if(ranges, [&](const auto& entry) { return entry.second->allocationAddress == range.address; }); + if (const auto observer = unmapObserver.load(std::memory_order_acquire)) observer(range.address, range.bytes); } namespace { @@ -517,10 +519,16 @@ void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std: const bool last = !allocationRemains(range, patch); apply(reinterpret_cast(cursor), pieceEnd - cursor, reinterpret_cast(range.allocationAddress), last); applyPatch(patch); + if (const auto observer = unmapObserver.load(std::memory_order_acquire)) observer(cursor, pieceEnd - cursor); any = true; cursor = pieceEnd; } require(any, "unmap address is not registered"); + if (const auto observer = unmapObserver.load(std::memory_order_acquire)) observer(reinterpret_cast(pointer), bytes); +} + +void GuestAllocationsSetUnmapObserver_nid_postfix(void (*callback)(std::uintptr_t, std::size_t)) { + unmapObserver.store(callback, std::memory_order_release); } Lease GuestAllocationsAcquire_nid_postfix() { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 8fbdd6a72..a5d4bcb31 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -27,6 +27,8 @@ ### Functional +- [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. + - [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. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A matching view still throws when it has another dimension (2D array of more than one layer or from another layer, cube) or level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [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. - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. Planes with mips, a base slice other than 0, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below).