perf(agc): compare large stale draw snapshots on helper threads

A stale storage snapshot is revalidated by comparing every tracker block
stamped since it was taken. A title that binds buffers a dispatch writes
in place every frame (the whole declared range stamped, few pages really
changed) makes the graphics worker compare gigabytes per second.

Compares of 1 MiB or more are now split by block across a small pool of
helper threads (hardware threads / 8, from 1 to 4). The compare is memory
bound, so the worker's share roughly halves rather than dividing by the
thread count. A caller that finds the pool busy compares on its own
thread. Results and the patch-in-place rules are unchanged.

agc_driver_graphics_tests parallelCompareTests: per-span results on the
pool and serial paths, and two threads comparing at once.
This commit is contained in:
Dean Galvin 2026-10-02 19:31:03 +13:00
parent 7c1b60c544
commit db2cd3559e
5 changed files with 178 additions and 2 deletions

View File

@ -168,6 +168,7 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineCache.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Resources.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Recorder.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ParallelCompare.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderInputState.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderResources.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderAddressResources.cpp
@ -752,6 +753,7 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL
Graphics/src/UnitShadow.cpp
Graphics/src/Sampler.cpp
Graphics/src/Recorder.cpp
Graphics/src/ParallelCompare.cpp
Graphics/src/ShaderInputState.cpp
${agcTextureDetileShaderHeader}
${agcCmaskClearShaderHeader}

View File

@ -0,0 +1,22 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PARALLELCOMPARE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PARALLELCOMPARE_HPP
#include <cstddef>
#include <cstdint>
#include <span>
namespace AgcDriver::Graphics {
struct CompareSpan {
const void* first;
const void* second;
std::size_t bytes;
};
void CompareSpans(std::span<const CompareSpan> spans, std::span<std::uint8_t> equal);
void CompareSpansFrom(std::span<const CompareSpan> spans, std::span<std::uint8_t> equal, std::size_t parallelBytes);
unsigned CompareHelpers();
}
#endif

View File

@ -0,0 +1,104 @@
#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp"
#include <algorithm>
#include <atomic>
#include <condition_variable>
#include <cstring>
#include <mutex>
#include <thread>
namespace AgcDriver::Graphics {
namespace {
class ComparePool {
public:
explicit ComparePool(unsigned count) {
for (unsigned i = 0; i < count; ++i) std::thread([this] { work(); }).detach();
}
bool Run(std::span<const CompareSpan> spans, std::span<std::uint8_t> equal) {
std::unique_lock caller(callers, std::try_to_lock);
if (!caller.owns_lock()) return false;
{
std::unique_lock lock(mutex);
finished.wait(lock, [&] { return working == 0; });
jobSpans = spans;
jobEqual = equal;
next.store(0, std::memory_order_relaxed);
done.store(0, std::memory_order_relaxed);
++generation;
}
wake.notify_all();
drain(spans, equal);
std::unique_lock lock(mutex);
finished.wait(lock, [&] { return working == 0 && done.load(std::memory_order_acquire) == spans.size(); });
jobSpans = {};
jobEqual = {};
return true;
}
private:
void drain(std::span<const CompareSpan> spans, std::span<std::uint8_t> equal) {
std::size_t compared = 0;
for (auto index = next.fetch_add(1, std::memory_order_relaxed); index < spans.size(); index = next.fetch_add(1, std::memory_order_relaxed)) {
const auto& span = spans[index];
equal[index] = std::memcmp(span.first, span.second, span.bytes) == 0 ? 1 : 0;
++compared;
}
if (compared != 0) done.fetch_add(compared, std::memory_order_acq_rel);
}
void work() {
std::uint64_t seen = 0;
std::unique_lock lock(mutex);
for (;;) {
wake.wait(lock, [&] { return generation != seen; });
seen = generation;
const auto spans = jobSpans;
const auto equal = jobEqual;
++working;
lock.unlock();
drain(spans, equal);
lock.lock();
--working;
if (working == 0) finished.notify_all();
}
}
std::mutex callers;
std::mutex mutex;
std::condition_variable wake;
std::condition_variable finished;
std::uint64_t generation = 0;
unsigned working = 0;
std::span<const CompareSpan> jobSpans;
std::span<std::uint8_t> jobEqual;
std::atomic<std::size_t> next{0};
std::atomic<std::size_t> done{0};
};
ComparePool* sharedPool() {
static ComparePool* const pool = new ComparePool(CompareHelpers());
return pool;
}
}
unsigned CompareHelpers() {
static const unsigned helpers = std::clamp(std::thread::hardware_concurrency() / 8u, 1u, 4u);
return helpers;
}
void CompareSpansFrom(std::span<const CompareSpan> spans, std::span<std::uint8_t> equal, std::size_t parallelBytes) {
std::size_t total = 0;
for (const auto& span : spans) total += span.bytes;
if (spans.size() > 1 && total >= parallelBytes) {
if (sharedPool()->Run(spans, equal)) return;
}
for (std::size_t index = 0; index < spans.size(); ++index) equal[index] = std::memcmp(spans[index].first, spans[index].second, spans[index].bytes) == 0 ? 1 : 0;
}
void CompareSpans(std::span<const CompareSpan> spans, std::span<std::uint8_t> equal) {
CompareSpansFrom(spans, equal, std::size_t{1} << 20u);
}
}

