From f31caf987233eacb12c0bfde1aae5561f2f37679 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 30 Aug 2026 02:45:24 +0300 Subject: [PATCH 1/6] rsx: Add missing mm_flush in blit engine before RCB reload --- rpcs3/Emu/RSX/Common/texture_cache.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rpcs3/Emu/RSX/Common/texture_cache.h b/rpcs3/Emu/RSX/Common/texture_cache.h index fc0824c77077..896384bc289d 100644 --- a/rpcs3/Emu/RSX/Common/texture_cache.h +++ b/rpcs3/Emu/RSX/Common/texture_cache.h @@ -3740,6 +3740,10 @@ namespace rsx } } + // MM flush before commit below + const auto mm_flush_range = utils::address_range64::start_length(reinterpret_cast(dst.pixels), dst_payload_length); + rsx::mm_flush({ mm_flush_range }); + // Commit any pending writes before we do the transfer. Writes will be done on super_ptr so locking beforehand is ok. m_rtts.prepare_transfer_target(cmd, dst_subres.surface, rsx::surface_access::transfer_write, std::forward(extras)...); } From d33e9f486ca1aecf9dcb65bee85d7515d5d590c8 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 30 Aug 2026 18:03:07 +0300 Subject: [PATCH 2/6] rsx: Loosen the access ordering guarantees a bit --- rpcs3/Emu/RSX/Common/texture_cache.h | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rpcs3/Emu/RSX/Common/texture_cache.h b/rpcs3/Emu/RSX/Common/texture_cache.h index 896384bc289d..dee6386b6bb0 100644 --- a/rpcs3/Emu/RSX/Common/texture_cache.h +++ b/rpcs3/Emu/RSX/Common/texture_cache.h @@ -3740,9 +3740,12 @@ namespace rsx } } - // MM flush before commit below - const auto mm_flush_range = utils::address_range64::start_length(reinterpret_cast(dst.pixels), dst_payload_length); - rsx::mm_flush({ mm_flush_range }); + // MM flush before commit below. For performance reasons, only flush when writing to CELL memory. + if (rsx::classify_location(dst.rsx_address) == CELL_GCM_LOCATION_MAIN) + { + const auto mm_flush_range = utils::address_range64::start_length(reinterpret_cast(dst.pixels), dst_payload_length); + rsx::mm_flush({ mm_flush_range }); + } // Commit any pending writes before we do the transfer. Writes will be done on super_ptr so locking beforehand is ok. m_rtts.prepare_transfer_target(cmd, dst_subres.surface, rsx::surface_access::transfer_write, std::forward(extras)...); From 3bf62f7d02a4423be60e87b466a524affea791ac Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 30 Aug 2026 21:16:54 +0300 Subject: [PATCH 3/6] rsx/mm: Avoid flushing entire queue unless actually necessary - Host MM operations are slow af --- rpcs3/Emu/RSX/Host/MM.cpp | 64 ++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/rpcs3/Emu/RSX/Host/MM.cpp b/rpcs3/Emu/RSX/Host/MM.cpp index e1313cc13a5c..1add47931ee3 100644 --- a/rpcs3/Emu/RSX/Host/MM.cpp +++ b/rpcs3/Emu/RSX/Host/MM.cpp @@ -14,14 +14,41 @@ namespace rsx rsx::simple_array g_deferred_mprotect_queue; shared_mutex g_mprotect_queue_lock; - void mm_flush_mprotect_queue_internal() + void mm_flush_mprotect_queue_internal(u32 count) { - for (const auto& block : g_deferred_mprotect_queue) + AUDIT(count <= g_deferred_mprotect_queue.size()); + + for (u32 i = 0; i < count; ++i) { + const auto& block = g_deferred_mprotect_queue[i]; utils::memory_protect(reinterpret_cast(block.range.start), block.range.length(), block.prot); } - g_deferred_mprotect_queue.clear(); + const u32 remaining = g_deferred_mprotect_queue.size() - count; + if (!remaining) + { + g_deferred_mprotect_queue.clear(); + return; + } + + // Pop count entries from the queue + std::memmove(g_deferred_mprotect_queue.data(), g_deferred_mprotect_queue.data() + count, remaining * sizeof(MM_block)); + g_deferred_mprotect_queue.resize(remaining); + } + + // Reverse scan to find the latest overlapping MM conflict. The result is a count of prefix blocks. + template + u32 mm_find_conflict_internal(F&& predicate) + { + for (u32 i = g_deferred_mprotect_queue.size(); i > 0; --i) + { + if (std::invoke(predicate, g_deferred_mprotect_queue[i - 1])) + { + return i; + } + } + + return 0; } void mm_defer_mprotect_internal(u64 start, u64 length, utils::protection prot) @@ -47,14 +74,10 @@ namespace rsx if (prot == utils::protection::rw || prot == utils::protection::wx) { - // Basically an unlock op. Flush if any overlap is detected - for (const auto& block : g_deferred_mprotect_queue) + // Basically an unlock op. Flush the conflicting prefix block if any overlap is detected. + if (const u32 count = mm_find_conflict_internal(FN(x.overlaps(range)))) { - if (block.overlaps(range)) - { - mm_flush_mprotect_queue_internal(); - break; - } + mm_flush_mprotect_queue_internal(count); } utils::memory_protect(ptr, length, prot); @@ -68,7 +91,7 @@ namespace rsx void mm_flush() { std::lock_guard lock(g_mprotect_queue_lock); - mm_flush_mprotect_queue_internal(); + mm_flush_mprotect_queue_internal(g_deferred_mprotect_queue.size()); } void mm_flush(u32 vm_address) @@ -80,31 +103,24 @@ namespace rsx } const auto addr = reinterpret_cast(vm::base(vm_address)); - for (const auto& block : g_deferred_mprotect_queue) + if (const u32 count = mm_find_conflict_internal(FN(x.overlaps(addr)))) { - if (block.overlaps(addr)) - { - mm_flush_mprotect_queue_internal(); - return; - } + mm_flush_mprotect_queue_internal(count); } } void mm_flush(const rsx::simple_array& ranges) { std::lock_guard lock(g_mprotect_queue_lock); - if (g_deferred_mprotect_queue.empty()) + if (g_deferred_mprotect_queue.empty() || ranges.empty()) { return; } - for (const auto& block : g_deferred_mprotect_queue) + const auto block_overlaps_ranges = [&](const MM_block& block) { return ranges.any(FN(block.overlaps(x))); }; + if (const u32 count = mm_find_conflict_internal(block_overlaps_ranges)) { - if (ranges.any(FN(block.overlaps(x)))) - { - mm_flush_mprotect_queue_internal(); - return; - } + mm_flush_mprotect_queue_internal(count); } } From cea9f648a34407a42c33cfa66859b9e7264ad8cd Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 30 Aug 2026 23:00:48 +0300 Subject: [PATCH 4/6] rsx/mm: Implement range merging mechanics --- rpcs3/Emu/RSX/Host/MM.cpp | 98 +++++++++++++++++++++++++++++++++++---- rpcs3/Emu/RSX/Host/MM.h | 14 ++++++ 2 files changed, 103 insertions(+), 9 deletions(-) diff --git a/rpcs3/Emu/RSX/Host/MM.cpp b/rpcs3/Emu/RSX/Host/MM.cpp index 1add47931ee3..40760c9dcc10 100644 --- a/rpcs3/Emu/RSX/Host/MM.cpp +++ b/rpcs3/Emu/RSX/Host/MM.cpp @@ -14,14 +14,32 @@ namespace rsx rsx::simple_array g_deferred_mprotect_queue; shared_mutex g_mprotect_queue_lock; + void mm_sanitize_queue_internal() + { + u32 w = 0, r = 0; + for (; r < g_deferred_mprotect_queue.size(); ++r) + { + auto& block = g_deferred_mprotect_queue[r]; + if (!block.range.valid()) + { + continue; + } + g_deferred_mprotect_queue[w++] = block; + } + g_deferred_mprotect_queue.resize(w); + } + void mm_flush_mprotect_queue_internal(u32 count) { AUDIT(count <= g_deferred_mprotect_queue.size()); for (u32 i = 0; i < count; ++i) { - const auto& block = g_deferred_mprotect_queue[i]; + auto& block = g_deferred_mprotect_queue[i]; + ensure(block.range.valid()); + utils::memory_protect(reinterpret_cast(block.range.start), block.range.length(), block.prot); + block.range.invalidate(); } const u32 remaining = g_deferred_mprotect_queue.size() - count; @@ -31,9 +49,8 @@ namespace rsx return; } - // Pop count entries from the queue - std::memmove(g_deferred_mprotect_queue.data(), g_deferred_mprotect_queue.data() + count, remaining * sizeof(MM_block)); - g_deferred_mprotect_queue.resize(remaining); + // Pop processed entries from the queue + mm_sanitize_queue_internal(); } // Reverse scan to find the latest overlapping MM conflict. The result is a count of prefix blocks. @@ -53,9 +70,57 @@ namespace rsx void mm_defer_mprotect_internal(u64 start, u64 length, utils::protection prot) { - // We could stack and merge requests here, but that is more trouble than it is truly worth. - // A fresh call to memory_protect only takes a few nanoseconds of setup overhead, it is not worth the risk of hanging because of conflicts. - g_deferred_mprotect_queue.push_back({ utils::address_range64::start_length(start, length), prot }); + const auto range = utils::address_range64::start_length(start, length); + bool has_invalid = false; + bool is_merged = false; + + // Attempt a merge first. The queue length is short but the time taken to run mprotect is very high in comparison. + for (auto it = g_deferred_mprotect_queue.rbegin(); + it != g_deferred_mprotect_queue.rend(); + ++it) + { + auto& block = *it; + if (!block.touches(range)) + { + continue; + } + + if (block.prot != prot) + { + // Optimization. If our new range swallows the old one, replace it. + if (block.range.inside(range)) + { + block.range.invalidate(); + has_invalid = true; + continue; + } + + if (!block.overlaps(range)) + { + // Adjacent. Skip. + continue; + } + + // Preserve ordering. Do not proceed with merge. + break; + } + + block.merge(range); + is_merged = true; + break; + } + + if (has_invalid) + { + mm_sanitize_queue_internal(); + } + + if (is_merged) + { + return; + } + + g_deferred_mprotect_queue.push_back({ range, prot }); } void mm_protect(void* ptr, u64 length, utils::protection prot) @@ -75,9 +140,24 @@ namespace rsx if (prot == utils::protection::rw || prot == utils::protection::wx) { // Basically an unlock op. Flush the conflicting prefix block if any overlap is detected. - if (const u32 count = mm_find_conflict_internal(FN(x.overlaps(range)))) + if (u32 count = mm_find_conflict_internal(FN(x.overlaps(range)))) { - mm_flush_mprotect_queue_internal(count); + // Check for degenerate ranges that we'll be crushing + for (auto pblock = &g_deferred_mprotect_queue[count - 1]; + count > 0 && pblock->range.inside(range); + count--, pblock--) + { + pblock->range.invalidate(); + } + + if (count) + { + mm_flush_mprotect_queue_internal(count); + } + else + { + mm_sanitize_queue_internal(); + } } utils::memory_protect(ptr, length, prot); diff --git a/rpcs3/Emu/RSX/Host/MM.h b/rpcs3/Emu/RSX/Host/MM.h index 43053cdd1791..7ba75e1a3035 100644 --- a/rpcs3/Emu/RSX/Host/MM.h +++ b/rpcs3/Emu/RSX/Host/MM.h @@ -13,6 +13,15 @@ namespace rsx utils::address_range64 range; utils::protection prot; + inline void merge(const utils::address_range64& other) + { + AUDIT(other.valid()); + range = utils::address_range64::start_end( + std::min(range.start, other.start), + std::max(range.end, other.end) + ); + } + inline bool overlaps(const utils::address_range64& test) const { return range.overlaps(test); @@ -22,6 +31,11 @@ namespace rsx { return range.overlaps(addr); } + + inline bool touches(const utils::address_range64& test) const + { + return range.touches(test); + } }; enum mm_backend_ctrl : u32 From 4dca259d841e6846b871419afc00b89b71429c78 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 30 Aug 2026 23:01:01 +0300 Subject: [PATCH 5/6] rsx/mm: Add unit tests --- rpcs3/tests/rpcs3_test.vcxproj | 1 + rpcs3/tests/test_rsx_mm_queue.cpp | 138 ++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 rpcs3/tests/test_rsx_mm_queue.cpp diff --git a/rpcs3/tests/rpcs3_test.vcxproj b/rpcs3/tests/rpcs3_test.vcxproj index 274d9d028db9..df73d02f0d20 100644 --- a/rpcs3/tests/rpcs3_test.vcxproj +++ b/rpcs3/tests/rpcs3_test.vcxproj @@ -100,6 +100,7 @@ + diff --git a/rpcs3/tests/test_rsx_mm_queue.cpp b/rpcs3/tests/test_rsx_mm_queue.cpp new file mode 100644 index 000000000000..46b79e96a1ca --- /dev/null +++ b/rpcs3/tests/test_rsx_mm_queue.cpp @@ -0,0 +1,138 @@ +#include +#include "Emu/RSX/Host/MM.cpp" + +namespace rsx::MM +{ + class MMQueue : public ::testing::Test + { + protected: + static constexpr u32 s_pages = 16; + + u64 m_page_size = 0; + u8* m_base = nullptr; + + void SetUp() override + { + g_cfg.video.disable_async_host_memory_manager.set(false); + + m_page_size = static_cast(utils::get_page_size()); + m_base = static_cast(utils::memory_reserve(m_page_size * s_pages)); + utils::memory_commit(m_base, m_page_size * s_pages); + + g_deferred_mprotect_queue.clear(); + } + + void TearDown() override + { + g_deferred_mprotect_queue.clear(); + utils::memory_release(m_base, m_page_size * s_pages); + } + + void* page(u32 index) const + { + return m_base + (index * m_page_size); + } + + utils::address_range64 pages(u32 first, u32 count) const + { + return utils::address_range64::start_length(reinterpret_cast(page(first)), count * m_page_size); + } + + void protect(u32 first, u32 count, utils::protection prot) + { + mm_protect(page(first), count * m_page_size, prot); + } + }; + + TEST_F(MMQueue, DeferredProtectIsQueuedUntilFlush) + { + protect(0, 1, utils::protection::no); + + ASSERT_EQ(g_deferred_mprotect_queue.size(), 1u); + EXPECT_EQ(g_deferred_mprotect_queue[0].range, pages(0, 1)); + EXPECT_EQ(g_deferred_mprotect_queue[0].prot, utils::protection::no); + + mm_flush(); + EXPECT_TRUE(g_deferred_mprotect_queue.empty()); + } + + TEST_F(MMQueue, MergesContiguousBlocks) + { + for (u32 i = 0; i < 4; i++) + { + protect(i, 1, utils::protection::ro); + } + + ASSERT_EQ(g_deferred_mprotect_queue.size(), 1u); + EXPECT_EQ(g_deferred_mprotect_queue[0].range, pages(0, 4)); + } + + TEST_F(MMQueue, MergesOverlappingBlocks) + { + protect(0, 2, utils::protection::ro); + protect(1, 2, utils::protection::ro); + + ASSERT_EQ(g_deferred_mprotect_queue.size(), 1u); + EXPECT_EQ(g_deferred_mprotect_queue[0].range, pages(0, 3)); + } + + TEST_F(MMQueue, DoesNotMergeAcrossProtections) + { + protect(0, 1, utils::protection::ro); + protect(1, 1, utils::protection::no); + + ASSERT_EQ(g_deferred_mprotect_queue.size(), 2u); + EXPECT_EQ(g_deferred_mprotect_queue[0].prot, utils::protection::ro); + EXPECT_EQ(g_deferred_mprotect_queue[1].prot, utils::protection::no); + } + + TEST_F(MMQueue, SwallowedBlockIsReplaced) + { + protect(2, 1, utils::protection::ro); + protect(1, 3, utils::protection::no); + + ASSERT_EQ(g_deferred_mprotect_queue.size(), 1u); + EXPECT_EQ(g_deferred_mprotect_queue[0].range, pages(1, 3)); + EXPECT_EQ(g_deferred_mprotect_queue[0].prot, utils::protection::no); + } + + TEST_F(MMQueue, UnlockFlushesPrefixAndKeepsTail) + { + protect(0, 1, utils::protection::ro); + protect(3, 2, utils::protection::no); + protect(8, 1, utils::protection::ro); + ASSERT_EQ(g_deferred_mprotect_queue.size(), 3u); + + protect(4, 1, utils::protection::rw); + + ASSERT_EQ(g_deferred_mprotect_queue.size(), 1u); + EXPECT_EQ(g_deferred_mprotect_queue[0].range, pages(8, 1)); + EXPECT_EQ(g_deferred_mprotect_queue[0].prot, utils::protection::ro); + } + + TEST_F(MMQueue, UnlockSwallowingWholeQueueLeavesNoResidue) + { + protect(1, 1, utils::protection::ro); + protect(3, 1, utils::protection::no); + ASSERT_EQ(g_deferred_mprotect_queue.size(), 2u); + + protect(0, 8, utils::protection::rw); + + ASSERT_TRUE(g_deferred_mprotect_queue.empty()); + + mm_flush(); + EXPECT_TRUE(g_deferred_mprotect_queue.empty()); + } + + TEST_F(MMQueue, FlushByRangeFlushesPrefixOnly) + { + protect(0, 1, utils::protection::ro); + protect(4, 1, utils::protection::no); + protect(8, 1, utils::protection::ro); + + mm_flush(rsx::simple_array{ pages(4, 1) }); + + ASSERT_EQ(g_deferred_mprotect_queue.size(), 1u); + EXPECT_EQ(g_deferred_mprotect_queue[0].range, pages(8, 1)); + } +} From 3db8cf5a37beaf36fbf5b8555086b72aaf504b0c Mon Sep 17 00:00:00 2001 From: kd-11 Date: Mon, 31 Aug 2026 00:03:55 +0300 Subject: [PATCH 6/6] rsx: Don't overfit memory allocations in system RAM - Avoids a lot of faults that would trigger slow flush operations when CELL is touching memory outside of the range we care about. --- rpcs3/Emu/RSX/Common/texture_cache_helpers.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rpcs3/Emu/RSX/Common/texture_cache_helpers.h b/rpcs3/Emu/RSX/Common/texture_cache_helpers.h index 51b29330fea9..807866eff3f0 100644 --- a/rpcs3/Emu/RSX/Common/texture_cache_helpers.h +++ b/rpcs3/Emu/RSX/Common/texture_cache_helpers.h @@ -232,8 +232,14 @@ namespace rsx if (src_is_render_target) { - // Attempt to optimize... - if (dst_dimensions.width == 1280 || dst_dimensions.width == 2560) [[likely]] + // Attempt to optimize for performance... + if (get_location(dst_range.start) == CELL_GCM_LOCATION_MAIN) + { + // Don't guess when working with main memory + const auto min_fitted_height = utils::aligned_div(dst_range.length(), dst_pitch); + dst_dimensions.height = std::min(dst_dimensions.height, min_fitted_height); + } + else if (dst_dimensions.width == 1280 || dst_dimensions.width == 2560) { // Optimizations table based on common width/height pairings. If we guess wrong, the upload resolver will fix it anyway // TODO: Add more entries based on empirical data