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:
parent
7c1b60c544
commit
db2cd3559e
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue