fix(agc): preserve recorder buffers through Windows thread teardown

This commit is contained in:
SamG-Coder 2026-09-28 09:55:01 +10:00
parent 9814c93f75
commit e424b6b7bf
5 changed files with 117 additions and 20 deletions

View File

@ -109,6 +109,16 @@ endforeach()
add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS})
if(BUILD_TESTING)
add_library(host_thread_local_test_library SHARED tests/HostThreadLocalLibrary.cpp)
target_include_directories(host_thread_local_test_library PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_executable(host_thread_local_tests tests/HostThreadLocal.cpp)
target_link_libraries(host_thread_local_tests PRIVATE host_thread_local_test_library)
if(MINGW)
target_compile_options(host_thread_local_test_library PRIVATE -fasynchronous-unwind-tables)
target_compile_options(host_thread_local_tests PRIVATE -fasynchronous-unwind-tables)
endif()
add_test(NAME host_thread_local COMMAND host_thread_local_tests)
set_tests_properties(host_thread_local PROPERTIES TIMEOUT 15)
add_executable(texture_format_lookup_tests tests/TextureFormatLookup.cpp)
target_include_directories(texture_format_lookup_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include)
target_link_libraries(texture_format_lookup_tests PRIVATE libSceAgcDriver libc)

View File

@ -1,3 +1,4 @@
#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/Texture.hpp"
@ -276,7 +277,8 @@ struct DeferredBatch {
std::vector<std::shared_ptr<void>> kept;
std::vector<std::function<void()>> completions;
};
thread_local std::vector<DeferredBatch> deferredBatches;
struct DeferredBatchesTag {};
auto& DeferredBatches() { return HostThreadLocal<std::vector<DeferredBatch>, DeferredBatchesTag>(); }
std::atomic<std::uint64_t> deferredPending{0};
// APS5_PROFILE_DRAW, for the [recorder] line: batches and objects released after an unlock and the
// time that took, on the release thread and inline (on the unlocking thread: the kill switch, a
@ -392,11 +394,11 @@ void JoinReleaseThread() {
// The unlock hook: hands what this thread deferred to the release thread, or destroys it here
// (APS5_RELEASE_ON_UNLOCK=1, the queue is full or stopping, or the hand-off cannot be made).
void ReleaseDeferredKeeps() {
if (deferredBatches.empty()) return;
if (DeferredBatches().empty()) return;
// Taken off the thread's list first: a destructor that took and released the mutex (none is
// known to) would re-enter here and must find nothing.
auto releasing = std::move(deferredBatches);
deferredBatches.clear();
auto releasing = std::move(DeferredBatches());
DeferredBatches().clear();
if (ReleaseOnUnlock()) {
DestroyDeferred(std::move(releasing), false);
return;
@ -475,10 +477,12 @@ Recorder* labelTableOwner = nullptr;
// The ranges of the calling worker's queued labels (NoteQueuedLabel, cleared by
// ForgetQueuedLabels): the flush hook, on the same thread, has them recorded before an access that
// overlaps one, through the function the driver installed.
thread_local std::vector<std::pair<std::uint64_t, std::uint64_t>> queuedLabelRanges;
struct QueuedLabelRangesTag {};
auto& QueuedLabelRanges() { return HostThreadLocal<std::vector<std::pair<std::uint64_t, std::uint64_t>>, QueuedLabelRangesTag>(); }
std::atomic<void (*)()> queuedLabelRecorder{nullptr};
// The dwords (with their stamps) this thread noted since its last CloseLabelGroup (see Recorder::CloseLabelGroup).
thread_local std::vector<std::pair<std::uint64_t, std::uint64_t>> labelGroupDwords;
struct LabelGroupDwordsTag {};
auto& LabelGroupDwords() { return HostThreadLocal<std::vector<std::pair<std::uint64_t, std::uint64_t>>, LabelGroupDwordsTag>(); }
// Late-rule counters (Recorder::LateCounts), per thread: lookups run on the waiting worker.
thread_local Recorder::LateStatistics lateCounts{};
@ -548,7 +552,7 @@ bool SeparateQueuedLabels() {
bool QueuedLabelOverlaps(std::uint64_t address, std::size_t bytes) {
const auto end = address + bytes;
for (const auto& [begin, finish] : queuedLabelRanges) {
for (const auto& [begin, finish] : QueuedLabelRanges()) {
if (address < finish && begin < end) return true;
}
return false;
@ -943,7 +947,7 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) {
// would reap nested in the reap in progress, landing later batches' write-backs before the
// rest of this one's, and this queue's queued label follows every batch of its own in flight
// anyway, so the write-back landing first is the order the hardware gives.
if (!queuedLabelRanges.empty() && QueuedLabelOverlaps(address, bytes)) {
if (!QueuedLabelRanges().empty() && QueuedLabelOverlaps(address, bytes)) {
if (completionDepth != 0 && HookCompletionGuard()) {
queuedLabelHookInCompletion.fetch_add(1, std::memory_order_relaxed);
} else if (auto* record = queuedLabelRecorder.load(std::memory_order_acquire); record != nullptr) {
@ -1074,9 +1078,9 @@ Recorder::~Recorder() {
// other threads (an unlock hook that found the queue stopping or full) are waited for before
// the caller destroys the device. The count is global: a newer recorder's batches (device
// replacement) are waited for too, which only prolongs the spin.
if (!deferredBatches.empty()) {
auto own = std::move(deferredBatches);
deferredBatches.clear();
if (!DeferredBatches().empty()) {
auto own = std::move(DeferredBatches());
DeferredBatches().clear();
DestroyDeferred(std::move(own), false);
}
JoinReleaseThread();
@ -1160,9 +1164,9 @@ Recorder::LateStatistics Recorder::LateCounts() {
}
void Recorder::CloseLabelGroup(std::uint64_t trackerGeneration) {
if (labelGroupDwords.empty()) return;
auto group = std::move(labelGroupDwords);
labelGroupDwords.clear();
if (LabelGroupDwords().empty()) return;
auto group = std::move(LabelGroupDwords());
LabelGroupDwords().clear();
if (trackerGeneration == 0) return;
std::lock_guard tableLock(labelTableMutex);
if (labelTableOwner == nullptr) return;
@ -1183,7 +1187,7 @@ void Recorder::NoteQueuedLabel(std::uint64_t address, std::span<const std::byte>
if (bytes.size() < 4 || address % 4 != 0) return;
// The range before the table: the hook covers the group whether or not a table exists to
// enter it in (no owner: the callback records it outright, by a CPU store when need be).
queuedLabelRanges.emplace_back(address, address + bytes.size());
QueuedLabelRanges().emplace_back(address, address + bytes.size());
queuedLabelsNoted.fetch_add(1, std::memory_order_relaxed);
std::lock_guard tableLock(labelTableMutex);
if (labelTableOwner == nullptr) return;
@ -1199,9 +1203,9 @@ void Recorder::NoteQueuedLabel(std::uint64_t address, std::span<const std::byte>
}
void Recorder::ForgetQueuedLabels() {
if (queuedLabelRanges.empty()) return;
auto ranges = std::move(queuedLabelRanges);
queuedLabelRanges.clear();
if (QueuedLabelRanges().empty()) return;
auto ranges = std::move(QueuedLabelRanges());
QueuedLabelRanges().clear();
std::lock_guard tableLock(labelTableMutex);
if (labelTableOwner == nullptr) return;
auto& recorded = labelTableOwner->labels;
@ -2124,7 +2128,7 @@ void Recorder::noteLabelOn(Batch& batch, std::uint64_t address, std::span<const
const auto dword = address + offset;
labels.insert_or_assign(dword, LabelEntry{value, queue, stamp, &batch, 0, false, behindCompletion});
batch.labelDwords.push_back(dword);
labelGroupDwords.emplace_back(dword, stamp);
LabelGroupDwords().emplace_back(dword, stamp);
}
recordedLabels.store(labels.size(), std::memory_order_relaxed);
}
@ -2855,7 +2859,7 @@ void Recorder::finish(std::unique_ptr<Batch> batch, bool wait, int source) {
// destroys the batch's objects here, under the mutex, counts nothing the destructor would
// wait for and must not escape (the rest of finish() releases the fence and command buffer).
try {
deferredBatches.push_back({std::move(batch->kept), std::move(batch->completions)});
DeferredBatches().push_back({std::move(batch->kept), std::move(batch->completions)});
deferredPending.fetch_add(1, std::memory_order_acq_rel);
} catch (...) {
}

View File

@ -0,0 +1,28 @@
#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTTHREADLOCAL_HPP
#define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTTHREADLOCAL_HPP
#include <memory>
#include <stdexcept>
#ifdef _WIN32
#include <windows.h>
#endif
template<typename TValue, typename TTag>
TValue& HostThreadLocal() {
#ifdef _WIN32
static const DWORD slot = FlsAlloc(+[](void* value) { delete static_cast<TValue*>(value); });
if (slot == FLS_OUT_OF_INDEXES) throw std::runtime_error("host thread-local slot allocation failed");
auto* value = static_cast<TValue*>(FlsGetValue(slot));
if (value == nullptr) {
auto created = std::make_unique<TValue>();
if (!FlsSetValue(slot, created.get())) throw std::runtime_error("host thread-local value registration failed");
value = created.release();
}
return *value;
#else
static thread_local TValue value;
return value;
#endif
}
#endif

View File

@ -0,0 +1,24 @@
#include <cstdlib>
#include <thread>
#ifdef _WIN32
#include <windows.h>
#endif
extern "C" void TouchHostThreadLocal();
extern "C" unsigned DestroyedHostThreadLocals();
int main() {
for (unsigned i = 0; i < 64; ++i) {
std::thread worker([] { TouchHostThreadLocal(); TouchHostThreadLocal(); });
worker.join();
if (DestroyedHostThreadLocals() != (i + 1) * 2) std::abort();
}
#ifdef _WIN32
for (unsigned i = 0; i < 64; ++i) {
const auto worker = CreateThread(nullptr, 0, +[](void*) -> DWORD { TouchHostThreadLocal(); return 0; }, nullptr, 0, nullptr);
if (!worker || WaitForSingleObject(worker, 5000) != WAIT_OBJECT_0) std::abort();
CloseHandle(worker);
if (DestroyedHostThreadLocals() != 128 + (i + 1) * 2) std::abort();
}
#endif
}

View File

@ -0,0 +1,31 @@
#include "prx/libc/include/HostThreadLocal.hpp"
#include <atomic>
#include <cstdlib>
#include <vector>
namespace {
std::atomic<unsigned> destroyed{0};
struct Value {
std::vector<unsigned> data = std::vector<unsigned>(256, 42);
~Value() {
for (auto item : data) if (item != 42) std::abort();
++destroyed;
}
};
struct First {};
struct Second {};
}
#ifdef _WIN32
#define TEST_EXPORT __declspec(dllexport)
#else
#define TEST_EXPORT
#endif
extern "C" TEST_EXPORT void TouchHostThreadLocal() {
auto& first = HostThreadLocal<Value, First>();
auto& second = HostThreadLocal<Value, Second>();
if (&first == &second || &first != &HostThreadLocal<Value, First>()) std::abort();
}
extern "C" TEST_EXPORT unsigned DestroyedHostThreadLocals() { return destroyed.load(); }