View File

@ -3,6 +3,7 @@
#include "prx/libc/include/HostThreadLocal.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/shaders/MeshArguments_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/SampleCounter_spv.h"
@ -2488,9 +2489,12 @@ bool Recorder::refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, s
thread_local std::vector<std::uint64_t> generations;
thread_local std::vector<std::uint8_t> changed;
thread_local std::vector<std::pair<std::size_t, std::size_t>> differing;
thread_local std::vector<CompareSpan> spans;
thread_local std::vector<std::uint8_t> equal;
generations.assign(count, entry.generation);
changed.assign(count, GuestMemory::BlockWritten);
differing.clear();
spans.clear();
const bool sole = entry.buffer.use_count() == 1;
GuestMemory::ChangedBlocks(address, bytes, generations, changed);
for (std::size_t k = 0; k < count; ++k) {
@ -2498,8 +2502,14 @@ bool Recorder::refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, s
const auto from = std::max(address, aligned + k * block);
const auto to = std::min(end, aligned + (k + 1) * block);
const auto offset = static_cast<std::size_t>(from - address);
const auto length = static_cast<std::size_t>(to - from);
if (std::memcmp(held.data() + offset, reinterpret_cast<const void*>(from), length) == 0) continue;
spans.push_back({held.data() + offset, reinterpret_cast<const void*>(from), static_cast<std::size_t>(to - from)});
}
equal.assign(spans.size(), 0);
CompareSpans(spans, equal);
for (std::size_t k = 0; k < spans.size(); ++k) {
if (equal[k] != 0) continue;
const auto offset = static_cast<std::size_t>(static_cast<const std::byte*>(spans[k].first) - held.data());
const auto length = spans[k].bytes;
if (!sole) return false;
if (!differing.empty() && differing.back().first + differing.back().second == offset) differing.back().second += length;
else differing.emplace_back(offset, length);

View File

@ -9,6 +9,7 @@
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "SceShaders.hpp"
@ -33,6 +34,7 @@
#include <string_view>
#include <tuple>
#include <type_traits>
#include <thread>
#include <vector>
namespace {
@ -3890,6 +3892,41 @@ void vertexZeroFillMergeTests() {
Require(solo.size() == 1 && solo[0] == 1, "a fetch past the records is copied alone");
}
void parallelCompareTests() {
using AgcDriver::Graphics::CompareSpan;
using AgcDriver::Graphics::CompareSpansFrom;
constexpr std::size_t block = 65536;
constexpr std::size_t count = 48;
std::vector<std::byte> first(block * count + 100, std::byte{7});
auto second = first;
const std::array<std::size_t, 4> changedAt{3 * block + 17, 20 * block, 33 * block + block - 1, 47 * block + 120};
for (const auto at : changedAt) second[at] = std::byte{9};
std::vector<CompareSpan> spans;
for (std::size_t k = 0; k < count; ++k) {
const auto length = k + 1 == count ? block + 100 : block;
spans.push_back({first.data() + k * block, second.data() + k * block, length});
}
const auto expected = [&](std::size_t k) {
return std::none_of(changedAt.begin(), changedAt.end(), [&](std::size_t at) { return at >= k * block && at < k * block + spans[k].bytes; });
};
for (const std::size_t threshold : {std::size_t{0}, std::size_t{1} << 40u}) {
for (int round = 0; round < 20; ++round) {
std::vector<std::uint8_t> equal(count, 2);
CompareSpansFrom(spans, equal, threshold);
for (std::size_t k = 0; k < count; ++k) Require((equal[k] == 1) == expected(k) && equal[k] <= 1, "parallel compare misreported a span");
}
}
std::vector<std::uint8_t> left(count, 2), right(count, 2);
std::thread other([&] {
for (int round = 0; round < 50; ++round) CompareSpansFrom(spans, right, 0);
});
for (int round = 0; round < 50; ++round) CompareSpansFrom(spans, left, 0);
other.join();
for (std::size_t k = 0; k < count; ++k) Require((left[k] == 1) == expected(k) && (right[k] == 1) == expected(k), "concurrent parallel compares misreported a span");
std::vector<std::uint8_t> none;
CompareSpansFrom({}, none, 0);
}
int main() {
#ifdef _WIN32
_putenv_s("APS5_PIN_WAIT_MS", "200");
@ -3981,6 +4018,7 @@ int main() {
nullVertexDescriptorTests();
vertexZeroFillTests();
vertexZeroFillMergeTests();
parallelCompareTests();
pixelParameterSlotTests();
rectListTests();
floatControlsModeTests();