diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7ec405492475..cbddfa86e2e7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2665,6 +2665,11 @@ if(BUILD_TESTING) add_test(NAME agc_shader_srt_read_patching COMMAND agc_shader_memory_tests --srt-read-patching) set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_table_snapshot_reuse COMMAND agc_shader_memory_tests --table-snapshot-reuse) + add_test(NAME agc_shader_region_merge COMMAND agc_shader_memory_tests --region-merge) + set_tests_properties(agc_shader_region_merge PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_shader_region_merge PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() set_tests_properties(agc_shader_table_snapshot_reuse PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_table_snapshot_reuse_unmasked COMMAND agc_shader_memory_tests --table-snapshot-reuse) set_tests_properties(agc_shader_table_snapshot_reuse_unmasked PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_MASKED_RESULT_MEMO=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp index c91decd2d2da..e1a7f3a69a34 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp @@ -787,24 +787,35 @@ std::vector ShaderMemory::collect(bool recentOnl std::vector cursors; std::size_t runs = 0; for (const auto& entry : supplied) { - if (recentOnly && !entry.recent) continue; + if ((recentOnly && !entry.recent) || entry.words->runs.empty()) continue; cursors.push_back({entry.words.get(), 0u, 0u}); runs += entry.words->runs.size() + entry.words->words.size() / PageWords; } std::vector result; result.reserve((recentOnly ? 0u : initial.size()) + pages.size() + runs); auto next = recentOnly ? initial.end() : initial.begin(); + const bool heap = cursors.size() > 1u; + const auto later = [](const Cursor& left, const Cursor& right) { + return left.words->runs[left.run].first > right.words->runs[right.run].first; + }; + if (heap) std::make_heap(cursors.begin(), cursors.end(), later); const auto emitBefore = [&](std::uint64_t address) { while (true) { - Cursor* lowest = nullptr; - for (auto& cursor : cursors) { - if (cursor.run < cursor.words->runs.size() && (lowest == nullptr || cursor.words->runs[cursor.run].first < lowest->words->runs[lowest->run].first)) lowest = &cursor; + Cursor* lowest = heap && !cursors.empty() ? &cursors.front() : nullptr; + if (!heap) { + for (auto& cursor : cursors) { + if (cursor.run < cursor.words->runs.size() && (lowest == nullptr || cursor.words->runs[cursor.run].first < lowest->words->runs[lowest->run].first)) lowest = &cursor; + } } const auto run = lowest != nullptr ? lowest->words->runs[lowest->run].first : std::numeric_limits::max(); if (next != initial.end() && next->first < address && next->first < run) { result.push_back({next->first, next->second}); ++next; } else if (lowest != nullptr && run < address) { + if (heap) { + std::pop_heap(cursors.begin(), cursors.end(), later); + lowest = &cursors.back(); + } const auto count = lowest->words->runs[lowest->run].second; const auto words = std::span(lowest->words->words).subspan(lowest->at, count); for (std::size_t done = 0; done < count;) { @@ -815,6 +826,10 @@ std::vector ShaderMemory::collect(bool recentOnl } lowest->at += count; ++lowest->run; + if (heap) { + if (lowest->run == lowest->words->runs.size()) cursors.pop_back(); + else std::push_heap(cursors.begin(), cursors.end(), later); + } } else { return; } diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 3efcc4e97acf..4780ab1d53d6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2582,6 +2582,80 @@ void ProtectTablePage(std::uintptr_t page, std::uint32_t protection) { GuestAllocations::GuestAllocationsInvalidate_nid_postfix(page, 4096u); } +void verifyRegionMerge(bool strided = false, bool benchmark = false) { + using namespace ShaderRecompiler; + constexpr std::size_t SlotBytes = 8192; + constexpr std::size_t TableBytes = 4096; + const std::size_t stride = strided ? 64u : 32u; + constexpr std::size_t MaximumBlocks = 64; + constexpr std::size_t Bytes = (MaximumBlocks + 8u) * SlotBytes; + auto* heap = AllocateWatchedTable(Bytes); + struct Release { + void* pointer; + ~Release() { GuestArena::GuestArenaRelease_nid_postfix(pointer, Bytes); } + } release{heap}; + std::vector guests(MaximumBlocks); + std::vector> userData(MaximumBlocks); + for (std::size_t block = 0; block < MaximumBlocks; ++block) { + auto* table = heap + block * SlotBytes / sizeof(std::uint32_t); + for (std::size_t record = 0; record < TableBytes / stride; ++record) { + const auto words = TableT(reinterpret_cast(0x100000000ull + record * 256u), TableFormat8888UNorm, TableType2D, 1u); + std::copy(words.begin(), words.end(), table + record * stride / sizeof(std::uint32_t)); + } + FillTableSrt(guests[block], table, stride, TableBytes / stride); + userData[block] = TableUserData(guests[block]); + } + const bool watched = AgcDriver::GuestMemory::CollectWrites(reinterpret_cast(heap), Bytes) != 0u; + for (const std::size_t count : {0u, 1u, 2u, 3u, 7u, 8u, 16u, 64u}) { + std::vector initial; + for (std::size_t block = 0; block <= count; ++block) { + for (std::size_t offset = TableBytes; offset < SlotBytes; offset += 64u) { + auto* word = heap + (block * SlotBytes + offset) / sizeof(std::uint32_t); + *word = static_cast(block * SlotBytes + offset); + initial.push_back({reinterpret_cast(word), std::as_bytes(std::span(word, 1))}); + } + } + AgcDriver::ShaderMemory memory(initial); + for (std::size_t position = 0; position < count; ++position) { + const auto block = (position * (count > 1u ? count - 1u : 1u)) % count; + const auto request = TableRequest(TableWholeCode, userData[block]); + { + AgcDriver::ShaderMemory warm({}); + static_cast(warm.Capture(request)); + } + static_cast(memory.Capture(request)); + } + if (watched) require(memory.SuppliedBlocks().size() == count, "region merge: the fixture did not retain every shared table"); + const auto verify = [&](const std::vector& regions, bool recent) { + for (std::size_t index = 0; index < regions.size(); ++index) { + const auto& region = regions[index]; + require(index == 0u || regions[index - 1u].guestAddress + regions[index - 1u].bytes.size() <= region.guestAddress, "region merge: regions were reordered or duplicated"); + require(std::memcmp(region.bytes.data(), reinterpret_cast(region.guestAddress), region.bytes.size()) == 0, "region merge: collected words changed"); + } + for (std::size_t block = 0; block < count; ++block) { + for (std::size_t record = 0; record < TableBytes / stride; ++record) require(TableCovered(regions, heap + (block * SlotBytes + record * stride) / sizeof(std::uint32_t), 32u), "region merge: a shared table record was omitted"); + } + for (const auto& entry : initial) require(TableCovered(regions, reinterpret_cast(entry.guestAddress), entry.bytes.size()) != recent, "region merge: initial-region visibility changed"); + }; + const auto regions = memory.Regions(); + verify(regions, false); + verify(memory.TakeRecentRegions(), true); + require(memory.TakeRecentRegions().empty(), "region merge: recent regions were not reset"); + verify(memory.Regions(), false); + if (!strided && benchmark) { + constexpr std::size_t Iterations = 1000; + std::size_t total = 0; + const auto started = std::chrono::steady_clock::now(); + for (std::size_t iteration = 0; iteration < Iterations; ++iteration) total += memory.Regions().size(); + const auto elapsed = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + require(total == Iterations * regions.size(), "region merge: repeated collection changed coverage"); + std::printf("region-merge blocks=%zu regions=%zu iterations=%zu us_per_call=%.3f\n", count, regions.size(), Iterations, elapsed / Iterations); + } + } + std::puts("region merge: mixed initial, captured and shared regions retain order, coverage and recent-read semantics"); +} + + void verifyTableSnapshotReuse() { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; @@ -9344,6 +9418,11 @@ int main(int argc, char** argv) { std::cout << "The result memo budget counts and trims the masked table's entries\n"; return 0; } + if (argc == 2 && (std::string_view(argv[1]) == "--region-merge" || std::string_view(argv[1]) == "--region-merge-bench")) { + verifyRegionMerge(false, std::string_view(argv[1]) == "--region-merge-bench"); + verifyRegionMerge(true); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--result-memo-budget-threads") { verifyResultMemoBudgetThreads(); std::cout << "The result memo ends within its byte budget when results are inserted during a trim\n";