diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index cb33a343f..762c9b1d7 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -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) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 8c11f3ccd..d9184240f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -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> kept; std::vector> completions; }; -thread_local std::vector deferredBatches; +struct DeferredBatchesTag {}; +auto& DeferredBatches() { return HostThreadLocal, DeferredBatchesTag>(); } std::atomic 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> queuedLabelRanges; +struct QueuedLabelRangesTag {}; +auto& QueuedLabelRanges() { return HostThreadLocal>, QueuedLabelRangesTag>(); } std::atomic queuedLabelRecorder{nullptr}; // The dwords (with their stamps) this thread noted since its last CloseLabelGroup (see Recorder::CloseLabelGroup). -thread_local std::vector> labelGroupDwords; +struct LabelGroupDwordsTag {}; +auto& LabelGroupDwords() { return HostThreadLocal>, 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 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 } 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 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 (...) { } diff --git a/core/libs/prx/libc/include/HostThreadLocal.hpp b/core/libs/prx/libc/include/HostThreadLocal.hpp new file mode 100644 index 000000000..076459cc9 --- /dev/null +++ b/core/libs/prx/libc/include/HostThreadLocal.hpp @@ -0,0 +1,28 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTTHREADLOCAL_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTTHREADLOCAL_HPP + +#include +#include +#ifdef _WIN32 +#include +#endif + +template +TValue& HostThreadLocal() { +#ifdef _WIN32 + static const DWORD slot = FlsAlloc(+[](void* value) { delete static_cast(value); }); + if (slot == FLS_OUT_OF_INDEXES) throw std::runtime_error("host thread-local slot allocation failed"); + auto* value = static_cast(FlsGetValue(slot)); + if (value == nullptr) { + auto created = std::make_unique(); + 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 diff --git a/core/libs/tests/HostThreadLocal.cpp b/core/libs/tests/HostThreadLocal.cpp new file mode 100644 index 000000000..4fedf466a --- /dev/null +++ b/core/libs/tests/HostThreadLocal.cpp @@ -0,0 +1,24 @@ +#include +#include +#ifdef _WIN32 +#include +#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 +} diff --git a/core/libs/tests/HostThreadLocalLibrary.cpp b/core/libs/tests/HostThreadLocalLibrary.cpp new file mode 100644 index 000000000..9f11e94d2 --- /dev/null +++ b/core/libs/tests/HostThreadLocalLibrary.cpp @@ -0,0 +1,31 @@ +#include "prx/libc/include/HostThreadLocal.hpp" +#include +#include +#include + +namespace { +std::atomic destroyed{0}; +struct Value { + std::vector data = std::vector(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(); + auto& second = HostThreadLocal(); + if (&first == &second || &first != &HostThreadLocal()) std::abort(); +} + +extern "C" TEST_EXPORT unsigned DestroyedHostThreadLocals() { return destroyed.load(); }