From 23c91d948e776bdd85e3744106ad8ad02a0055b5 Mon Sep 17 00:00:00 2001 From: "john.maia" Date: Fri, 9 Oct 2026 23:51:22 +0100 Subject: [PATCH] perf(agc): store write-back changes sixteen bytes at a time GuestMemory::WriteChanged stores only the bytes the GPU changed, so that CPU writes to the bytes around them survive. It found them a byte at a time inside every changed 256-byte block and stored each run with memcpy. Where changed and unchanged bytes interleave (the low bytes of floats), every run is a few bytes long, and that loop took 43% of the GPU submission thread in Disney Epic Mickey: Rebrushed's first level (0.9-1.0 ms per MiB). Each 16-byte chunk is now compared with SSE2. A chunk with no change is skipped, a chunk that changed entirely is stored whole, and a mixed one is stored with MASKMOVDQU, which writes only the bytes selected by the mask. An SFENCE after the loop orders those non-temporal stores before the stores that follow. Bytes the GPU did not change are still never written. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0134RWCyiyydJBbfKz8g9Bwe --- .../Execution/src/GuestMemory.cpp | 37 ++++++++++--------- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 23 ++++++++++++ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 6f0bc11d2823..5029a9422e3a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #include #include @@ -1875,28 +1877,29 @@ void WriteChanged(std::uint64_t address, std::span current, std const TimedAccess timed(CounterChangedWrite, current.size()); auto* destination = reinterpret_cast(address); CheckRange(destination, current.size(), 1, true); - constexpr std::size_t block = 256; const auto size = current.size(); - const auto differs = [&](std::size_t at) { - const auto length = std::min(block, size - at); - return std::memcmp(current.data() + at, original.data() + at, length) != 0; - }; // Stamped like a GPU write: a collect memoized for this packet would not see the page fault. storeOwn(address, size, [&] { std::size_t firstChanged = size; std::size_t lastChanged = 0; - for (std::size_t at = 0; at < size; at += block) { - if (!differs(at)) continue; - const auto blockEnd = std::min(at + block, size); - for (std::size_t run = at; run < blockEnd;) { - if (current[run] == original[run]) { ++run; continue; } - auto runEnd = run + 1; - while (runEnd < blockEnd && current[runEnd] != original[runEnd]) ++runEnd; - std::memcpy(destination + run, current.data() + run, runEnd - run); - firstChanged = std::min(firstChanged, run); - lastChanged = std::max(lastChanged, runEnd); - run = runEnd; - } + std::size_t at = 0; + for (; at + 16 <= size; at += 16) { + const auto bytes = _mm_loadu_si128(reinterpret_cast(current.data() + at)); + const auto same = _mm_cmpeq_epi8(bytes, _mm_loadu_si128(reinterpret_cast(original.data() + at))); + const auto equal = static_cast(_mm_movemask_epi8(same)); + if (equal == 0xffffu) continue; + if (equal == 0) _mm_storeu_si128(reinterpret_cast<__m128i*>(destination + at), bytes); + else _mm_maskmoveu_si128(bytes, _mm_xor_si128(same, _mm_set1_epi8(-1)), reinterpret_cast(destination + at)); + const auto changed = ~equal & 0xffffu; + firstChanged = std::min(firstChanged, at + static_cast(std::countr_zero(changed))); + lastChanged = std::max(lastChanged, at + 32 - static_cast(std::countl_zero(changed))); + } + _mm_sfence(); + for (; at < size; ++at) { + if (current[at] == original[at]) continue; + destination[at] = current[at]; + firstChanged = std::min(firstChanged, at); + lastChanged = std::max(lastChanged, at + 1); } return firstChanged < lastChanged ? std::pair{address + firstChanged, address + lastChanged} : std::pair{0, 0}; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index e6ef42a268a9..63c7ca3cfca8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -537,6 +537,28 @@ void testWriteChangedKeepsUntouchedBytes() { check(guest[3] == 7 && guest[100] == 0x55, "write-back rolled back a byte the GPU did not change"); } +void testWriteChangedStoresExactlyTheChangedRuns() { + alignas(256) static std::uint8_t guest[1024 + 13]; + std::vector original(sizeof(guest)), current(sizeof(guest)); + std::uint32_t seed = 12345; + const auto next = [&] { return seed = seed * 1103515245u + 12345u, seed >> 16u; }; + for (std::size_t i = 0; i < sizeof(guest);) { + const auto length = 1 + next() % 40; + const bool changed = next() % 2 == 0; + for (std::size_t j = i; j < std::min(sizeof(guest), i + length); ++j) { + original[j] = static_cast(next()); + current[j] = changed && next() % 9 != 0 ? static_cast(static_cast(original[j]) ^ (1u + next() % 255u)) : original[j]; + } + i += length; + } + for (std::size_t i = 0; i < sizeof(guest); ++i) guest[i] = static_cast(0xa0u ^ i); + AgcDriver::GuestMemory::WriteChanged(reinterpret_cast(guest), current, original); + for (std::size_t i = 0; i < sizeof(guest); ++i) { + const auto expected = current[i] != original[i] ? static_cast(current[i]) : static_cast(0xa0u ^ i); + check(guest[i] == expected, "write-back stored a byte the GPU did not change or missed one it did"); + } +} + void testEventWrite() { for (const auto eventType : {0x07u, 0x0fu, 0x10u}) { AgcDriver::Pm4::Validate(makePacket(0x46, {0x400u | eventType}), 0); @@ -1049,6 +1071,7 @@ int main(int argc, char** argv) { testCatalog(); testNopPad(); testWriteChangedKeepsUntouchedBytes(); + testWriteChangedStoresExactlyTheChangedRuns(); testRegisters(); testRegisterFile(); testContextAndBases();