diff --git a/Makefile.common b/Makefile.common index 5c8d8c81cc..0a1d9d90f2 100644 --- a/Makefile.common +++ b/Makefile.common @@ -1159,7 +1159,6 @@ DOLPHIN_SOURCES_CXX += \ $(CORE_DIR)/Source/Core/Common/NandPaths.cpp \ $(CORE_DIR)/Source/Core/Common/Network.cpp \ $(CORE_DIR)/Source/Core/Common/PcapFile.cpp \ - $(CORE_DIR)/Source/Core/Common/Profiler.cpp \ $(CORE_DIR)/Source/Core/Common/Random.cpp \ $(CORE_DIR)/Source/Core/Common/SDCardUtil.cpp \ $(CORE_DIR)/Source/Core/Common/SettingsHandler.cpp \ @@ -1204,7 +1203,6 @@ DOLPHIN_SOURCES_CXX += \ $(CORE_DIR)/Source/Core/DiscIO/VolumeDisc.cpp \ $(CORE_DIR)/Source/Core/DiscIO/VolumeFileBlobReader.cpp \ $(CORE_DIR)/Source/Core/DiscIO/VolumeGC.cpp \ - $(CORE_DIR)/Source/Core/DiscIO/VolumeVerifier.cpp \ $(CORE_DIR)/Source/Core/DiscIO/VolumeWad.cpp \ $(CORE_DIR)/Source/Core/DiscIO/VolumeWii.cpp \ $(CORE_DIR)/Source/Core/DiscIO/WbfsBlob.cpp \ @@ -1440,7 +1438,6 @@ DOLPHIN_SOURCES_CXX += \ $(CORE_DIR)/Source/Core/Core/DSPEmulator.cpp \ $(CORE_DIR)/Source/Core/Core/FifoPlayer/FifoDataFile.cpp \ $(CORE_DIR)/Source/Core/Core/FifoPlayer/FifoPlayer.cpp \ - $(CORE_DIR)/Source/Core/Core/FifoPlayer/FifoRecorder.cpp \ $(CORE_DIR)/Source/Core/Core/FreeLookManager.cpp \ $(CORE_DIR)/Source/Core/Core/GeckoCode.cpp \ $(CORE_DIR)/Source/Core/Core/GeckoCodeConfig.cpp \ @@ -1845,8 +1842,7 @@ ifeq ($(OS_ANDROID),1) $(CORE_DIR)/Source/Core/Common/Logging/ConsoleListenerDroid.cpp \ $(CORE_DIR)/Source/Core/Common/MemArenaAndroid.cpp \ $(CORE_DIR)/Source/Core/Common/GL/GLInterface/EGL.cpp \ - $(CORE_DIR)/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp \ - $(CORE_DIR)/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.cpp + $(CORE_DIR)/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp endif ifeq ($(OS_APPLE),1) DOLPHIN_SOURCES_CXX += \ diff --git a/Source/Core/Common/AtomicHashMap.h b/Source/Core/Common/AtomicHashMap.h new file mode 100644 index 0000000000..5453af2d68 --- /dev/null +++ b/Source/Core/Common/AtomicHashMap.h @@ -0,0 +1,226 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +#include "Common/Quiesce.h" + +namespace Common +{ +// A map any thread may read and write without a lock, for keys that are only ever added (a +// value can be replaced or removed, the key's slot stays). Each bucket is a list pushed at +// its head with a CAS; a value is a published copy, swapped whole and freed once no reader +// is inside the map's quiesce. +template > +class AtomicHashMap final +{ +public: + AtomicHashMap() = default; + AtomicHashMap(const AtomicHashMap&) = delete; + AtomicHashMap& operator=(const AtomicHashMap&) = delete; + + ~AtomicHashMap() + { + for (auto& bucket : m_buckets) + { + auto* node = static_cast(retro_atomic_load_acquire_ptr(&bucket)); + while (node) + { + Node* const next = node->next; + delete static_cast(retro_atomic_load_acquire_ptr(&node->value)); + delete node; + node = next; + } + } + quiesce_deinit(&m_quiesce); + } + + // A copy of the value for key, if it has one. + template + std::optional Get(const Key& key) const + { + quiesce_enter(&m_quiesce); + std::optional result; + if (const Node* node = Find(Bucket(key), key, nullptr)) + { + if (const auto* value = static_cast(retro_atomic_load_acquire_ptr(&node->value))) + result = value->value; + } + quiesce_exit(&m_quiesce); + return result; + } + + void Set(const K& key, V value) { Replace(key, new Value{{}, std::move(value)}); } + + template + void Erase(const Key& key) + { + quiesce_enter(&m_quiesce); + if (Node* node = Find(Bucket(key), key, nullptr)) + Retire(retro_atomic_exchange_ptr(&node->value, nullptr)); + quiesce_exit(&m_quiesce); + } + +private: + struct Value + { + quiesce_node_t node; // first: the free callback casts it back + V value; + }; + struct Node + { + Node* next; // fixed once published + K key; + retro_atomic_ptr_t value; // Value*, or null when removed + }; + + template + retro_atomic_ptr_t* Bucket(const Key& key) const + { + return &m_buckets[Hash{}(key) % BUCKETS]; + } + + template + static Node* Find(retro_atomic_ptr_t* bucket, const Key& key, const Node* stop) + { + for (auto* node = static_cast(retro_atomic_load_acquire_ptr(bucket)); node != stop; + node = node->next) + { + if (node->key == key) + return node; + } + return nullptr; + } + + static void FreeValue(quiesce_node_t* node) { delete reinterpret_cast(node); } + + void Retire(void* value) + { + if (value) + quiesce_retire(&m_quiesce, &static_cast(value)->node, FreeValue); + } + + void Replace(const K& key, Value* value) + { + quiesce_enter(&m_quiesce); + retro_atomic_ptr_t* const bucket = Bucket(key); + Node* node = Find(bucket, key, nullptr); + if (!node) + { + auto* fresh = new Node{static_cast(retro_atomic_load_acquire_ptr(bucket)), key, {}}; + retro_atomic_ptr_init(&fresh->value, nullptr); + for (;;) + { + if (retro_atomic_cas_ptr(bucket, fresh->next, fresh)) + { + node = fresh; + break; + } + // Another thread pushed meanwhile, maybe this very key. + Node* const head = static_cast(retro_atomic_load_acquire_ptr(bucket)); + if ((node = Find(bucket, key, fresh->next)) != nullptr) + { + delete fresh; + break; + } + fresh->next = head; + } + } + Retire(retro_atomic_exchange_ptr(&node->value, value)); + quiesce_exit(&m_quiesce); + } + + mutable retro_atomic_ptr_t m_buckets[BUCKETS]{}; + mutable quiesce_t m_quiesce{}; +}; + +// The bare form: each key gets one atomic pointer slot, inserted on first use and never +// removed, and the caller decides what the pointer means and when what it points to may go. +template > +class AtomicSlotMap final +{ +public: + AtomicSlotMap() = default; + AtomicSlotMap(const AtomicSlotMap&) = delete; + AtomicSlotMap& operator=(const AtomicSlotMap&) = delete; + + ~AtomicSlotMap() + { + for (auto& bucket : m_buckets) + { + auto* node = static_cast(retro_atomic_load_acquire_ptr(&bucket)); + while (node) + { + Node* const next = node->next; + delete node; + node = next; + } + } + } + + // The slot for key, made empty (null) the first time. + retro_atomic_ptr_t* Slot(const K& key) + { + retro_atomic_ptr_t* const bucket = &m_buckets[Hash{}(key) % BUCKETS]; + auto* head = static_cast(retro_atomic_load_acquire_ptr(bucket)); + if (Node* node = Find(head, nullptr, key)) + return &node->slot; + + auto* fresh = new Node{head, key, {}}; + retro_atomic_ptr_init(&fresh->slot, nullptr); + while (!retro_atomic_cas_ptr(bucket, fresh->next, fresh)) + { + // Another thread pushed meanwhile, maybe this very key. + auto* newer = static_cast(retro_atomic_load_acquire_ptr(bucket)); + if (Node* node = Find(newer, fresh->next, key)) + { + delete fresh; + return &node->slot; + } + fresh->next = newer; + } + return &fresh->slot; + } + + // Calls fn(slot) for every slot; for teardown, when no other thread uses the map. + template + void ForEach(Fn&& fn) + { + for (auto& bucket : m_buckets) + { + for (auto* node = static_cast(retro_atomic_load_acquire_ptr(&bucket)); node; + node = node->next) + { + fn(&node->slot); + } + } + } + +private: + struct Node + { + Node* next; // fixed once published + K key; + retro_atomic_ptr_t slot; + }; + + static Node* Find(Node* node, const Node* stop, const K& key) + { + for (; node != stop; node = node->next) + { + if (node->key == key) + return node; + } + return nullptr; + } + + retro_atomic_ptr_t m_buckets[BUCKETS]{}; +}; +} // namespace Common diff --git a/Source/Core/Common/CallGate.cpp b/Source/Core/Common/CallGate.cpp index 790013ca5c..75789ea3dd 100644 --- a/Source/Core/Common/CallGate.cpp +++ b/Source/Core/Common/CallGate.cpp @@ -36,6 +36,9 @@ bool CallGateEnter(CallGate* gate) // Counted before the check, the closer's store before its read: either the closer sees // this call or this sees the gate closed. retro_atomic_fetch_add_seq_cst_int(&gate->calls, 1); + // Orders the count before every later load of the caller's, not just the one below, so a + // CallGateDrain that unpublished something first is seen to. + retro_atomic_thread_fence_seq_cst(); if (retro_atomic_load_seq_cst_int(&gate->closed)) { retro_atomic_fetch_add_seq_cst_int(&gate->calls, -1); @@ -59,6 +62,12 @@ void CallGateExit(CallGate* gate) void CallGateClose(CallGate* gate) { retro_atomic_fetch_add_seq_cst_int(&gate->closed, 1); + CallGateDrain(gate); +} + +void CallGateDrain(CallGate* gate) +{ + retro_atomic_thread_fence_seq_cst(); const int own = OwnCalls(gate); for (;;) { @@ -73,4 +82,8 @@ void CallGateClose(CallGate* gate) retro_eventcount_commit_wait(Left(), key); } } +void CallGateReopen(CallGate* gate) +{ + retro_atomic_fetch_add_seq_cst_int(&gate->closed, -1); +} } // namespace Common diff --git a/Source/Core/Common/CallGate.h b/Source/Core/Common/CallGate.h index 56498854d9..e8b380f997 100644 --- a/Source/Core/Common/CallGate.h +++ b/Source/Core/Common/CallGate.h @@ -21,4 +21,9 @@ struct CallGate bool CallGateEnter(CallGate* gate); void CallGateExit(CallGate* gate); void CallGateClose(CallGate* gate); +// Returns once the calls inside now have left, leaving the gate open: for when the caller +// has already unpublished what those calls might be using. +void CallGateDrain(CallGate* gate); +// Undoes one CallGateClose; the gate lets calls in again once every close is undone. +void CallGateReopen(CallGate* gate); } // namespace Common diff --git a/Source/Core/Common/FatFsUtil.cpp b/Source/Core/Common/FatFsUtil.cpp index ea0bb85b49..caccd14593 100644 --- a/Source/Core/Common/FatFsUtil.cpp +++ b/Source/Core/Common/FatFsUtil.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -18,6 +17,7 @@ #include "diskio.h" // clang-format on +#include #include "Common/Align.h" #include "Common/FileUtil.h" #include "Common/IOFile.h" @@ -34,7 +34,33 @@ enum : u32 MAX_CLUSTER_SIZE = 64 * SECTOR_SIZE, }; -static std::mutex s_fatfs_mutex; +// FatFs keeps global state, so one user at a time. Users are rare and never overlap in +// practice; one that finds FatFs taken gives up with an error instead of waiting. +static retro_atomic_int_t s_fatfs_in_use; + +namespace +{ +class FatFsUse +{ +public: + FatFsUse() : m_owned{retro_atomic_cas_int(&s_fatfs_in_use, 0, 1) != 0} + { + if (!m_owned) + ERROR_LOG_FMT(COMMON, "FatFs is busy on another thread"); + } + ~FatFsUse() + { + if (m_owned) + retro_atomic_store_release_int(&s_fatfs_in_use, 0); + } + FatFsUse(const FatFsUse&) = delete; + FatFsUse& operator=(const FatFsUse&) = delete; + explicit operator bool() const { return m_owned; } + +private: + bool m_owned; +}; +} // namespace static Common::FatFsCallbacks* s_callbacks; namespace @@ -204,33 +230,6 @@ extern "C" void ff_memfree(void* mblock) } #endif -#if FF_FS_REENTRANT -extern "C" int ff_cre_syncobj(BYTE vol, FF_SYNC_t* sobj) -{ - *sobj = new std::recursive_mutex(); - return *sobj != nullptr; -} - -extern "C" int ff_req_grant(FF_SYNC_t sobj) -{ - std::recursive_mutex* m = reinterpret_cast(sobj); - m->lock(); - return 1; -} - -extern "C" void ff_rel_grant(FF_SYNC_t sobj) -{ - std::recursive_mutex* m = reinterpret_cast(sobj); - m->unlock(); -} - -extern "C" int ff_del_syncobj(FF_SYNC_t sobj) -{ - delete reinterpret_cast(sobj); - return 1; -} -#endif - static const char* FatFsErrorToString(FRESULT error_code) { // These are taken from the comment next to each value in ff.h @@ -516,7 +515,9 @@ bool SyncSDFolderToSDImage(const std::function& cancelled, bool determin } size = AlignUp(size, MAX_CLUSTER_SIZE); - std::lock_guard lk(s_fatfs_mutex); + const FatFsUse fatfs_use; + if (!fatfs_use) + return false; SDCardFatFsCallbacks callbacks; s_callbacks = &callbacks; Common::ScopeGuard callbacks_guard{[] { s_callbacks = nullptr; }}; @@ -763,7 +764,9 @@ bool SyncSDImageToSDFolder(const std::function& cancelled) if (image_path.empty() || target_dir.empty()) return false; - std::lock_guard lk(s_fatfs_mutex); + const FatFsUse fatfs_use; + if (!fatfs_use) + return false; SDCardFatFsCallbacks callbacks; s_callbacks = &callbacks; Common::ScopeGuard callbacks_guard{[] { s_callbacks = nullptr; }}; @@ -837,7 +840,9 @@ bool SyncSDImageToSDFolder(const std::function& cancelled) void RunInFatFsContext(FatFsCallbacks& callbacks, const std::function& function) { - std::lock_guard lk(s_fatfs_mutex); + const FatFsUse fatfs_use; + if (!fatfs_use) + return; s_callbacks = &callbacks; Common::ScopeGuard callbacks_guard{[] { s_callbacks = nullptr; }}; function(); diff --git a/Source/Core/Common/HttpRequest.cpp b/Source/Core/Common/HttpRequest.cpp index 94a466434a..4d16ae3fa9 100644 --- a/Source/Core/Common/HttpRequest.cpp +++ b/Source/Core/Common/HttpRequest.cpp @@ -5,7 +5,6 @@ #include #include -#include #include @@ -41,7 +40,6 @@ public: std::string EscapeComponent(const std::string& string); private: - static inline std::once_flag s_curl_was_initialized; ProgressCallback m_callback; Headers m_response_headers; std::unique_ptr m_curl{nullptr, curl_easy_cleanup}; @@ -120,7 +118,12 @@ int HttpRequest::Impl::CurlProgressCallback(Impl* impl, curl_off_t dltotal, curl HttpRequest::Impl::Impl(std::chrono::milliseconds timeout_ms, ProgressCallback callback) : m_callback(std::move(callback)) { - std::call_once(s_curl_was_initialized, [] { curl_global_init(CURL_GLOBAL_DEFAULT); }); + // Once per process; a function-local static is initialized exactly once. + static const bool s_curl_initialized = [] { + curl_global_init(CURL_GLOBAL_DEFAULT); + return true; + }(); + (void)s_curl_initialized; m_curl.reset(curl_easy_init()); if (!m_curl) diff --git a/Source/Core/Common/JITMemoryTracker.cpp b/Source/Core/Common/JITMemoryTracker.cpp index c4d517edb0..74c70e5186 100644 --- a/Source/Core/Common/JITMemoryTracker.cpp +++ b/Source/Core/Common/JITMemoryTracker.cpp @@ -11,84 +11,98 @@ namespace Common { JITMemoryTracker::JITMemoryTracker() = default; +JITMemoryTracker::~JITMemoryTracker() +{ + for (auto& slot : m_jit_regions) + delete static_cast(retro_atomic_exchange_ptr(&slot, nullptr)); + quiesce_deinit(&m_quiesce); +} + +void JITMemoryTracker::FreeRegion(quiesce_node_t* node) +{ + delete reinterpret_cast(node); +} + void JITMemoryTracker::RegisterJITRegion(void* ptr, size_t size) { - std::scoped_lock lk(m_mutex); - - if (m_jit_regions.find(ptr) != m_jit_regions.end()) + quiesce_enter(&m_quiesce); + const bool known = FindRegion(ptr) != nullptr; + quiesce_exit(&m_quiesce); + if (known) { PanicAlertFmt("JITMemoryTracker: region {} already registered", ptr); return; } - m_jit_regions[ptr] = {ptr, size, 0}; + auto* info = new JITRegionInfo{{}, ptr, size, 0}; + for (auto& slot : m_jit_regions) + { + if (!retro_atomic_load_acquire_ptr(&slot) && retro_atomic_cas_ptr(&slot, nullptr, info)) + return; + } + PanicAlertFmt("JITMemoryTracker: more than {} JIT regions", MAX_REGIONS); + delete info; } void JITMemoryTracker::UnregisterJITRegion(void* ptr) { - std::scoped_lock lk(m_mutex); - - m_jit_regions.erase(ptr); + for (auto& slot : m_jit_regions) + { + auto* info = static_cast(retro_atomic_load_acquire_ptr(&slot)); + if (info && info->start_ptr == ptr && retro_atomic_cas_ptr(&slot, info, nullptr)) + { + quiesce_retire(&m_quiesce, &info->node, FreeRegion); + return; + } + } } JITMemoryTracker::JITRegionInfo* JITMemoryTracker::FindRegion(void* ptr) { - if (m_jit_regions.find(ptr) != m_jit_regions.end()) + JITRegionInfo* containing = nullptr; + for (auto& slot : m_jit_regions) { - return &m_jit_regions[ptr]; + auto* info = static_cast(retro_atomic_load_acquire_ptr(&slot)); + if (!info) + continue; + if (info->start_ptr == ptr) + return info; + void* region_end = static_cast(static_cast(info->start_ptr) + info->size); + if (!containing && ptr >= info->start_ptr && ptr <= region_end) + containing = info; } - - for (auto& info : m_jit_regions) - { - void* region_end = static_cast(static_cast(info.first) + info.second.size); - if (ptr >= info.first && ptr <= region_end) - { - return &info.second; - } - } - - return nullptr; + return containing; } void JITMemoryTracker::JITRegionWriteEnableExecuteDisable(void* ptr) { - std::scoped_lock lk(m_mutex); - + quiesce_enter(&m_quiesce); JITRegionInfo* info = FindRegion(ptr); - if (!info) + if (info) { - return; - } + if (info->nest_counter == 0) + UnWriteProtectMemory(info->start_ptr, info->size, false); - if (info->nest_counter == 0) - { - UnWriteProtectMemory(info->start_ptr, info->size, false); + info->nest_counter++; } - - info->nest_counter++; + quiesce_exit(&m_quiesce); } void JITMemoryTracker::JITRegionWriteDisableExecuteEnable(void* ptr) { - std::scoped_lock lk(m_mutex); - + quiesce_enter(&m_quiesce); JITRegionInfo* info = FindRegion(ptr); - if (!info) + if (info) { - return; - } + info->nest_counter--; - info->nest_counter--; - - if (info->nest_counter < 0) - { - PanicAlertFmt("JITMemoryTracker: Nest counter underflow for region {}", ptr); - } - else if (info->nest_counter == 0) - { - WriteProtectMemory(info->start_ptr, info->size, true); + if (info->nest_counter < 0) + PanicAlertFmt("JITMemoryTracker: Nest counter underflow for region {}", ptr); + else if (info->nest_counter == 0) + WriteProtectMemory(info->start_ptr, info->size, true); } + quiesce_exit(&m_quiesce); } } // namespace Common diff --git a/Source/Core/Common/JITMemoryTracker.h b/Source/Core/Common/JITMemoryTracker.h index 150807a908..ae0ead8d64 100644 --- a/Source/Core/Common/JITMemoryTracker.h +++ b/Source/Core/Common/JITMemoryTracker.h @@ -3,16 +3,23 @@ #pragma once +#include #include -#include -#include + +#include + +#include "Common/Quiesce.h" namespace Common { +// Tracks the JIT code regions whose write and execute permissions are toggled around code +// emission. Each JIT emits into its own region on its own thread, so a region's nest count is +// that thread's alone; the set of regions is shared, and kept in atomic slots. class JITMemoryTracker final { public: JITMemoryTracker(); + ~JITMemoryTracker(); void RegisterJITRegion(void* ptr, size_t size); void UnregisterJITRegion(void* ptr); @@ -23,14 +30,18 @@ public: private: struct JITRegionInfo { + quiesce_node_t node; // first: the free callback casts it back void* start_ptr; size_t size; int nest_counter; }; + static void FreeRegion(quiesce_node_t* node); + // Only inside m_quiesce. JITRegionInfo* FindRegion(void* ptr); - std::map m_jit_regions; - std::mutex m_mutex; + static constexpr size_t MAX_REGIONS = 64; + std::array m_jit_regions{}; + quiesce_t m_quiesce{}; }; -} +} // namespace Common diff --git a/Source/Core/Common/Logging/LogManager.cpp b/Source/Core/Common/Logging/LogManager.cpp index 76d4e4f594..6df6764564 100644 --- a/Source/Core/Common/Logging/LogManager.cpp +++ b/Source/Core/Common/Logging/LogManager.cpp @@ -7,14 +7,19 @@ #include #include #include -#include #include -#include #include #include #include +#ifdef _WIN32 +#include +#else +#include +#include +#endif + #include "Common/Config/Config.h" #include "Common/FileUtil.h" #include "Common/Logging/ConsoleListener.h" @@ -32,32 +37,64 @@ const Config::Info LOGGER_WRITE_TO_WINDOW{ const Config::Info LOGGER_VERBOSITY{{Config::System::Logger, "Options", "Verbosity"}, LogLevel::LNOTICE}; +// Any thread may log. The file is opened for appending, where the OS makes each write land +// whole at the end, so a message is one write and nothing is shared in user space. class FileLogListener : public LogListener { public: FileLogListener(const std::string& filename) { - File::OpenFStream(m_logfile, filename, std::ios::app); + File::CreateFullPath(filename); +#ifdef _WIN32 + m_file = CreateFileW(UTF8ToWString(filename).c_str(), FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, nullptr); +#else + m_file = open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644); +#endif SetEnable(true); } + ~FileLogListener() override + { +#ifdef _WIN32 + if (m_file != INVALID_HANDLE_VALUE) + CloseHandle(m_file); +#else + if (m_file >= 0) + close(m_file); +#endif + } + void Log(LogLevel, const char* msg) override { if (!IsEnabled() || !IsValid()) return; - std::lock_guard lk(m_log_lock); - m_logfile << msg << std::flush; + const size_t length = std::strlen(msg); +#ifdef _WIN32 + DWORD written; + WriteFile(m_file, msg, static_cast(length), &written, nullptr); +#else + [[maybe_unused]] const ssize_t written = write(m_file, msg, length); +#endif } - bool IsValid() const { return m_logfile.good(); } - bool IsEnabled() const { return m_enable; } - void SetEnable(bool enable) { m_enable = enable; } +#ifdef _WIN32 + bool IsValid() const { return m_file != INVALID_HANDLE_VALUE; } +#else + bool IsValid() const { return m_file >= 0; } +#endif + bool IsEnabled() const { return m_enable.load(std::memory_order_relaxed); } + void SetEnable(bool enable) { m_enable.store(enable, std::memory_order_relaxed); } private: - std::mutex m_log_lock; - std::ofstream m_logfile; - bool m_enable; +#ifdef _WIN32 + HANDLE m_file = INVALID_HANDLE_VALUE; +#else + int m_file = -1; +#endif + std::atomic m_enable{false}; }; void GenericLogFmtImpl(LogLevel level, LogType type, const char* file, int line, diff --git a/Source/Core/Common/MemoryUtil.cpp b/Source/Core/Common/MemoryUtil.cpp index 5d5085aa09..aac0aec877 100644 --- a/Source/Core/Common/MemoryUtil.cpp +++ b/Source/Core/Common/MemoryUtil.cpp @@ -6,6 +6,8 @@ #include #include +#include + #include "Common/CommonFuncs.h" #include "Common/Logging/Log.h" #include "Common/MsgHandler.h" @@ -35,15 +37,14 @@ #include "Common/JITMemoryTracker.h" #ifdef __LIBRETRO__ #include -#include #include #include "DolphinLibretro/Common/Globals.h" static void* s_last_exec_rw = nullptr; static unsigned s_last_exec_mode = 0; // Regions the frontend allocated for us. Whether they are dual-mapped says // nothing about who owns them, so ownership is recorded rather than inferred. -static std::mutex s_frontend_regions_mutex; -static std::vector s_frontend_regions; +// Slots claimed and released with a CAS; a handful of regions at most. +static retro_atomic_ptr_t s_frontend_regions[64]; #endif #endif @@ -84,9 +85,10 @@ void* AllocateExecutableMemory(size_t size) s_last_exec_mode = alloc.mode; SetJitType(alloc.mode == RETRO_EXEC_MEM_MODE_DUAL_MAP ? JitType::LuckTXM : JitType::Legacy); + for (auto& slot : s_frontend_regions) { - std::scoped_lock lk(s_frontend_regions_mutex); - s_frontend_regions.push_back(alloc.rx); + if (!retro_atomic_load_acquire_ptr(&slot) && retro_atomic_cas_ptr(&slot, nullptr, alloc.rx)) + break; } g_jit_memory_tracker.RegisterJITRegion(alloc.rx, size); return alloc.rx; @@ -423,12 +425,10 @@ void FreeExecutableMemory(void* ptr, size_t size) g_jit_memory_tracker.UnregisterJITRegion(ptr); #endif #if defined(IPHONEOS) && defined(__LIBRETRO__) + for (auto& slot : s_frontend_regions) { - std::scoped_lock lk(s_frontend_regions_mutex); - auto it = std::find(s_frontend_regions.begin(), s_frontend_regions.end(), ptr); - if (it != s_frontend_regions.end()) + if (retro_atomic_cas_ptr(&slot, ptr, nullptr)) { - s_frontend_regions.erase(it); struct retro_exec_mem_free f = {}; f.rx = ptr; if (Libretro::environ_cb) diff --git a/Source/Core/Common/Profiler.cpp b/Source/Core/Common/Profiler.cpp deleted file mode 100644 index cf3c0b4a3c..0000000000 --- a/Source/Core/Common/Profiler.cpp +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2014 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "Common/Profiler.h" - -#include -#include -#include -#include -#include -#include - -#include "Common/Timer.h" - -namespace Common -{ -static const u32 PROFILER_FIELD_LENGTH = 8; -static const u32 PROFILER_FIELD_LENGTH_FP = PROFILER_FIELD_LENGTH + 3; -static const int PROFILER_LAZY_DELAY = 60; // in frames - -std::list Profiler::s_all_profilers; -std::mutex Profiler::s_mutex; -u32 Profiler::s_max_length = 0; -u64 Profiler::s_frame_time; -u64 Profiler::s_usecs_frame; - -std::string Profiler::s_lazy_result; -int Profiler::s_lazy_delay = 0; - -Profiler::Profiler(std::string name) - : m_name(std::move(name)), m_usecs(0), m_usecs_min(UINT64_MAX), m_usecs_max(0), m_usecs_quad(0), - m_calls(0) -{ - s_max_length = std::max(s_max_length, u32(m_name.length())); - - std::lock_guard lk(s_mutex); - s_all_profilers.push_back(this); -} - -Profiler::~Profiler() -{ - std::lock_guard lk(s_mutex); - s_all_profilers.remove(this); -} - -bool Profiler::operator<(const Profiler& b) const -{ - return m_usecs < b.m_usecs; -} - -std::string Profiler::ToString() -{ - if (s_lazy_delay > 0) - { - s_lazy_delay--; - return s_lazy_result; - } - s_lazy_delay = PROFILER_LAZY_DELAY - 1; - - // don't write anything if no profilation is enabled - std::lock_guard lk(s_mutex); - if (s_all_profilers.empty()) - return ""; - - u64 end = Common::Timer::NowUs(); - s_usecs_frame = end - s_frame_time; - s_frame_time = end; - - std::ostringstream buffer; - buffer << std::setw(s_max_length) << std::left << "" - << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH) << std::right << "calls" - << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH) << std::right << "sum" - << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH_FP) << std::right << "rel" - << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH) << std::right << "min" - << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH_FP) << std::right << "avg" - << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH_FP) << std::right << "stdev" - << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH) << std::right << "max" - << " "; - buffer << "/ usec" << std::endl; - - s_all_profilers.sort([](Profiler* a, Profiler* b) { return *b < *a; }); - - for (auto profiler : s_all_profilers) - { - buffer << profiler->Read() << std::endl; - } - s_lazy_result = buffer.str(); - return s_lazy_result; -} - -void Profiler::Start(u64* time, int* depth) -{ - if ((*depth)++ == 0) - { - *time = Common::Timer::NowUs(); - } -} - -void Profiler::Stop(u64* time, int* depth) -{ - if (--(*depth) == 0) - { - u64 end = Common::Timer::NowUs(); - - u64 diff = end - *time; - - std::lock_guard lk(m_mutex); - m_usecs += diff; - m_usecs_min = std::min(m_usecs_min, diff); - m_usecs_max = std::max(m_usecs_max, diff); - m_usecs_quad += diff * diff; - m_calls++; - } -} - -std::string Profiler::Read() -{ - std::lock_guard lk(m_mutex); - - double avg = 0; - double stdev = 0; - double time_rel = 0; - if (m_calls) - { - avg = double(m_usecs) / m_calls; - stdev = std::sqrt(double(m_usecs_quad) / m_calls - avg * avg); - } - else - { - m_usecs_min = 0; - } - if (s_usecs_frame) - { - time_rel = double(m_usecs) * 100 / s_usecs_frame; - } - - std::ostringstream buffer; - - buffer << std::setw(s_max_length) << std::left << m_name << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH) << std::right << m_calls << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH) << std::right << m_usecs << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH_FP) << std::right << time_rel << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH) << std::right << m_usecs_min << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH_FP) << std::right << std::fixed << std::setprecision(2) - << avg << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH_FP) << std::right << std::fixed << std::setprecision(2) - << stdev << " "; - buffer << std::setw(PROFILER_FIELD_LENGTH) << std::right << m_usecs_max; - - m_usecs = 0; - m_usecs_min = UINT64_MAX; - m_usecs_max = 0; - m_usecs_quad = 0; - m_calls = 0; - - return buffer.str(); -} -} // namespace Common diff --git a/Source/Core/Common/Profiler.h b/Source/Core/Common/Profiler.h deleted file mode 100644 index 91d9827564..0000000000 --- a/Source/Core/Common/Profiler.h +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2014 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include - -#include "CommonTypes.h" - -namespace Common -{ -class Profiler -{ -public: - Profiler(std::string name); - ~Profiler(); - - static std::string ToString(); - - void Start(u64* time, int* depth); - void Stop(u64* time, int* depth); - std::string Read(); - - bool operator<(const Profiler& b) const; - -private: - static std::list s_all_profilers; - static std::mutex s_mutex; - static u32 s_max_length; - static u64 s_frame_time; - static u64 s_usecs_frame; - - static std::string s_lazy_result; - static int s_lazy_delay; - - std::mutex m_mutex; - std::string m_name; - u64 m_usecs; - u64 m_usecs_min; - u64 m_usecs_max; - u64 m_usecs_quad; - u64 m_calls; -}; - -class ProfilerExecuter -{ -public: - ProfilerExecuter(Profiler* profiler, u64* time, int* depth) - : m_profiler(profiler), m_time(time), m_depth(depth) - { - m_profiler->Start(m_time, m_depth); - } - ~ProfilerExecuter() { m_profiler->Stop(m_time, m_depth); } - -private: - Profiler* m_profiler; - u64* m_time; - int* m_depth; -}; -} // namespace Common - -#define PROFILE(name) \ - static Common::Profiler prof_gen(name); \ - static thread_local u64 prof_time; \ - static thread_local int prof_depth; \ - Common::ProfilerExecuter prof_e(&prof_gen, &prof_time, &prof_depth); diff --git a/Source/Core/Common/SocketContext.cpp b/Source/Core/Common/SocketContext.cpp index dcc637891a..5eb79db8d9 100644 --- a/Source/Core/Common/SocketContext.cpp +++ b/Source/Core/Common/SocketContext.cpp @@ -10,45 +10,36 @@ namespace Common { #ifdef _WIN32 +// Winsock counts WSAStartup calls itself and tears down at the matching last WSACleanup, so each +// context makes its own pair and nothing is shared between threads. SocketContext::SocketContext() { - std::lock_guard g(s_lock); - if (s_num_objects == 0) + WSADATA data; + const int ret = WSAStartup(MAKEWORD(2, 2), &data); + if (ret == 0) { - const int ret = WSAStartup(MAKEWORD(2, 2), &s_data); - if (ret == 0) - { - INFO_LOG_FMT(COMMON, "WSAStartup succeeded, wVersion={}.{}, wHighVersion={}.{}", - int(LOBYTE(s_data.wVersion)), int(HIBYTE(s_data.wVersion)), - int(LOBYTE(s_data.wHighVersion)), int(HIBYTE(s_data.wHighVersion))); - } - else - { - // The WSAStartup function directly returns the extended error code in the return value. - // A call to the WSAGetLastError function is not needed and should not be used. - // - // Source: - // https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsastartup - ERROR_LOG_FMT(COMMON, "WSAStartup failed with error {}: {}", ret, - Common::DecodeNetworkError(ret)); - } + INFO_LOG_FMT(COMMON, "WSAStartup succeeded, wVersion={}.{}, wHighVersion={}.{}", + int(LOBYTE(data.wVersion)), int(HIBYTE(data.wVersion)), + int(LOBYTE(data.wHighVersion)), int(HIBYTE(data.wHighVersion))); + m_started = true; + } + else + { + // The WSAStartup function directly returns the extended error code in the return value. + // A call to the WSAGetLastError function is not needed and should not be used. + // + // Source: + // https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsastartup + ERROR_LOG_FMT(COMMON, "WSAStartup failed with error {}: {}", ret, + Common::DecodeNetworkError(ret)); } - s_num_objects++; } SocketContext::~SocketContext() { - std::lock_guard g(s_lock); - s_num_objects--; - if (s_num_objects == 0) - { + if (m_started) WSACleanup(); - } } -std::mutex SocketContext::s_lock; -size_t SocketContext::s_num_objects = 0; -WSADATA SocketContext::s_data; - #else SocketContext::SocketContext() = default; SocketContext::~SocketContext() = default; diff --git a/Source/Core/Common/SocketContext.h b/Source/Core/Common/SocketContext.h index 5dcf45df5d..b1fedff6e1 100644 --- a/Source/Core/Common/SocketContext.h +++ b/Source/Core/Common/SocketContext.h @@ -4,8 +4,8 @@ #pragma once #ifdef _WIN32 -#include #include + #endif namespace Common @@ -24,9 +24,7 @@ public: private: #ifdef _WIN32 - static std::mutex s_lock; - static size_t s_num_objects; - static WSADATA s_data; + bool m_started = false; #endif }; } // namespace Common diff --git a/Source/Core/Common/SymbolDB.cpp b/Source/Core/Common/SymbolDB.cpp index e1778d2ca5..a501750fa3 100644 --- a/Source/Core/Common/SymbolDB.cpp +++ b/Source/Core/Common/SymbolDB.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include @@ -40,7 +39,6 @@ void Symbol::Rename(const std::string& symbol_name) void SymbolDB::List() { - std::lock_guard lock(m_mutex); for (const auto& func : m_functions) { DEBUG_LOG_FMT(OSHLE, "{} @ {:08x}: {} bytes (hash {:08x}) : {} calls", func.second.name, @@ -51,13 +49,11 @@ void SymbolDB::List() bool SymbolDB::IsEmpty() const { - std::lock_guard lock(m_mutex); return m_functions.empty() && m_notes.empty(); } bool SymbolDB::Clear(const char* prefix) { - std::lock_guard lock(m_mutex); // TODO: honor prefix m_map_name.clear(); @@ -72,7 +68,6 @@ bool SymbolDB::Clear(const char* prefix) void SymbolDB::Index() { - std::lock_guard lock(m_mutex); Index(&m_functions); } @@ -87,7 +82,6 @@ void SymbolDB::Index(XFuncMap* functions) const Symbol* SymbolDB::GetSymbolFromName(std::string_view name) const { - std::lock_guard lock(m_mutex); for (auto& func : m_functions) { @@ -100,7 +94,6 @@ const Symbol* SymbolDB::GetSymbolFromName(std::string_view name) const std::vector SymbolDB::GetSymbolsFromName(std::string_view name) const { - std::lock_guard lock(m_mutex); std::vector symbols; for (auto& func : m_functions) @@ -114,7 +107,6 @@ std::vector SymbolDB::GetSymbolsFromName(std::string_view name) c const Symbol* SymbolDB::GetSymbolFromHash(u32 hash) const { - std::lock_guard lock(m_mutex); auto iter = m_checksum_to_function.find(hash); if (iter == m_checksum_to_function.end()) @@ -125,7 +117,6 @@ const Symbol* SymbolDB::GetSymbolFromHash(u32 hash) const std::vector SymbolDB::GetSymbolsFromHash(u32 hash) const { - std::lock_guard lock(m_mutex); const auto iter = m_checksum_to_function.find(hash); @@ -137,13 +128,11 @@ std::vector SymbolDB::GetSymbolsFromHash(u32 hash) const void SymbolDB::AddCompleteSymbol(const Symbol& symbol) { - std::lock_guard lock(m_mutex); m_functions[symbol.address] = symbol; } bool SymbolDB::RenameSymbol(const Symbol& symbol, const std::string& symbol_name) { - std::lock_guard lock(m_mutex); auto it = m_functions.find(symbol.address); if (it == m_functions.end()) @@ -156,7 +145,6 @@ bool SymbolDB::RenameSymbol(const Symbol& symbol, const std::string& symbol_name bool SymbolDB::RenameSymbol(const Symbol& symbol, const std::string& symbol_name, const std::string& object_name) { - std::lock_guard lock(m_mutex); auto it = m_functions.find(symbol.address); if (it == m_functions.end()) diff --git a/Source/Core/Common/SymbolDB.h b/Source/Core/Common/SymbolDB.h index 5074b03ad5..663cebab47 100644 --- a/Source/Core/Common/SymbolDB.h +++ b/Source/Core/Common/SymbolDB.h @@ -7,7 +7,6 @@ #pragma once #include -#include #include #include #include @@ -94,6 +93,8 @@ enum FFLAG_STRAIGHT = (1 << 5) }; +// Used by the emulation (CPU) thread alone: boot fills it before that thread runs, and every +// later reader and writer (HLE, the JIT, the interpreter, the debugger stub) runs on it. class SymbolDB { public: @@ -122,7 +123,6 @@ public: template void ForEachSymbol(F f) const { - std::lock_guard lock(m_mutex); for (const auto& [addr, symbol] : m_functions) f(symbol); } @@ -130,7 +130,6 @@ public: template void ForEachSymbolWithMutation(F f) { - std::lock_guard lock(m_mutex); for (auto& [addr, symbol] : m_functions) { f(symbol); @@ -141,7 +140,6 @@ public: template void ForEachNote(F f) const { - std::lock_guard lock(m_mutex); for (const auto& [addr, note] : m_notes) f(note); } @@ -149,7 +147,6 @@ public: template void ForEachNoteWithMutation(F f) { - std::lock_guard lock(m_mutex); for (auto& [addr, note] : m_notes) { f(note); @@ -169,6 +166,5 @@ protected: XNoteMap m_notes; XFuncPtrMap m_checksum_to_function; std::string m_map_name; - mutable std::recursive_mutex m_mutex; }; } // namespace Common diff --git a/Source/Core/Core/ActionReplay.cpp b/Source/Core/Core/ActionReplay.cpp index 9dbb0450a8..63b961b954 100644 --- a/Source/Core/Core/ActionReplay.cpp +++ b/Source/Core/Core/ActionReplay.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -82,8 +81,8 @@ enum SUB_MASTER_CODE = 0x03, }; -// General lock. Protects codes list and internal log. -static std::mutex s_lock; +// Codes list and internal log: emulation thread only. Cheats are changed through +// RunOnCPUThread, the patch engine runs on it, and nothing else touches them. static std::vector s_active_codes; static std::vector s_synced_codes; static std::vector s_internal_log; @@ -117,8 +116,6 @@ void ApplyCodes(std::span codes, const std::string& game_id, u16 r { if (!Config::AreCheatsEnabled()) return; - - std::lock_guard guard(s_lock); s_disable_logging = false; s_active_codes.clear(); @@ -149,7 +146,6 @@ std::vector ApplyAndReturnCodes(std::span codes) { if (Config::AreCheatsEnabled()) { - std::lock_guard guard(s_lock); s_disable_logging = false; s_active_codes.clear(); std::copy_if(codes.begin(), codes.end(), std::back_inserter(s_active_codes), @@ -167,7 +163,6 @@ void AddCode(ARCode code) if (code.enabled) { - std::lock_guard guard(s_lock); s_disable_logging = false; s_active_codes.emplace_back(std::move(code)); } @@ -177,8 +172,6 @@ size_t CountEnabledCodes() { if (!Config::AreCheatsEnabled()) return 0; - - std::lock_guard guard(s_lock); return s_active_codes.size(); } @@ -357,13 +350,11 @@ void EnableSelfLogging(bool enable) std::vector GetSelfLog() { - std::lock_guard guard(s_lock); return s_internal_log; } void ClearSelfLog() { - std::lock_guard guard(s_lock); s_internal_log.clear(); } @@ -1011,7 +1002,6 @@ void RunAllActive(const Core::CPUThreadGuard& cpu_guard) // If the mutex is idle then acquiring it should be cheap, fast mutexes // are only atomic ops unless contested. It should be rare for this to // be contested. - std::lock_guard guard(s_lock); std::erase_if(s_active_codes, [&cpu_guard](const ARCode& code) { const bool success = RunCodeLocked(cpu_guard, code); LogInfo("\n"); diff --git a/Source/Core/Core/ConfigLoaders/BaseConfigLoader.cpp b/Source/Core/Core/ConfigLoaders/BaseConfigLoader.cpp index 8b35d4d684..db3174e414 100644 --- a/Source/Core/Core/ConfigLoaders/BaseConfigLoader.cpp +++ b/Source/Core/Core/ConfigLoaders/BaseConfigLoader.cpp @@ -8,10 +8,11 @@ #include #include #include -#include #include #include +#include + #include "Common/CommonTypes.h" #include "Common/Config/Config.h" #include "Common/Config/Layer.h" @@ -28,25 +29,24 @@ #include "Core/SysConf.h" #include "Core/System.h" -static bool s_sysconf_controlled_by_guest = false; -static std::recursive_mutex s_sysconf_lock; +// Once the guest has SYSCONF, the host neither loads nor saves it, so the host's own accesses +// (config load and save) and the guest's never overlap; the hand-over itself is this flag. +static retro_atomic_int_t s_sysconf_controlled_by_guest; namespace ConfigLoaders { void TransferSYSCONFControlToGuest() { - std::lock_guard lock(s_sysconf_lock); - ASSERT(!s_sysconf_controlled_by_guest); - s_sysconf_controlled_by_guest = true; + const int was_controlled = retro_atomic_exchange_int(&s_sysconf_controlled_by_guest, 1); + ASSERT(!was_controlled); } void TransferSYSCONFControlFromGuest(WriteBackChangedValues write_back_changed_values) { - std::lock_guard lock(s_sysconf_lock); - ASSERT(s_sysconf_controlled_by_guest); - s_sysconf_controlled_by_guest = false; + const int was_controlled = retro_atomic_exchange_int(&s_sysconf_controlled_by_guest, 0); + ASSERT(was_controlled); if (write_back_changed_values == WriteBackChangedValues::No) return; @@ -83,8 +83,8 @@ void TransferSYSCONFControlFromGuest(WriteBackChangedValues write_back_changed_v void SaveToSYSCONF(Config::LayerType layer, SkipIfControlledByGuest skip, std::function predicate) { - std::lock_guard lock(s_sysconf_lock); - if (skip == SkipIfControlledByGuest::Yes && s_sysconf_controlled_by_guest) + if (skip == SkipIfControlledByGuest::Yes && + retro_atomic_load_acquire_int(&s_sysconf_controlled_by_guest)) return; IOS::HLE::Kernel ios; @@ -233,8 +233,7 @@ public: private: void LoadFromSYSCONF(Config::Layer* layer) { - std::lock_guard lock(s_sysconf_lock); - if (s_sysconf_controlled_by_guest) + if (retro_atomic_load_acquire_int(&s_sysconf_controlled_by_guest)) return; IOS::HLE::Kernel ios; diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp index 262f032aaf..bc5b2c760b 100644 --- a/Source/Core/Core/ConfigManager.cpp +++ b/Source/Core/Core/ConfigManager.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -79,6 +78,8 @@ void SConfig::Shutdown() SConfig::~SConfig() { SaveSettings(); + delete static_cast(retro_atomic_exchange_ptr(&m_metadata, nullptr)); + quiesce_deinit(&m_metadata_quiesce); } void SConfig::SaveSettings() @@ -116,22 +117,43 @@ void SConfig::ResetAllSettings() Config::OnConfigChanged(); } +void SConfig::FreeMetadata(quiesce_node_t* node) +{ + delete reinterpret_cast(node); +} + +SConfig::Metadata SConfig::ReadMetadata() const +{ + quiesce_enter(&m_metadata_quiesce); + const auto* metadata = static_cast(retro_atomic_load_acquire_ptr(&m_metadata)); + Metadata copy = metadata ? *metadata : Metadata{}; + quiesce_exit(&m_metadata_quiesce); + return copy; +} + +template +void SConfig::EditMetadata(Edit&& edit) +{ + auto* next = new Metadata(ReadMetadata()); + next->node = {}; + edit(*next); + if (void* old = retro_atomic_exchange_ptr(&m_metadata, next)) + quiesce_retire(&m_metadata_quiesce, &static_cast(old)->node, FreeMetadata); +} + const std::string SConfig::GetGameID() const { - std::lock_guard lock(m_metadata_lock); - return m_game_id; + return ReadMetadata().game_id; } const std::string SConfig::GetGameTDBID() const { - std::lock_guard lock(m_metadata_lock); - return m_gametdb_id; + return ReadMetadata().gametdb_id; } const std::string SConfig::GetGameIDElfDol() const { - std::lock_guard lock(m_metadata_lock); - return m_game_id_elf_dol; + return ReadMetadata().game_id_elf_dol; } const std::string SConfig::GetGameIDForTextures() const @@ -141,47 +163,41 @@ const std::string SConfig::GetGameIDForTextures() const const std::vector SConfig::GetGameIDsForTextures() const { - std::lock_guard lock(m_metadata_lock); + const Metadata metadata = ReadMetadata(); - if (m_game_id_elf_dol.empty()) - return {m_game_id}; + if (metadata.game_id_elf_dol.empty()) + return {metadata.game_id}; - return {fmt::format("{}-{}", m_game_id_elf_dol, m_game_id), m_game_id}; + return {fmt::format("{}-{}", metadata.game_id_elf_dol, metadata.game_id), metadata.game_id}; } const std::string SConfig::GetTitleName() const { - std::lock_guard lock(m_metadata_lock); - return m_title_name; + return ReadMetadata().title_name; } const std::string SConfig::GetTitleDescription() const { - std::lock_guard lock(m_metadata_lock); - return m_title_description; + return ReadMetadata().title_description; } u64 SConfig::GetTitleID() const { - std::lock_guard lock(m_metadata_lock); - return m_title_id; + return ReadMetadata().title_id; } u16 SConfig::GetRevision() const { - std::lock_guard lock(m_metadata_lock); - return m_revision; + return ReadMetadata().revision; } u32 SConfig::GetSimulatedMemorySize() const { - std::lock_guard lock(m_metadata_lock); - return m_simulated_memory_size; + return ReadMetadata().simulated_memory_size; } void SConfig::ResetRunningGameMetadata() { - std::lock_guard lock(m_metadata_lock); SetRunningGameMetadata("00000000", "", 0, 0, DiscIO::Region::Unknown, 0); SetElfDolID(""); } @@ -189,8 +205,6 @@ void SConfig::ResetRunningGameMetadata() void SConfig::SetRunningGameMetadata(const DiscIO::Volume& volume, const DiscIO::Partition& partition) { - std::lock_guard lock(m_metadata_lock); - if (partition == volume.GetGamePartition()) { SetRunningGameMetadata(volume.GetGameID(), volume.GetGameTDBID(), @@ -208,7 +222,6 @@ void SConfig::SetRunningGameMetadata(const DiscIO::Volume& volume, void SConfig::SetRunningGameMetadata(const IOS::ES::TMDReader& tmd, DiscIO::Platform platform) { - std::lock_guard lock(m_metadata_lock); const u64 tmd_title_id = tmd.GetTitleId(); // If we're launching a disc game, we want to read the revision from @@ -226,7 +239,6 @@ void SConfig::SetRunningGameMetadata(const IOS::ES::TMDReader& tmd, DiscIO::Plat void SConfig::SetRunningGameMetadata(const std::string& game_id) { - std::lock_guard lock(m_metadata_lock); SetRunningGameMetadata(game_id, "", 0, 0, DiscIO::Region::Unknown, 0); } @@ -234,15 +246,10 @@ void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::stri u64 title_id, u16 revision, DiscIO::Region region, u32 simulated_memory_size) { - std::lock_guard lock(m_metadata_lock); - const bool was_changed = m_game_id != game_id || m_gametdb_id != gametdb_id || - m_title_id != title_id || m_revision != revision || - m_simulated_memory_size != simulated_memory_size; - m_game_id = game_id; - m_gametdb_id = gametdb_id; - m_title_id = title_id; - m_revision = revision; - m_simulated_memory_size = simulated_memory_size; + const Metadata current = ReadMetadata(); + const bool was_changed = current.game_id != game_id || current.gametdb_id != gametdb_id || + current.title_id != title_id || current.revision != revision || + current.simulated_memory_size != simulated_memory_size; if (game_id.length() == 6) { @@ -258,26 +265,41 @@ void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::stri m_debugger_game_id.clear(); } - if (!was_changed) - return; - - if (game_id == "00000000") + std::string title_name = current.title_name; + std::string title_description = current.title_description; + if (was_changed && game_id == "00000000") { - m_title_name.clear(); - m_title_description.clear(); - return; + title_name.clear(); + title_description.clear(); } + else if (was_changed) + { + const Core::TitleDatabase title_database; + auto& system = Core::System::GetInstance(); + const DiscIO::Language language = GetLanguageAdjustedForRegion(system.IsWii(), region); + title_name = title_database.GetTitleName(gametdb_id, language); + title_description = title_database.Describe(gametdb_id, language); + } + + EditMetadata([&](Metadata& metadata) { + metadata.game_id = game_id; + metadata.gametdb_id = gametdb_id; + metadata.title_id = title_id; + metadata.revision = revision; + metadata.simulated_memory_size = simulated_memory_size; + metadata.title_name = title_name; + metadata.title_description = title_description; + }); + + if (!was_changed || game_id == "00000000") + return; AchievementManager::GetInstance().CloseGame(); - const Core::TitleDatabase title_database; - auto& system = Core::System::GetInstance(); - const DiscIO::Language language = GetLanguageAdjustedForRegion(system.IsWii(), region); - m_title_name = title_database.GetTitleName(m_gametdb_id, language); - m_title_description = title_database.Describe(m_gametdb_id, language); - NOTICE_LOG_FMT(CORE, "Active title: {}", m_title_description); + NOTICE_LOG_FMT(CORE, "Active title: {}", title_description); Host_TitleChanged(); + auto& system = Core::System::GetInstance(); const bool is_running_or_starting = Core::IsRunningOrStarting(system); if (is_running_or_starting) Core::UpdateTitle(system); @@ -288,8 +310,7 @@ void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::stri void SConfig::SetElfDolID(const std::string& game_id) { - std::lock_guard lock(m_metadata_lock); - m_game_id_elf_dol = game_id; + EditMetadata([&game_id](Metadata& metadata) { metadata.game_id_elf_dol = game_id; }); } void SConfig::OnESTitleChanged() @@ -546,17 +567,17 @@ DiscIO::Language SConfig::GetLanguageAdjustedForRegion(bool wii, DiscIO::Region Common::IniFile SConfig::LoadDefaultGameIni() const { - return LoadDefaultGameIni(GetGameID(), m_revision); + return LoadDefaultGameIni(GetGameID(), GetRevision()); } Common::IniFile SConfig::LoadLocalGameIni() const { - return LoadLocalGameIni(GetGameID(), m_revision); + return LoadLocalGameIni(GetGameID(), GetRevision()); } Common::IniFile SConfig::LoadGameIni() const { - return LoadGameIni(GetGameID(), m_revision); + return LoadGameIni(GetGameID(), GetRevision()); } Common::IniFile SConfig::LoadDefaultGameIni(std::string_view id, std::optional revision) @@ -593,7 +614,7 @@ std::string SConfig::GetGameTDBImageRegionCode(bool wii, DiscIO::Region region) { // Taiwanese games share the Japanese region code however their title ID ends with 'W'. // GameTDB differentiates the covers using the code "ZH". - if (m_game_id.size() >= 4 && m_game_id.at(3) == 'W') + if (const std::string game_id = GetGameID(); game_id.size() >= 4 && game_id.at(3) == 'W') return "ZH"; return "JA"; diff --git a/Source/Core/Core/ConfigManager.h b/Source/Core/Core/ConfigManager.h index 7a218319a0..1fbc7e22c7 100644 --- a/Source/Core/Core/ConfigManager.h +++ b/Source/Core/Core/ConfigManager.h @@ -3,12 +3,14 @@ #pragma once -#include #include #include #include #include +#include + +#include "Common/Quiesce.h" #include "Common/CommonTypes.h" namespace Common @@ -136,15 +138,29 @@ private: u32 simulated_memory_size); static SConfig* m_Instance; - mutable std::recursive_mutex m_metadata_lock; - std::string m_game_id; - std::string m_game_id_elf_dol; - std::string m_gametdb_id; - std::string m_title_name; - std::string m_title_description; - u64 m_title_id; - u16 m_revision; - // Memory size requested by game's header. - u32 m_simulated_memory_size; + // The running game's identity: written by the emulation thread at boot and on a title + // change, read from any thread. Published whole, each getter copies from one version. + struct Metadata + { + quiesce_node_t node; // first: the free callback casts it back + std::string game_id; + std::string game_id_elf_dol; + std::string gametdb_id; + std::string title_name; + std::string title_description; + u64 title_id = 0; + u16 revision = 0; + // Memory size requested by game's header. + u32 simulated_memory_size = 0; + }; + static void FreeMetadata(quiesce_node_t* node); + // A copy of the current metadata. + Metadata ReadMetadata() const; + // Swaps in edit's result, made from a copy of the current metadata. Emulation thread. + template + void EditMetadata(Edit&& edit); + + mutable quiesce_t m_metadata_quiesce{}; + retro_atomic_ptr_t m_metadata{}; }; diff --git a/Source/Core/Core/FifoPlayer/FifoRecorder.cpp b/Source/Core/Core/FifoPlayer/FifoRecorder.cpp deleted file mode 100644 index c589b97975..0000000000 --- a/Source/Core/Core/FifoPlayer/FifoRecorder.cpp +++ /dev/null @@ -1,455 +0,0 @@ -// Copyright 2011 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "Core/FifoPlayer/FifoRecorder.h" - -#include -#include -#include - -#include "Common/Logging/Log.h" -#include "Common/MsgHandler.h" - -#include "Core/HW/Memmap.h" -#include "Core/System.h" - -#include "VideoCommon/BPMemory.h" -#include "VideoCommon/CommandProcessor.h" -#include "VideoCommon/OpcodeDecoding.h" -#include "VideoCommon/TextureDecoder.h" -#include "VideoCommon/VideoEvents.h" -#include "VideoCommon/XFMemory.h" - -class FifoRecorder::FifoRecordAnalyzer : public OpcodeDecoder::Callback -{ -public: - explicit FifoRecordAnalyzer(FifoRecorder* owner) : m_owner(owner) {} - explicit FifoRecordAnalyzer(FifoRecorder* owner, const u32* cpmem) - : m_owner(owner), m_cpmem(cpmem) - { - } - - OPCODE_CALLBACK(void OnXF(u16 address, u8 count, const u8* data)) {} - OPCODE_CALLBACK(void OnCP(u8 command, u32 value)) { GetCPState().LoadCPReg(command, value); } - OPCODE_CALLBACK(void OnBP(u8 command, u32 value)) {} - OPCODE_CALLBACK(void OnIndexedLoad(CPArray array, u32 index, u16 address, u8 size)); - OPCODE_CALLBACK(void OnPrimitiveCommand(OpcodeDecoder::Primitive primitive, u8 vat, - u32 vertex_size, u16 num_vertices, - const u8* vertex_data)); - OPCODE_CALLBACK(void OnDisplayList(u32 address, u32 size)) - { - WARN_LOG_FMT(VIDEO, - "Unhandled display list call {:08x} {:08x}; should have been inlined earlier", - address, size); - } - OPCODE_CALLBACK(void OnNop(u32 count)) {} - OPCODE_CALLBACK(void OnUnknown(u8 opcode, const u8* data)) {} - - OPCODE_CALLBACK(void OnCommand(const u8* data, u32 size)) {} - - OPCODE_CALLBACK(CPState& GetCPState()) { return m_cpmem; } - -private: - void ProcessVertexComponent(CPArray array_index, VertexComponentFormat array_type, - u32 component_offset, u32 component_size, u32 vertex_size, - u16 num_vertices, const u8* vertex_data, u32 byte_offset = 0); - - FifoRecorder* const m_owner; - CPState m_cpmem; -}; - -void FifoRecorder::FifoRecordAnalyzer::OnIndexedLoad(CPArray array, u32 index, u16 address, u8 size) -{ - const u32 load_address = m_cpmem.array_bases[array] + m_cpmem.array_strides[array] * index; - - m_owner->UseMemory(load_address, size * sizeof(u32), MemoryUpdate::Type::XFData); -} - -// TODO: The following code is copied with modifications from VertexLoaderBase. -// Surely there's a better solution? -#include "VideoCommon/VertexLoader_Color.h" -#include "VideoCommon/VertexLoader_Normal.h" -#include "VideoCommon/VertexLoader_Position.h" -#include "VideoCommon/VertexLoader_TextCoord.h" - -void FifoRecorder::FifoRecordAnalyzer::OnPrimitiveCommand(OpcodeDecoder::Primitive primitive, - u8 vat, u32 vertex_size, u16 num_vertices, - const u8* vertex_data) -{ - const auto& vtx_desc = m_cpmem.vtx_desc; - const auto& vtx_attr = m_cpmem.vtx_attr[vat]; - - u32 offset = 0; - - if (vtx_desc.low.PosMatIdx) - offset++; - for (auto texmtxidx : vtx_desc.low.TexMatIdx) - { - if (texmtxidx) - offset++; - } - const u32 pos_size = VertexLoader_Position::GetSize(vtx_desc.low.Position, vtx_attr.g0.PosFormat, - vtx_attr.g0.PosElements); - const u32 pos_direct_size = VertexLoader_Position::GetSize( - VertexComponentFormat::Direct, vtx_attr.g0.PosFormat, vtx_attr.g0.PosElements); - ProcessVertexComponent(CPArray::Position, vtx_desc.low.Position, offset, pos_direct_size, - vertex_size, num_vertices, vertex_data); - offset += pos_size; - - const u32 norm_size = - VertexLoader_Normal::GetSize(vtx_desc.low.Normal, vtx_attr.g0.NormalFormat, - vtx_attr.g0.NormalElements, vtx_attr.g0.NormalIndex3); - const u32 norm_direct_size = - VertexLoader_Normal::GetSize(VertexComponentFormat::Direct, vtx_attr.g0.NormalFormat, - vtx_attr.g0.NormalElements, vtx_attr.g0.NormalIndex3); - if (vtx_attr.g0.NormalIndex3 && IsIndexed(vtx_desc.low.Normal) && - vtx_attr.g0.NormalElements == NormalComponentCount::NTB) - { - // We're in 3-index mode, and we're using an indexed format and have the - // normal/tangent/binormal, so we actually need to deal with 3-index mode. - const u32 index_size = vtx_desc.low.Normal == VertexComponentFormat::Index16 ? 2 : 1; - ASSERT(norm_size == index_size * 3); - // 3-index mode uses one index each for the normal, tangent and binormal; - // the tangent and binormal are internally offset. - // The offset is based on the component size, not to the index itself; - // for instance, with 32-bit float normals, each normal vector is 3*sizeof(float) = 12 bytes, - // so the normal vector is offset by 0 bytes, the tangent by 12, and the binormal by 24. - // Using a byte offset instead of increasing the index means that using the same index for all - // elements is the same as not using the 3-index mode (increasing the index would give differing - // results if the normal array's stride was something other than 12, for instance if vertices - // were contiguous in main memory instead of individual components being used). - const u32 element_size = GetElementSize(vtx_attr.g0.NormalFormat) * 3; - ProcessVertexComponent(CPArray::Normal, vtx_desc.low.Normal, offset, element_size, vertex_size, - num_vertices, vertex_data); - ProcessVertexComponent(CPArray::Normal, vtx_desc.low.Normal, offset + index_size, element_size, - vertex_size, num_vertices, vertex_data, element_size); - ProcessVertexComponent(CPArray::Normal, vtx_desc.low.Normal, offset + 2 * index_size, - element_size, vertex_size, num_vertices, vertex_data, 2 * element_size); - } - else - { - ProcessVertexComponent(CPArray::Normal, vtx_desc.low.Normal, offset, norm_direct_size, - vertex_size, num_vertices, vertex_data); - } - offset += norm_size; - - for (u32 i = 0; i < vtx_desc.low.Color.Size(); i++) - { - const u32 color_size = - VertexLoader_Color::GetSize(vtx_desc.low.Color[i], vtx_attr.GetColorFormat(i)); - const u32 color_direct_size = - VertexLoader_Color::GetSize(VertexComponentFormat::Direct, vtx_attr.GetColorFormat(i)); - ProcessVertexComponent(CPArray::Color0 + i, vtx_desc.low.Color[i], offset, color_direct_size, - vertex_size, num_vertices, vertex_data); - offset += color_size; - } - for (u32 i = 0; i < vtx_desc.high.TexCoord.Size(); i++) - { - const u32 tc_size = VertexLoader_TextCoord::GetSize( - vtx_desc.high.TexCoord[i], vtx_attr.GetTexFormat(i), vtx_attr.GetTexElements(i)); - const u32 tc_direct_size = VertexLoader_TextCoord::GetSize( - VertexComponentFormat::Direct, vtx_attr.GetTexFormat(i), vtx_attr.GetTexElements(i)); - ProcessVertexComponent(CPArray::TexCoord0 + i, vtx_desc.high.TexCoord[i], offset, - tc_direct_size, vertex_size, num_vertices, vertex_data); - offset += tc_size; - } - - ASSERT(offset == vertex_size); -} - -// If a component is indexed, the array it indexes into for data must be saved. -void FifoRecorder::FifoRecordAnalyzer::ProcessVertexComponent( - CPArray array_index, VertexComponentFormat array_type, u32 component_offset, u32 component_size, - u32 vertex_size, u16 num_vertices, const u8* vertex_data, u32 byte_offset) -{ - // Skip if not indexed array - if (!IsIndexed(array_type)) - return; - - u16 max_index = 0; - - // Determine min and max indices - if (array_type == VertexComponentFormat::Index8) - { - for (u16 vertex_num = 0; vertex_num < num_vertices; vertex_num++) - { - const u8 index = vertex_data[component_offset]; - vertex_data += vertex_size; - - // 0xff skips the vertex - if (index != 0xff) - { - if (index > max_index) - max_index = index; - } - } - } - else - { - for (u16 vertex_num = 0; vertex_num < num_vertices; vertex_num++) - { - const u16 index = Common::swap16(&vertex_data[component_offset]); - vertex_data += vertex_size; - - // 0xffff skips the vertex - if (index != 0xffff) - { - if (index > max_index) - max_index = index; - } - } - } - - const u32 array_start = m_cpmem.array_bases[array_index] + byte_offset; - const u32 array_size = m_cpmem.array_strides[array_index] * max_index + component_size; - - m_owner->UseMemory(array_start, array_size, MemoryUpdate::Type::VertexStream); -} - -FifoRecorder::FifoRecorder(Core::System& system) : m_system(system) -{ -} - -FifoRecorder::~FifoRecorder() = default; - -void FifoRecorder::StartRecording(s32 numFrames, CallbackFunc finishedCb) -{ - std::lock_guard lk(m_mutex); - - m_File = std::make_unique(); - - // TODO: This, ideally, would be deallocated when done recording. - // However, care needs to be taken since global state - // and multithreading don't play well nicely together. - // The video thread may call into functions that utilize these - // despite 'end recording' being requested via StopRecording(). - // (e.g. OpcodeDecoder calling UseMemory()) - // - // Basically: - // - Singletons suck - // - Global variables suck - // - Multithreading with the above two sucks - // - auto& memory = m_system.GetMemory(); - m_Ram.resize(memory.GetRamSize()); - m_ExRam.resize(memory.GetExRamSize()); - - std::ranges::fill(m_Ram, 0); - std::ranges::fill(m_ExRam, 0); - - m_File->SetIsWii(m_system.IsWii()); - - if (!m_IsRecording) - { - m_WasRecording = false; - m_IsRecording = true; - m_RecordFramesRemaining = numFrames; - } - - m_RequestedRecordingEnd = false; - m_FinishedCb = std::move(finishedCb); - - m_end_of_frame_event = - m_system.GetVideoEvents().after_frame_event.Register([this](const Core::System& system) { - const bool was_recording = OpcodeDecoder::g_record_fifo_data; - OpcodeDecoder::g_record_fifo_data = IsRecording(); - - if (!OpcodeDecoder::g_record_fifo_data) - { - // Remove this frame end callback when recording finishes - m_end_of_frame_event.reset(); - return; - } - - if (!was_recording) - { - RecordInitialVideoMemory(); - } - - const auto& fifo = system.GetCommandProcessor().GetFifo(); - EndFrame(fifo.CPBase.load(std::memory_order_relaxed), - fifo.CPEnd.load(std::memory_order_relaxed)); - }); -} - -void FifoRecorder::RecordInitialVideoMemory() -{ - const u32* bpmem_ptr = reinterpret_cast(&bpmem); - u32 cpmem[256] = {}; - // The FIFO recording format splits XF memory into xfmem and xfregs; follow - // that split here. - const u32* xfmem_ptr = reinterpret_cast(&xfmem); - const u32* xfregs_ptr = reinterpret_cast(&xfmem) + FifoDataFile::XF_MEM_SIZE; - u32 xfregs_size = sizeof(XFMemory) / 4 - FifoDataFile::XF_MEM_SIZE; - - g_main_cp_state.FillCPMemoryArray(cpmem); - - SetVideoMemory(bpmem_ptr, cpmem, xfmem_ptr, xfregs_ptr, xfregs_size, s_tex_mem.data()); -} - -void FifoRecorder::StopRecording() -{ - std::lock_guard lk(m_mutex); - m_RequestedRecordingEnd = true; -} - -bool FifoRecorder::IsRecordingDone() const -{ - return m_WasRecording && m_File != nullptr; -} - -FifoDataFile* FifoRecorder::GetRecordedFile() const -{ - return m_File.get(); -} - -void FifoRecorder::WriteGPCommand(const u8* data, u32 size) -{ - if (!m_SkipNextData) - { - // Assumes data contains all information for the command - // Calls FifoRecorder::UseMemory - const u32 analyzed_size = OpcodeDecoder::RunCommand(data, size, *m_record_analyzer); - - // Make sure FifoPlayer's command analyzer agrees about the size of the command. - if (analyzed_size != size) - { - PanicAlertFmt("FifoRecorder: Expected command to be {} bytes long, we were given {} bytes", - analyzed_size, size); - } - - // Copy data to buffer - size_t currentSize = m_FifoData.size(); - m_FifoData.resize(currentSize + size); - memcpy(&m_FifoData[currentSize], data, size); - } - - if (m_FrameEnded && !m_FifoData.empty()) - { - m_CurrentFrame.fifoData = m_FifoData; - - { - std::lock_guard lk(m_mutex); - - // Copy frame to file - // The file will be responsible for freeing the memory allocated for each frame's fifoData - m_File->AddFrame(m_CurrentFrame); - - if (m_FinishedCb && m_RequestedRecordingEnd) - m_FinishedCb(); - } - - m_CurrentFrame.memoryUpdates.clear(); - m_FifoData.clear(); - m_FrameEnded = false; - } - - m_SkipNextData = m_SkipFutureData; -} - -void FifoRecorder::UseMemory(u32 address, u32 size, MemoryUpdate::Type type, bool dynamicUpdate) -{ - auto& memory = m_system.GetMemory(); - - u8* curData; - u8* newData; - if (address & 0x10000000) - { - curData = &m_ExRam[address & memory.GetExRamMask()]; - newData = &memory.GetEXRAM()[address & memory.GetExRamMask()]; - } - else - { - curData = &m_Ram[address & memory.GetRamMask()]; - newData = &memory.GetRAM()[address & memory.GetRamMask()]; - } - - if (!dynamicUpdate && memcmp(curData, newData, size) != 0) - { - // Update current memory - memcpy(curData, newData, size); - - // Record memory update - MemoryUpdate memUpdate; - memUpdate.address = address; - memUpdate.fifoPosition = (u32)(m_FifoData.size()); - memUpdate.type = type; - memUpdate.data.resize(size); - std::copy_n(newData, size, memUpdate.data.begin()); - - m_CurrentFrame.memoryUpdates.push_back(std::move(memUpdate)); - } - else if (dynamicUpdate) - { - // Shadow the data so it won't be recorded as changed by a future UseMemory - memcpy(curData, newData, size); - } -} - -void FifoRecorder::EndFrame(u32 fifoStart, u32 fifoEnd) -{ - // m_IsRecording is assumed to be true at this point, otherwise this function would not be called - std::lock_guard lk(m_mutex); - - m_FrameEnded = true; - - m_CurrentFrame.fifoStart = fifoStart; - m_CurrentFrame.fifoEnd = fifoEnd; - - if (m_WasRecording) - { - // If recording a fixed number of frames then check if the end of the recording was reached - if (m_RecordFramesRemaining > 0) - { - --m_RecordFramesRemaining; - if (m_RecordFramesRemaining == 0) - m_RequestedRecordingEnd = true; - } - } - else - { - m_WasRecording = true; - - // Skip the first data which will be the frame copy command - m_SkipNextData = true; - m_SkipFutureData = false; - - m_FrameEnded = false; - - m_FifoData.reserve(1024 * 1024 * 4); - m_FifoData.clear(); - } - - if (m_RequestedRecordingEnd) - { - // Skip data after the next time WriteFifoData is called - m_SkipFutureData = true; - // Signal video backend that it should not call this function when the next frame ends - m_IsRecording = false; - } -} - -void FifoRecorder::SetVideoMemory(const u32* bpMem, const u32* cpMem, const u32* xfMem, - const u32* xfRegs, u32 xfRegsSize, const u8* texMem_ptr) -{ - std::lock_guard lk(m_mutex); - - if (m_File) - { - memcpy(m_File->GetBPMem(), bpMem, FifoDataFile::BP_MEM_SIZE * 4); - memcpy(m_File->GetCPMem(), cpMem, FifoDataFile::CP_MEM_SIZE * 4); - memcpy(m_File->GetXFMem(), xfMem, FifoDataFile::XF_MEM_SIZE * 4); - - u32 xfRegsCopySize = std::min((u32)FifoDataFile::XF_REGS_SIZE, xfRegsSize); - memcpy(m_File->GetXFRegs(), xfRegs, xfRegsCopySize * 4); - - memcpy(m_File->GetTexMem(), texMem_ptr, FifoDataFile::TEX_MEM_SIZE); - } - - m_record_analyzer = std::make_unique(this, cpMem); -} - -bool FifoRecorder::IsRecording() const -{ - return m_IsRecording; -} diff --git a/Source/Core/Core/FifoPlayer/FifoRecorder.h b/Source/Core/Core/FifoPlayer/FifoRecorder.h deleted file mode 100644 index 637d386c39..0000000000 --- a/Source/Core/Core/FifoPlayer/FifoRecorder.h +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2011 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include -#include - -#include "Common/Assert.h" -#include "Common/HookableEvent.h" -#include "Core/FifoPlayer/FifoDataFile.h" - -namespace Core -{ -class System; -} - -class FifoRecorder -{ -public: - using CallbackFunc = std::function; - - explicit FifoRecorder(Core::System& system); - FifoRecorder(const FifoRecorder&) = delete; - FifoRecorder(FifoRecorder&&) = delete; - FifoRecorder& operator=(const FifoRecorder&) = delete; - FifoRecorder& operator=(FifoRecorder&&) = delete; - ~FifoRecorder(); - - void StartRecording(s32 numFrames, CallbackFunc finishedCb); - void StopRecording(); - - bool IsRecordingDone() const; - - FifoDataFile* GetRecordedFile() const; - // Called from video thread - - // Must write one full GP command at a time - void WriteGPCommand(const u8* data, u32 size); - - // Track memory that has been used and write it to the fifolog if it has changed. - // If memory is updated by the video backend (dynamicUpdate == true) take special care to make - // sure the data - // isn't baked into the fifolog. - void UseMemory(u32 address, u32 size, MemoryUpdate::Type type, bool dynamicUpdate = false); - - void EndFrame(u32 fifoStart, u32 fifoEnd); - - // This function must be called before writing GP commands - // bpMem must point to the actual bp mem array used by the plugin because it will be read as fifo - // data is recorded - void SetVideoMemory(const u32* bpMem, const u32* cpMem, const u32* xfMem, const u32* xfRegs, - u32 xfRegsSize, const u8* texMem); - - // Checked once per frame prior to callng EndFrame() - bool IsRecording() const; - -private: - class FifoRecordAnalyzer; - - void RecordInitialVideoMemory(); - - // Accessed from both GUI and video threads - - std::recursive_mutex m_mutex; - // True if video thread should send data - bool m_IsRecording = false; - // True if m_IsRecording was true during last frame - bool m_WasRecording = false; - bool m_RequestedRecordingEnd = false; - s32 m_RecordFramesRemaining = 0; - CallbackFunc m_FinishedCb; - std::unique_ptr m_File; - - // Accessed only from video thread - - bool m_SkipNextData = true; - bool m_SkipFutureData = true; - bool m_FrameEnded = false; - FifoFrameInfo m_CurrentFrame; - std::unique_ptr m_record_analyzer; - std::vector m_FifoData; - std::vector m_Ram; - std::vector m_ExRam; - - Common::EventHook m_end_of_frame_event; - - Core::System& m_system; -}; diff --git a/Source/Core/Core/FreeLookManager.cpp b/Source/Core/Core/FreeLookManager.cpp index 146e71cc1d..f3083fb383 100644 --- a/Source/Core/Core/FreeLookManager.cpp +++ b/Source/Core/Core/FreeLookManager.cpp @@ -237,7 +237,7 @@ void FreeLookController::Update() void FreeLookController::UpdateInput(CameraControllerInput* camera_controller) { - const auto lock = GetStateLock(); + const auto input_scope = ReadScope(); // Preserve the old controller gate state const auto old_gate = ControlReference::GetInputGate(); Common::ScopeGuard gate_guard{[old_gate] { ControlReference::SetInputGate(old_gate); }}; diff --git a/Source/Core/Core/GeckoCode.cpp b/Source/Core/Core/GeckoCode.cpp index 79b8449e92..13ca2421c1 100644 --- a/Source/Core/Core/GeckoCode.cpp +++ b/Source/Core/Core/GeckoCode.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include @@ -47,20 +46,16 @@ static Installation s_code_handler_installed = Installation::Uninstalled; // the currently active codes static std::vector s_active_codes; static std::vector s_synced_codes; -static std::mutex s_active_codes_lock; size_t CountEnabledCodes() { if (!Config::AreCheatsEnabled()) return 0; - - std::lock_guard guard(s_active_codes_lock); return s_active_codes.size(); } void SetActiveCodes(std::span gcodes, const std::string& game_id, u16 revision) { - std::lock_guard lk(s_active_codes_lock); s_active_codes.clear(); if (Config::AreCheatsEnabled()) @@ -96,7 +91,6 @@ void UpdateSyncedCodes(std::span gcodes) std::vector SetAndReturnActiveCodes(std::span gcodes) { - std::lock_guard lk(s_active_codes_lock); s_active_codes.clear(); if (Config::AreCheatsEnabled()) @@ -112,7 +106,7 @@ std::vector SetAndReturnActiveCodes(std::span gcodes return s_active_codes; } -// Requires s_active_codes_lock +// Emulation thread only, like everything touching the active codes. // NOTE: Refer to "codehandleronly.s" from Gecko OS. static Installation InstallCodeHandlerLocked(const Core::CPUThreadGuard& guard) { @@ -221,14 +215,12 @@ static Installation InstallCodeHandlerLocked(const Core::CPUThreadGuard& guard) // modifications will be reset] void DoState(PointerWrap& p) { - std::lock_guard codes_lock(s_active_codes_lock); p.Do(s_code_handler_installed); // FIXME: The active codes list will disagree with the embedded GCT } void Shutdown() { - std::lock_guard codes_lock(s_active_codes_lock); s_active_codes.clear(); s_code_handler_installed = Installation::Uninstalled; } @@ -238,9 +230,8 @@ void RunCodeHandler(const Core::CPUThreadGuard& guard) if (!Config::AreCheatsEnabled()) return; - // NOTE: Need to release the lock because of GUI deadlocks with PanicAlert in HostWrite_* + // Emulation thread only; the codes are never touched elsewhere. { - std::lock_guard codes_lock(s_active_codes_lock); if (s_code_handler_installed != Installation::Installed) { // Don't spam retry if the install failed. The corrupt / missing disk file is not likely to be diff --git a/Source/Core/Core/HW/DSPLLE/DSPLLE.cpp b/Source/Core/Core/HW/DSPLLE/DSPLLE.cpp index 83fc7b002f..5b360bda0f 100644 --- a/Source/Core/Core/HW/DSPLLE/DSPLLE.cpp +++ b/Source/Core/Core/HW/DSPLLE/DSPLLE.cpp @@ -3,9 +3,9 @@ #include "Core/HW/DSPLLE/DSPLLE.h" -#include #include +#include "Common/CallGate.h" #include "Common/ChunkFile.h" #include "Common/CommonPaths.h" #include "Common/CommonTypes.h" @@ -60,8 +60,8 @@ void DSPLLE::DSPThread(void* data) const int cycles = static_cast(dsp_lle->m_cycle_count.load()); if (cycles > 0) { - std::unique_lock dsp_thread_lock(dsp_lle->m_dsp_thread_mutex, std::try_to_lock); - if (dsp_thread_lock) + // Shut while a PauseAndLock holds the DSP; a run already inside finishes first. + if (Common::CallGateEnter(&dsp_lle->m_dsp_thread_gate)) { if (dsp_lle->m_dsp_core.IsJITCreated()) { @@ -72,6 +72,7 @@ void DSPLLE::DSPThread(void* data) dsp_lle->m_dsp_core.GetInterpreter().RunCyclesThread(cycles); } dsp_lle->m_cycle_count.store(0); + Common::CallGateExit(&dsp_lle->m_dsp_thread_gate); continue; } } @@ -286,12 +287,12 @@ u32 DSPLLE::DSP_UpdateRate() void DSPLLE::PauseAndLock() { - m_dsp_thread_mutex.lock(); + Common::CallGateClose(&m_dsp_thread_gate); } void DSPLLE::UnpauseAndUnlock() { - m_dsp_thread_mutex.unlock(); + Common::CallGateReopen(&m_dsp_thread_gate); if (m_is_dsp_on_thread) { diff --git a/Source/Core/Core/HW/DSPLLE/DSPLLE.h b/Source/Core/Core/HW/DSPLLE/DSPLLE.h index 8cba6adefd..81ddd5a454 100644 --- a/Source/Core/Core/HW/DSPLLE/DSPLLE.h +++ b/Source/Core/Core/HW/DSPLLE/DSPLLE.h @@ -4,10 +4,10 @@ #pragma once #include -#include #include +#include "Common/CallGate.h" #include "Common/CommonTypes.h" #include "Common/Flag.h" #include "Core/DSP/DSPCore.h" @@ -45,7 +45,7 @@ private: DSPCore m_dsp_core; sthread_t* m_dsp_thread = nullptr; - std::mutex m_dsp_thread_mutex; + Common::CallGate m_dsp_thread_gate; bool m_is_dsp_on_thread = false; Common::Flag m_is_running; std::atomic m_cycle_count{}; diff --git a/Source/Core/Core/HW/EXI/BBA/BuiltIn.cpp b/Source/Core/Core/HW/EXI/BBA/BuiltIn.cpp index a27c098ee2..8b52d09045 100644 --- a/Source/Core/Core/HW/EXI/BBA/BuiltIn.cpp +++ b/Source/Core/Core/HW/EXI/BBA/BuiltIn.cpp @@ -113,12 +113,13 @@ void CEXIETHERNET::BuiltInBBAInterface::Deactivate() m_read_thread_shutdown.Set(); m_active = false; + // Wait for read thread to exit; the network stack is this thread's again after. + Common::JoinThread(m_read_thread); + + RunRequests(); m_network_ref.Clear(); m_arp_table.clear(); m_upnp_httpd.close(); - - // Wait for read thread to exit. - Common::JoinThread(m_read_thread); } bool CEXIETHERNET::BuiltInBBAInterface::IsActivated() @@ -607,16 +608,65 @@ const Common::MACAddress& CEXIETHERNET::BuiltInBBAInterface::ResolveAddress(u32 } } +void CEXIETHERNET::BuiltInBBAInterface::Post(Request::Kind kind, std::vector frame) +{ + auto* request = new Request{{}, kind, std::move(frame)}; + mpsc_stack_push(&m_requests, &request->link); +} + +void CEXIETHERNET::BuiltInBBAInterface::RunRequests() +{ + for (mpsc_stack_node_t* node = mpsc_stack_reverse(mpsc_stack_drain(&m_requests)); node;) + { + auto* request = reinterpret_cast(node); + node = node->next; + switch (request->kind) + { + case Request::Kind::Frame: + ProcessFrame(request->frame.data(), static_cast(request->frame.size())); + break; + case Request::Kind::Start: + InitUDPPort(26502); // Kirby Air Ride + InitUDPPort(26512); // Mario Kart: Double Dash!! and 1080° Avalanche + break; + case Request::Kind::Stop: + m_network_ref.Clear(); + m_queue_read = 0; + m_queue_write = 0; + break; + } + delete request; + } +} + bool CEXIETHERNET::BuiltInBBAInterface::SendFrame(const u8* frame, u32 size) { - std::lock_guard lock(m_mtx); + // CPU thread: check the frame and hand it to the read thread, which owns the network stack. + const std::optional ethertype = Common::PacketView(frame, size).GetEtherType(); + if (!ethertype.has_value()) + { + ERROR_LOG_FMT(SP1, "Unable to send frame with invalid ethernet header"); + return false; + } + if (*ethertype != Common::IPV4_ETHERTYPE && *ethertype != Common::ARP_ETHERTYPE) + { + ERROR_LOG_FMT(SP1, "Unsupported EtherType {:#06x}", *ethertype); + return false; + } + Post(Request::Kind::Frame, std::vector(frame, frame + size)); + m_eth_ref->SendComplete(); + return true; +} + +void CEXIETHERNET::BuiltInBBAInterface::ProcessFrame(const u8* frame, u32 size) +{ const Common::PacketView view(frame, size); const std::optional ethertype = view.GetEtherType(); if (!ethertype.has_value()) { ERROR_LOG_FMT(SP1, "Unable to send frame with invalid ethernet header"); - return false; + return; } switch (*ethertype) @@ -627,7 +677,7 @@ bool CEXIETHERNET::BuiltInBBAInterface::SendFrame(const u8* frame, u32 size) if (!ip_proto.has_value()) { ERROR_LOG_FMT(SP1, "Unable to send frame with invalid IP header"); - return false; + return; } switch (*ip_proto) @@ -638,7 +688,7 @@ bool CEXIETHERNET::BuiltInBBAInterface::SendFrame(const u8* frame, u32 size) if (!udp_packet.has_value()) { ERROR_LOG_FMT(SP1, "Unable to send frame with invalid UDP header"); - return false; + return; } if (ntohs(udp_packet->udp_header.destination_port) == 67) @@ -658,7 +708,7 @@ bool CEXIETHERNET::BuiltInBBAInterface::SendFrame(const u8* frame, u32 size) if (!tcp_packet.has_value()) { ERROR_LOG_FMT(SP1, "Unable to send frame with invalid TCP header"); - return false; + return; } HandleTCPFrame(*tcp_packet); @@ -685,7 +735,7 @@ bool CEXIETHERNET::BuiltInBBAInterface::SendFrame(const u8* frame, u32 size) if (!arp_packet.has_value()) { ERROR_LOG_FMT(SP1, "Unable to send frame with invalid ARP header"); - return false; + return; } HandleARP(*arp_packet); @@ -694,11 +744,9 @@ bool CEXIETHERNET::BuiltInBBAInterface::SendFrame(const u8* frame, u32 size) default: ERROR_LOG_FMT(SP1, "Unsupported EtherType {:#06x}", *ethertype); - return false; + return; } - m_eth_ref->SendComplete(); - return true; } void CEXIETHERNET::BuiltInBBAInterface::ReadThreadHandler(void* data) @@ -707,6 +755,8 @@ void CEXIETHERNET::BuiltInBBAInterface::ReadThreadHandler(void* data) std::size_t datasize = 0; while (!self->m_read_thread_shutdown.IsSet()) { + self->RunRequests(); + if (datasize == 0) { // Make thread less CPU hungry @@ -724,7 +774,6 @@ void CEXIETHERNET::BuiltInBBAInterface::ReadThreadHandler(void* data) if ((wp - rp) >= 8) continue; - std::lock_guard lock(self->m_mtx); // process queue file first if (self->m_queue_read != self->m_queue_write) { @@ -781,17 +830,14 @@ void CEXIETHERNET::BuiltInBBAInterface::RecvStart() { if (m_read_enabled.IsSet()) return; - InitUDPPort(26502); // Kirby Air Ride - InitUDPPort(26512); // Mario Kart: Double Dash!! and 1080° Avalanche + Post(Request::Kind::Start); m_read_enabled.Set(); } void CEXIETHERNET::BuiltInBBAInterface::RecvStop() { m_read_enabled.Clear(); - m_network_ref.Clear(); - m_queue_read = 0; - m_queue_write = 0; + Post(Request::Kind::Stop); } } // namespace ExpansionInterface diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.h b/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.h index df3fabd586..9b9109ec76 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.h +++ b/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.h @@ -5,7 +5,6 @@ #include #include -#include #include #include "SFML/Network/IpAddress.hpp" @@ -20,6 +19,7 @@ #include #endif +#include #include "Common/Flag.h" #include "Common/Network.h" #include "Core/HW/EXI/BBA/BuiltIn.h" @@ -448,7 +448,26 @@ private: u8 m_queue_read = 0; u8 m_queue_write = 0; std::array, 16> m_queue_data; - std::mutex m_mtx; + + // The read thread owns the whole network stack. The CPU thread hands it outgoing frames and + // start/stop requests through this stack instead of sharing the stack's state. + struct Request + { + enum class Kind + { + Frame, + Start, + Stop, + }; + mpsc_stack_node_t link; // first: the drained chain is cast back + Kind kind; + std::vector frame; + }; + mpsc_stack_t m_requests{}; + void Post(Request::Kind kind, std::vector frame = {}); + // Read thread: runs what the CPU thread posted, oldest first. + void RunRequests(); + void ProcessFrame(const u8* frame, u32 size); std::string m_local_ip; u32 m_current_ip = 0; Common::MACAddress m_current_mac{}; diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceGecko.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceGecko.cpp index d79dca9fee..5ed3f71387 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceGecko.cpp +++ b/Source/Core/Core/HW/EXI/EXI_DeviceGecko.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include @@ -22,11 +21,16 @@ u16 GeckoSockServer::server_port; int GeckoSockServer::client_count; sthread_t* GeckoSockServer::connectionThread = nullptr; Common::Flag GeckoSockServer::server_running; -std::mutex GeckoSockServer::connection_lock; -std::queue> GeckoSockServer::waiting_socks; +mpsc_stack_t GeckoSockServer::waiting_socks; +std::deque> GeckoSockServer::taken_socks; + +// Bytes held each way; the Gecko moves one at a time, so this is ample. +constexpr size_t GECKO_FIFO_SIZE = 64 * 1024; GeckoSockServer::GeckoSockServer() : client_running(false) { + retro_spsc_init(&send_fifo, GECKO_FIFO_SIZE); + retro_spsc_init(&recv_fifo, GECKO_FIFO_SIZE); if (!connectionThread) connectionThread = sthread_create(GeckoConnectionWaiter, nullptr); } @@ -46,6 +50,9 @@ GeckoSockServer::~GeckoSockServer() server_running.Clear(); Common::JoinThread(connectionThread); } + + retro_spsc_free(&send_fifo); + retro_spsc_free(&recv_fifo); } void GeckoSockServer::GeckoConnectionWaiter(void*) @@ -73,8 +80,8 @@ void GeckoSockServer::GeckoConnectionWaiter(void*) { if (server.accept(*new_client) == sf::Socket::Status::Done) { - std::lock_guard lk(connection_lock); - waiting_socks.push(std::move(new_client)); + auto* waiting = new WaitingSock{{}, std::move(new_client)}; + mpsc_stack_push(&waiting_socks, &waiting->link); new_client = std::make_unique(); } @@ -87,22 +94,30 @@ bool GeckoSockServer::GetAvailableSock() { bool sock_filled = false; - std::lock_guard lk(connection_lock); - - if (!waiting_socks.empty()) + // The stack gives the newest first; queue them oldest first. + for (mpsc_stack_node_t* node = mpsc_stack_reverse(mpsc_stack_drain(&waiting_socks)); node;) { - client = std::move(waiting_socks.front()); + auto* waiting = reinterpret_cast(node); + node = node->next; + taken_socks.push_back(std::move(waiting->socket)); + delete waiting; + } + + if (!taken_socks.empty()) + { + client = std::move(taken_socks.front()); if (clientThread) { client_running.Clear(); Common::JoinThread(clientThread); - recv_fifo = std::deque(); - send_fifo = std::deque(); + // Both ends are quiet now. + retro_spsc_clear(&recv_fifo); + retro_spsc_clear(&send_fifo); } clientThread = sthread_create(Common::ThreadEntry<&GeckoSockServer::ClientThread>, this); client_count++; - waiting_socks.pop(); + taken_socks.pop_front(); sock_filled = true; } @@ -122,8 +137,6 @@ void GeckoSockServer::ClientThread() bool did_nothing = true; { - std::lock_guard lk(transfer_lock); - // what's an ideal buffer size? std::array buffer; std::size_t got = 0; @@ -135,20 +148,21 @@ void GeckoSockServer::ClientThread() { did_nothing = false; - recv_fifo.insert(recv_fifo.end(), buffer.data(), buffer.data() + got); + // A full ring drops the excess; a guest that stopped reading would never take it. + retro_spsc_write(&recv_fifo, buffer.data(), got); } - if (!send_fifo.empty()) + if (const size_t pending = retro_spsc_read_avail(&send_fifo)) { did_nothing = false; - std::vector packet(send_fifo.begin(), send_fifo.end()); - send_fifo.clear(); + std::vector packet(pending); + retro_spsc_read(&send_fifo, packet.data(), pending); if (client->send(&packet[0], packet.size()) == sf::Socket::Status::Disconnected) client_running.Clear(); } - } // unlock transfer + } if (did_nothing) Common::YieldCPU(); @@ -187,12 +201,9 @@ void CEXIGecko::ImmReadWrite(u32& _uData, u32 _uSize) // |= 0x08000000 if successful case CMD_RECV: { - std::lock_guard lk(transfer_lock); - if (!recv_fifo.empty()) - { - _uData = 0x08000000 | (recv_fifo.front() << 16); - recv_fifo.pop_front(); - } + u8 byte; + if (retro_spsc_read(&recv_fifo, &byte, 1) == 1) + _uData = 0x08000000 | (byte << 16); break; } @@ -200,8 +211,8 @@ void CEXIGecko::ImmReadWrite(u32& _uData, u32 _uSize) // |= 0x04000000 if successful case CMD_SEND: { - std::lock_guard lk(transfer_lock); - send_fifo.push_back(_uData >> 20); + const u8 byte = static_cast(_uData >> 20); + retro_spsc_write(&send_fifo, &byte, 1); _uData = 0x04000000; break; } @@ -216,8 +227,7 @@ void CEXIGecko::ImmReadWrite(u32& _uData, u32 _uSize) // |= 0x04000000 if data in recv FIFO case CMD_CHK_RX: { - std::lock_guard lk(transfer_lock); - _uData = recv_fifo.empty() ? 0 : 0x04000000; + _uData = retro_spsc_read_avail(&recv_fifo) ? 0x04000000 : 0; break; } diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceGecko.h b/Source/Core/Core/HW/EXI/EXI_DeviceGecko.h index 9714ae0c01..648746c8b7 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceGecko.h +++ b/Source/Core/Core/HW/EXI/EXI_DeviceGecko.h @@ -6,9 +6,10 @@ #include #include #include -#include #include +#include +#include #include #include "Common/CommonTypes.h" @@ -28,10 +29,10 @@ public: std::unique_ptr client; void ClientThread(); sthread_t* clientThread = nullptr; - std::mutex transfer_lock; - std::deque send_fifo; - std::deque recv_fifo; + // Bytes between the emulated Gecko (CPU thread) and the client thread, one ring each way. + retro_spsc_t send_fifo; // CPU thread -> client + retro_spsc_t recv_fifo; // client -> CPU thread private: static int client_count; @@ -43,8 +44,15 @@ private: static u16 server_port; static Common::Flag server_running; static sthread_t* connectionThread; - static std::mutex connection_lock; - static std::queue> waiting_socks; + // Accepted sockets, pushed by the connection thread and taken by the CPU thread. + struct WaitingSock + { + mpsc_stack_node_t link; // first: the drained chain is cast back + std::unique_ptr socket; + }; + static mpsc_stack_t waiting_socks; + // The CPU thread's own queue of them, oldest first. + static std::deque> taken_socks; }; class CEXIGecko : public IEXIDevice, private GeckoSockServer diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceMic.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceMic.cpp index ce97f034ea..652ba160b4 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceMic.cpp +++ b/Source/Core/Core/HW/EXI/EXI_DeviceMic.cpp @@ -5,7 +5,6 @@ #include #include -#include #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" @@ -21,15 +20,18 @@ namespace ExpansionInterface { +// The most the emulated microphone can buffer: 64 samples a read, 500 reads deep. +constexpr size_t MIC_STREAM_BYTES = 64 * 500 * sizeof(s16); + void CEXIMic::StreamInit() { - stream_buffer = nullptr; - samples_avail = stream_wpos = stream_rpos = 0; + retro_spsc_init(&m_stream, MIC_STREAM_BYTES); } void CEXIMic::StreamTerminate() { StreamStop(); + retro_spsc_free(&m_stream); } void CEXIMic::StreamStart() @@ -37,55 +39,47 @@ void CEXIMic::StreamStart() if (!Libretro::Input::g_has_microphone_support) return; - if (m_retro_mic) + if (retro_atomic_load_acquire_ptr(&m_retro_mic)) StreamStop(); - stream_size = buff_size_samples * 500; - stream_buffer = new s16[stream_size]; + // Start from an empty stream; the consumer side may drop what is there. + retro_spsc_skip(&m_stream, retro_spsc_read_avail(&m_stream)); retro_microphone_params_t params{}; params.rate = sample_rate; - m_retro_mic = Libretro::Input::g_microphone_interface.open_mic(¶ms); + retro_microphone_t* mic = Libretro::Input::g_microphone_interface.open_mic(¶ms); - if (!m_retro_mic) + if (!mic) { WARN_LOG_FMT(EXPANSIONINTERFACE, "CEXIMic: failed to open GC mic at {}Hz", sample_rate); return; } - Libretro::Input::g_microphone_interface.set_mic_state( - static_cast(m_retro_mic), true); + Libretro::Input::g_microphone_interface.set_mic_state(mic, true); + retro_atomic_store_release_ptr(&m_retro_mic, mic); } void CEXIMic::StreamStop() { - if (m_retro_mic) + auto* mic = static_cast(retro_atomic_exchange_ptr(&m_retro_mic, nullptr)); + if (mic) { - Libretro::Input::g_microphone_interface.close_mic( - static_cast(m_retro_mic)); - m_retro_mic = nullptr; + // A read the frontend thread already started finishes before the mic goes. + Common::CallGateClose(&m_mic_gate); + Libretro::Input::g_microphone_interface.close_mic(mic); + Common::CallGateReopen(&m_mic_gate); } - samples_avail = stream_wpos = stream_rpos = 0; - - delete[] stream_buffer; - stream_buffer = nullptr; + retro_spsc_skip(&m_stream, retro_spsc_read_avail(&m_stream)); } void CEXIMic::StreamReadOne() { - std::lock_guard lk(ring_lock); + if (retro_atomic_exchange_int(&m_overflowed, 0)) + status.buff_ovrflw = 1; - if (samples_avail >= buff_size_samples) - { - s16* last_buffer = &stream_buffer[stream_rpos]; - std::memcpy(ring_buffer, last_buffer, buff_size); - - samples_avail -= buff_size_samples; - - stream_rpos += buff_size_samples; - stream_rpos %= stream_size; - } + if (retro_spsc_read_avail(&m_stream) >= static_cast(buff_size)) + retro_spsc_read(&m_stream, ring_buffer, buff_size); } // EXI Mic Device @@ -229,29 +223,24 @@ void CEXIMic::TransferByte(u8& byte) void CEXIMic::PollLibretroMic() { - if (!m_retro_mic || !status.is_active) + // Frontend thread. + if (!Common::CallGateEnter(&m_mic_gate)) return; - s16 buf[64]; // max buff_size_samples (ring_base << 2 = 128 bytes / 2 = 64 samples) - - const int frames = Libretro::Input::g_microphone_interface.read_mic( - static_cast(m_retro_mic), buf, buff_size_samples); - - if (frames <= 0) - return; - - std::lock_guard lk(ring_lock); - for (int i = 0; i < frames; i++) + if (auto* mic = static_cast(retro_atomic_load_acquire_ptr(&m_retro_mic))) { - stream_buffer[stream_wpos] = buf[i]; - stream_wpos = (stream_wpos + 1) % stream_size; - } - samples_avail += frames; - if (samples_avail > stream_size) - { - samples_avail = 0; - status.buff_ovrflw = 1; + s16 buf[64]; // max buff_size_samples (ring_base << 2 = 128 bytes / 2 = 64 samples) + + const int frames = Libretro::Input::g_microphone_interface.read_mic(mic, buf, std::size(buf)); + if (frames > 0) + { + const size_t bytes = frames * sizeof(s16); + if (retro_spsc_write(&m_stream, buf, bytes) != bytes) + retro_atomic_store_release_int(&m_overflowed, 1); + } } + + Common::CallGateExit(&m_mic_gate); } } // namespace ExpansionInterface diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceMic.h b/Source/Core/Core/HW/EXI/EXI_DeviceMic.h index f5e21f82d0..e94ae3db1f 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceMic.h +++ b/Source/Core/Core/HW/EXI/EXI_DeviceMic.h @@ -3,8 +3,11 @@ #pragma once -#include +#include +#include + +#include "Common/CallGate.h" #include "Common/CommonTypes.h" #include "Core/HW/EXI/EXI_Device.h" @@ -79,21 +82,21 @@ private: UStatus status; - std::mutex ring_lock; - // status bits converted to nice numbers int sample_rate; int buff_size; int buff_size_samples; - // Arbitrarily small ringbuffer used by audio input backend in order to - // keep delay tolerable - s16* stream_buffer; - int stream_size; - int stream_wpos; - int stream_rpos; - int samples_avail; + // Samples from the frontend's microphone (written on the frontend thread by + // PollLibretroMic) to the emulated one (read on the CPU thread). Small, to keep the delay + // tolerable. + retro_spsc_t m_stream; + // Set by the frontend thread when samples had to be dropped; taken by the CPU thread. + retro_atomic_int_t m_overflowed{}; - void* m_retro_mic = nullptr; + // The open frontend microphone, or null. The CPU thread opens and closes it; the frontend + // thread reads it inside m_mic_gate, so a close waits for a read in progress. + retro_atomic_ptr_t m_retro_mic{}; + Common::CallGate m_mic_gate; }; } // namespace ExpansionInterface diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp index 528fbbb453..a14d6ab4fa 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp +++ b/Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp @@ -21,8 +21,12 @@ namespace ExpansionInterface { +// A receive ring this size holds dozens of frames; the guest drains it long before then. +constexpr size_t MODEM_RECEIVE_RING_SIZE = 256 * 1024; + CEXIModem::CEXIModem(Core::System& system, ModemDeviceType type) : IEXIDevice(system) { + retro_spsc_init(&m_receive_buffer, MODEM_RECEIVE_RING_SIZE); switch (type) { case ModemDeviceType::TAPSERVER: @@ -40,6 +44,7 @@ CEXIModem::~CEXIModem() { m_network_interface->RecvStop(); m_network_interface->Deactivate(); + retro_spsc_free(&m_receive_buffer); } bool CEXIModem::IsPresent() const @@ -54,6 +59,7 @@ void CEXIModem::SetCS(int cs) bool CEXIModem::IsInterruptSet() { + SyncReceiveState(); return !!(m_regs[Register::INTERRUPT_MASK] & m_regs[Register::PENDING_INTERRUPT_MASK]); } @@ -153,6 +159,7 @@ u32 CEXIModem::ImmRead(u32 size) { return 0x02020000; // Device ID (modem) } + SyncReceiveState(); u32 ret = 0; for (u8 z = 0; z < size; z++) { @@ -216,11 +223,8 @@ void CEXIModem::HandleReadModemTransfer(void* data, u32 size) else if ((m_transfer_descriptor & 0x0F000000) == 0x08000000) { // Packet receive buffer - std::lock_guard g(m_receive_buffer_lock); - const std::size_t bytes_to_copy = std::min(size, m_receive_buffer.size()); - std::memcpy(data, m_receive_buffer.data(), bytes_to_copy); - m_receive_buffer = m_receive_buffer.substr(bytes_to_copy); - OnReceiveBufferSizeChangedLocked(true); + retro_spsc_read(&m_receive_buffer, data, size); + SyncReceiveState(); } else { @@ -304,15 +308,17 @@ void CEXIModem::SetInterruptFlag(u8 what, bool enabled, bool from_cpu) from_cpu ? CoreTiming::FromThread::CPU : CoreTiming::FromThread::NON_CPU, 0); } -void CEXIModem::OnReceiveBufferSizeChangedLocked(bool from_cpu) +void CEXIModem::SyncReceiveState() { - // The caller is expected to hold m_receive_buffer_lock when calling this. - const u16 bytes_available = - static_cast(std::min(m_receive_buffer.size(), 0x200)); + const std::size_t received = retro_spsc_read_avail(&m_receive_buffer); + const u16 bytes_available = static_cast(std::min(received, 0x200)); m_regs[Register::BYTES_AVAILABLE_HIGH] = (bytes_available >> 8) & 0xFF; m_regs[Register::BYTES_AVAILABLE_LOW] = bytes_available & 0xFF; - SetInterruptFlag(Interrupt::RECEIVE_BUFFER_ABOVE_THRESHOLD, - m_receive_buffer.size() >= GetRxThreshold(), from_cpu); + const bool above = received >= GetRxThreshold(); + if (above) + m_regs[Register::PENDING_INTERRUPT_MASK] |= Interrupt::RECEIVE_BUFFER_ABOVE_THRESHOLD; + else + m_regs[Register::PENDING_INTERRUPT_MASK] &= ~Interrupt::RECEIVE_BUFFER_ABOVE_THRESHOLD; // TODO: There is a second interrupt here, which the GameCube modem library // expects to be used when large frames are received. However, the correct // semantics for this interrupt aren't obvious. Reverse-engineering some games @@ -330,16 +336,10 @@ void CEXIModem::SendComplete() void CEXIModem::AddToReceiveBuffer(std::string&& data) { - std::lock_guard g(m_receive_buffer_lock); - if (m_receive_buffer.empty()) - { - m_receive_buffer = std::move(data); - } - else - { - m_receive_buffer += data; - } - OnReceiveBufferSizeChangedLocked(false); + // Network thread: hand the bytes over and have the CPU thread look at the interrupt. + if (retro_spsc_write(&m_receive_buffer, data.data(), data.size()) != data.size()) + ERROR_LOG_FMT(SP1, "Modem receive ring full; dropped part of a frame"); + m_system.GetExpansionInterface().ScheduleUpdateInterrupts(CoreTiming::FromThread::NON_CPU, 0); } void CEXIModem::AddATReply(const std::string& data) diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceModem.h b/Source/Core/Core/HW/EXI/EXI_DeviceModem.h index b6e79c54cf..401d6374f3 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceModem.h +++ b/Source/Core/Core/HW/EXI/EXI_DeviceModem.h @@ -3,11 +3,13 @@ #pragma once -#include #include "Core/HW/EXI/BBA/TAPServerConnection.h" #include "Core/HW/EXI/EXI_Device.h" +#include + + class PointerWrap; namespace ExpansionInterface @@ -83,7 +85,8 @@ private: void SetInterruptFlag(u8 what, bool enabled, bool from_cpu); void HandleReadModemTransfer(void* data, u32 size); void HandleWriteModemTransfer(const void* data, u32 size); - void OnReceiveBufferSizeChangedLocked(bool from_cpu); + // CPU thread: brings the receive registers and interrupt in line with the ring. + void SyncReceiveState(); void SendComplete(); void AddToReceiveBuffer(std::string&& data); void RunAllPendingATCommands(); @@ -155,8 +158,9 @@ private: std::string m_at_command_data; std::string m_at_reply_data; std::string m_send_buffer; - std::mutex m_receive_buffer_lock; - std::string m_receive_buffer; + // Received bytes, written by the network thread and read by the CPU thread, which alone + // touches m_regs. + retro_spsc_t m_receive_buffer; std::array m_regs{}; }; } // namespace ExpansionInterface diff --git a/Source/Core/Core/HW/GBAPadEmu.cpp b/Source/Core/Core/HW/GBAPadEmu.cpp index fd68bc9a1d..f6cd27004f 100644 --- a/Source/Core/Core/HW/GBAPadEmu.cpp +++ b/Source/Core/Core/HW/GBAPadEmu.cpp @@ -64,7 +64,7 @@ ControllerEmu::ControlGroup* GBAPad::GetGroup(GBAPadGroup group) const GCPadStatus GBAPad::GetInput() { - const auto lock = GetStateLock(); + const auto input_scope = ReadScope(); GCPadStatus pad = {}; // Buttons @@ -83,7 +83,7 @@ GCPadStatus GBAPad::GetInput() void GBAPad::SetReset(bool reset) { - const auto lock = GetStateLock(); + const auto input_scope = ReadScope(); m_reset_pending = reset; } diff --git a/Source/Core/Core/HW/GCKeyboardEmu.cpp b/Source/Core/Core/HW/GCKeyboardEmu.cpp index ba022fb6f9..e653cc9d30 100644 --- a/Source/Core/Core/HW/GCKeyboardEmu.cpp +++ b/Source/Core/Core/HW/GCKeyboardEmu.cpp @@ -114,7 +114,7 @@ ControllerEmu::ControlGroup* GCKeyboard::GetGroup(KeyboardGroup group) KeyboardStatus GCKeyboard::GetInput() const { - const auto lock = GetStateLock(); + const auto input_scope = ReadScope(); KeyboardStatus kb = {}; diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp index c56d69cd25..2a3f77774d 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -231,7 +230,7 @@ void GCMemcardDirectory::FlushThread() return; } - FlushToFile(); + WriteQueuedJobs(); } } @@ -307,7 +306,6 @@ s32 GCMemcardDirectory::Read(u32 src_address, s32 length, u8* dest_address) s32 GCMemcardDirectory::Write(u32 dest_address, s32 length, const u8* src_address) { - std::unique_lock l(m_write_mutex); if (length != 0x80) INFO_LOG_FMT(EXPANSIONINTERFACE, "Writing to {:#x}. Length: {:#x}", dest_address, length); const s32 block = dest_address / Memcard::BLOCK_SIZE; @@ -365,11 +363,13 @@ s32 GCMemcardDirectory::Write(u32 dest_address, s32 length, const u8* src_addres memcpy(m_last_block_address + offset, src_address, length); - l.unlock(); if (extra) extra = Write(dest_address + length, extra, src_address + length); if (offset + length == Memcard::BLOCK_SIZE) + { + QueueDirtySaves(); m_flush_trigger.Set(); + } return length + extra; } @@ -578,8 +578,14 @@ bool GCMemcardDirectory::SetUsedBlocks(int save_index) void GCMemcardDirectory::FlushToFile() { - std::unique_lock l(m_write_mutex); - Memcard::DEntry invalid; + QueueDirtySaves(); + WriteQueuedJobs(); +} + +void GCMemcardDirectory::QueueDirtySaves() +{ + const auto post = [this](FlushJob* job) { mpsc_stack_push(&m_flush_jobs, &job->link); }; + for (Memcard::GCIFile& save : m_saves) { if (save.m_dirty) @@ -615,40 +621,12 @@ void GCMemcardDirectory::FlushToFile() } save.m_filename = default_save_name; } - File::IOFile gci(save.m_filename, "wb"); - if (gci) - { - gci.WriteBytes(&save.m_gci_header, Memcard::DENTRY_SIZE); - for (const Memcard::GCMBlock& block : save.m_save_data) - gci.WriteBytes(block.m_block.data(), Memcard::BLOCK_SIZE); - - if (gci.IsGood()) - { - Core::DisplayMessage("Wrote save contents to GCI Folder", 4000); - } - else - { - Core::DisplayMessage( - fmt::format("Failed to write save contents to {}", save.m_filename), 10000); - ERROR_LOG_FMT(EXPANSIONINTERFACE, "Failed to save data to {}", save.m_filename); - } - } - else - { - Core::DisplayMessage( - fmt::format("Failed to open file at {} for writing", save.m_filename), 10000); - ERROR_LOG_FMT(EXPANSIONINTERFACE, "Failed to open file at {} for writing", - save.m_filename); - } + post(new FlushJob{{}, save.m_filename, false, save.m_gci_header, save.m_save_data}); } else if (save.m_filename.length() != 0) { save.m_dirty = false; - std::string& old_name = save.m_filename; - std::string deleted_name = old_name + ".deleted"; - if (File::Exists(deleted_name)) - File::Delete(deleted_name); - File::Rename(old_name, deleted_name); + post(new FlushJob{{}, save.m_filename, true, {}, {}}); save.m_filename.clear(); save.m_save_data.clear(); save.m_used_blocks.clear(); @@ -667,17 +645,67 @@ void GCMemcardDirectory::FlushToFile() save.m_save_data.clear(); } } -#if _WRITE_MC_HEADER - u8 mc[BLOCK_SIZE * MC_FST_BLOCKS]; - Read(0, BLOCK_SIZE * MC_FST_BLOCKS, mc); - File::IOFile hdrfile(m_save_directory + MC_HDR, "wb"); - hdrfile.WriteBytes(mc, BLOCK_SIZE * MC_FST_BLOCKS); -#endif +} + +void GCMemcardDirectory::WriteQueuedJobs() +{ + // Oldest first, so a later job for the same file wins. + std::vector> jobs; + for (mpsc_stack_node_t* node = mpsc_stack_reverse(mpsc_stack_drain(&m_flush_jobs)); node;) + { + auto* job = reinterpret_cast(node); + node = node->next; + jobs.emplace_back(job); + } + + for (size_t i = 0; i < jobs.size(); ++i) + { + const FlushJob& job = *jobs[i]; + const bool superseded = std::any_of(jobs.begin() + i + 1, jobs.end(), + [&job](const auto& later) { + return later->filename == job.filename; + }); + if (superseded) + continue; + + if (job.retire) + { + const std::string deleted_name = job.filename + ".deleted"; + if (File::Exists(deleted_name)) + File::Delete(deleted_name); + File::Rename(job.filename, deleted_name); + continue; + } + + File::IOFile gci(job.filename, "wb"); + if (gci) + { + gci.WriteBytes(&job.header, Memcard::DENTRY_SIZE); + for (const Memcard::GCMBlock& block : job.blocks) + gci.WriteBytes(block.m_block.data(), Memcard::BLOCK_SIZE); + + if (gci.IsGood()) + { + Core::DisplayMessage("Wrote save contents to GCI Folder", 4000); + } + else + { + Core::DisplayMessage(fmt::format("Failed to write save contents to {}", job.filename), + 10000); + ERROR_LOG_FMT(EXPANSIONINTERFACE, "Failed to save data to {}", job.filename); + } + } + else + { + Core::DisplayMessage(fmt::format("Failed to open file at {} for writing", job.filename), + 10000); + ERROR_LOG_FMT(EXPANSIONINTERFACE, "Failed to open file at {} for writing", job.filename); + } + } } void GCMemcardDirectory::DoState(PointerWrap& p) { - std::unique_lock l(m_write_mutex); m_last_block = -1; m_last_block_address = nullptr; p.Do(m_save_directory); diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.h b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.h index 7012dee99b..4d2e3d0d80 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.h +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.h @@ -3,12 +3,13 @@ #pragma once -#include #include #include #include +#include + #include "Common/Event.h" #include "Common/Flag.h" #include "Core/HW/GCMemcard/GCIFile.h" @@ -33,6 +34,7 @@ public: GCMemcardDirectory(GCMemcardDirectory&&) = delete; GCMemcardDirectory& operator=(GCMemcardDirectory&&) = delete; + // Writes everything still pending to disk now, on the calling thread. void FlushToFile(); void FlushThread(); s32 Read(u32 src_address, s32 length, u8* dest_address) override; @@ -49,6 +51,21 @@ private: inline void SyncSaves(); bool SetUsedBlocks(int save_index); + // A file to write or retire, made by the CPU thread from its saves and written by the + // flush thread, so the two never touch the same data. + struct FlushJob + { + mpsc_stack_node_t link; // first: the drained chain is cast back + std::string filename; + bool retire; // rename to .deleted instead of writing + Memcard::DEntry header; + std::vector blocks; + }; + // CPU thread: turns the dirty saves into jobs for the flush thread. + void QueueDirtySaves(); + // Flush thread (or the destructor): writes the queued jobs, the newest per file. + void WriteQueuedJobs(); + u32 m_game_id; s32 m_last_block; u8* m_last_block_address; @@ -62,7 +79,7 @@ private: std::string m_save_directory; Common::Event m_flush_trigger; - std::mutex m_write_mutex; + mpsc_stack_t m_flush_jobs{}; Common::Flag m_exiting; sthread_t* m_flush_thread = nullptr; }; diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.cpp b/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.cpp index 4fec579204..403d3b1882 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.cpp +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.cpp @@ -4,9 +4,10 @@ #include "Core/HW/GCMemcard/GCMemcardRaw.h" #include +#include +#include #include #include -#include #include #include @@ -77,6 +78,7 @@ MemoryCard::MemoryCard(std::string filename, ExpansionInterface::Slot card_slot, // Class members (including inherited ones) have now been initialized, so // it's safe to startup the flush thread (which reads them). m_flush_buffer = std::make_unique(m_memory_card_size); + memcpy(&m_flush_buffer[0], &m_memcard_data[0], m_memory_card_size); m_flush_thread = sthread_create(Common::ThreadEntry<&MemoryCard::FlushThread>, this); } @@ -88,6 +90,38 @@ MemoryCard::~MemoryCard() Common::JoinThread(m_flush_thread); } + for (mpsc_stack_node_t* node = mpsc_stack_drain(&m_changes); node;) + { + mpsc_stack_node_t* const next = node->next; + std::free(node); + node = next; + } +} + +void MemoryCard::PostChange(u32 offset, u32 length, const u8* data) +{ + auto* change = static_cast( + std::malloc(offsetof(Change, data) + (data ? length : 0) + 1)); + change->offset = offset; + change->length = length; + change->fill = data == nullptr; + if (data) + memcpy(change->data, data, length); + mpsc_stack_push(&m_changes, &change->link); +} + +void MemoryCard::ApplyChanges() +{ + for (mpsc_stack_node_t* node = mpsc_stack_reverse(mpsc_stack_drain(&m_changes)); node;) + { + auto* change = reinterpret_cast(node); + node = node->next; + if (change->fill) + memset(&m_flush_buffer[change->offset], 0xFF, change->length); + else + memcpy(&m_flush_buffer[change->offset], change->data, change->length); + std::free(change); + } } void MemoryCard::FlushThread() @@ -145,10 +179,7 @@ void MemoryCard::FlushThread() return; } - { - std::unique_lock l(m_flush_mutex); - memcpy(&m_flush_buffer[0], &m_memcard_data[0], m_memory_card_size); - } + ApplyChanges(); file.WriteBytes(&m_flush_buffer[0], m_memory_card_size); if (do_exit) @@ -186,10 +217,8 @@ s32 MemoryCard::Write(u32 dest_address, s32 length, const u8* src_address) return -1; } - { - std::unique_lock l(m_flush_mutex); - memcpy(&m_memcard_data[dest_address], src_address, length); - } + memcpy(&m_memcard_data[dest_address], src_address, length); + PostChange(dest_address, length, src_address); MakeDirty(); return length; } @@ -201,20 +230,15 @@ void MemoryCard::ClearBlock(u32 address) PanicAlertFmtT("MemoryCard: ClearBlock called on invalid address ({0:#x})", address); return; } - else - { - std::unique_lock l(m_flush_mutex); - memset(&m_memcard_data[address], 0xFF, Memcard::BLOCK_SIZE); - } + memset(&m_memcard_data[address], 0xFF, Memcard::BLOCK_SIZE); + PostChange(address, Memcard::BLOCK_SIZE, nullptr); MakeDirty(); } void MemoryCard::ClearAll() { - { - std::unique_lock l(m_flush_mutex); - memset(&m_memcard_data[0], 0xFF, m_memory_card_size); - } + memset(&m_memcard_data[0], 0xFF, m_memory_card_size); + PostChange(0, m_memory_card_size, nullptr); MakeDirty(); } @@ -223,4 +247,7 @@ void MemoryCard::DoState(PointerWrap& p) p.Do(m_card_slot); p.Do(m_memory_card_size); p.DoArray(&m_memcard_data[0], m_memory_card_size); + // A loaded state replaces the whole card; the flush thread gets it as one change. + if (p.IsReadMode()) + PostChange(0, m_memory_card_size, &m_memcard_data[0]); } diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.h b/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.h index 3176dc6f9c..f6637094c0 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.h +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardRaw.h @@ -4,10 +4,11 @@ #pragma once #include -#include #include +#include #include + #include "Common/Event.h" #include "Common/Flag.h" #include "Core/HW/GCMemcard/GCMemcard.h" @@ -31,6 +32,21 @@ public: void DoState(PointerWrap& p) override; private: + // A change the flush thread has yet to copy into its own buffer: length bytes at offset, + // taken from data, or set to 0xFF when fill. + struct Change + { + mpsc_stack_node_t link; // first: the drained chain is cast back + u32 offset; + u32 length; + bool fill; + u8 data[1]; + }; + // CPU thread: hands a change to the flush thread. + void PostChange(u32 offset, u32 length, const u8* data); + // Flush thread: applies what was posted, oldest first. + void ApplyChanges(); + bool IsAddressInBounds(u32 address, u32 length) const { const u64 end_address = static_cast(address) + static_cast(length); @@ -38,10 +54,12 @@ private: } std::string m_filename; + // The card as the emulated side sees it; CPU thread only. std::unique_ptr m_memcard_data; + // The flush thread's own copy, brought up to date from m_changes before each write. std::unique_ptr m_flush_buffer; + mpsc_stack_t m_changes{}; sthread_t* m_flush_thread = nullptr; - std::mutex m_flush_mutex; Common::Event m_flush_trigger; Common::Flag m_dirty; u32 m_memory_card_size; diff --git a/Source/Core/Core/HW/GCPadEmu.cpp b/Source/Core/Core/HW/GCPadEmu.cpp index c832cd3cbb..679ed9c60e 100644 --- a/Source/Core/Core/HW/GCPadEmu.cpp +++ b/Source/Core/Core/HW/GCPadEmu.cpp @@ -142,7 +142,7 @@ GCPadStatus GCPad::GetInput() const { using ControllerEmu::MapFloat; - const auto lock = GetStateLock(); + const auto input_scope = ReadScope(); GCPadStatus pad = {}; if (!(m_always_connected_setting.GetValue() || IsDefaultDeviceConnected() || @@ -187,7 +187,7 @@ GCPadStatus GCPad::GetInput() const void GCPad::SetOutput(const ControlState strength) { - const auto lock = GetStateLock(); + const auto input_scope = ReadScope(); m_rumble->controls[0]->control_ref->State(strength); } @@ -273,7 +273,7 @@ bool GCPad::GetMicButton() const return true; return false; #else - const auto lock = GetStateLock(); + const auto input_scope = ReadScope(); return m_mic->controls.back()->GetState(); #endif } diff --git a/Source/Core/Core/HW/SI/SI_DeviceGBA.cpp b/Source/Core/Core/HW/SI/SI_DeviceGBA.cpp index 38c19da4f7..3eef49c3f4 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceGBA.cpp +++ b/Source/Core/Core/HW/SI/SI_DeviceGBA.cpp @@ -6,9 +6,10 @@ #include #include #include -#include #include +#include + #include "Common/CommonTypes.h" #include "Common/Flag.h" #include "Common/Logging/Log.h" @@ -23,10 +24,40 @@ namespace SerialInterface namespace { sthread_t* s_connection_thread = nullptr; -std::queue> s_waiting_socks; -std::queue> s_waiting_clocks; -std::mutex s_cs_gba; -std::mutex s_cs_gba_clk; +// Accepted sockets, pushed by the connection thread and taken by the CPU thread, which keeps +// its own queue of them oldest first. +struct WaitingSock +{ + mpsc_stack_node_t link; // first: the drained chain is cast back + std::unique_ptr socket; +}; +mpsc_stack_t s_waiting_socks; +mpsc_stack_t s_waiting_clocks; +std::queue> s_taken_socks; +std::queue> s_taken_clocks; + +void PushSock(mpsc_stack_t* stack, std::unique_ptr socket) +{ + auto* waiting = new WaitingSock{{}, std::move(socket)}; + mpsc_stack_push(stack, &waiting->link); +} + +std::unique_ptr TakeSock(mpsc_stack_t* stack, + std::queue>& taken) +{ + for (mpsc_stack_node_t* node = mpsc_stack_reverse(mpsc_stack_drain(stack)); node;) + { + auto* waiting = reinterpret_cast(node); + node = node->next; + taken.push(std::move(waiting->socket)); + delete waiting; + } + if (taken.empty()) + return nullptr; + std::unique_ptr socket = std::move(taken.front()); + taken.pop(); + return socket; +} int s_num_connected; Common::Flag s_server_running; } // namespace @@ -60,15 +91,13 @@ static void GBAConnectionWaiter(void*) { if (server.accept(*new_client) == sf::Socket::Status::Done) { - std::lock_guard lk(s_cs_gba); - s_waiting_socks.push(std::move(new_client)); + PushSock(&s_waiting_socks, std::move(new_client)); new_client = std::make_unique(); } if (clock_server.accept(*new_client) == sf::Socket::Status::Done) { - std::lock_guard lk(s_cs_gba_clk); - s_waiting_clocks.push(std::move(new_client)); + PushSock(&s_waiting_clocks, std::move(new_client)); new_client = std::make_unique(); } @@ -83,26 +112,14 @@ void GBAConnectionWaiter_Shutdown() Common::JoinThread(s_connection_thread); } -template -static std::unique_ptr MoveFromFront(std::queue>& ptrs) -{ - if (ptrs.empty()) - return nullptr; - std::unique_ptr ptr = std::move(ptrs.front()); - ptrs.pop(); - return ptr; -} - static std::unique_ptr GetNextSock() { - std::lock_guard lk(s_cs_gba); - return MoveFromFront(s_waiting_socks); + return TakeSock(&s_waiting_socks, s_taken_socks); } static std::unique_ptr GetNextClock() { - std::lock_guard lk(s_cs_gba_clk); - return MoveFromFront(s_waiting_clocks); + return TakeSock(&s_waiting_clocks, s_taken_clocks); } GBASockServer::GBASockServer() diff --git a/Source/Core/Core/HW/Wiimote.cpp b/Source/Core/Core/HW/Wiimote.cpp index 1679ffd151..95fca8e897 100644 --- a/Source/Core/Core/HW/Wiimote.cpp +++ b/Source/Core/Core/HW/Wiimote.cpp @@ -83,7 +83,7 @@ HIDWiimote* GetHIDWiimoteSource(unsigned int index) break; case WiimoteSource::Real: - hid_source = WiimoteReal::g_wiimotes[index].get(); + hid_source = WiimoteReal::GetWiimote(index); break; default: diff --git a/Source/Core/Core/HW/WiimoteEmu/EmuSubroutines.cpp b/Source/Core/Core/HW/WiimoteEmu/EmuSubroutines.cpp index 22fbe7d234..64d5edaf99 100644 --- a/Source/Core/Core/HW/WiimoteEmu/EmuSubroutines.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/EmuSubroutines.cpp @@ -586,7 +586,7 @@ ExtensionNumber Wiimote::GetActiveExtensionNumber() const return m_active_extension; } -ControllerEmu::SubscribableSettingValue& Wiimote::GetMotionPlusSetting() +ControllerEmu::SettingValue& Wiimote::GetMotionPlusSetting() { return m_motion_plus_setting; } diff --git a/Source/Core/Core/HW/WiimoteEmu/Extension/Shinkansen.cpp b/Source/Core/Core/HW/WiimoteEmu/Extension/Shinkansen.cpp index db52979a3d..0feabe4acd 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Extension/Shinkansen.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/Extension/Shinkansen.cpp @@ -106,7 +106,7 @@ void Shinkansen::Update(const DesiredExtensionState& target_state) ext_data.buttons = desired_state.buttons ^ 0xFFFF; Common::BitCastPtr(&m_reg.controller_data) = ext_data; - const auto lock = ControllerEmu::EmulatedController::GetStateLock(); + const auto input_scope = ControllerEmu::EmulatedController::ReadScope(); m_led->controls[0]->control_ref->State(m_reg.identifier[1]); } diff --git a/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp b/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp index bbbea66e37..8597f91863 100644 --- a/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp @@ -546,7 +546,7 @@ void Wiimote::SetWiimoteDeviceIndex(u8 index) void Wiimote::PrepareInput(WiimoteEmu::DesiredWiimoteState* target_state, SensorBarState sensor_bar_state) { - const auto lock = GetStateLock(); + const auto input_scope = ReadScope(); BuildDesiredWiimoteState(target_state, sensor_bar_state); } @@ -676,7 +676,7 @@ void Wiimote::SendDataReport(const DesiredWiimoteState& target_state) ButtonData Wiimote::GetCurrentlyPressedButtons() { - const auto lock = GetStateLock(); + const auto input_scope = ReadScope(); ButtonData buttons{}; m_buttons->GetState(&buttons.hex, button_bitmasks, m_input_override_function); @@ -823,7 +823,7 @@ bool Wiimote::IsUpright() const void Wiimote::SetRumble(bool on) { - const auto lock = GetStateLock(); + const auto input_scope = ReadScope(); m_rumble->controls.front()->control_ref->State(on); } diff --git a/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.h b/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.h index 1e07e2abca..d21410cb75 100644 --- a/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.h +++ b/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.h @@ -173,7 +173,7 @@ public: // Active extension number is exposed for TAS. ExtensionNumber GetActiveExtensionNumber() const; - ControllerEmu::SubscribableSettingValue& GetMotionPlusSetting(); + ControllerEmu::SettingValue& GetMotionPlusSetting(); static Common::Vec3 OverrideVec3(const ControllerEmu::ControlGroup* control_group, Common::Vec3 vec, @@ -305,7 +305,7 @@ private: ControllerEmu::SettingValue m_sideways_setting; ControllerEmu::SettingValue m_upright_setting; ControllerEmu::SettingValue m_battery_setting; - ControllerEmu::SubscribableSettingValue m_motion_plus_setting; + ControllerEmu::SettingValue m_motion_plus_setting; ControllerEmu::SettingValue m_fov_x_setting; ControllerEmu::SettingValue m_fov_y_setting; diff --git a/Source/Core/Core/HW/WiimoteReal/WiimoteReal.cpp b/Source/Core/Core/HW/WiimoteReal/WiimoteReal.cpp index 9f03f3a031..cafa700033 100644 --- a/Source/Core/Core/HW/WiimoteReal/WiimoteReal.cpp +++ b/Source/Core/Core/HW/WiimoteReal/WiimoteReal.cpp @@ -4,15 +4,19 @@ #include "Core/HW/WiimoteReal/WiimoteReal.h" #include -#include #include #include +#include +#include +#include + #include #include "Common/Assert.h" #include "Common/CommonTypes.h" #include "Common/Config/Config.h" +#include "Common/Quiesce.h" #include "Common/Swap.h" #include "Common/Thread.h" @@ -42,16 +46,52 @@ static void HandleWiimoteDisconnect(int index); static bool s_real_wiimotes_initialized = false; -// This is used to store connected Wiimotes' IDs, so we don't connect -// more than once to the same device. -static std::unordered_set s_known_ids; -static std::mutex s_known_ids_mutex; +// Slots and the pool are owned by one thread, the manager (the scanner's pool thread). Other +// threads hand it commands; the slots themselves are published pointers anyone may read. +// A slot changes only while the manager holds the CPU thread (CPUThreadGuard), so the +// emulated Bluetooth device, which keeps the pointer, never sees one go away under it. +retro_atomic_ptr_t g_wiimotes[MAX_BBMOTES]; -std::recursive_mutex g_wiimotes_mutex; +Wiimote* GetWiimote(unsigned int index) +{ + return static_cast(retro_atomic_load_acquire_ptr(&g_wiimotes[index])); +} -// Real wii remotes assigned to a particular slot. -// Assignments must be done from the CPU thread with the above mutex held. -std::unique_ptr g_wiimotes[MAX_BBMOTES]; +// Connected Wiimotes' IDs, so we don't connect more than once to the same device. The scanner +// reads and adds to it, a Wiimote going away takes its ID out; each change swaps in a copy. +struct KnownIds +{ + quiesce_node_t node; // first: the free callback casts it back + std::unordered_set ids; +}; +static quiesce_t s_known_ids_quiesce; +static retro_atomic_ptr_t s_known_ids; + +static void FreeKnownIds(quiesce_node_t* node) +{ + delete reinterpret_cast(node); +} + +template +static void EditKnownIds(Edit&& edit) +{ + quiesce_enter(&s_known_ids_quiesce); + for (;;) + { + void* const cur = retro_atomic_load_acquire_ptr(&s_known_ids); + auto* next = new KnownIds{{}, cur ? static_cast(cur)->ids : + std::unordered_set{}}; + edit(next->ids); + if (retro_atomic_cas_ptr(&s_known_ids, cur, next)) + { + if (cur) + quiesce_retire(&s_known_ids_quiesce, &static_cast(cur)->node, FreeKnownIds); + break; + } + delete next; + } + quiesce_exit(&s_known_ids_quiesce); +} struct WiimotePoolEntry { @@ -70,17 +110,58 @@ struct WiimotePoolEntry }; // Connected wii remotes are placed here when no open slot is set to "Real". -// They are then automatically disconnected after some time. +// They are then automatically disconnected after some time. Manager only. static std::vector s_wiimote_pool; +// What other threads ask of the manager. +struct ManagerCommand +{ + enum class Kind + { + AddToPool, + ConnectBalanceBoard, + SourceChange, + ProcessPool, + EmuStop, + EmuResume, + EmuPause, + }; + mpsc_stack_node_t link; // first: the drained chain is cast back + Kind kind; + std::unique_ptr wiimote; + unsigned int index; +}; +static mpsc_stack_t s_manager_commands; +static retro_eventcount_t s_manager_wake; +static retro_atomic_int_t s_manager_running; +static thread_local bool t_is_manager = false; + +static void PostToManager(ManagerCommand::Kind kind, std::unique_ptr wiimote = {}, + unsigned int index = 0) +{ + auto* command = new ManagerCommand{{}, kind, std::move(wiimote), index}; + mpsc_stack_push(&s_manager_commands, &command->link); + retro_eventcount_notify(&s_manager_wake); +} + +static void PutWiimoteInSlot(unsigned int index, std::unique_ptr wiimote) +{ + delete static_cast(retro_atomic_exchange_ptr(&g_wiimotes[index], wiimote.release())); +} + +static std::unique_ptr TakeWiimoteFromSlot(unsigned int index) +{ + return std::unique_ptr( + static_cast(retro_atomic_exchange_ptr(&g_wiimotes[index], nullptr))); +} + static WiimoteScanner s_wiimote_scanner; // Attempt to fill a real wiimote slot from the pool or by stealing from ControllerInterface. +// Manager only. static void TryToFillWiimoteSlot(u32 index) { - std::lock_guard lk(g_wiimotes_mutex); - - if (g_wiimotes[index] || + if (GetWiimote(index) || Config::Get(Config::GetInfoForWiimoteSource(index)) != WiimoteSource::Real) { return; @@ -107,12 +188,9 @@ void PopulateDevices() } // Attempts to fill enabled real wiimote slots. -// Push/pull wiimotes to/from ControllerInterface as needed. -// Should be called PopulateDevices() to be in line with other implementations. -void ProcessWiimotePool() +// Push/pull wiimotes to/from ControllerInterface as needed. Manager only. +static void ProcessWiimotePool() { - std::lock_guard lk(g_wiimotes_mutex); - for (u32 index = 0; index != MAX_WIIMOTES; ++index) TryToFillWiimoteSlot(index); @@ -129,7 +207,7 @@ void ProcessWiimotePool() } } -void AddWiimoteToPool(std::unique_ptr wiimote) +static void AddWiimoteToPoolNow(std::unique_ptr wiimote) { // Our real wiimote class requires an index. // Within the pool it's only going to be used for logging purposes. @@ -143,10 +221,20 @@ void AddWiimoteToPool(std::unique_ptr wiimote) wiimote->EmuStop(); - std::lock_guard lk(g_wiimotes_mutex); s_wiimote_pool.emplace_back(WiimotePoolEntry{std::move(wiimote)}); } +void AddWiimoteToPool(std::unique_ptr wiimote) +{ + // ControllerInterface hands remotes back from inside the manager's own pool processing, + // which expects them in the pool on return. + if (t_is_manager) + AddWiimoteToPoolNow(std::move(wiimote)); + else if (retro_atomic_load_acquire_int(&s_manager_running)) + PostToManager(ManagerCommand::Kind::AddToPool, std::move(wiimote)); + // With no manager the remote is simply let go, which disconnects it. +} + // Records each ring holds: 256 reports is over a second of input at the Wiimote's 200 Hz. static constexpr size_t REPORT_RING_RECORDS = 256; @@ -182,8 +270,7 @@ void Wiimote::QueueWrite(TimePoint time, const u8* data, size_t size) void Wiimote::Shutdown() { - std::lock_guard lk(s_known_ids_mutex); - s_known_ids.erase(GetId()); + EditKnownIds([this](std::unordered_set& ids) { ids.erase(GetId()); }); StopThread(); ClearReadQueue(); @@ -563,12 +650,11 @@ void Wiimote::EmuPause() static unsigned int CalculateWantedWiimotes() { - std::lock_guard lk(g_wiimotes_mutex); // Figure out how many real Wiimotes are required unsigned int wanted_wiimotes = 0; for (int i = 0; i < MAX_WIIMOTES; ++i) { - if (Config::Get(Config::GetInfoForWiimoteSource(i)) == WiimoteSource::Real && !g_wiimotes[i]) + if (Config::Get(Config::GetInfoForWiimoteSource(i)) == WiimoteSource::Real && !GetWiimote(i)) ++wanted_wiimotes; } return wanted_wiimotes; @@ -576,10 +662,9 @@ static unsigned int CalculateWantedWiimotes() static unsigned int CalculateWantedBB() { - std::lock_guard lk(g_wiimotes_mutex); unsigned int wanted_bb = 0; if (Config::Get(Config::WIIMOTE_BB_SOURCE) == WiimoteSource::Real && - !g_wiimotes[WIIMOTE_BALANCE_BOARD]) + !GetWiimote(WIIMOTE_BALANCE_BOARD)) { ++wanted_bb; } @@ -600,12 +685,17 @@ void WiimoteScanner::StopThread() { SetScanMode(WiimoteScanMode::DO_NOT_SCAN); - for (const auto& backend : m_backends) + // The scanner thread frees its backends only after it sees the running flag drop, which + // happens after this, so the list stays valid here. + if (const auto* backends = static_cast*>( + retro_atomic_load_acquire_ptr(&m_backends_published))) { - backend->RequestStopSearching(); + for (WiimoteScannerBackend* backend : *backends) + backend->RequestStopSearching(); } m_scan_thread_running.Clear(); + retro_eventcount_notify(&s_manager_wake); Common::JoinThread(m_scan_thread); } } @@ -616,29 +706,111 @@ void WiimoteScanner::SetScanMode(WiimoteScanMode scan_mode) m_scan_mode_changed_or_population_event.Set(); } +// Manager only. static void CheckForDisconnectedWiimotes() { - std::lock_guard lk(g_wiimotes_mutex); for (unsigned int i = 0; i < MAX_BBMOTES; ++i) - if (g_wiimotes[i] && !g_wiimotes[i]->IsConnected()) + if (const Wiimote* wiimote = GetWiimote(i); wiimote && !wiimote->IsConnected()) HandleWiimoteDisconnect(i); } +static void RunManagerCommand(ManagerCommand& command) +{ + switch (command.kind) + { + case ManagerCommand::Kind::AddToPool: + AddWiimoteToPoolNow(std::move(command.wiimote)); + break; + case ManagerCommand::Kind::ConnectBalanceBoard: + TryToConnectBalanceBoard(std::move(command.wiimote)); + break; + case ManagerCommand::Kind::SourceChange: + { + std::unique_ptr removed_wiimote; + { + const Core::CPUThreadGuard guard(Core::System::GetInstance()); + removed_wiimote = TakeWiimoteFromSlot(command.index); + WiimoteCommon::UpdateSource(command.index); + } + + // The Wiimote pool isn't currently designed to handle Balance Boards. + // Refactoring will be needed to differentiate between remotes/boards. + const bool is_balance_board = command.index == WIIMOTE_BALANCE_BOARD; + + if (removed_wiimote != nullptr && !is_balance_board) + AddWiimoteToPoolNow(std::move(removed_wiimote)); + g_controller_interface.PlatformPopulateDevices([] { ProcessWiimotePool(); }); + break; + } + case ManagerCommand::Kind::ProcessPool: + g_controller_interface.PlatformPopulateDevices([] { ProcessWiimotePool(); }); + break; + case ManagerCommand::Kind::EmuStop: + case ManagerCommand::Kind::EmuResume: + case ManagerCommand::Kind::EmuPause: + for (unsigned int i = 0; i < MAX_BBMOTES; ++i) + { + Wiimote* const wiimote = GetWiimote(i); + if (!wiimote || !wiimote->IsConnected()) + continue; + if (command.kind == ManagerCommand::Kind::EmuStop) + wiimote->EmuStop(); + else if (command.kind == ManagerCommand::Kind::EmuResume) + wiimote->EmuResume(); + else + wiimote->EmuPause(); + } + break; + } +} + +static void RunManagerCommands() +{ + // The stack gives the newest first; run them in the order they were posted. + for (mpsc_stack_node_t* node = + mpsc_stack_reverse(mpsc_stack_drain(&s_manager_commands)); + node;) + { + auto* command = reinterpret_cast(node); + node = node->next; + RunManagerCommand(*command); + delete command; + } +} + void WiimoteScanner::PoolThreadFunc() { Common::SetCurrentThreadName("Wiimote Pool Thread"); + t_is_manager = true; // Toggle between 1010 and 0101. u8 led_value = 0b1010; - auto next_time = std::chrono::steady_clock::now(); + constexpr auto TICK = std::chrono::milliseconds(250); + auto next_tick = std::chrono::steady_clock::now(); while (m_scan_thread_running.IsSet()) { - std::this_thread::sleep_until(next_time); - next_time += std::chrono::milliseconds(250); + RunManagerCommands(); - std::lock_guard lk(g_wiimotes_mutex); + const auto now = std::chrono::steady_clock::now(); + if (now < next_tick) + { + // Sleep until the next tick or a command, whichever comes first. + const int key = retro_eventcount_prepare_wait(&s_manager_wake); + if (!mpsc_stack_empty(&s_manager_commands) || !m_scan_thread_running.IsSet()) + { + retro_eventcount_cancel_wait(&s_manager_wake); + continue; + } + retro_eventcount_commit_wait_timeout( + &s_manager_wake, key, + std::chrono::duration_cast(next_tick - now).count()); + continue; + } + next_tick = now + TICK; + + CheckForDisconnectedWiimotes(); // Remove stale pool entries. for (auto it = s_wiimote_pool.begin(); it != s_wiimote_pool.end();) @@ -669,12 +841,15 @@ void WiimoteScanner::PoolThreadFunc() led_value ^= 0b1111; } + + // What was posted before the stop still runs, so no remote handed over is lost. + RunManagerCommands(); + t_is_manager = false; } void WiimoteScanner::PopulateDevices() { - m_populate_devices.Set(); - m_scan_mode_changed_or_population_event.Set(); + PostToManager(ManagerCommand::Kind::ProcessPool); } void WiimoteScanner::ThreadFunc() @@ -682,6 +857,7 @@ void WiimoteScanner::ThreadFunc() #if defined(__LIBRETRO__) && defined(SKIP_WIMOTESCANNER_THREAD) return; #endif + retro_atomic_store_release_int(&s_manager_running, 1); sthread_t* pool_thread = sthread_create(Common::ThreadEntry<&WiimoteScanner::PoolThreadFunc>, this); @@ -693,30 +869,25 @@ void WiimoteScanner::ThreadFunc() // HIDAPI backend on macOS has an error condition when IOHIDManagerCreate and IOHIDManagerClose // are called on different threads (and so reference different CFRunLoops) which can cause an // EXC_BAD_ACCES crash. - { - std::lock_guard lg(m_backends_mutex); + std::vector> backends; + backends.emplace_back(std::make_unique()); + backends.emplace_back(std::make_unique()); + backends.emplace_back(std::make_unique()); + backends.emplace_back(std::make_unique()); - m_backends.emplace_back(std::make_unique()); - m_backends.emplace_back(std::make_unique()); - m_backends.emplace_back(std::make_unique()); - m_backends.emplace_back(std::make_unique()); - } + std::vector published; + for (const auto& backend : backends) + published.push_back(backend.get()); + retro_atomic_store_release_ptr(&m_backends_published, &published); while (m_scan_thread_running.IsSet()) { m_scan_mode_changed_or_population_event.WaitFor(std::chrono::milliseconds(500)); - if (m_populate_devices.TestAndClear()) - { - g_controller_interface.PlatformPopulateDevices([] { ProcessWiimotePool(); }); - } - // Currently does nothing. To be removed. - for (const auto& backend : m_backends) + for (const auto& backend : backends) backend->Update(); - CheckForDisconnectedWiimotes(); - // If we don't want Wiimotes in ControllerInterface, we may not need them at all. if (!Config::Get(Config::MAIN_CONNECT_WIIMOTES_FOR_CONTROLLER_INTERFACE)) { @@ -742,43 +913,35 @@ void WiimoteScanner::ThreadFunc() // This allows Windows, hidapi, and DolphinBar remotes to be quickly discovered. const bool should_perform_inquiry = scan_mode != WiimoteScanMode::DO_NOT_SCAN; - for (const auto& backend : m_backends) + for (const auto& backend : backends) { auto results = should_perform_inquiry ? backend->FindNewWiimotes() : backend->FindAttachedWiimotes(); + + for (auto& wiimote : results.wii_remotes) { - std::unique_lock wm_lk(g_wiimotes_mutex); + EditKnownIds([&wiimote](std::unordered_set& ids) { + ids.insert(wiimote->GetId()); + }); + PostToManager(ManagerCommand::Kind::AddToPool, std::move(wiimote)); + PostToManager(ManagerCommand::Kind::ProcessPool); + } - for (auto& wiimote : results.wii_remotes) - { - { - std::lock_guard lk(s_known_ids_mutex); - s_known_ids.insert(wiimote->GetId()); - } - - AddWiimoteToPool(std::move(wiimote)); - g_controller_interface.PlatformPopulateDevices([] { ProcessWiimotePool(); }); - } - - for (auto& bboard : results.balance_boards) - { - { - std::lock_guard lk(s_known_ids_mutex); - s_known_ids.insert(bboard->GetId()); - } - - TryToConnectBalanceBoard(std::move(bboard)); - } + for (auto& bboard : results.balance_boards) + { + EditKnownIds([&bboard](std::unordered_set& ids) { + ids.insert(bboard->GetId()); + }); + PostToManager(ManagerCommand::Kind::ConnectBalanceBoard, std::move(bboard)); } } } - { - std::lock_guard lg(m_backends_mutex); - m_backends.clear(); - } + retro_atomic_store_release_ptr(&m_backends_published, nullptr); + backends.clear(); Common::JoinThread(pool_thread); + retro_atomic_store_release_int(&s_manager_running, 0); NOTICE_LOG_FMT(WIIMOTE, "Wiimote scanning thread has stopped."); } @@ -956,9 +1119,7 @@ void Initialize(::Wiimote::InitializeMode init_mode) // called on emulation shutdown void Stop() { - for (auto& wiimote : g_wiimotes) - if (wiimote && wiimote->IsConnected()) - wiimote->EmuStop(); + PostToManager(ManagerCommand::Kind::EmuStop); } // called when the Dolphin app exits @@ -969,33 +1130,39 @@ void Shutdown() NOTICE_LOG_FMT(WIIMOTE, "WiimoteReal::Shutdown"); - std::lock_guard lk(g_wiimotes_mutex); + // The manager has stopped, so this thread now owns the slots and the pool. + t_is_manager = true; for (unsigned int i = 0; i < MAX_BBMOTES; ++i) HandleWiimoteDisconnect(i); // Release remotes from ControllerInterface and empty the pool. ciface::WiimoteController::ReleaseDevices(); s_wiimote_pool.clear(); + t_is_manager = false; + + // Commands nobody will run now. + for (mpsc_stack_node_t* node = mpsc_stack_drain(&s_manager_commands); node;) + { + auto* command = reinterpret_cast(node); + node = node->next; + delete command; + } } void Resume() { - for (auto& wiimote : g_wiimotes) - if (wiimote && wiimote->IsConnected()) - wiimote->EmuResume(); + PostToManager(ManagerCommand::Kind::EmuResume); } void Pause() { - for (auto& wiimote : g_wiimotes) - if (wiimote && wiimote->IsConnected()) - wiimote->EmuPause(); + PostToManager(ManagerCommand::Kind::EmuPause); } -// Called from the Wiimote scanner thread (or UI thread on source change) +// Manager only. static bool TryToConnectWiimoteToSlot(std::unique_ptr& wm, unsigned int i) { - if (Config::Get(Config::GetInfoForWiimoteSource(i)) != WiimoteSource::Real || g_wiimotes[i]) + if (Config::Get(Config::GetInfoForWiimoteSource(i)) != WiimoteSource::Real || GetWiimote(i)) return false; if (!wm->Connect(i)) @@ -1013,7 +1180,7 @@ static bool TryToConnectWiimoteToSlot(std::unique_ptr& wm, unsigned int { const Core::CPUThreadGuard guard(Core::System::GetInstance()); - g_wiimotes[i] = std::move(wm); + PutWiimoteInSlot(i, std::move(wm)); WiimoteCommon::UpdateSource(i); } @@ -1030,12 +1197,13 @@ static void TryToConnectBalanceBoard(std::unique_ptr wm) NOTICE_LOG_FMT(WIIMOTE, "No open slot for real balance board."); } +// Manager only. static void HandleWiimoteDisconnect(int index) { const Core::CPUThreadGuard guard(Core::System::GetInstance()); // The Wii Remote object must exist through the call to UpdateSource // to prevent WiimoteDevice from having a dangling HIDWiimote pointer. - const auto temp_real_wiimote = std::move(g_wiimotes[index]); + const auto temp_real_wiimote = TakeWiimoteFromSlot(index); WiimoteCommon::UpdateSource(index); } @@ -1066,8 +1234,11 @@ bool IsBalanceBoardName(std::string_view name) // This is called from the scanner backends (currently on the scanner thread). bool IsNewWiimote(const std::string& identifier) { - std::lock_guard lk(s_known_ids_mutex); - return !s_known_ids.contains(identifier); + quiesce_enter(&s_known_ids_quiesce); + const auto* known = static_cast(retro_atomic_load_acquire_ptr(&s_known_ids)); + const bool is_new = !known || !known->ids.contains(identifier); + quiesce_exit(&s_known_ids_quiesce); + return is_new; } bool IsKnownDeviceId(const USBUtils::DeviceInfo& device_info) @@ -1077,25 +1248,12 @@ bool IsKnownDeviceId(const USBUtils::DeviceInfo& device_info) void HandleWiimoteSourceChange(unsigned int index) { - std::lock_guard wm_lk(g_wiimotes_mutex); - - { - const Core::CPUThreadGuard guard(Core::System::GetInstance()); - auto removed_wiimote = std::move(g_wiimotes[index]); - - // The Wiimote pool isn't currently designed to handle Balance Boards. - // Refactoring will be needed to differentiate between remotes/boards. - const bool is_balance_board = index == WIIMOTE_BALANCE_BOARD; - - if (removed_wiimote != nullptr && !is_balance_board) - AddWiimoteToPool(std::move(removed_wiimote)); - } - g_controller_interface.PlatformPopulateDevices([] { ProcessWiimotePool(); }); + PostToManager(ManagerCommand::Kind::SourceChange, {}, index); } void HandleWiimotesInControllerInterfaceSettingChange() { - g_controller_interface.PlatformPopulateDevices([] { ProcessWiimotePool(); }); + PostToManager(ManagerCommand::Kind::ProcessPool); } } // namespace WiimoteReal diff --git a/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h b/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h index 764a24204d..e38627d538 100644 --- a/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h +++ b/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -21,6 +20,7 @@ #include "Core/HW/WiimoteCommon/WiimoteReport.h" #include "Core/USBUtils.h" +#include #include #include @@ -235,8 +235,9 @@ private: void ThreadFunc(); void PoolThreadFunc(); - std::vector> m_backends; - mutable std::mutex m_backends_mutex; + // The scanner thread's backends, as a std::vector* while it runs, + // so StopThread can interrupt a search. + retro_atomic_ptr_t m_backends_published{}; sthread_t* m_scan_thread = nullptr; Common::Flag m_scan_thread_running; @@ -245,9 +246,9 @@ private: std::atomic m_scan_mode{WiimoteScanMode::DO_NOT_SCAN}; }; -// Mutex is recursive as ControllerInterface may call AddWiimoteToPool within ProcessWiimotePool. -extern std::recursive_mutex g_wiimotes_mutex; -extern std::unique_ptr g_wiimotes[MAX_BBMOTES]; +// The real remote in a slot, or null. Changed only by the scanner's pool thread, while it holds +// the CPU thread. +Wiimote* GetWiimote(unsigned int index); void AddWiimoteToPool(std::unique_ptr); @@ -267,5 +268,4 @@ void InitAdapterClass(); void HandleWiimotesInControllerInterfaceSettingChange(); void PopulateDevices(); -void ProcessWiimotePool(); } // namespace WiimoteReal diff --git a/Source/Core/Core/HotkeyManager.cpp b/Source/Core/Core/HotkeyManager.cpp index a94e4d4cb0..7ed9a77972 100644 --- a/Source/Core/Core/HotkeyManager.cpp +++ b/Source/Core/Core/HotkeyManager.cpp @@ -354,7 +354,7 @@ void HotkeyManager::GetInput(HotkeyStatus* kb, bool ignore_focus) for (size_t key = 0; key < bitmasks.size(); key++) bitmasks[key] = static_cast(1 << key); - const auto lock = GetStateLock(); + const auto input_scope = ReadScope(); for (std::size_t group = 0; group < s_groups_info.size(); group++) { if (s_groups_info[group].ignore_focus != ignore_focus) diff --git a/Source/Core/Core/IOS/Network/IP/Top.cpp b/Source/Core/Core/IOS/Network/IP/Top.cpp index 05e3419734..ad89b56072 100644 --- a/Source/Core/Core/IOS/Network/IP/Top.cpp +++ b/Source/Core/Core/IOS/Network/IP/Top.cpp @@ -101,10 +101,8 @@ NetIPTopDevice::NetIPTopDevice(EmulationKernel& ios, const std::string& device_n m_work_queue.Reset("Network Worker", [this](const AsyncTask& task) { const IPCReply reply = task.handler(); - { - std::lock_guard lg(m_async_reply_lock); - m_async_replies.emplace(AsyncReply{task.request, reply.return_value}); - } + auto* async_reply = new AsyncReply{{}, task.request, reply.return_value}; + mpsc_stack_push(&m_async_replies, &async_reply->link); }); } @@ -565,14 +563,13 @@ std::optional NetIPTopDevice::IOCtlV(const IOCtlVRequest& request) void NetIPTopDevice::Update() { + // Oldest first, as the worker finished them. + for (mpsc_stack_node_t* node = mpsc_stack_reverse(mpsc_stack_drain(&m_async_replies)); node;) { - std::lock_guard lg(m_async_reply_lock); - while (!m_async_replies.empty()) - { - const auto& reply = m_async_replies.front(); - GetEmulationKernel().EnqueueIPCReply(reply.request, reply.return_value); - m_async_replies.pop(); - } + auto* reply = reinterpret_cast(node); + node = node->next; + GetEmulationKernel().EnqueueIPCReply(reply->request, reply->return_value); + delete reply; } GetEmulationKernel().GetSocketManager()->Update(); } diff --git a/Source/Core/Core/IOS/Network/IP/Top.h b/Source/Core/Core/IOS/Network/IP/Top.h index aff33aa5fb..417fb1c900 100644 --- a/Source/Core/Core/IOS/Network/IP/Top.h +++ b/Source/Core/Core/IOS/Network/IP/Top.h @@ -3,10 +3,11 @@ #pragma once -#include #include #include +#include + #include "Common/CommonTypes.h" #include "Common/SocketContext.h" #include "Common/WorkQueueThread.h" @@ -84,6 +85,7 @@ private: struct AsyncReply { + mpsc_stack_node_t link; // first: the drained chain is cast back IOS::HLE::Request request; s32 return_value; }; @@ -122,7 +124,7 @@ private: Common::SocketContext m_socket_context; Common::WorkQueueThreadSP m_work_queue; - std::mutex m_async_reply_lock; - std::queue m_async_replies; + // Replies from the network worker, handed to the emulation thread by Update. + mpsc_stack_t m_async_replies{}; }; } // namespace IOS::HLE diff --git a/Source/Core/Core/IOS/Network/KD/NetKDRequest.cpp b/Source/Core/Core/IOS/Network/KD/NetKDRequest.cpp index 7d00c5e9c1..1fae5343ae 100644 --- a/Source/Core/Core/IOS/Network/KD/NetKDRequest.cpp +++ b/Source/Core/Core/IOS/Network/KD/NetKDRequest.cpp @@ -161,14 +161,12 @@ NetKDRequestDevice::NetKDRequestDevice(EmulationKernel& ios, const std::string& return; #endif // Enable all NWC24 permissions - m_scheduler_buffer[1] = Common::swap32(-1); + StoreSchedulerWord(1, Common::swap32(-1)); m_work_queue.Reset("WiiConnect24 Worker", [this](const AsyncTask& task) { const IPCReply reply = task.handler(); - { - std::lock_guard lg(m_async_reply_lock); - m_async_replies.emplace(AsyncReply{task.request, reply.return_value}); - } + auto* async_reply = new AsyncReply{{}, task.request, reply.return_value}; + mpsc_stack_push(&m_async_replies, &async_reply->link); }); m_handle_mail = !ios.GetIOSC().IsUsingDefaultId() && !m_send_list.IsDisabled(); @@ -184,29 +182,54 @@ NetKDRequestDevice::~NetKDRequestDevice() if (socket_manager) socket_manager->Clean(); - { - std::lock_guard lg(m_scheduler_lock); - if (!m_scheduler_timer_thread) - return; + if (!m_scheduler_timer_thread) + return; - m_shutdown_event.Set(); - } + m_shutdown_event.Set(); Common::JoinThread(m_scheduler_timer_thread); m_scheduler_work_queue.Shutdown(); m_work_queue.Shutdown(); + + for (mpsc_stack_node_t* node = mpsc_stack_drain(&m_async_replies); node;) + { + auto* reply = reinterpret_cast(node); + node = node->next; + delete reply; + } } void NetKDRequestDevice::Update() { + // Oldest first, as the workers finished them. + for (mpsc_stack_node_t* node = mpsc_stack_reverse(mpsc_stack_drain(&m_async_replies)); node;) { - std::lock_guard lg(m_async_reply_lock); - while (!m_async_replies.empty()) - { - const auto& reply = m_async_replies.front(); - GetEmulationKernel().EnqueueIPCReply(reply.request, reply.return_value); - m_async_replies.pop(); - } + auto* reply = reinterpret_cast(node); + node = node->next; + GetEmulationKernel().EnqueueIPCReply(reply->request, reply->return_value); + delete reply; + } +} + +u32 NetKDRequestDevice::LoadSchedulerWord(size_t index) const +{ + return static_cast( + retro_atomic_load_acquire_int(const_cast(&m_scheduler_buffer[index]))); +} + +void NetKDRequestDevice::StoreSchedulerWord(size_t index, u32 swapped) +{ + retro_atomic_store_release_int(&m_scheduler_buffer[index], static_cast(swapped)); +} + +void NetKDRequestDevice::IncrementSchedulerWord(size_t index) +{ + int cur = retro_atomic_load_acquire_int(&m_scheduler_buffer[index]); + while (!retro_atomic_cas_int( + &m_scheduler_buffer[index], cur, + static_cast(Common::swap32(Common::swap32(static_cast(cur)) + 1)))) + { + cur = retro_atomic_load_acquire_int(&m_scheduler_buffer[index]); } } @@ -221,15 +244,17 @@ void NetKDRequestDevice::SchedulerTimer() while (true) { { - std::lock_guard lg(m_scheduler_lock); - if (m_mail_span <= mail_time_state && m_handle_mail) + if (static_cast(retro_atomic_load_acquire_int(&m_mail_span)) <= mail_time_state && + m_handle_mail) { m_scheduler_work_queue.Push([this] { SchedulerWorker(SchedulerEvent::Mail); }); INFO_LOG_FMT(IOS_WC24, "NET_KD_REQ: Dispatching Mail Task from Scheduler"); mail_time_state = 0; } - if (m_download_span <= download_time_state && !m_dl_list.IsDisabled()) + // The worker checks whether downloads are disabled; the list is its. + if (static_cast(retro_atomic_load_acquire_int(&m_download_span)) <= + download_time_state) { INFO_LOG_FMT(IOS_WC24, "NET_KD_REQ: Dispatching Download Task from Scheduler"); m_scheduler_work_queue.Push([this] { SchedulerWorker(SchedulerEvent::Download); }); @@ -255,10 +280,7 @@ void NetKDRequestDevice::SchedulerWorker(const SchedulerEvent event) if (code != NWC24::WC24_OK) { if (code == NWC24::WC24_ERR_NOT_FOUND) - { - std::lock_guard lg(m_scheduler_lock); - m_download_span = DEFAULT_SCHEDULER_SPAN_MINUTES; - } + retro_atomic_store_release_int(&m_download_span, DEFAULT_SCHEDULER_SPAN_MINUTES); LogError(ErrorType::KD_Download, code); return; @@ -340,33 +362,26 @@ void NetKDRequestDevice::LogError(ErrorType error_type, s32 error_code) break; } - std::lock_guard lg(m_scheduler_buffer_lock); - - m_scheduler_buffer[32 + (m_error_count % 32)] = Common::swap32(new_code); - m_error_count++; - - m_scheduler_buffer[5] = Common::swap32(m_error_count); - m_scheduler_buffer[2] = Common::swap32(new_code); + const u32 error_index = static_cast(retro_atomic_fetch_add_int(&m_error_count, 1)); + StoreSchedulerWord(32 + (error_index % 32), Common::swap32(new_code)); + StoreSchedulerWord(5, Common::swap32(error_index + 1)); + StoreSchedulerWord(2, Common::swap32(new_code)); } NWC24::ErrorCode NetKDRequestDevice::KDCheckMail(u32* mail_flag, u32* interval) { bool success = false; Common::ScopeGuard state_guard([&] { - std::lock_guard lg(m_scheduler_buffer_lock); if (success) { // m_scheduler_buffer[11] contains the amount of times we have checked for mail this IOS // session. - m_scheduler_buffer[11] = Common::swap32(Common::swap32(m_scheduler_buffer[11]) + 1); + IncrementSchedulerWord(11); } - m_scheduler_buffer[4] = static_cast(CurrentFunction::None); + StoreSchedulerWord(4, static_cast(CurrentFunction::None)); }); - { - std::lock_guard lg(m_scheduler_buffer_lock); - m_scheduler_buffer[4] = Common::swap32(static_cast(CurrentFunction::Check)); - } + StoreSchedulerWord(4, Common::swap32(static_cast(CurrentFunction::Check))); u64 random_number{}; Common::Random::Generate(&random_number, sizeof(u64)); @@ -417,7 +432,6 @@ NWC24::ErrorCode NetKDRequestDevice::KDCheckMail(u32* mail_flag, u32* interval) *mail_flag = std::strncmp(str_mail_flag.data(), m_send_list.GetMailFlag().data(), 22) != 0; { - std::lock_guard scheduler_lg(m_scheduler_lock); bool did_parse = TryParse(m_http.GetHeaderValue("X-Wii-Mail-Check-Span"), interval); if (did_parse) { @@ -426,17 +440,13 @@ NWC24::ErrorCode NetKDRequestDevice::KDCheckMail(u32* mail_flag, u32* interval) *interval = 1; } - m_mail_span = *interval; + retro_atomic_store_release_int(&m_mail_span, static_cast(*interval)); } - did_parse = TryParse(m_http.GetHeaderValue("X-Wii-Download-Span"), &m_download_span); + u32 download_span = 0; + did_parse = TryParse(m_http.GetHeaderValue("X-Wii-Download-Span"), &download_span); if (did_parse) - { - if (m_download_span == 0) - { - m_download_span = 1; - } - } + retro_atomic_store_release_int(&m_download_span, static_cast(std::max(download_span, 1u))); } success = true; @@ -521,22 +531,18 @@ NWC24::ErrorCode NetKDRequestDevice::KDSendMail() { bool success = false; Common::ScopeGuard exit_guard([&] { - std::lock_guard lg(m_scheduler_buffer_lock); if (success) { // m_scheduler_buffer[11] contains the amount of times we have sent for mail this IOS // session. - m_scheduler_buffer[14] = Common::swap32(Common::swap32(m_scheduler_buffer[14]) + 1); + IncrementSchedulerWord(14); } - m_scheduler_buffer[4] = static_cast(CurrentFunction::None); + StoreSchedulerWord(4, static_cast(CurrentFunction::None)); m_send_list.WriteSendList(); }); - { - std::lock_guard lg(m_scheduler_buffer_lock); - m_scheduler_buffer[4] = Common::swap32(static_cast(CurrentFunction::Send)); - } + StoreSchedulerWord(4, Common::swap32(static_cast(CurrentFunction::Send))); m_send_list.ReadSendList(); const std::string auth = @@ -1143,9 +1149,11 @@ std::optional NetKDRequestDevice::IOCtl(const IOCtlRequest& request) // On a real Wii, GetSchedulerStat copies memory containing a list of error codes recorded by // KD among other things. - std::lock_guard lg(m_scheduler_buffer_lock); + std::array buffer; + for (size_t i = 0; i < buffer.size(); ++i) + buffer[i] = LoadSchedulerWord(i); const u32 out_size = std::min(request.buffer_out_size, 256U); - memory.CopyToEmu(request.buffer_out, m_scheduler_buffer.data(), out_size); + memory.CopyToEmu(request.buffer_out, buffer.data(), out_size); break; } @@ -1197,9 +1205,10 @@ std::optional NetKDRequestDevice::IOCtl(const IOCtlRequest& request) INFO_LOG_FMT(IOS_WC24, "NET_KD_REQ: IOCTL_NWC24_KD_GET_TIME_TRIGGERS"); - std::lock_guard lg(m_scheduler_buffer_lock); - memory.Write_U32(m_mail_span, request.buffer_out + 4); - memory.Write_U32(m_download_span, request.buffer_out + 8); + memory.Write_U32(static_cast(retro_atomic_load_acquire_int(&m_mail_span)), + request.buffer_out + 4); + memory.Write_U32(static_cast(retro_atomic_load_acquire_int(&m_download_span)), + request.buffer_out + 8); WriteReturnValue(memory, NWC24::WC24_OK, request.buffer_out); return_value = IPC_SUCCESS; diff --git a/Source/Core/Core/IOS/Network/KD/NetKDRequest.h b/Source/Core/Core/IOS/Network/KD/NetKDRequest.h index 9ab5c1887f..bd75fd8545 100644 --- a/Source/Core/Core/IOS/Network/KD/NetKDRequest.h +++ b/Source/Core/Core/IOS/Network/KD/NetKDRequest.h @@ -5,7 +5,6 @@ #include #include -#include #include #include @@ -21,6 +20,8 @@ #include "Core/IOS/Network/KD/NWC24Config.h" #include "Core/IOS/Network/KD/NWC24DL.h" #include "Core/IOS/Network/KD/NetKDTime.h" +#include +#include namespace IOS::HLE { @@ -49,10 +50,17 @@ private: struct AsyncReply { + mpsc_stack_node_t link; // first: the drained chain is cast back IOS::HLE::Request request; s32 return_value; }; + // The scheduler buffer is read by the emulation thread and written by both worker threads, + // a word at a time; the words are kept byte-swapped as the guest reads them. + u32 LoadSchedulerWord(size_t index) const; + void StoreSchedulerWord(size_t index, u32 swapped); + void IncrementSchedulerWord(size_t index); + template std::optional LaunchAsyncTask(Method method, const Request& request) { @@ -114,19 +122,18 @@ private: NWC24::Mail::WC24FriendList m_friend_list; Common::WorkQueueThreadSP m_work_queue; Common::AsyncWorkThreadSP m_scheduler_work_queue; - std::mutex m_async_reply_lock; - std::mutex m_scheduler_buffer_lock; - std::queue m_async_replies; - u32 m_error_count = 0; - std::array m_scheduler_buffer{}; + // Replies from the worker threads, handed to the emulation thread by Update. + mpsc_stack_t m_async_replies{}; + retro_atomic_int_t m_error_count{}; + std::array m_scheduler_buffer{}; std::shared_ptr m_time_device; // TODO: Maybe move away from Common::HttpRequest? Common::HttpRequest m_http{std::chrono::minutes{1}}; - u32 m_download_span = 2; - u32 m_mail_span = 1; - bool m_handle_mail; + // Minutes between scheduled tasks, set by the workers from server replies. + retro_atomic_int_t m_download_span{2}; + retro_atomic_int_t m_mail_span{1}; + bool m_handle_mail; // set before the threads start Common::Event m_shutdown_event; - std::mutex m_scheduler_lock; sthread_t* m_scheduler_timer_thread = nullptr; }; } // namespace IOS::HLE diff --git a/Source/Core/Core/IOS/USB/Emulated/Infinity.cpp b/Source/Core/Core/IOS/USB/Emulated/Infinity.cpp index 442c0ef59d..f9336e5497 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Infinity.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Infinity.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include @@ -468,7 +467,6 @@ void InfinityBase::GetPresentFigures(u8 sequence, std::array& reply_buf) void InfinityBase::GetFigureIdentifier(u8 fig_num, u8 sequence, std::array& reply_buf) { - std::lock_guard lock(m_infinity_mutex); InfinityFigure& figure = GetFigureByOrder(fig_num); @@ -486,7 +484,6 @@ void InfinityBase::GetFigureIdentifier(u8 fig_num, u8 sequence, std::array& reply_buf, u8 sequence) { - std::lock_guard lock(m_infinity_mutex); InfinityFigure& figure = GetFigureByOrder(fig_num); @@ -513,7 +510,6 @@ void InfinityBase::QueryBlock(u8 fig_num, u8 block, std::array& reply_bu void InfinityBase::WriteBlock(u8 fig_num, u8 block, const u8* to_write_buf, std::array& reply_buf, u8 sequence) { - std::lock_guard lock(m_infinity_mutex); InfinityFigure& figure = GetFigureByOrder(fig_num); @@ -594,7 +590,6 @@ std::string InfinityBase::LoadFigure(const std::array& buf, File::IOFile in_file, FigureUIPosition position) { - std::lock_guard lock(m_infinity_mutex); u8 order_added; std::vector sha1_calc = {SHA1_CONSTANT.begin(), SHA1_CONSTANT.end() - 1}; @@ -642,7 +637,6 @@ InfinityBase::LoadFigure(const std::array(position)]; if (figure.inf_file.IsOpen()) diff --git a/Source/Core/Core/IOS/USB/Emulated/Infinity.h b/Source/Core/Core/IOS/USB/Emulated/Infinity.h index 7ec67035fa..4e29fb967a 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Infinity.h +++ b/Source/Core/Core/IOS/USB/Emulated/Infinity.h @@ -4,7 +4,6 @@ #pragma once #include -#include #include #include #include @@ -107,7 +106,6 @@ public: std::string FindFigure(u32 character) const; protected: - std::mutex m_infinity_mutex; std::array m_figures; private: diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index 8b11f5b673..9d7fe69992 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -26,6 +26,8 @@ Microphone::Microphone(const MicrophoneState& sampler) : m_sampler(sampler) Microphone::~Microphone() { StreamTerminate(); + if (m_stream_allocated) + retro_spsc_free(&m_stream); } void Microphone::Initialize() @@ -62,18 +64,19 @@ void Microphone::StreamStart(u32 sampling_rate) return; } - { - std::lock_guard lock(m_ring_lock); - m_stream_buffer.resize(GetStreamSize()); - m_stream_wpos = 0; - m_stream_rpos = 0; - m_samples_avail = 0; - } + // Not being polled (StreamStop ran or it never started), so the ring is this thread's. + if (m_stream_allocated) + retro_spsc_free(&m_stream); + m_stream_allocated = retro_spsc_init(&m_stream, GetStreamSize() * sizeof(SampleType)); Libretro::Input::g_microphone_interface.set_mic_state( static_cast(m_retro_mic), true); - Libretro::Input::g_active_microphones.push_back(this); + for (auto& slot : Libretro::Input::g_active_microphones) + { + if (!retro_atomic_load_acquire_ptr(&slot) && retro_atomic_cas_ptr(&slot, nullptr, this)) + break; + } INFO_LOG_FMT(IOS_USB, "Microphone: opened mic at {}Hz", sampling_rate); } @@ -83,7 +86,10 @@ void Microphone::StreamStop() if (!m_retro_mic) return; - std::erase(Libretro::Input::g_active_microphones, this); + for (auto& slot : Libretro::Input::g_active_microphones) + retro_atomic_cas_ptr(&slot, this, nullptr); + // A poll of this microphone already under way finishes before it closes. + Common::CallGateDrain(&Libretro::Input::g_microphone_poll_gate); Libretro::Input::g_microphone_interface.close_mic( static_cast(m_retro_mic)); m_retro_mic = nullptr; @@ -108,8 +114,6 @@ void Microphone::PollRetroArchMic() long Microphone::DataCallback(const SampleType* input_buffer, long nframes) { - std::lock_guard lock(m_ring_lock); - // Skip data if sampling is off or mute is on if (!m_sampler.IsSampleOn() || m_sampler.IsMuted()) return nframes; @@ -120,21 +124,17 @@ long Microphone::DataCallback(const SampleType* input_buffer, long nframes) return MathUtil::SaturatingCast(sample * gain); }; - const u32 stream_size = GetStreamSize(); const bool are_samples_byte_swapped = AreSamplesByteSwapped(); + std::vector converted; + converted.reserve(buffer.size()); for (const SampleType le_sample : std::ranges::transform_view(buffer, apply_gain)) { UpdateLoudness(le_sample); - m_stream_buffer[m_stream_wpos] = - are_samples_byte_swapped ? Common::swap16(le_sample) : le_sample; - m_stream_wpos = (m_stream_wpos + 1) % stream_size; + converted.push_back(are_samples_byte_swapped ? Common::swap16(le_sample) : le_sample); } - m_samples_avail += nframes; - if (m_samples_avail > stream_size) - { - m_samples_avail = stream_size; - } + // A full ring keeps the older samples; the guest is behind and will catch up on those. + retro_spsc_write(&m_stream, converted.data(), converted.size() * sizeof(SampleType)); return nframes; } @@ -143,21 +143,15 @@ u16 Microphone::ReadIntoBuffer(u8* ptr, u32 size) { static constexpr u32 SINGLE_READ_SIZE = BUFF_SIZE_SAMPLES * sizeof(SampleType); - std::lock_guard lock(m_ring_lock); - - const u32 stream_size = GetStreamSize(); u8* begin = ptr; + if (!m_stream_allocated) + return 0; for (u8* end = begin + size; ptr < end; ptr += SINGLE_READ_SIZE, size -= SINGLE_READ_SIZE) { - if (size < SINGLE_READ_SIZE || m_samples_avail < BUFF_SIZE_SAMPLES) + if (size < SINGLE_READ_SIZE || retro_spsc_read_avail(&m_stream) < SINGLE_READ_SIZE) break; - SampleType* last_buffer = &m_stream_buffer[m_stream_rpos]; - std::memcpy(ptr, last_buffer, SINGLE_READ_SIZE); - - m_samples_avail -= BUFF_SIZE_SAMPLES; - m_stream_rpos += BUFF_SIZE_SAMPLES; - m_stream_rpos %= stream_size; + retro_spsc_read(&m_stream, ptr, SINGLE_READ_SIZE); } return static_cast(ptr - begin); } @@ -196,8 +190,8 @@ void Microphone::UpdateLoudness(const SampleType sample) bool Microphone::HasData(u32 sample_count = BUFF_SIZE_SAMPLES) const { - std::lock_guard lock(m_ring_lock); - return m_samples_avail >= sample_count; + return m_stream_allocated && + retro_spsc_read_avail(&m_stream) >= sample_count * sizeof(SampleType); } Microphone::FloatType Microphone::ComputeGain(FloatType relative_db) const diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h index 6fc9c67d7c..a586227342 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -6,10 +6,10 @@ #include #include #include -#include #include #include +#include #include "Common/CommonTypes.h" #include @@ -62,10 +62,10 @@ private: void StreamStop(); virtual u32 GetStreamSize() const = 0; - std::vector m_stream_buffer{}; - u32 m_stream_wpos = 0; - u32 m_stream_rpos = 0; - u32 m_samples_avail = 0; + // Samples from the frontend thread (DataCallback) to the emulation thread + // (ReadIntoBuffer). Sized in StreamStart, while nothing polls this microphone. + retro_spsc_t m_stream{}; + bool m_stream_allocated = false; // TODO: Find how this level is calculated on real hardware std::atomic m_loudness_level = 0; @@ -105,8 +105,6 @@ private: }; Loudness m_loudness; - mutable std::mutex m_ring_lock; - const MicrophoneState& m_sampler; void* m_retro_mic = nullptr; diff --git a/Source/Core/Core/IOS/USB/Emulated/Skylanders/Skylander.cpp b/Source/Core/Core/IOS/USB/Emulated/Skylanders/Skylander.cpp index 14017eecb5..fe18fe0c25 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Skylanders/Skylander.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Skylanders/Skylander.cpp @@ -4,7 +4,6 @@ #include "Core/IOS/USB/Emulated/Skylanders/Skylander.h" #include -#include #include #include "AudioCommon/AudioCommon.h" @@ -1090,7 +1089,6 @@ void SkylanderUSB::ScheduleTransfer(std::unique_ptr command, void SkylanderPortal::Activate() { - std::lock_guard lock(sky_mutex); if (m_activated) { // If the portal was already active no change is needed @@ -1112,7 +1110,6 @@ void SkylanderPortal::Activate() void SkylanderPortal::Deactivate() { - std::lock_guard lock(sky_mutex); for (auto& s : skylanders) { @@ -1131,14 +1128,12 @@ void SkylanderPortal::Deactivate() bool SkylanderPortal::IsActivated() { - std::lock_guard lock(sky_mutex); return m_activated; } void SkylanderPortal::UpdateStatus() { - std::lock_guard lock(sky_mutex); if (!m_status_updated) { @@ -1162,7 +1157,6 @@ void SkylanderPortal::UpdateStatus() // 0x03 = trap void SkylanderPortal::SetLEDs(u8 side, u8 red, u8 green, u8 blue) { - std::lock_guard lock(sky_mutex); if (side == 0x00) { m_color_right.red = red; @@ -1195,7 +1189,6 @@ void SkylanderPortal::SetLEDs(u8 side, u8 red, u8 green, u8 blue) std::array SkylanderPortal::GetStatus() { - std::lock_guard lock(sky_mutex); u32 status = 0; u8 active = 0x00; @@ -1233,8 +1226,6 @@ void SkylanderPortal::QueryBlock(u8 sky_num, u8 block, u8* reply_buf) if (!IsSkylanderNumberValid(sky_num) || !IsBlockNumberValid(block)) return; - std::lock_guard lock(sky_mutex); - const auto& skylander = skylanders[sky_num]; reply_buf[0] = 'Q'; @@ -1255,8 +1246,6 @@ void SkylanderPortal::WriteBlock(u8 sky_num, u8 block, const u8* to_write_buf, u if (!IsSkylanderNumberValid(sky_num) || !IsBlockNumberValid(block)) return; - std::lock_guard lock(sky_mutex); - auto& skylander = skylanders[sky_num]; reply_buf[0] = 'W'; @@ -1280,7 +1269,6 @@ bool SkylanderPortal::RemoveSkylander(u8 sky_num) return false; DEBUG_LOG_FMT(IOS_USB, "Cleared Skylander from slot {}", sky_num); - std::lock_guard lock(sky_mutex); auto& skylander = skylanders[sky_num]; if (skylander.figure->FileIsOpen()) @@ -1302,7 +1290,6 @@ bool SkylanderPortal::RemoveSkylander(u8 sky_num) u8 SkylanderPortal::LoadSkylander(std::unique_ptr figure) { - std::lock_guard lock(sky_mutex); u32 sky_serial = 0; std::array block = {}; diff --git a/Source/Core/Core/IOS/USB/Emulated/Skylanders/Skylander.h b/Source/Core/Core/IOS/USB/Emulated/Skylanders/Skylander.h index 9de058f59f..55bfd9dd79 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Skylanders/Skylander.h +++ b/Source/Core/Core/IOS/USB/Emulated/Skylanders/Skylander.h @@ -4,7 +4,6 @@ #pragma once #include -#include #include #include @@ -148,8 +147,6 @@ private: static bool IsSkylanderNumberValid(u8 sky_num); static bool IsBlockNumberValid(u8 block); - std::mutex sky_mutex; - bool m_activated = true; bool m_status_updated = false; u8 m_interrupt_counter = 0; diff --git a/Source/Core/Core/IOS/USB/Host.cpp b/Source/Core/Core/IOS/USB/Host.cpp index a3372fdcfb..de59d12ea0 100644 --- a/Source/Core/Core/IOS/USB/Host.cpp +++ b/Source/Core/Core/IOS/USB/Host.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -33,6 +32,7 @@ USBHost::USBHost(EmulationKernel& ios, const std::string& device_name) USBHost::~USBHost() { GetSystem().GetUSBScanner().RemoveClient(this); + delete static_cast(retro_atomic_exchange_ptr(&m_pending_devices, nullptr)); } std::optional USBHost::Open(const OpenRequest& request) @@ -56,15 +56,13 @@ void USBHost::DoState(PointerWrap& p) { // After a state has loaded, there may be insertion hooks for devices that were // already plugged in, and which need to be triggered. - std::lock_guard lk(m_devices_mutex); m_devices.clear(); - OnDevicesChanged(GetSystem().GetUSBScanner().GetDevices()); + OnDevicesChangedInternal(GetSystem().GetUSBScanner().GetDevices()); } } std::shared_ptr USBHost::GetDeviceById(const u64 device_id) const { - std::lock_guard lk(m_devices_mutex); const auto it = m_devices.find(device_id); if (it == m_devices.end()) return nullptr; @@ -87,18 +85,30 @@ bool USBHost::ShouldAddDevice(const USB::Device& device) const void USBHost::Update() { if (Core::WantsDeterminism()) + { OnDevicesChangedInternal(GetSystem().GetUSBScanner().GetDevices()); + return; + } + + if (auto* pending = static_cast( + retro_atomic_exchange_ptr(&m_pending_devices, nullptr))) + { + OnDevicesChangedInternal(*pending); + delete pending; + } } +// Scan thread: only the newest list matters, so it replaces any still waiting. void USBHost::OnDevicesChanged(const USBScanner::DeviceMap& new_devices) { - if (!Core::WantsDeterminism()) - OnDevicesChangedInternal(new_devices); + if (Core::WantsDeterminism()) + return; + delete static_cast( + retro_atomic_exchange_ptr(&m_pending_devices, new USBScanner::DeviceMap(new_devices))); } void USBHost::OnDevicesChangedInternal(const USBScanner::DeviceMap& new_devices) { - std::lock_guard lk(m_devices_mutex); bool changes = false; diff --git a/Source/Core/Core/IOS/USB/Host.h b/Source/Core/Core/IOS/USB/Host.h index 81efb09320..4b507419cf 100644 --- a/Source/Core/Core/IOS/USB/Host.h +++ b/Source/Core/Core/IOS/USB/Host.h @@ -6,10 +6,10 @@ #include #include #include -#include #include #include +#include #include "Common/CommonTypes.h" #include "Core/IOS/Device.h" #include "Core/IOS/IOS.h" @@ -49,13 +49,16 @@ protected: std::optional HandleTransfer(const std::shared_ptr& device, u32 request, const std::function& submit) const; + // Emulation thread only. Device lists from the scan thread wait in m_pending_devices until + // Update takes them, so this and everything derived from it in the subclasses stay on one + // thread. std::map> m_devices; - mutable std::recursive_mutex m_devices_mutex; private: void Update() override; void OnDevicesChangedInternal(const USBScanner::DeviceMap& new_devices); bool m_has_initialised = false; + retro_atomic_ptr_t m_pending_devices{}; // USBScanner::DeviceMap* }; } // namespace IOS::HLE diff --git a/Source/Core/Core/IOS/USB/LibusbDevice.cpp b/Source/Core/Core/IOS/USB/LibusbDevice.cpp index 6f31f6dfcd..f6a2e64ecc 100644 --- a/Source/Core/Core/IOS/USB/LibusbDevice.cpp +++ b/Source/Core/Core/IOS/USB/LibusbDevice.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -176,10 +175,9 @@ int LibusbDevice::CancelTransfer(const u8 endpoint) { INFO_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Cancelling transfers (endpoint {:#x})", m_vid, m_pid, m_active_interface, endpoint); - const auto iterator = m_transfer_endpoints.find(endpoint); - if (iterator == m_transfer_endpoints.cend()) + if (!m_transfer_endpoints[endpoint].IsUsed()) return IPC_ENOENT; - iterator->second.CancelTransfers(); + m_transfer_endpoints[endpoint].CancelTransfers(); return IPC_SUCCESS; } @@ -472,23 +470,50 @@ static const std::map s_transfer_types = { void LibusbDevice::TransferEndpoint::AddTransfer(std::unique_ptr command, libusb_transfer* transfer) { - std::lock_guard lk{m_transfers_mutex}; - m_transfers.emplace(transfer, std::move(command)); + m_used = true; + auto* in_flight = new InFlight{transfer, std::move(command)}; + for (auto& slot : m_in_flight) + { + if (!retro_atomic_load_acquire_ptr(&slot) && retro_atomic_cas_ptr(&slot, nullptr, in_flight)) + return; + } + // More than MAX_IN_FLIGHT outstanding on one endpoint: the reply goes out when it completes, + // it just cannot be cancelled. + ERROR_LOG_FMT(IOS_USB, "Too many transfers in flight on one endpoint"); + delete in_flight; +} + +LibusbDevice::TransferEndpoint::~TransferEndpoint() +{ + for (auto& slot : m_in_flight) + delete static_cast(retro_atomic_exchange_ptr(&slot, nullptr)); } void LibusbDevice::TransferEndpoint::HandleTransfer( libusb_transfer* transfer, const std::function& fn) { - std::lock_guard lk{m_transfers_mutex}; - const auto iterator = m_transfers.find(transfer); - if (iterator == m_transfers.cend()) + // libusb's event thread: completions never run concurrently with each other. + std::unique_ptr in_flight; + for (auto& slot : m_in_flight) + { + auto* candidate = static_cast(retro_atomic_load_acquire_ptr(&slot)); + if (candidate && candidate->transfer == transfer) + { + in_flight.reset(static_cast(retro_atomic_exchange_ptr(&slot, nullptr))); + break; + } + } + // A cancel loop that read the slot before it emptied may still be cancelling this + // transfer; libusb frees it once this returns. + Common::CallGateDrain(&m_cancel_gate); + if (!in_flight) { ERROR_LOG_FMT(IOS_USB, "No such transfer"); return; } const std::unique_ptr buffer(transfer->buffer); - const auto& cmd = *iterator->second.get(); + const auto& cmd = *in_flight->command; const auto* device = static_cast(transfer->user_data); s32 return_value = LIBUSB_SUCCESS; switch (transfer->status) @@ -512,17 +537,18 @@ void LibusbDevice::TransferEndpoint::HandleTransfer( break; } cmd.OnTransferComplete(return_value); - m_transfers.erase(transfer); } void LibusbDevice::TransferEndpoint::CancelTransfers() { - std::lock_guard lk(m_transfers_mutex); - if (m_transfers.empty()) - return; - INFO_LOG_FMT(IOS_USB, "Cancelling {} transfer(s)", m_transfers.size()); - for (const auto& pending_transfer : m_transfers) - libusb_cancel_transfer(pending_transfer.first); + // Never shut: completions only wait for this loop to finish. + Common::CallGateEnter(&m_cancel_gate); + for (auto& slot : m_in_flight) + { + if (const auto* in_flight = static_cast(retro_atomic_load_acquire_ptr(&slot))) + libusb_cancel_transfer(in_flight->transfer); + } + Common::CallGateExit(&m_cancel_gate); } int LibusbDevice::GetNumberOfAltSettings(const u8 interface_number) diff --git a/Source/Core/Core/IOS/USB/LibusbDevice.h b/Source/Core/Core/IOS/USB/LibusbDevice.h index f3b4f0ff44..9471f586c0 100644 --- a/Source/Core/Core/IOS/USB/LibusbDevice.h +++ b/Source/Core/Core/IOS/USB/LibusbDevice.h @@ -7,10 +7,14 @@ #include #include #include -#include #include #include +#include + +#include + +#include "Common/CallGate.h" #include "Common/CommonTypes.h" #include "Core/IOS/USB/Common.h" #include "Core/LibusbUtils.h" @@ -63,12 +67,31 @@ private: void HandleTransfer(libusb_transfer* tr, const std::function& function); void CancelTransfers(); + // Whether any transfer was ever queued here. + bool IsUsed() const { return m_used; } + + TransferEndpoint() = default; + TransferEndpoint(const TransferEndpoint&) = delete; + TransferEndpoint& operator=(const TransferEndpoint&) = delete; + ~TransferEndpoint(); private: - std::mutex m_transfers_mutex; - std::map> m_transfers; + // A transfer in flight. Queued by the emulation thread, completed (and freed) by libusb's + // event thread, cancelled by the emulation thread: each slot is claimed with a CAS and + // given up with an exchange, and a completion waits out a cancel loop that may still be + // looking at its transfer before libusb frees it. + struct InFlight + { + libusb_transfer* transfer; + std::unique_ptr command; + }; + static constexpr size_t MAX_IN_FLIGHT = 64; + std::array m_in_flight{}; + Common::CallGate m_cancel_gate; + bool m_used = false; // emulation thread }; - std::map m_transfer_endpoints; + // Indexed by endpoint address, so the event thread never looks one up in a changing map. + std::array m_transfer_endpoints; static void CtrlTransferCallback(libusb_transfer* transfer); static void TransferCallback(libusb_transfer* transfer); diff --git a/Source/Core/Core/IOS/USB/OH0/OH0.cpp b/Source/Core/Core/IOS/USB/OH0/OH0.cpp index 0b6051499b..1399ff8f0a 100644 --- a/Source/Core/Core/IOS/USB/OH0/OH0.cpp +++ b/Source/Core/Core/IOS/USB/OH0/OH0.cpp @@ -70,7 +70,6 @@ std::optional OH0::IOCtlV(const IOCtlVRequest& request) void OH0::DoState(PointerWrap& p) { { - std::lock_guard lk(m_devices_mutex); if (p.IsReadMode() && !m_devices.empty()) { Core::DisplayMessage("It is suggested that you unplug and replug all connected USB devices.", @@ -113,7 +112,6 @@ IPCReply OH0::GetDeviceList(const IOCtlVRequest& request) const const u8 interface_class = memory.Read_U8(request.in_vectors[1].address); u8 entries_count = 0; - std::lock_guard lk(m_devices_mutex); for (const auto& device : m_devices) { if (entries_count >= max_entries_count) @@ -170,7 +168,6 @@ IPCReply OH0::SetRhPortStatus(const IOCtlVRequest& request) std::optional OH0::RegisterRemovalHook(const u64 device_id, const IOCtlRequest& request) { - std::lock_guard lock{m_hooks_mutex}; // IOS only allows a single device removal hook. const bool inserted = m_removal_hooks.try_emplace(device_id, request.address).second; if (!inserted) @@ -190,8 +187,6 @@ std::optional OH0::RegisterInsertionHook(const IOCtlVRequest& request) const u16 pid = memory.Read_U16(request.in_vectors[1].address); if (HasDeviceWithVidPid(vid, pid)) return IPCReply(IPC_SUCCESS); - - std::lock_guard lock{m_hooks_mutex}; // TODO: figure out whether IOS allows more than one hook. m_insertion_hooks[{vid, pid}] = request.address; return std::nullopt; @@ -210,7 +205,6 @@ std::optional OH0::RegisterInsertionHookWithID(const IOCtlVRequest& re const bool trigger_only_for_new_device = memory.Read_U8(request.in_vectors[2].address) == 1; if (!trigger_only_for_new_device && HasDeviceWithVidPid(vid, pid)) return IPCReply(IPC_SUCCESS); - std::lock_guard lock{m_hooks_mutex}; // TODO: figure out whether IOS allows more than one hook. m_insertion_hooks.insert({{vid, pid}, request.address}); // The output vector is overwritten with an ID to use with ioctl 31 for cancelling the hook. @@ -230,7 +224,6 @@ std::optional OH0::RegisterClassChangeHook(const IOCtlVRequest& reques bool OH0::HasDeviceWithVidPid(const u16 vid, const u16 pid) const { - std::lock_guard lk(m_devices_mutex); return std::ranges::any_of(m_devices, [=](const auto& device) { return device.second->GetVid() == vid && device.second->GetPid() == pid; }); @@ -248,7 +241,6 @@ void OH0::OnDeviceChange(const ChangeEvent event, std::shared_ptr d // This fixes a problem where Rock Band 3 randomly fails to detect reconnected microphones. // Real IOS behavior untested. - std::lock_guard lk(m_devices_mutex); m_opened_devices.erase(device->GetId()); } } @@ -256,7 +248,6 @@ void OH0::OnDeviceChange(const ChangeEvent event, std::shared_ptr d template void OH0::TriggerHook(std::map& hooks, T value, const ReturnCode return_value) { - std::lock_guard lk{m_hooks_mutex}; const auto hook = hooks.find(value); if (hook == hooks.end()) return; @@ -267,7 +258,6 @@ void OH0::TriggerHook(std::map& hooks, T value, const ReturnCode return_ std::pair OH0::DeviceOpen(const u16 vid, const u16 pid) { - std::lock_guard lk(m_devices_mutex); bool has_device_with_vid_pid = false; for (const auto& device : m_devices) @@ -291,7 +281,6 @@ std::pair OH0::DeviceOpen(const u16 vid, const u16 pid) void OH0::DeviceClose(const u64 device_id) { TriggerHook(m_removal_hooks, device_id, IPC_ENOENT); - std::lock_guard lk(m_devices_mutex); m_opened_devices.erase(device_id); } diff --git a/Source/Core/Core/IOS/USB/OH0/OH0.h b/Source/Core/Core/IOS/USB/OH0/OH0.h index d002c79d12..abcab30c1a 100644 --- a/Source/Core/Core/IOS/USB/OH0/OH0.h +++ b/Source/Core/Core/IOS/USB/OH0/OH0.h @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -75,6 +74,5 @@ private: std::map m_insertion_hooks; std::map m_removal_hooks; std::set m_opened_devices; - std::mutex m_hooks_mutex; }; } // namespace IOS::HLE diff --git a/Source/Core/Core/IOS/USB/USBScanner.cpp b/Source/Core/Core/IOS/USB/USBScanner.cpp index be817dd03a..3375b69a5a 100644 --- a/Source/Core/Core/IOS/USB/USBScanner.cpp +++ b/Source/Core/Core/IOS/USB/USBScanner.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -34,6 +33,26 @@ namespace IOS::HLE USBScanner::~USBScanner() { StopScanning(); + delete static_cast(retro_atomic_exchange_ptr(&m_devices, nullptr)); + delete static_cast(retro_atomic_exchange_ptr(&m_clients, nullptr)); + quiesce_deinit(&m_quiesce); +} + +void USBScanner::FreeDevices(quiesce_node_t* node) +{ + delete reinterpret_cast(node); +} + +void USBScanner::FreeClients(quiesce_node_t* node) +{ + delete reinterpret_cast(node); +} + +void USBScanner::PublishDevices(DeviceMap map) +{ + auto* next = new Devices{{}, std::move(map)}; + if (void* old = retro_atomic_exchange_ptr(&m_devices, next)) + quiesce_retire(&m_quiesce, &static_cast(old)->node, FreeDevices); } #if defined(__LIBRETRO__) && defined(__LIBUSB__) @@ -52,19 +71,15 @@ void USBScanner::WaitForFirstScan() } } +// Clients come and go on the emulation thread only, so each change is a plain swap. bool USBScanner::AddClient(USBHost* client) { - bool should_start_scanning; - bool client_is_new; - - std::lock_guard thread_lock(m_thread_start_stop_mutex); - - { - std::lock_guard clients_lock(m_clients_mutex); - should_start_scanning = m_clients.empty(); - auto [it, newly_added] = m_clients.insert(client); - client_is_new = newly_added; - } + const auto* cur = static_cast(retro_atomic_load_acquire_ptr(&m_clients)); + auto* next = new Clients{{}, cur ? cur->set : std::set{}}; + const bool should_start_scanning = next->set.empty(); + const bool client_is_new = next->set.insert(client).second; + if (void* old = retro_atomic_exchange_ptr(&m_clients, next)) + quiesce_retire(&m_quiesce, &static_cast(old)->node, FreeClients); if (should_start_scanning) StartScanning(); @@ -74,24 +89,24 @@ bool USBScanner::AddClient(USBHost* client) bool USBScanner::RemoveClient(USBHost* client) { - std::lock_guard thread_lock(m_thread_start_stop_mutex); + const auto* cur = static_cast(retro_atomic_load_acquire_ptr(&m_clients)); + if (!cur || !cur->set.contains(client)) + return false; - bool client_was_removed; - bool should_stop_scanning; + auto* next = new Clients{{}, cur->set}; + next->set.erase(client); + const bool should_stop_scanning = next->set.empty(); + if (void* old = retro_atomic_exchange_ptr(&m_clients, next)) + quiesce_retire(&m_quiesce, &static_cast(old)->node, FreeClients); - { - std::lock_guard clients_lock(m_clients_mutex); - const bool was_empty = m_clients.empty(); - client_was_removed = m_clients.erase(client) != 0; - should_stop_scanning = !was_empty && m_clients.empty(); - } + // The scan thread may be calling this client from the old set; let it finish. + Common::CallGateClose(&m_notify_gate); + Common::CallGateReopen(&m_notify_gate); - // The scan thread might be trying to lock m_clients_mutex, so we need to not hold a lock on - // m_clients_mutex when trying to join with the scan thread. if (should_stop_scanning) StopScanning(); - return client_was_removed; + return true; } void USBScanner::StartScanning() @@ -109,9 +124,11 @@ void USBScanner::ScanThread() { if (UpdateDevices()) m_first_scan_complete_flag.Set(true); + if (m_notify_pending) + NotifyClients(); Common::SleepCurrentThread(50); } - m_devices.clear(); + PublishDevices({}); m_first_scan_complete_flag.Set(false); } @@ -123,8 +140,11 @@ void USBScanner::StopScanning() USBScanner::DeviceMap USBScanner::GetDevices() const { - std::lock_guard lk(m_devices_mutex); - return m_devices; + quiesce_enter(&m_quiesce); + const auto* devices = static_cast(retro_atomic_load_acquire_ptr(&m_devices)); + DeviceMap copy = devices ? devices->map : DeviceMap{}; + quiesce_exit(&m_quiesce); + return copy; } // This is called from the scan thread. Returns false if we failed to update the device list. @@ -134,19 +154,41 @@ bool USBScanner::UpdateDevices() if (!AddNewDevices(&new_devices)) return false; - std::lock_guard lk(m_devices_mutex); - if (!std::ranges::equal(std::views::keys(m_devices), std::views::keys(new_devices))) + // Only this thread replaces the list, so the current one stays valid here. + const auto* devices = static_cast(retro_atomic_load_acquire_ptr(&m_devices)); + static const DeviceMap s_none; + const DeviceMap& current = devices ? devices->map : s_none; + if (!std::ranges::equal(std::views::keys(current), std::views::keys(new_devices))) { - m_devices = std::move(new_devices); - - std::lock_guard clients_lock(m_clients_mutex); - for (USBHost* client : m_clients) - client->OnDevicesChanged(m_devices); + PublishDevices(std::move(new_devices)); + NotifyClients(); } return true; } +void USBScanner::NotifyClients() +{ + if (!Common::CallGateEnter(&m_notify_gate)) + { + m_notify_pending = true; + return; + } + m_notify_pending = false; + + quiesce_enter(&m_quiesce); + const auto* devices = static_cast(retro_atomic_load_acquire_ptr(&m_devices)); + const auto* clients = static_cast(retro_atomic_load_acquire_ptr(&m_clients)); + if (devices && clients) + { + for (USBHost* client : clients->set) + client->OnDevicesChanged(devices->map); + } + quiesce_exit(&m_quiesce); + + Common::CallGateExit(&m_notify_gate); +} + bool USBScanner::AddNewDevices(DeviceMap* new_devices) const { AddEmulatedDevices(new_devices); diff --git a/Source/Core/Core/IOS/USB/USBScanner.h b/Source/Core/Core/IOS/USB/USBScanner.h index c57e94e2ba..8dbd90d430 100644 --- a/Source/Core/Core/IOS/USB/USBScanner.h +++ b/Source/Core/Core/IOS/USB/USBScanner.h @@ -5,11 +5,14 @@ #include #include -#include #include #include +#include + +#include "Common/CallGate.h" +#include "Common/Quiesce.h" #include "Common/CommonTypes.h" #include "Common/Flag.h" #include "Common/WaitableFlag.h" @@ -51,15 +54,36 @@ private: static void AddEmulatedDevices(DeviceMap* new_devices); static void AddDevice(std::unique_ptr device, DeviceMap* new_devices); - DeviceMap m_devices; - mutable std::mutex m_devices_mutex; + // The device list and the clients are each published whole: the scan thread replaces the + // list, the emulation thread (the only one adding and removing clients) replaces the set. + // Old copies go once no reader is inside m_quiesce. + struct Devices + { + quiesce_node_t node; // first: the free callback casts it back + DeviceMap map; + }; + struct Clients + { + quiesce_node_t node; // first, as above + std::set set; + }; + static void FreeDevices(quiesce_node_t* node); + static void FreeClients(quiesce_node_t* node); + void PublishDevices(DeviceMap map); + // Tells the clients about the current devices. Scan thread. + void NotifyClients(); - std::set m_clients; - std::mutex m_clients_mutex; + mutable quiesce_t m_quiesce{}; + retro_atomic_ptr_t m_devices{}; + retro_atomic_ptr_t m_clients{}; + // The scan thread is inside it while calling clients, so RemoveClient can wait that out + // before its caller goes away. + Common::CallGate m_notify_gate; + // A notification skipped while a removal had the gate shut; the next scan sends it. + bool m_notify_pending = false; Common::Flag m_thread_running; sthread_t* m_thread = nullptr; - std::mutex m_thread_start_stop_mutex; Common::WaitableFlag m_first_scan_complete_flag; LibusbUtils::Context m_context; diff --git a/Source/Core/Core/IOS/USB/USBV5.cpp b/Source/Core/Core/IOS/USB/USBV5.cpp index b96d4e9c2a..80ba5e9072 100644 --- a/Source/Core/Core/IOS/USB/USBV5.cpp +++ b/Source/Core/Core/IOS/USB/USBV5.cpp @@ -132,8 +132,6 @@ std::optional USBV5ResourceManager::GetDeviceChange(const IOCtlRequest { if (request.buffer_out_size != 0x180 || m_devicechange_hook_request) return IPCReply(IPC_EINVAL); - - std::lock_guard lk{m_devicechange_hook_address_mutex}; m_devicechange_hook_request = std::make_unique(GetSystem(), request.address); // If there are pending changes, the reply is sent immediately (instead of on device // insertion/removal). @@ -166,8 +164,6 @@ IPCReply USBV5ResourceManager::Shutdown(const IOCtlRequest& request) { return IPCReply(IPC_EINVAL); } - - std::lock_guard lk{m_devicechange_hook_address_mutex}; if (m_devicechange_hook_request) { GetEmulationKernel().EnqueueIPCReply(*m_devicechange_hook_request, IPC_SUCCESS); @@ -196,8 +192,6 @@ std::optional USBV5ResourceManager::HandleDeviceIOCtl(const IOCtlReque { if (request.buffer_in == 0 || request.buffer_in_size != 0x20) return IPCReply(IPC_EINVAL); - - std::lock_guard lock{m_usbv5_devices_mutex}; USBV5Device* device = GetUSBV5Device(request.buffer_in); if (!device) return IPCReply(IPC_EINVAL); @@ -223,7 +217,6 @@ IPCReply USBV5ResourceManager::GetUSBVersion(const IOCtlRequest& request) const void USBV5ResourceManager::OnDeviceChange(const ChangeEvent event, std::shared_ptr device) { - std::lock_guard lock{m_usbv5_devices_mutex}; const u64 host_device_id = device->GetId(); if (event == ChangeEvent::Inserted) { @@ -255,12 +248,11 @@ void USBV5ResourceManager::OnDeviceChange(const ChangeEvent event, void USBV5ResourceManager::OnDeviceChangeEnd() { - std::lock_guard lk{m_devicechange_hook_address_mutex}; TriggerDeviceChangeReply(); ++m_current_device_number; } -// Must be called with m_devicechange_hook_address_mutex locked +// Emulation thread only, like the rest of the host state. void USBV5ResourceManager::TriggerDeviceChangeReply() { if (!m_devicechange_hook_request) @@ -271,8 +263,6 @@ void USBV5ResourceManager::TriggerDeviceChangeReply() auto& system = GetSystem(); auto& memory = system.GetMemory(); - - std::lock_guard lock{m_usbv5_devices_mutex}; u8 num_devices = 0; for (auto it = m_usbv5_devices.crbegin(); it != m_usbv5_devices.crend(); ++it) { diff --git a/Source/Core/Core/IOS/USB/USBV5.h b/Source/Core/Core/IOS/USB/USBV5.h index 051d0dd943..6fc6322db4 100644 --- a/Source/Core/Core/IOS/USB/USBV5.h +++ b/Source/Core/Core/IOS/USB/USBV5.h @@ -6,7 +6,6 @@ #include #include #include -#include #include "Common/CommonTypes.h" #include "Core/IOS/Device.h" @@ -91,7 +90,6 @@ protected: virtual bool HasInterfaceNumberInIDs() const = 0; bool m_has_pending_changes = true; - std::mutex m_devicechange_hook_address_mutex; std::unique_ptr m_devicechange_hook_request; // Each interface of a USB device is internally considered as a unique device. @@ -104,7 +102,6 @@ protected: u64 host_id = 0; }; std::array m_usbv5_devices{}; - mutable std::mutex m_usbv5_devices_mutex; u16 m_current_device_number = 0x21; }; } // namespace IOS::HLE diff --git a/Source/Core/Core/IOS/USB/USB_HID/HIDv4.cpp b/Source/Core/Core/IOS/USB/USB_HID/HIDv4.cpp index eb9c50a85a..b3df82e9ed 100644 --- a/Source/Core/Core/IOS/USB/USB_HID/HIDv4.cpp +++ b/Source/Core/Core/IOS/USB/USB_HID/HIDv4.cpp @@ -4,7 +4,6 @@ #include "Core/IOS/USB/USB_HID/HIDv4.h" #include -#include #include #include #include @@ -86,7 +85,6 @@ IPCReply USB_HIDv4::CancelInterrupt(const IOCtlRequest& request) std::optional USB_HIDv4::GetDeviceChange(const IOCtlRequest& request) { - std::lock_guard lk{m_devicechange_hook_address_mutex}; if (request.buffer_out == 0 || request.buffer_out_size != 0x600) return IPCReply(IPC_EINVAL); @@ -104,7 +102,6 @@ std::optional USB_HIDv4::GetDeviceChange(const IOCtlRequest& request) IPCReply USB_HIDv4::Shutdown(const IOCtlRequest& request) { - std::lock_guard lk{m_devicechange_hook_address_mutex}; if (m_devicechange_hook_request != nullptr) { auto& system = GetSystem(); @@ -158,7 +155,6 @@ void USB_HIDv4::DoState(PointerWrap& p) std::shared_ptr USB_HIDv4::GetDeviceByIOSID(const s32 ios_id) const { - std::lock_guard lk{m_id_map_mutex}; const auto iterator = m_ios_ids.find(ios_id); if (iterator == m_ios_ids.cend()) return nullptr; @@ -168,7 +164,6 @@ std::shared_ptr USB_HIDv4::GetDeviceByIOSID(const s32 ios_id) const void USB_HIDv4::OnDeviceChange(ChangeEvent event, std::shared_ptr device) { { - std::lock_guard id_map_lock{m_id_map_mutex}; if (event == ChangeEvent::Inserted) { const auto id = device->GetId(); @@ -189,7 +184,6 @@ void USB_HIDv4::OnDeviceChange(ChangeEvent event, std::shared_ptr d } { - std::lock_guard lk{m_devicechange_hook_address_mutex}; TriggerDeviceChangeReply(); } } @@ -211,7 +205,6 @@ void USB_HIDv4::TriggerDeviceChangeReply() auto& memory = system.GetMemory(); { - std::lock_guard lk(m_devices_mutex); const u32 dest = m_devicechange_hook_request->buffer_out; u32 offset = 0; for (const auto& device : m_devices) @@ -270,7 +263,6 @@ static std::vector GetDescriptors(const USB::Device& device) std::vector USB_HIDv4::GetDeviceEntry(const USB::Device& device) const { - std::lock_guard id_map_lock{m_id_map_mutex}; // The structure for a device section is as follows: // 0-4 bytes: total size of the device data, including the size and the device ID diff --git a/Source/Core/Core/IOS/USB/USB_HID/HIDv4.h b/Source/Core/Core/IOS/USB/USB_HID/HIDv4.h index 4bc168a38c..cada1b7537 100644 --- a/Source/Core/Core/IOS/USB/USB_HID/HIDv4.h +++ b/Source/Core/Core/IOS/USB/USB_HID/HIDv4.h @@ -5,7 +5,6 @@ #include #include -#include #include #include @@ -45,10 +44,7 @@ private: bool m_has_pending_changes = true; bool m_is_shut_down = false; - std::mutex m_devicechange_hook_address_mutex; std::unique_ptr m_devicechange_hook_request; - - mutable std::mutex m_id_map_mutex; // IOS device IDs <=> USB device IDs std::map m_ios_ids; std::map m_device_ids; diff --git a/Source/Core/Core/IOS/USB/USB_HID/HIDv5.cpp b/Source/Core/Core/IOS/USB/USB_HID/HIDv5.cpp index 3150dbad0f..68b73b8eff 100644 --- a/Source/Core/Core/IOS/USB/USB_HID/HIDv5.cpp +++ b/Source/Core/Core/IOS/USB/USB_HID/HIDv5.cpp @@ -5,7 +5,6 @@ #include #include -#include #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" @@ -57,8 +56,6 @@ std::optional USB_HIDv5::IOCtlV(const IOCtlVRequest& request) // IOS does not check the number of vectors, but let's do that to avoid out-of-bounds reads. if (request.in_vectors.size() + request.io_vectors.size() != 2) return IPCReply(IPC_EINVAL); - - std::lock_guard lock{m_usbv5_devices_mutex}; USBV5Device* device = GetUSBV5Device(request.in_vectors[0].address); if (!device) return IPCReply(IPC_EINVAL); diff --git a/Source/Core/Core/IOS/USB/USB_VEN/VEN.cpp b/Source/Core/Core/IOS/USB/USB_VEN/VEN.cpp index fc97a93298..0012bc243e 100644 --- a/Source/Core/Core/IOS/USB/USB_VEN/VEN.cpp +++ b/Source/Core/Core/IOS/USB/USB_VEN/VEN.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" @@ -68,8 +67,6 @@ std::optional USB_VEN::IOCtlV(const IOCtlVRequest& request) { if (request.in_vectors.size() + request.io_vectors.size() != s_num_vectors.at(request.request)) return IPCReply(IPC_EINVAL); - - std::lock_guard lock{m_usbv5_devices_mutex}; USBV5Device* device = GetUSBV5Device(request.in_vectors[0].address); if (!device) return IPCReply(IPC_EINVAL); diff --git a/Source/Core/Core/LibusbUtils.cpp b/Source/Core/Core/LibusbUtils.cpp index a077650c3a..e9ece63252 100644 --- a/Source/Core/Core/LibusbUtils.cpp +++ b/Source/Core/Core/LibusbUtils.cpp @@ -3,7 +3,6 @@ #include "Core/LibusbUtils.h" -#include #if defined(__LIBUSB__) #include @@ -67,8 +66,7 @@ public: int GetDeviceList(const GetDeviceListCallback& callback) const { - std::lock_guard lock{m_device_list_mutex}; - + // libusb_get_device_list is thread-safe in libusb itself. libusb_device** list; ssize_t count = libusb_get_device_list(m_context, &list); if (count < 0) @@ -97,7 +95,6 @@ private: } libusb_context* m_context = nullptr; - mutable std::mutex m_device_list_mutex; Common::Flag m_event_thread_running; sthread_t* m_event_thread = nullptr; }; diff --git a/Source/Core/Core/Movie.cpp b/Source/Core/Core/Movie.cpp index e8a9295fe3..b9e0ee8d13 100644 --- a/Source/Core/Core/Movie.cpp +++ b/Source/Core/Core/Movie.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -107,7 +106,25 @@ MovieManager::MovieManager(Core::System& system) : m_system(system) { } -MovieManager::~MovieManager() = default; +MovieManager::~MovieManager() +{ + for (auto& slot : m_input_display) + delete static_cast(retro_atomic_exchange_ptr(&slot, nullptr)); + quiesce_deinit(&m_input_display_quiesce); +} + +void MovieManager::FreeDisplayString(quiesce_node_t* node) +{ + delete reinterpret_cast(node); +} + +void MovieManager::SetInputDisplay(int index, std::string text) +{ + auto* next = new DisplayString{{}, std::move(text)}; + if (void* old = retro_atomic_exchange_ptr(&m_input_display[index], next)) + quiesce_retire(&m_input_display_quiesce, &static_cast(old)->node, + FreeDisplayString); +} // NOTE: GPU Thread std::string MovieManager::GetInputDisplay() @@ -132,17 +149,24 @@ std::string MovieManager::GetInputDisplay() std::string input_display; { - std::lock_guard guard(m_input_display_lock); + quiesce_enter(&m_input_display_quiesce); + const auto text = [this](int index) -> const std::string& { + static const std::string s_empty; + const auto* display = + static_cast(retro_atomic_load_acquire_ptr(&m_input_display[index])); + return display ? display->text : s_empty; + }; for (int i = 0; i < 4; ++i) { if (IsUsingPad(i)) - input_display += m_input_display[i] + '\n'; + input_display += text(i) + '\n'; } for (int i = 0; i < 4; ++i) { if (IsUsingWiimote(i)) - input_display += m_input_display[i + 4] + '\n'; + input_display += text(i + 4) + '\n'; } + quiesce_exit(&m_input_display_quiesce); } return input_display; } @@ -216,8 +240,8 @@ void MovieManager::Init(const BootParameters& boot) memset(&m_pad_state, 0, sizeof(m_pad_state)); - for (auto& disp : m_input_display) - disp.clear(); + for (int i = 0; i < static_cast(m_input_display.size()); ++i) + SetInputDisplay(i, {}); if (!IsMovieActive()) { @@ -758,8 +782,7 @@ void MovieManager::CheckPadStatus(const GCPadStatus* PadStatus, int controllerID { std::string display_str = GenerateInputDisplayString(m_pad_state, controllerID); - std::lock_guard guard(m_input_display_lock); - m_input_display[controllerID] = std::move(display_str); + SetInputDisplay(controllerID, std::move(display_str)); } } @@ -784,8 +807,7 @@ void MovieManager::CheckWiimoteStatus(int wiimote, const DesiredWiimoteState& de { std::string display_str = GenerateWiiInputDisplayString(wiimote, desired_state); - std::lock_guard guard(m_input_display_lock); - m_input_display[wiimote + 4] = std::move(display_str); + SetInputDisplay(wiimote + 4, std::move(display_str)); } if (IsRecordingInput()) @@ -1186,8 +1208,7 @@ void MovieManager::PlayController(GCPadStatus* PadStatus, int controllerID) { std::string display_str = GenerateInputDisplayString(m_pad_state, controllerID); - std::lock_guard guard(m_input_display_lock); - m_input_display[controllerID] = std::move(display_str); + SetInputDisplay(controllerID, std::move(display_str)); } CheckInputEnd(); diff --git a/Source/Core/Core/Movie.h b/Source/Core/Core/Movie.h index 4183111f0b..6af097c819 100644 --- a/Source/Core/Core/Movie.h +++ b/Source/Core/Core/Movie.h @@ -4,12 +4,14 @@ #pragma once #include -#include #include #include #include #include +#include + +#include "Common/Quiesce.h" #include "Common/CommonTypes.h" #include "Core/HW/WiimoteEmu/DesiredWiimoteState.h" @@ -269,8 +271,17 @@ private: std::string m_current_file_name; // m_input_display is used by both CPU and GPU (is mutable). - std::mutex m_input_display_lock; - std::array m_input_display; + // One string per pad and Wii remote, written by the CPU thread and shown by the GPU thread: + // each is published whole and freed through m_input_display_quiesce. + struct DisplayString + { + quiesce_node_t node; // first: the free callback casts it back + std::string text; + }; + static void FreeDisplayString(quiesce_node_t* node); + void SetInputDisplay(int index, std::string text); + std::array m_input_display{}; + mutable quiesce_t m_input_display_quiesce{}; Core::System& m_system; }; diff --git a/Source/Core/Core/NetworkCaptureLogger.cpp b/Source/Core/Core/NetworkCaptureLogger.cpp index 5c8b77dd11..a5bccdd304 100644 --- a/Source/Core/Core/NetworkCaptureLogger.cpp +++ b/Source/Core/Core/NetworkCaptureLogger.cpp @@ -86,7 +86,43 @@ PCAPSSLCaptureLogger::PCAPSSLCaptureLogger() new File::IOFile(filepath, "wb", File::SharedAccess::Read), Common::PCAP::LinkType::Ethernet); } -PCAPSSLCaptureLogger::~PCAPSSLCaptureLogger() = default; +PCAPSSLCaptureLogger::~PCAPSSLCaptureLogger() +{ + // Nothing logs any more; write what is still queued. + for (mpsc_stack_node_t* node = mpsc_stack_reverse(mpsc_stack_drain(&m_packets)); node;) + { + auto* packet = reinterpret_cast(node); + node = node->next; + m_file->AddPacket(packet->bytes.data(), packet->bytes.size()); + delete packet; + } +} + +void PCAPSSLCaptureLogger::Write(const u8* data, std::size_t length) +{ + auto* packet = new Packet{{}, std::vector(data, data + length)}; + mpsc_stack_push(&m_packets, &packet->link); + // Pairs with the writer's fence: it either sees this packet or this thread sees it gone. + retro_atomic_thread_fence_seq_cst(); + + for (;;) + { + if (!retro_atomic_cas_int(&m_writing, 0, 1)) + return; // the writer at work takes this packet too + for (mpsc_stack_node_t* node = mpsc_stack_reverse(mpsc_stack_drain(&m_packets)); node;) + { + auto* queued = reinterpret_cast(node); + node = node->next; + m_file->AddPacket(queued->bytes.data(), queued->bytes.size()); + delete queued; + } + retro_atomic_store_release_int(&m_writing, 0); + // A packet queued after the drain, by a thread that saw this one still writing. + retro_atomic_thread_fence_seq_cst(); + if (mpsc_stack_empty(&m_packets)) + return; + } +} void PCAPSSLCaptureLogger::OnNewSocket(s32 socket) { @@ -123,9 +159,8 @@ void PCAPSSLCaptureLogger::LogBBA(const void* data, std::size_t length) if (!Config::Get(Config::MAIN_NETWORK_DUMP_BBA)) return; - // Concurrency between CEXIETHERNET's RecvHandlePacket and SendFromDirectFIFO - const std::lock_guard lock(m_io_mutex); - m_file->AddPacket(static_cast(data), length); + // CEXIETHERNET's RecvHandlePacket and SendFromDirectFIFO run on different threads. + Write(static_cast(data), length); } void PCAPSSLCaptureLogger::Log(LogType log_type, const void* data, std::size_t length, s32 socket, @@ -208,7 +243,7 @@ void PCAPSSLCaptureLogger::LogIPv4(LogType log_type, const u8* data, u16 length, } packet.insert(packet.end(), data, data + length); - m_file->AddPacket(packet.data(), packet.size()); + Write(packet.data(), packet.size()); } NetworkCaptureType PCAPSSLCaptureLogger::GetCaptureType() const diff --git a/Source/Core/Core/NetworkCaptureLogger.h b/Source/Core/Core/NetworkCaptureLogger.h index 7642e8676c..2ac9a77521 100644 --- a/Source/Core/Core/NetworkCaptureLogger.h +++ b/Source/Core/Core/NetworkCaptureLogger.h @@ -6,7 +6,7 @@ #include #include #include -#include +#include #ifdef _WIN32 #include @@ -17,6 +17,8 @@ using socklen_t = int; #include #endif +#include +#include #include "Common/CommonTypes.h" namespace Common @@ -113,8 +115,19 @@ private: void LogIPv4(LogType log_type, const u8* data, u16 length, s32 socket, const sockaddr_in& from, const sockaddr_in& to); + // Packets come from the CPU thread, the BBA thread and the network worker. Each one queues + // its packet, and whichever finds no writer at work becomes it and writes everything + // queued, its own and the others', so nobody waits on the file. + struct Packet + { + mpsc_stack_node_t link; // first: the drained chain is cast back + std::vector bytes; + }; + void Write(const u8* data, std::size_t length); + std::unique_ptr m_file; - std::mutex m_io_mutex; + mpsc_stack_t m_packets{}; + retro_atomic_int_t m_writing{}; std::map m_read_sequence_number; std::map m_write_sequence_number; }; diff --git a/Source/Core/Core/PatchEngine.cpp b/Source/Core/Core/PatchEngine.cpp index 77a8adb33b..ddd5a4d677 100644 --- a/Source/Core/Core/PatchEngine.cpp +++ b/Source/Core/Core/PatchEngine.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -47,7 +46,6 @@ constexpr std::array s_patch_type_strings{{ static std::vector s_on_frame; static std::vector s_on_frame_memory; -static std::mutex s_on_frame_memory_mutex; const char* PatchTypeAsString(PatchType type) { @@ -254,7 +252,6 @@ static void ApplyPatches(const Core::CPUThreadGuard& guard, const std::vector memory_patch_indices) { - std::lock_guard lock(s_on_frame_memory_mutex); for (std::size_t index : memory_patch_indices) { guard.GetSystem().GetPowerPC().GetDebugInterface().ApplyExistingPatch(guard, index); @@ -291,13 +288,11 @@ static bool IsStackValid(const Core::CPUThreadGuard& guard) void AddMemoryPatch(std::size_t index) { - std::lock_guard lock(s_on_frame_memory_mutex); s_on_frame_memory.push_back(index); } void RemoveMemoryPatch(std::size_t index) { - std::lock_guard lock(s_on_frame_memory_mutex); std::erase(s_on_frame_memory, index); } diff --git a/Source/Core/Core/PowerPC/CachedInterpreter/CachedInterpreter_Disassembler.cpp b/Source/Core/Core/PowerPC/CachedInterpreter/CachedInterpreter_Disassembler.cpp index 665329729e..21fbcbda54 100644 --- a/Source/Core/Core/PowerPC/CachedInterpreter/CachedInterpreter_Disassembler.cpp +++ b/Source/Core/Core/PowerPC/CachedInterpreter/CachedInterpreter_Disassembler.cpp @@ -5,13 +5,13 @@ #include #include -#include #include #include #include "Core/HLE/HLE.h" + s32 CachedInterpreterEmitter::PoisonCallback(std::ostream& stream, const void* operands) { stream << "PoisonCallback()\n"; @@ -89,8 +89,6 @@ s32 CachedInterpreter::CheckIdle(std::ostream& stream, const CheckIdleOperands& return sizeof(AnyCallback) + sizeof(operands); } -static std::once_flag s_sorted_lookup_flag; - std::size_t CachedInterpreter::Disassemble(const JitBlock& block, std::ostream& stream) { using LookupKV = std::pair; @@ -118,11 +116,14 @@ std::size_t CachedInterpreter::Disassemble(const JitBlock& block, std::ostream& #undef LOOKUP_KV - std::call_once(s_sorted_lookup_flag, [] { + // Sorted once per process; a function-local static is initialized exactly once. + static const bool s_sorted = [] { const auto end = std::ranges::sort(sorted_lookup, {}, &LookupKV::first); ASSERT_MSG(DYNA_REC, std::ranges::adjacent_find(sorted_lookup, {}, &LookupKV::first) == end, "Sorted lookup should not contain duplicate keys."); - }); + return true; + }(); + (void)s_sorted; std::size_t instruction_count = 0; for (const u8* normal_entry = block.normalEntry; normal_entry != block.near_end; diff --git a/Source/Core/Core/PowerPC/PPCSymbolDB.cpp b/Source/Core/Core/PowerPC/PPCSymbolDB.cpp index e002dca5ad..8f13eb8a67 100644 --- a/Source/Core/Core/PowerPC/PPCSymbolDB.cpp +++ b/Source/Core/Core/PowerPC/PPCSymbolDB.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -36,7 +35,6 @@ PPCSymbolDB::~PPCSymbolDB() = default; // Adds the function to the list, unless it's already there const Common::Symbol* PPCSymbolDB::AddFunction(const Core::CPUThreadGuard& guard, u32 start_addr) { - std::lock_guard lock(m_mutex); // It's already in the list if (m_functions.contains(start_addr)) @@ -57,7 +55,6 @@ void PPCSymbolDB::AddKnownSymbol(const Core::CPUThreadGuard& guard, u32 startAdd const std::string& name, const std::string& object_name, Common::Symbol::Type type) { - std::lock_guard lock(m_mutex); AddKnownSymbol(guard, startAddr, size, name, object_name, type, &m_functions, &m_checksum_to_function); } @@ -106,7 +103,6 @@ void PPCSymbolDB::AddKnownSymbol(const Core::CPUThreadGuard& guard, u32 startAdd void PPCSymbolDB::AddKnownNote(u32 start_addr, u32 size, const std::string& name) { - std::lock_guard lock(m_mutex); AddKnownNote(start_addr, size, name, &m_notes); } @@ -124,7 +120,6 @@ void PPCSymbolDB::AddKnownNote(u32 start_addr, u32 size, const std::string& name void PPCSymbolDB::DetermineNoteLayers() { - std::lock_guard lock(m_mutex); DetermineNoteLayers(&m_notes); } @@ -148,7 +143,6 @@ void PPCSymbolDB::DetermineNoteLayers(XNoteMap* notes) const Common::Symbol* PPCSymbolDB::GetSymbolFromAddr(u32 addr) const { - std::lock_guard lock(m_mutex); if (m_functions.empty()) return nullptr; @@ -173,7 +167,6 @@ const Common::Symbol* PPCSymbolDB::GetSymbolFromAddr(u32 addr) const const Common::Note* PPCSymbolDB::GetNoteFromAddr(u32 addr) const { - std::lock_guard lock(m_mutex); if (m_notes.empty()) return nullptr; @@ -204,13 +197,11 @@ const Common::Note* PPCSymbolDB::GetNoteFromAddr(u32 addr) const void PPCSymbolDB::DeleteFunction(u32 start_address) { - std::lock_guard lock(m_mutex); m_functions.erase(start_address); } void PPCSymbolDB::DeleteNote(u32 start_address) { - std::lock_guard lock(m_mutex); m_notes.erase(start_address); } @@ -223,7 +214,6 @@ std::string PPCSymbolDB::GetDescription(u32 addr) const void PPCSymbolDB::FillInCallers() { - std::lock_guard lock(m_mutex); FillInCallers(&m_functions); } @@ -260,7 +250,6 @@ void PPCSymbolDB::FillInCallers(XFuncMap* functions) void PPCSymbolDB::PrintCalls(u32 funcAddr) const { - std::lock_guard lock(m_mutex); const auto iter = m_functions.find(funcAddr); if (iter == m_functions.end()) @@ -283,7 +272,6 @@ void PPCSymbolDB::PrintCalls(u32 funcAddr) const void PPCSymbolDB::PrintCallers(u32 funcAddr) const { - std::lock_guard lock(m_mutex); const auto iter = m_functions.find(funcAddr); if (iter == m_functions.end()) @@ -303,7 +291,6 @@ void PPCSymbolDB::PrintCallers(u32 funcAddr) const void PPCSymbolDB::LogFunctionCall(u32 addr) { - std::lock_guard lock(m_mutex); auto iter = m_functions.find(addr); if (iter == m_functions.end()) @@ -341,7 +328,6 @@ bool PPCSymbolDB::LoadMapOnBoot(const Core::CPUThreadGuard& guard) return Clear(); { - std::lock_guard lock(m_mutex); // If the map is already loaded (such as restarting the same game), skip reloading. if (!IsEmpty() && existing_map_file == m_map_name) return false; @@ -615,8 +601,6 @@ bool PPCSymbolDB::LoadMap(const Core::CPUThreadGuard& guard, std::string filenam Index(&new_functions); DetermineNoteLayers(&new_notes); FillInCallers(&new_functions); - - std::lock_guard lock(m_mutex); std::swap(m_functions, new_functions); std::swap(m_notes, new_notes); std::swap(m_checksum_to_function, checksum_to_function); @@ -633,8 +617,6 @@ bool PPCSymbolDB::SaveSymbolMap(const std::string& filename) const if (!file) return false; - std::lock_guard lock(m_mutex); - // Write .text section auto function_symbols = m_functions | std::views::filter([](const auto& f) { return f.second.type == Common::Symbol::Type::Function; @@ -700,8 +682,6 @@ bool PPCSymbolDB::SaveCodeMap(const Core::CPUThreadGuard& guard, const std::stri // Write ".text" at the top f.WriteString(".text\n"); - std::lock_guard lock(m_mutex); - const auto& ppc_debug_interface = guard.GetSystem().GetPowerPC().GetDebugInterface(); u32 next_address = 0; diff --git a/Source/Core/Core/System.cpp b/Source/Core/Core/System.cpp index 563b804757..dd0753d9d9 100644 --- a/Source/Core/Core/System.cpp +++ b/Source/Core/Core/System.cpp @@ -9,7 +9,6 @@ #include "Core/Config/MainSettings.h" #include "Core/CoreTiming.h" #include "Core/FifoPlayer/FifoPlayer.h" -#include "Core/FifoPlayer/FifoRecorder.h" #include "Core/HW/AudioInterface.h" #include "Core/HW/CPU.h" #include "Core/HW/DSP.h" @@ -57,7 +56,7 @@ struct System::Impl m_serial_interface(system), m_system_timers(system), m_video_interface(system), m_interpreter(system, m_power_pc.GetPPCState(), m_mmu, m_power_pc.GetBranchWatch(), m_power_pc.GetSymbolDB()), - m_jit_interface(system), m_fifo_player(system), m_fifo_recorder(system), m_movie(system) + m_jit_interface(system), m_fifo_player(system), m_movie(system) { } @@ -104,7 +103,6 @@ struct System::Impl JitInterface m_jit_interface; VideoCommon::CustomResourceManager m_custom_resource_manager; FifoPlayer m_fifo_player; - FifoRecorder m_fifo_recorder; Movie::MovieManager m_movie; }; @@ -212,11 +210,6 @@ FifoPlayer& System::GetFifoPlayer() const return m_impl->m_fifo_player; } -FifoRecorder& System::GetFifoRecorder() const -{ - return m_impl->m_fifo_recorder; -} - GeometryShaderManager& System::GetGeometryShaderManager() const { return m_impl->m_geometry_shader_manager; diff --git a/Source/Core/Core/System.h b/Source/Core/Core/System.h index b4ab66e033..b22d267490 100644 --- a/Source/Core/Core/System.h +++ b/Source/Core/Core/System.h @@ -52,7 +52,6 @@ namespace Fifo class FifoManager; } class FifoPlayer; -class FifoRecorder; namespace GPFifo { class GPFifoManager; @@ -176,7 +175,6 @@ public: ExpansionInterface::ExpansionInterfaceManager& GetExpansionInterface() const; Fifo::FifoManager& GetFifo() const; FifoPlayer& GetFifoPlayer() const; - FifoRecorder& GetFifoRecorder() const; GeometryShaderManager& GetGeometryShaderManager() const; GPFifo::GPFifoManager& GetGPFifo() const; HSP::HSPManager& GetHSP() const; diff --git a/Source/Core/DiscIO/VolumeVerifier.cpp b/Source/Core/DiscIO/VolumeVerifier.cpp deleted file mode 100644 index a02e17a1a7..0000000000 --- a/Source/Core/DiscIO/VolumeVerifier.cpp +++ /dev/null @@ -1,1522 +0,0 @@ -// Copyright 2019 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "DiscIO/VolumeVerifier.h" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "Common/Align.h" -#include "Common/Assert.h" -#include "Common/CPUDetect.h" -#include "Common/CommonPaths.h" -#include "Common/CommonTypes.h" -#include "Common/Contains.h" -#include "Common/Crypto/SHA1.h" -#include "Common/FileUtil.h" -#include "Common/Hash.h" -#include "Common/HttpRequest.h" -#include "Common/IOFile.h" -#include "Common/Logging/Log.h" -#include "Common/MinizipUtil.h" -#include "Common/MsgHandler.h" -#include "Common/ParallelFor.h" -#include "Common/ScopeGuard.h" -#include "Common/StringUtil.h" -#include "Common/Thread.h" -#include "Common/Swap.h" -#include "Common/Version.h" -#include "Core/IOS/Device.h" -#include "Core/IOS/ES/ES.h" -#include "Core/IOS/ES/Formats.h" -#include "Core/IOS/IOS.h" -#include "Core/IOS/IOSC.h" -#include "DiscIO/Blob.h" -#include "DiscIO/DiscScrubber.h" -#include "DiscIO/DiscUtils.h" -#include "DiscIO/Enums.h" -#include "DiscIO/Filesystem.h" -#include "DiscIO/Volume.h" -#include "DiscIO/VolumeWii.h" - -namespace DiscIO -{ -RedumpVerifier::DownloadState RedumpVerifier::m_gc_download_state; -RedumpVerifier::DownloadState RedumpVerifier::m_wii_download_state; - -void RedumpVerifier::Start(const Volume& volume) -{ - if (!volume.IsDatelDisc()) - { - m_game_id = volume.GetGameID(); - if (m_game_id.size() > 4) - m_game_id = m_game_id.substr(0, 4); - } - - m_revision = volume.GetRevision().value_or(0); - m_disc_number = volume.GetDiscNumber().value_or(0); - m_size = volume.GetDataSize(); - - m_platform = volume.GetVolumeType(); - m_thread = sthread_create(Common::ThreadEntry<&RedumpVerifier::MatchThread>, this); -} - -RedumpVerifier::~RedumpVerifier() -{ - Common::JoinThread(m_thread); -} - -void RedumpVerifier::MatchThread() -{ - m_potential_matches = FindPotentialMatches(m_platform); -} - -std::vector RedumpVerifier::FindPotentialMatches(Platform platform) -{ - std::string system; - DownloadState* download_state; - switch (platform) - { - case Platform::GameCubeDisc: - system = "gc"; - download_state = &m_gc_download_state; - break; - - case Platform::WiiDisc: - system = "wii"; - download_state = &m_wii_download_state; - break; - - default: - m_result.status = Status::Error; - return {}; - } - - { - std::lock_guard lk(download_state->mutex); - download_state->status = DownloadDatfile(system, download_state->status); - } - - switch (download_state->status) - { - case DownloadStatus::FailButOldCacheAvailable: - ERROR_LOG_FMT(DISCIO, "Failed to fetch data from Redump.info, using old cached data instead"); - [[fallthrough]]; - case DownloadStatus::Success: - return ScanDatfile(ReadDatfile(system), system); - - case DownloadStatus::Fail: - default: - m_result = {Status::Error, Common::GetStringT("Failed to connect to Redump.info")}; - return {}; - } -} - -static std::string GetPathForSystem(const std::string& system) -{ - return File::GetUserPath(D_REDUMPCACHE_IDX) + DIR_SEP + system + ".zip"; -} - -RedumpVerifier::DownloadStatus RedumpVerifier::DownloadDatfile(const std::string& system, - DownloadStatus old_status) -{ - if (old_status == DownloadStatus::Success) - return old_status; - - Common::HttpRequest request; - - const std::optional> result = - request.Get("https://redump.info/datfile/" + system + "/serial,version", - {{"User-Agent", Common::GetEmulatorName()}}); - - const std::string output_path = GetPathForSystem(system); - - if (!result) - { - return File::Exists(output_path) ? DownloadStatus::FailButOldCacheAvailable : - DownloadStatus::Fail; - } - - if (result->size() > 1 && (*result)[0] == '<' && (*result)[1] == '!') - { - // This is an HTML page, not a zip file like we want - return File::Exists(output_path) ? DownloadStatus::FailButOldCacheAvailable : - DownloadStatus::Fail; - } - - File::CreateFullPath(output_path); - if (!File::IOFile(output_path, "wb").WriteBytes(result->data(), result->size())) - ERROR_LOG_FMT(DISCIO, "Failed to write downloaded datfile to {}", output_path); - return DownloadStatus::Success; -} - -std::vector RedumpVerifier::ReadDatfile(const std::string& system) -{ - void* zip_reader = mz_zip_reader_create(); - if (!zip_reader) - return {}; - - Common::ScopeGuard file_guard{[&] { mz_zip_reader_delete(&zip_reader); }}; - - if (mz_zip_reader_open_file(zip_reader, GetPathForSystem(system).c_str()) != MZ_OK) - return {}; - - // Check that the zip file contains exactly one file - if (mz_zip_reader_goto_first_entry(zip_reader) != MZ_OK) - return {}; - if (mz_zip_reader_goto_next_entry(zip_reader) != MZ_END_OF_LIST) - return {}; - - // Read the file - if (mz_zip_reader_goto_first_entry(zip_reader) != MZ_OK) - return {}; - mz_zip_file* file_info; - mz_zip_reader_entry_get_info(zip_reader, &file_info); - std::vector data(file_info->uncompressed_size); - if (!Common::ReadFileFromZip(zip_reader, data.data(), file_info->uncompressed_size)) - return {}; - - return data; -} - -static u8 ParseHexDigit(char c) -{ - if (c < '0') - return 0; // Error - - if (c >= 'a') - c -= 'a' - 'A'; - if (c >= 'A') - c -= 'A' - ('9' + 1); - c -= '0'; - - if (c >= 0x10) - return 0; // Error - - return c; -} - -static std::vector ParseHash(const char* str) -{ - std::vector hash; - while (str[0] && str[1]) - { - hash.push_back(static_cast(ParseHexDigit(str[0]) * 0x10 + ParseHexDigit(str[1]))); - str += 2; - } - return hash; -} - -std::vector RedumpVerifier::ScanDatfile(std::span data, - const std::string& system) -{ - pugi::xml_document doc; - if (!doc.load_buffer(data.data(), data.size())) - { - m_result = {Status::Error, Common::GetStringT("Failed to parse Redump.info data")}; - return {}; - } - - std::vector potential_matches; - bool serials_exist = false; - bool versions_exist = false; - const pugi::xml_node datafile = doc.child("datafile"); - for (const pugi::xml_node game : datafile.children("game")) - { - std::string version_string = game.child("version").text().as_string(); - if (!version_string.empty()) - versions_exist = true; - - // Strip out prefix (e.g. "v1.02" -> "02", "Rev 2" -> "2") - const size_t last_non_numeric = version_string.find_last_not_of("0123456789"); - if (last_non_numeric != std::string::npos) - version_string = version_string.substr(last_non_numeric + 1); - - const int version = version_string.empty() ? 0 : std::stoi(version_string); - - const std::string serials = game.child("serial").text().as_string(); - if (!serials.empty()) - serials_exist = true; - - // The revisions for Korean GameCube games whose four-char game IDs end in E are numbered from - // 0x30 in ring codes and in disc headers, but Redump switched to numbering them from 0 in 2019. - if (version % 0x30 != m_revision % 0x30) - continue; - - if (serials.empty() || serials.starts_with("DS")) - { - // GC Datel discs have no serials in Redump, Wii Datel discs have serials like "DS000101" - if (!m_game_id.empty()) - continue; // Non-empty m_game_id means we're verifying a non-Datel disc - } - else - { - bool serial_match_found = false; - - // If a disc has multiple possible serials, they are delimited with ", ". We want to loop - // through all the serials until we find a match, because even though they normally only - // differ in the region code at the end (which we don't care about), there is an edge case - // disc with the game ID "G96P" and the serial "DL-DOL-D96P-EUR, DL-DOL-G96P-EUR". - for (const std::string& serial_str : SplitString(serials, ',')) - { - const std::string_view serial = StripWhitespace(serial_str); - - // Skip the prefix, normally either "DL-DOL-" or "RVL-" (depending on the console), - // but there are some exceptions like the "RVLE-SBSE-USA-B0" serial. - const size_t first_dash = serial.find_first_of('-', 3); - const size_t game_id_start = - first_dash == std::string::npos ? std::string::npos : first_dash + 1; - - if (game_id_start == std::string::npos || serial.size() < game_id_start + 4) - { - ERROR_LOG_FMT(DISCIO, "Invalid serial in redump datfile: {}", serial_str); - continue; - } - - const std::string_view game_id = serial.substr(game_id_start, 4); - if (game_id != m_game_id) - continue; - - u8 disc_number = 0; - if (serial.size() > game_id_start + 5 && serial[game_id_start + 5] >= '0' && - serial[game_id_start + 5] <= '9') - { - disc_number = serial[game_id_start + 5] - '0'; - } - if (disc_number != m_disc_number) - continue; - - serial_match_found = true; - break; - } - if (!serial_match_found) - continue; - } - - PotentialMatch& potential_match = potential_matches.emplace_back(); - const pugi::xml_node rom = game.child("rom"); - potential_match.size = rom.attribute("size").as_ullong(); - potential_match.hashes.crc32 = ParseHash(rom.attribute("crc").value()); - potential_match.hashes.md5 = ParseHash(rom.attribute("md5").value()); - potential_match.hashes.sha1 = ParseHash(rom.attribute("sha1").value()); - } - - if (!serials_exist || !versions_exist) - { - // If we reach this, the user has most likely downloaded a datfile manually, - // so show a panic alert rather than just using ERROR_LOG - - // i18n: "Serial" refers to serial numbers, e.g. RVL-RSBE-USA - PanicAlertFmtT( - "Serial and/or version data is missing from {0}\n" - "Please append \"{1}\" (without the quotes) to the datfile URL when downloading\n" - "Example: {2}", - GetPathForSystem(system), "serial,version", "http://redump.info/datfile/gc/serial,version"); - m_result = {Status::Error, Common::GetStringT("Failed to parse Redump.info data")}; - return {}; - } - - return potential_matches; -} - -static bool HashesMatch(const std::vector& calculated, const std::vector& expected) -{ - return calculated.empty() || calculated == expected; -} - -RedumpVerifier::Result RedumpVerifier::Finish(const Hashes>& hashes) -{ - if (m_result.status == Status::Error) - return m_result; - - if (hashes.crc32.empty() && hashes.md5.empty() && hashes.sha1.empty()) - return m_result; - - Common::JoinThread(m_thread); - const std::vector potential_matches = std::move(m_potential_matches); - for (PotentialMatch p : potential_matches) - { - if (HashesMatch(hashes.crc32, p.hashes.crc32) && HashesMatch(hashes.md5, p.hashes.md5) && - HashesMatch(hashes.sha1, p.hashes.sha1) && m_size == p.size) - { - return {Status::GoodDump, Common::GetStringT("Good dump")}; - } - } - - // We only return bad dump if there's a disc that we know this dump should match but that doesn't - // match. For disc without IDs (i.e. Datel discs), we don't have a good way of knowing whether we - // have a bad dump or just a dump that isn't in Redump, so we always pick unknown instead of bad - // dump for those to be on the safe side. (Besides, it's possible to dump a Datel disc correctly - // and have it not match Redump if you don't use the same replacement value for bad sectors.) - if (!potential_matches.empty() && !m_game_id.empty()) - return {Status::BadDump, Common::GetStringT("Bad dump")}; - - return {Status::Unknown, Common::GetStringT("Unknown disc")}; -} - -constexpr u64 DEFAULT_READ_SIZE = 0x20000; // Arbitrary value - -VolumeVerifier::VolumeVerifier(const Volume& volume, bool redump_verification, - Hashes hashes_to_calculate) - : m_volume(volume), m_redump_verification(redump_verification), - m_hashes_to_calculate(hashes_to_calculate), - m_calculating_any_hash(hashes_to_calculate.crc32 || hashes_to_calculate.md5 || - hashes_to_calculate.sha1), - m_max_progress(volume.GetDataSize()), m_data_size_type(volume.GetDataSizeType()) -{ - if (!m_calculating_any_hash) - m_redump_verification = false; - m_async_worker.Reset("VolumeVerifier"); -} - -VolumeVerifier::~VolumeVerifier() -{ - WaitForAsyncOperations(); -} - -Hashes VolumeVerifier::GetDefaultHashesToCalculate() -{ - Hashes hashes_to_calculate{.crc32 = true, .md5 = true, .sha1 = true}; - // If the system can compute certain hashes faster than others, only default-enable the fast ones. - const bool sha1_hw_accel = Common::SHA1::CreateContext()->HwAccelerated(); - // For crc32, we assume zlib-ng will be fast if cpu supports crc32 - const bool crc32_hw_accel = cpu_info.bCRC32; - if (crc32_hw_accel || sha1_hw_accel) - { - hashes_to_calculate.crc32 = crc32_hw_accel; - // md5 has no accelerated implementation at the moment, always default to off - hashes_to_calculate.md5 = false; - // Always enable SHA1, to avoid situation where only crc32 is computed - hashes_to_calculate.sha1 = true; - } - return hashes_to_calculate; -} - -void VolumeVerifier::Start() -{ - ASSERT(!m_started); - m_started = true; - - if (m_redump_verification) - m_redump_verifier.Start(m_volume); - - m_is_tgc = m_volume.GetBlobType() == BlobType::TGC; - m_is_datel = m_volume.IsDatelDisc(); - m_is_triforce = m_volume.GetVolumeType() == Platform::Triforce; - m_is_not_retail = (m_volume.GetVolumeType() == Platform::WiiDisc && !m_volume.HasWiiHashes()) || - IsDebugSigned(); - - const std::vector partitions = CheckPartitions(); - - if (IsDisc(m_volume.GetVolumeType())) - m_biggest_referenced_offset = GetBiggestReferencedOffset(m_volume, partitions); - - CheckMisc(); - - SetUpHashing(); -} - -std::vector VolumeVerifier::CheckPartitions() -{ - if (m_volume.GetVolumeType() == Platform::WiiWAD) - return {}; - - const std::vector partitions = m_volume.GetPartitions(); - if (partitions.empty()) - { - if (!m_volume.GetFileSystem(m_volume.GetGamePartition())) - { - AddProblem(Severity::High, - Common::GetStringT("The filesystem is invalid or could not be read.")); - return {}; - } - return {m_volume.GetGamePartition()}; - } - - std::optional partitions_in_first_table = m_volume.ReadSwapped(0x40000, PARTITION_NONE); - if (partitions_in_first_table && *partitions_in_first_table > 8) - { - // Not sure if 8 actually is the limit, but there certainly aren't any discs - // released that have as many partitions as 8 in the first partition table. - // The only game that has that many partitions in total is Super Smash Bros. Brawl, - // and that game places all partitions other than UPDATE and DATA in the second table. - AddProblem(Severity::Low, - Common::GetStringT("There are too many partitions in the first partition table.")); - } - - std::vector types; - for (const Partition& partition : partitions) - { - const std::optional type = m_volume.GetPartitionType(partition); - if (type) - types.emplace_back(*type); - } - - if (!Common::Contains(types, PARTITION_UPDATE)) - AddProblem(Severity::Low, Common::GetStringT("The update partition is missing.")); - - const bool has_data_partition = Common::Contains(types, PARTITION_DATA); - if (!m_is_datel && !has_data_partition) - AddProblem(Severity::High, Common::GetStringT("The data partition is missing.")); - - const bool has_channel_partition = Common::Contains(types, PARTITION_CHANNEL); - if (ShouldHaveChannelPartition() && !has_channel_partition) - AddProblem(Severity::Medium, Common::GetStringT("The channel partition is missing.")); - - const bool has_install_partition = Common::Contains(types, PARTITION_INSTALL); - if (ShouldHaveInstallPartition() && !has_install_partition) - AddProblem(Severity::High, Common::GetStringT("The install partition is missing.")); - - if (ShouldHaveMasterpiecePartitions() && - types.cend() == std::ranges::find_if(types, [](u32 type) { return type >= 0xFF; })) - { - // i18n: This string is referring to a game mode in Super Smash Bros. Brawl called Masterpieces - // where you play demos of NES/SNES/N64 games. Official translations: - // 名作トライアル (Japanese), Masterpieces (English), Meisterstücke (German), Chefs-d'œuvre - // (French), Clásicos (Spanish), Capolavori (Italian), 클래식 게임 체험판 (Korean). - // If your language is not one of the languages above, consider leaving the string untranslated - // so that people will recognize it as the name of the game mode. - AddProblem(Severity::Medium, Common::GetStringT("The Masterpiece partitions are missing.")); - } - - for (const Partition& partition : partitions) - { - if (m_volume.GetPartitionType(partition) == PARTITION_UPDATE && partition.offset != 0x50000) - { - AddProblem(Severity::Low, - Common::GetStringT("The update partition is not at its normal position.")); - } - - const u64 normal_data_offset = m_volume.HasWiiHashes() ? 0xF800000 : 0x838000; - if (m_volume.GetPartitionType(partition) == PARTITION_DATA && - partition.offset != normal_data_offset && !has_channel_partition && !has_install_partition) - { - AddProblem(Severity::Low, - Common::GetStringT( - "The data partition is not at its normal position. This will affect the " - "emulated loading times. You will be unable to share input recordings and use " - "NetPlay with anyone who is using a good dump.")); - } - } - - std::vector valid_partitions; - for (const Partition& partition : partitions) - { - if (CheckPartition(partition)) - valid_partitions.push_back(partition); - } - - return valid_partitions; -} - -bool VolumeVerifier::CheckPartition(const Partition& partition) -{ - std::optional type = m_volume.GetPartitionType(partition); - if (!type) - { - // Not sure if this can happen in practice - AddProblem(Severity::Medium, Common::GetStringT("The type of a partition could not be read.")); - return false; - } - - Severity severity = Severity::Medium; - if (*type == PARTITION_DATA || *type == PARTITION_INSTALL) - severity = Severity::High; - else if (*type == PARTITION_UPDATE) - severity = Severity::Low; - - const std::string name = GetPartitionName(type); - - if (partition.offset % VolumeWii::BLOCK_TOTAL_SIZE != 0 || - m_volume.PartitionOffsetToRawOffset(0, partition) % VolumeWii::BLOCK_TOTAL_SIZE != 0) - { - AddProblem(Severity::Medium, - Common::FmtFormatT("The {0} partition is not properly aligned.", name)); - } - - bool invalid_header = false; - bool blank_contents = false; - std::vector disc_header(0x80); - if (!m_volume.Read(0, disc_header.size(), disc_header.data(), partition)) - { - invalid_header = true; - } - else if (Common::swap32(disc_header.data() + 0x18) != WII_DISC_MAGIC) - { - for (size_t i = 0; i < disc_header.size(); i += 4) - { - if (Common::swap32(disc_header.data() + i) != i) - { - invalid_header = true; - break; - } - } - - // The loop above ends without setting invalid_header for discs that legitimately lack - // updates. No such discs have been released to end users. Most such discs are debug signed, - // but there is apparently at least one that is retail signed, the Movie-Ch Install Disc. - if (!invalid_header) - blank_contents = true; - } - if (invalid_header) - { - // This can happen when certain programs that create WBFS files scrub the entirety of - // the Masterpiece partitions in Super Smash Bros. Brawl without removing them from - // the partition table. https://bugs.dolphin-emu.org/issues/8733 - AddProblem(severity, - Common::FmtFormatT("The {0} partition does not seem to contain valid data.", name)); - return false; - } - - if (!m_is_datel) - { - const auto console_type = - IsDebugSigned() ? IOS::HLE::IOSC::ConsoleType::RVT : IOS::HLE::IOSC::ConsoleType::Retail; - IOS::HLE::Kernel ios(console_type); - auto& es = ios.GetESCore(); - const std::vector& cert_chain = m_volume.GetCertificateChain(partition); - - if (IOS::HLE::IPC_SUCCESS != - es.VerifyContainer(IOS::HLE::ESCore::VerifyContainerType::Ticket, - IOS::HLE::ESCore::VerifyMode::DoNotUpdateCertStore, - m_volume.GetTicket(partition), cert_chain) || - IOS::HLE::IPC_SUCCESS != - es.VerifyContainer(IOS::HLE::ESCore::VerifyContainerType::TMD, - IOS::HLE::ESCore::VerifyMode::DoNotUpdateCertStore, - m_volume.GetTMD(partition), cert_chain)) - { - AddProblem(Severity::Low, - Common::FmtFormatT("The {0} partition is not correctly signed.", name)); - } - } - - if (m_volume.HasWiiHashes() && !m_volume.CheckH3TableIntegrity(partition)) - { - AddProblem(Severity::Low, - Common::FmtFormatT("The H3 hash table for the {0} partition is not correct.", name)); - } - - // Prepare for hash verification in the Process step - if (m_volume.HasWiiHashes()) - { - const u64 data_size = - m_volume.ReadSwappedAndShifted(partition.offset + 0x2bc, PARTITION_NONE).value_or(0); - const size_t blocks = static_cast(data_size / VolumeWii::BLOCK_TOTAL_SIZE); - - if (data_size % VolumeWii::BLOCK_TOTAL_SIZE != 0) - { - std::string text = Common::FmtFormatT( - "The data size for the {0} partition is not evenly divisible by the block size.", name); - AddProblem(Severity::Low, std::move(text)); - } - - u64 offset = m_volume.PartitionOffsetToRawOffset(0, partition); - for (size_t block_index = 0; block_index < blocks; - block_index += VolumeWii::BLOCKS_PER_GROUP, offset += VolumeWii::GROUP_TOTAL_SIZE) - { - m_groups.emplace_back( - GroupToVerify{partition, offset, block_index, - std::min(block_index + VolumeWii::BLOCKS_PER_GROUP, blocks)}); - } - - m_block_errors.emplace(partition, 0); - } - - if (blank_contents) - return false; - - const FileSystem* filesystem = m_volume.GetFileSystem(partition); - if (!filesystem) - { - if (m_is_datel) - { - // Datel's Wii Freeloader has an invalid FST in its only partition - return true; - } - - AddProblem(severity, - Common::FmtFormatT("The {0} partition does not have a valid file system.", name)); - return false; - } - - if (type == PARTITION_UPDATE) - { - const IOS::ES::TMDReader& tmd = m_volume.GetTMD(m_volume.GetGamePartition()); - if (tmd.IsValid()) - { - const u64 ios_ver = tmd.GetIOSId() & 0xFF; - bool has_correct_ios = false; - - if (ios_ver == 9) - { - // IOS9 is the only IOS which can be assumed to exist in a working state on any Wii - // regardless of what updates have been installed. At least Mario Party 8 - // (RM8E01, revision 2) uses IOS9 without having it in its update partition. - has_correct_ios = true; - } - else - { - std::unique_ptr file_info = filesystem->FindFileInfo("_sys"); - if (file_info) - { - const std::string ios_ver_str = std::to_string(ios_ver); - const std::string correct_ios = - IsDebugSigned() ? ("firmware.64." + ios_ver_str + ".") : ("ios" + ios_ver_str + "-"); - for (const FileInfo& f : *file_info) - { - std::string file_name = f.GetName(); - Common::ToLower(&file_name); - if (file_name.starts_with(correct_ios)) - { - has_correct_ios = true; - break; - } - } - } - } - - if (!has_correct_ios) - { - // This is reached for hacked dumps where the update partition has been replaced with - // a very old update partition so that no updates will be installed. - AddProblem(Severity::Low, - Common::GetStringT( - "The update partition does not contain the IOS used by this title.")); - } - } - } - - const IOS::ES::TicketReader& ticket = m_volume.GetTicket(partition); - if (!ticket.IsValid()) - { - AddProblem(severity, - // i18n: "Ticket" here is a kind of digital authorization to use a certain title - // (e.g. a game) - Common::FmtFormatT("The {0} partition does not have a valid ticket.", name)); - } - - const IOS::ES::TMDReader& tmd = m_volume.GetTMD(partition); - if (!tmd.IsValid()) - { - AddProblem(severity, Common::FmtFormatT("The {0} partition does not have a valid TMD.", name)); - } - - return true; -} - -std::string VolumeVerifier::GetPartitionName(std::optional type) const -{ - if (!type) - return "???"; - - std::string name = NameForPartitionType(*type, false); - if (ShouldHaveMasterpiecePartitions() && *type > 0xFF) - { - // i18n: This string is referring to a game mode in Super Smash Bros. Brawl called Masterpieces - // where you play demos of NES/SNES/N64 games. This string is referring to a specific such demo - // rather than the game mode as a whole, so please use the singular form. Official translations: - // 名作トライアル (Japanese), Masterpieces (English), Meisterstücke (German), Chefs-d'œuvre - // (French), Clásicos (Spanish), Capolavori (Italian), 클래식 게임 체험판 (Korean). - // If your language is not one of the languages above, consider leaving the string untranslated - // so that people will recognize it as the name of the game mode. - return Common::FmtFormatT("{0} (Masterpiece)", name); - } - return name; -} - -bool VolumeVerifier::IsDebugSigned() const -{ - const IOS::ES::TicketReader& ticket = m_volume.GetTicket(m_volume.GetGamePartition()); - return ticket.IsValid() ? ticket.GetConsoleType() == IOS::HLE::IOSC::ConsoleType::RVT : false; -} - -bool VolumeVerifier::ShouldHaveChannelPartition() const -{ - static constexpr std::array channel_discs = { - "RFNE01", "RFNJ01", "RFNK01", "RFNP01", "RFNW01", "RFPE01", "RFPJ01", "RFPK01", "RFPP01", - "RFPW01", "RGWE41", "RGWJ41", "RGWP41", "RGWX41", "RMCE01", "RMCJ01", "RMCK01", "RMCP01", - }; - static_assert(std::ranges::is_sorted(channel_discs)); - - return std::ranges::binary_search(channel_discs, m_volume.GetGameID()); -} - -bool VolumeVerifier::ShouldHaveInstallPartition() const -{ - static constexpr std::array dragon_quest_x = {"S4MJGD", "S4SJGD", "S6TJGD", - "SDQJGD"}; - const std::string& game_id = m_volume.GetGameID(); - return std::ranges::any_of(dragon_quest_x, - [&game_id](std::string_view x) { return x == game_id; }); -} - -bool VolumeVerifier::ShouldHaveMasterpiecePartitions() const -{ - static constexpr std::array ssbb = {"RSBE01", "RSBJ01", "RSBK01", "RSBP01"}; - const std::string& game_id = m_volume.GetGameID(); - return std::ranges::any_of(ssbb, [&game_id](std::string_view x) { return x == game_id; }); -} - -bool VolumeVerifier::ShouldBeDualLayer() const -{ - // The Japanese versions of Xenoblade and The Last Story are single-layer - // (unlike the other versions) and must not be added to this list. - static constexpr std::array dual_layer_discs = { - "R3ME01", "R3MP01", "R3OE01", "R3OJ01", "R3OP01", "RSBE01", "RSBJ01", "RSBK01", "RSBP01", - "RXMJ8P", "S59E01", "S59JC8", "S59P01", "S5QJC8", "SAKENS", "SAKPNS", "SK8V52", "SK8X52", - "SLSEXJ", "SLSP01", "SQIE4Q", "SQIP4Q", "SQIY4Q", "SR5E41", "SR5P41", "SUOE41", "SUOP41", - "SVXX52", "SVXY52", "SX4E01", "SX4P01", "SZ3EGT", "SZ3PGT", - }; - static_assert(std::ranges::is_sorted(dual_layer_discs)); - - return std::ranges::binary_search(dual_layer_discs, m_volume.GetGameID()); -} - -void VolumeVerifier::CheckVolumeSize() -{ - u64 volume_size = m_volume.GetDataSize(); - const bool is_disc = IsDisc(m_volume.GetVolumeType()); - const bool should_be_dual_layer = is_disc && ShouldBeDualLayer(); - bool volume_size_roughly_known = m_data_size_type != DataSizeType::UpperBound; - - if (should_be_dual_layer && m_biggest_referenced_offset <= SL_DVD_R_SIZE) - { - AddProblem(Severity::Medium, - Common::GetStringT( - "This game has been hacked to fit on a single-layer DVD. Some content such as " - "pre-rendered videos, extra languages or entire game modes will be broken. " - "This problem generally only exists in illegal copies of games.")); - } - - if (m_data_size_type != DataSizeType::Accurate) - { - AddProblem(Severity::Low, - Common::GetStringT("The format that the disc image is saved in does not " - "store the size of the disc image.")); - - if (!volume_size_roughly_known && m_volume.HasWiiHashes()) - { - volume_size = m_biggest_verified_offset; - volume_size_roughly_known = true; - } - } - - if (m_content_index != m_content_offsets.size() || m_group_index != m_groups.size() || - (!m_is_datel && volume_size_roughly_known && m_biggest_referenced_offset > volume_size)) - { - const bool second_layer_missing = is_disc && volume_size_roughly_known && - volume_size >= SL_DVD_SIZE && volume_size <= SL_DVD_R_SIZE; - std::string text = - second_layer_missing ? - Common::GetStringT("This disc image is too small and lacks some data. The problem is " - "most likely that this is a dual-layer disc that has been dumped " - "as a single-layer disc.") : - Common::GetStringT("This disc image is too small and lacks some data. If your " - "dumping program saved the disc image as several parts, you need " - "to merge them into one file."); - AddProblem(Severity::High, std::move(text)); - return; - } - - // The reason why this condition is checking for m_data_size_type != UpperBound instead of - // m_data_size_type == Accurate is because we want to show the warning about input recordings and - // NetPlay for NFS disc images (which are the only disc images that have it set to LowerBound). - if (is_disc && m_data_size_type != DataSizeType::UpperBound && !m_is_tgc) - { - const Platform platform = m_volume.GetVolumeType(); - const bool should_be_gc_size = platform == Platform::GameCubeDisc || m_is_datel; - - const bool valid_gamecube = volume_size == MINI_DVD_SIZE; - const bool valid_retail_wii = volume_size == SL_DVD_SIZE || volume_size == DL_DVD_SIZE; - const bool valid_debug_wii = volume_size == SL_DVD_R_SIZE || volume_size == DL_DVD_R_SIZE; - - const bool debug = IsDebugSigned(); - if ((should_be_gc_size && !valid_gamecube) || - (!should_be_gc_size && (debug ? !valid_debug_wii : !valid_retail_wii))) - { - if (debug && valid_retail_wii) - { - AddProblem( - Severity::Low, - Common::GetStringT("This debug disc image has the size of a retail disc image.")); - } - else - { - u64 normal_size; - if (should_be_gc_size) - normal_size = MINI_DVD_SIZE; - else if (!should_be_dual_layer) - normal_size = SL_DVD_SIZE; - else - normal_size = DL_DVD_SIZE; - - if (volume_size < normal_size) - { - AddProblem( - Severity::Low, - Common::GetStringT( - "This disc image has an unusual size. This will likely make the emulated " - "loading times longer. You will likely be unable to share input recordings " - "and use NetPlay with anyone who is using a good dump.")); - } - else - { - AddProblem(Severity::Low, Common::GetStringT("This disc image has an unusual size.")); - } - } - } - } -} - -void VolumeVerifier::CheckMisc() -{ - const std::string game_id_unencrypted = m_volume.GetGameID(PARTITION_NONE); - const std::string game_id_encrypted = m_volume.GetGameID(m_volume.GetGamePartition()); - - if (game_id_unencrypted != game_id_encrypted) - { - bool inconsistent_game_id = true; - if (game_id_encrypted == "RELSAB") - { - if (game_id_unencrypted.starts_with("410")) - { - // This is the Wii Backup Disc (aka "pinkfish" disc), - // which legitimately has an inconsistent game ID. - inconsistent_game_id = false; - } - else if (game_id_unencrypted.starts_with("010")) - { - // Hacked version of the Wii Backup Disc (aka "pinkfish" disc). - std::string proper_game_id = game_id_unencrypted; - proper_game_id[0] = '4'; - AddProblem(Severity::Low, Common::FmtFormatT("The game ID is {0} but should be {1}.", - game_id_unencrypted, proper_game_id)); - inconsistent_game_id = false; - } - } - - if (inconsistent_game_id) - { - AddProblem(Severity::Low, Common::GetStringT("The game ID is inconsistent.")); - } - } - - const Region region = m_volume.GetRegion(); - - constexpr std::string_view GAMECUBE_PLACEHOLDER_ID = "RELSAB"; - constexpr std::string_view WII_PLACEHOLDER_ID = "RABAZZ"; - - if (game_id_encrypted.size() < 4) - { - AddProblem(Severity::Low, Common::GetStringT("The game ID is unusually short.")); - } - else if (!m_is_datel && game_id_encrypted != GAMECUBE_PLACEHOLDER_ID && - game_id_encrypted != WII_PLACEHOLDER_ID) - { - char country_code; - if (IsDisc(m_volume.GetVolumeType())) - country_code = game_id_encrypted[3]; - else - country_code = static_cast(m_volume.GetTitleID().value_or(0) & 0xff); - - const Platform platform = m_volume.GetVolumeType(); - const std::optional revision = m_volume.GetRevision(); - - if (CountryCodeToRegion(country_code, platform, region, revision) != region) - { - AddProblem(Severity::Medium, - Common::GetStringT( - "The region code does not match the game ID. If this is because the " - "region code has been modified, the game might run at the wrong speed, " - "graphical elements might be offset, or the game might not run at all.")); - } - } - - const IOS::ES::TMDReader& tmd = m_volume.GetTMD(m_volume.GetGamePartition()); - if (tmd.IsValid()) - { - const u64 ios_id = tmd.GetIOSId() & 0xFF; - - // List of launch day Korean IOSes obtained from https://hackmii.com/2008/09/korean-wii/. - // More IOSes were released later that were used in Korean games, but they're all over 40. - // Also, old IOSes like IOS36 did eventually get released for Korean Wiis as part of system - // updates, but there are likely no Korean games using them since those IOSes were old by then. - if (region == Region::NTSC_K && ios_id < 40 && ios_id != 4 && ios_id != 9 && ios_id != 21 && - ios_id != 37) - { - // This is intended to catch Korean games (usually but not always pirated) that have the IOS - // slot set to 36 as a side effect of having to fakesign after changing the common key slot to - // 0. (IOS36 was the last IOS with the Trucha bug.) https://bugs.dolphin-emu.org/issues/10319 - AddProblem( - Severity::High, - // i18n: You may want to leave the term "ERROR #002" untranslated, - // since the emulated software always displays it in English. - Common::GetStringT("This Korean title is set to use an IOS that typically isn't used on " - "Korean consoles. This is likely to lead to ERROR #002.")); - } - - if (ios_id >= 0x80) - { - // This is intended to catch the same kind of fakesigned Korean games, - // but this time with the IOS slot set to cIOS instead of IOS36. - AddProblem(Severity::High, Common::GetStringT("This title is set to use an invalid IOS.")); - } - } - - m_ticket = m_volume.GetTicket(m_volume.GetGamePartition()); - if (m_ticket.IsValid()) - { - const u8 specified_common_key_index = m_ticket.GetCommonKeyIndex(); - - // Wii discs only use common key 0 (regular) and common key 1 (Korean), not common key 2 (vWii). - if (m_volume.GetVolumeType() == Platform::WiiDisc && specified_common_key_index > 1) - { - AddProblem(Severity::High, - // i18n: This is "common" as in "shared", not the opposite of "uncommon" - Common::GetStringT("This title is set to use an invalid common key.")); - } - - if (m_volume.GetVolumeType() == Platform::WiiWAD) - { - m_ticket = m_volume.GetTicketWithFixedCommonKey(); - const u8 fixed_common_key_index = m_ticket.GetCommonKeyIndex(); - if (specified_common_key_index != fixed_common_key_index) - { - // Many fakesigned WADs have the common key index set to a (random?) bogus value. - // For WADs, Dolphin will detect this and use the correct key, making this low severity. - AddProblem(Severity::Low, - // i18n: This is "common" as in "shared", not the opposite of "uncommon" - Common::FmtFormatT("The specified common key index is {0} but should be {1}.", - specified_common_key_index, fixed_common_key_index)); - } - } - } - - if (m_volume.GetVolumeType() == Platform::WiiWAD) - { - IOS::HLE::Kernel ios(m_ticket.GetConsoleType()); - auto& es = ios.GetESCore(); - const std::vector& cert_chain = m_volume.GetCertificateChain(PARTITION_NONE); - - if (!m_ticket.IsValid()) - { - // i18n: "Ticket" here is a kind of digital authorization to use a certain title (e.g. a game) - AddProblem(Severity::High, Common::GetStringT("The ticket is invalid.")); - } - else if (IOS::HLE::IPC_SUCCESS != - es.VerifyContainer(IOS::HLE::ESCore::VerifyContainerType::Ticket, - IOS::HLE::ESCore::VerifyMode::DoNotUpdateCertStore, m_ticket, - cert_chain)) - { - // i18n: "Ticket" here is a kind of digital authorization to use a certain title (e.g. a game) - AddProblem(Severity::Low, Common::GetStringT("The ticket is not correctly signed.")); - } - - if (!tmd.IsValid()) - { - AddProblem(Severity::High, Common::GetStringT("The TMD is invalid.")); - } - else if (IOS::HLE::IPC_SUCCESS != - es.VerifyContainer(IOS::HLE::ESCore::VerifyContainerType::TMD, - IOS::HLE::ESCore::VerifyMode::DoNotUpdateCertStore, tmd, - cert_chain)) - { - AddProblem( - Severity::Medium, - Common::GetStringT("The TMD is not correctly signed. If you move or copy this title to " - "the SD Card, the Wii System Menu will not launch it anymore and will " - "also refuse to copy or move it back to the NAND.")); - } - } - - if (m_volume.IsNKit()) - { - AddProblem( - Severity::Low, - Common::GetStringT("This disc image is in the NKit format. It is not a good dump in its " - "current form, but it might become a good dump if converted back. " - "The CRC32 of this file might match the CRC32 of a good dump even " - "though the files are not identical.")); - } - - if (IsDisc(m_volume.GetVolumeType()) && game_id_unencrypted.starts_with("R8P")) - CheckSuperPaperMario(); -} - -void VolumeVerifier::CheckSuperPaperMario() -{ - // When Super Paper Mario (any region/revision) reads setup/aa1_01.dat when starting a new game, - // it also reads a few extra bytes so that the read length is divisible by 0x20. If these extra - // bytes are zeroes like in good dumps, the game works correctly, but otherwise it can freeze - // (depending on the exact values of the extra bytes). https://bugs.dolphin-emu.org/issues/11900 - - const Partition partition = m_volume.GetGamePartition(); - const FileSystem* fs = m_volume.GetFileSystem(partition); - if (!fs) - return; - - std::unique_ptr file_info = fs->FindFileInfo("setup/aa1_01.dat"); - if (!file_info) - return; - - const u64 offset = file_info->GetOffset() + file_info->GetSize(); - const u64 length = Common::AlignUp(offset, 0x20) - offset; - std::vector data(length); - if (!m_volume.Read(offset, length, data.data(), partition)) - return; - - if (std::ranges::any_of(data, [](u8 x) { return x != 0; })) - { - AddProblem(Severity::High, - Common::GetStringT("Some padding data that should be zero is not zero. " - "This can make the game freeze at certain points.")); - } -} - -void VolumeVerifier::SetUpHashing() -{ - if (m_volume.GetVolumeType() == Platform::WiiWAD) - { - m_content_offsets = m_volume.GetContentOffsets(); - } - else if (m_volume.GetVolumeType() == Platform::WiiDisc) - { - // Set up a DiscScrubber for checking whether blocks with errors are unused - m_scrubber.SetupScrub(m_volume); - } - - std::ranges::sort(m_groups, {}, &GroupToVerify::offset); - - if (m_hashes_to_calculate.crc32) - m_crc32_context = Common::StartCRC32(); - - if (m_hashes_to_calculate.md5) - { - mbedtls_md5_init(&m_md5_context); - mbedtls_md5_starts_ret(&m_md5_context); - } - - if (m_hashes_to_calculate.sha1) - { - m_sha1_context = Common::SHA1::CreateContext(); - } -} - -void VolumeVerifier::WaitForAsyncOperations() -{ - m_async_worker.WaitForCompletion(); -} - -void VolumeVerifier::RunAsyncTask(void* ctx, size_t index) -{ - (*static_cast>*>(ctx))[index](); -} - -bool VolumeVerifier::ReadChunkAndWaitForAsyncOperations(u64 bytes_to_read) -{ - std::vector data(bytes_to_read); - - const u64 bytes_to_copy = std::min(m_excess_bytes, bytes_to_read); - if (bytes_to_copy > 0) - std::memcpy(data.data(), m_data.data() + m_data.size() - m_excess_bytes, bytes_to_copy); - bytes_to_read -= bytes_to_copy; - - if (bytes_to_read > 0) - { - if (!m_volume.Read(m_progress + bytes_to_copy, bytes_to_read, data.data() + bytes_to_copy, - PARTITION_NONE)) - { - return false; - } - } - - WaitForAsyncOperations(); - m_data = std::move(data); - return true; -} - -void VolumeVerifier::Process() -{ - ASSERT(m_started); - ASSERT(!m_done); - - if (m_progress >= m_max_progress) - return; - - IOS::ES::Content content{}; - bool content_read = false; - bool group_read = false; - u64 bytes_to_read = DEFAULT_READ_SIZE; - u64 excess_bytes = 0; - if (m_content_index < m_content_offsets.size() && - m_content_offsets[m_content_index] == m_progress) - { - m_volume.GetTMD(PARTITION_NONE).GetContent(m_content_index, &content); - bytes_to_read = Common::AlignUp(content.size, 0x40); - content_read = true; - - const u16 next_content_index = m_content_index + 1; - if (next_content_index < m_content_offsets.size() && - m_content_offsets[next_content_index] < m_progress + bytes_to_read) - { - excess_bytes = m_progress + bytes_to_read - m_content_offsets[next_content_index]; - } - } - else if (m_content_index < m_content_offsets.size() && - m_content_offsets[m_content_index] > m_progress) - { - bytes_to_read = std::min(bytes_to_read, m_content_offsets[m_content_index] - m_progress); - } - else if (m_group_index < m_groups.size() && m_groups[m_group_index].offset == m_progress) - { - const size_t blocks = - m_groups[m_group_index].block_index_end - m_groups[m_group_index].block_index_start; - bytes_to_read = VolumeWii::BLOCK_TOTAL_SIZE * blocks; - group_read = true; - - if (m_group_index + 1 < m_groups.size() && - m_groups[m_group_index + 1].offset < m_progress + bytes_to_read) - { - excess_bytes = m_progress + bytes_to_read - m_groups[m_group_index + 1].offset; - } - } - else if (m_group_index < m_groups.size() && m_groups[m_group_index].offset > m_progress) - { - bytes_to_read = std::min(bytes_to_read, m_groups[m_group_index].offset - m_progress); - } - - if (m_progress + bytes_to_read > m_max_progress) - { - const u64 bytes_over_max = m_progress + bytes_to_read - m_max_progress; - - if (m_data_size_type == DataSizeType::LowerBound) - { - // Disc images in NFS format can have the last referenced block be past m_max_progress. - // For NFS, reading beyond m_max_progress doesn't return an error, so let's read beyond it. - excess_bytes = std::max(excess_bytes, bytes_over_max); - } - else - { - // Don't read beyond the end of the disc. - bytes_to_read -= bytes_over_max; - excess_bytes -= std::min(excess_bytes, bytes_over_max); - content_read = false; - group_read = false; - } - } - - const bool is_data_needed = m_calculating_any_hash || content_read || group_read; - const bool read_failed = is_data_needed && !ReadChunkAndWaitForAsyncOperations(bytes_to_read); - - if (read_failed) - { - ERROR_LOG_FMT(DISCIO, "Read failed at {:#x} to {:#x}", m_progress, m_progress + bytes_to_read); - - m_read_errors_occurred = true; - m_calculating_any_hash = false; - } - - m_excess_bytes = excess_bytes; - const u64 byte_increment = bytes_to_read - excess_bytes; - - // The tasks for this chunk run on the async worker, spread over the parallel_for pool, while - // the caller reads the next chunk. The previous chunk's tasks must be done before reuse. - WaitForAsyncOperations(); - m_async_tasks.clear(); - - if (m_calculating_any_hash) - { - if (m_hashes_to_calculate.crc32) - { - m_async_tasks.emplace_back([this, byte_increment] { - m_crc32_context = Common::UpdateCRC32(m_crc32_context, m_data.data(), - static_cast(byte_increment)); - }); - } - - if (m_hashes_to_calculate.md5) - { - m_async_tasks.emplace_back([this, byte_increment] { - mbedtls_md5_update_ret(&m_md5_context, m_data.data(), byte_increment); - }); - } - - if (m_hashes_to_calculate.sha1) - { - m_async_tasks.emplace_back([this, byte_increment] { - m_sha1_context->Update(m_data.data(), byte_increment); - }); - } - } - - if (content_read) - { - m_async_tasks.emplace_back([this, read_failed, content] { - if (read_failed || !m_volume.CheckContentIntegrity(content, m_data, m_ticket)) - { - AddProblem(Severity::High, Common::FmtFormatT("Content {0:08x} is corrupt.", content.id)); - } - }); - - m_content_index++; - } - - if (group_read) - { - m_async_tasks.emplace_back([this, read_failed, group_index = m_group_index] { - const GroupToVerify& group = m_groups[group_index]; - u64 offset_in_group = 0; - for (u64 block_index = group.block_index_start; block_index < group.block_index_end; - ++block_index, offset_in_group += VolumeWii::BLOCK_TOTAL_SIZE) - { - const u64 block_offset = group.offset + offset_in_group; - - if (!read_failed && m_volume.CheckBlockIntegrity( - block_index, m_data.data() + offset_in_group, group.partition)) - { - m_biggest_verified_offset = - std::max(m_biggest_verified_offset, block_offset + VolumeWii::BLOCK_TOTAL_SIZE); - } - else - { - if (m_scrubber.CanBlockBeScrubbed(block_offset)) - { - WARN_LOG_FMT(DISCIO, "Integrity check failed for unused block at {:#x}", block_offset); - m_unused_block_errors[group.partition]++; - } - else - { - WARN_LOG_FMT(DISCIO, "Integrity check failed for block at {:#x}", block_offset); - m_block_errors[group.partition]++; - } - } - } - }); - - m_group_index++; - } - - if (!m_async_tasks.empty()) - { - m_async_worker.Push( - [this] { parallel_for(m_async_tasks.size(), RunAsyncTask, &m_async_tasks); }); - } - - m_progress += byte_increment; -} - -u64 VolumeVerifier::GetBytesProcessed() const -{ - return m_progress; -} - -u64 VolumeVerifier::GetTotalBytes() const -{ - return m_max_progress; -} - -void VolumeVerifier::Finish() -{ - if (m_done) - return; - m_done = true; - - WaitForAsyncOperations(); - - if (m_calculating_any_hash) - { - if (m_hashes_to_calculate.crc32) - { - m_result.hashes.crc32 = std::vector(4); - const u32 crc32_be = Common::swap32(m_crc32_context); - std::memcpy(m_result.hashes.crc32.data(), &crc32_be, 4); - } - - if (m_hashes_to_calculate.md5) - { - m_result.hashes.md5 = std::vector(16); - mbedtls_md5_finish_ret(&m_md5_context, m_result.hashes.md5.data()); - } - - if (m_hashes_to_calculate.sha1) - { - const auto digest = m_sha1_context->Finish(); - m_result.hashes.sha1 = std::vector(digest.begin(), digest.end()); - } - } - - if (m_read_errors_occurred) - AddProblem(Severity::Medium, Common::GetStringT("Some of the data could not be read.")); - - CheckVolumeSize(); - - for (auto [partition, blocks] : m_block_errors) - { - if (blocks > 0) - { - const std::string name = GetPartitionName(m_volume.GetPartitionType(partition)); - AddProblem(Severity::Medium, - Common::FmtFormatT("Errors were found in {0} blocks in the {1} partition.", blocks, - name)); - } - } - - for (auto [partition, blocks] : m_unused_block_errors) - { - if (blocks > 0) - { - const std::string name = GetPartitionName(m_volume.GetPartitionType(partition)); - AddProblem(Severity::Low, - Common::FmtFormatT("Errors were found in {0} unused blocks in the {1} partition.", - blocks, name)); - } - } - - // Show the most serious problems at the top - std::ranges::stable_sort(m_result.problems, std::ranges::greater{}, &Problem::severity); - const Severity highest_severity = - m_result.problems.empty() ? Severity::None : m_result.problems[0].severity; - - if (m_redump_verification) - m_result.redump = m_redump_verifier.Finish(m_result.hashes); - - if (m_result.redump.status == RedumpVerifier::Status::GoodDump || - (m_volume.GetVolumeType() == Platform::WiiWAD && !m_is_not_retail && - m_result.problems.empty())) - { - if (m_result.problems.empty()) - { - m_result.summary_text = Common::GetStringT("This is a good dump."); - } - else - { - m_result.summary_text = - Common::GetStringT("This is a good dump according to Redump.info, but Dolphin has found " - "problems. This might be a bug in Dolphin."); - } - return; - } - - if (m_is_datel) - { - m_result.summary_text = Common::GetStringT("Dolphin is unable to verify unlicensed discs."); - return; - } - - if (m_is_tgc) - { - m_result.summary_text = - Common::GetStringT("Dolphin is unable to verify typical TGC files properly, " - "since they are not dumps of actual discs."); - return; - } - - if (m_is_triforce) - { - m_result.summary_text = - Common::GetStringT("Dolphin is currently unable to verify Triforce games."); - return; - } - - if (m_result.redump.status == RedumpVerifier::Status::BadDump && - highest_severity <= Severity::Low) - { - if (m_volume.GetBlobType() == BlobType::NFS) - { - m_result.summary_text = - Common::GetStringT("Compared to the Wii disc release of the game, this is a bad dump. " - "Despite this, it's possible that this is a good dump compared to the " - "Wii U eShop release of the game. Dolphin can't verify this."); - } - else - { - m_result.summary_text = Common::GetStringT( - "This is a bad dump. This doesn't necessarily mean that the game won't run correctly."); - } - } - else - { - if (m_result.redump.status == RedumpVerifier::Status::BadDump) - { - m_result.summary_text = Common::GetStringT("This is a bad dump.") + "\n\n"; - } - - switch (highest_severity) - { - case Severity::None: - if (IsWii(m_volume.GetVolumeType()) && !m_is_not_retail) - { - m_result.summary_text = Common::GetStringT( - "No problems were found. This does not guarantee that this is a good dump, " - "but since Wii titles contain a lot of verification data, it does mean that " - "there most likely are no problems that will affect emulation."); - } - else - { - m_result.summary_text = Common::GetStringT("No problems were found."); - } - break; - case Severity::Low: - if (m_volume.GetBlobType() == BlobType::NFS) - { - m_result.summary_text = Common::GetStringT( - "Compared to the Wii disc release of the game, problems of low severity were found. " - "Despite this, it's possible that this is a good dump compared to the Wii U eShop " - "release of the game. Dolphin can't verify this."); - } - else - { - m_result.summary_text = - Common::GetStringT("Problems with low severity were found. They will most " - "likely not prevent the game from running."); - } - break; - case Severity::Medium: - m_result.summary_text += - Common::GetStringT("Problems with medium severity were found. The whole game " - "or certain parts of the game might not work correctly."); - break; - case Severity::High: - m_result.summary_text += Common::GetStringT( - "Problems with high severity were found. The game will most likely not work at all."); - break; - } - } - - if (m_volume.GetVolumeType() == Platform::GameCubeDisc) - { - m_result.summary_text += - Common::GetStringT("\n\nBecause GameCube disc images contain little verification data, " - "there may be problems that Dolphin is unable to detect."); - } - else if (m_is_not_retail) - { - m_result.summary_text += - Common::GetStringT("\n\nBecause this title is not for retail Wii consoles, " - "Dolphin cannot ensure that it hasn't been tampered with, even if " - "signatures appear valid."); - } -} - -const VolumeVerifier::Result& VolumeVerifier::GetResult() const -{ - return m_result; -} - -void VolumeVerifier::AddProblem(Severity severity, std::string text) -{ - m_result.problems.emplace_back(Problem{severity, std::move(text)}); -} - -} // namespace DiscIO diff --git a/Source/Core/DiscIO/VolumeVerifier.h b/Source/Core/DiscIO/VolumeVerifier.h deleted file mode 100644 index f80836317d..0000000000 --- a/Source/Core/DiscIO/VolumeVerifier.h +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright 2019 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - -#include "Common/CommonTypes.h" -#include "Common/Crypto/SHA1.h" -#include "Common/WorkQueueThread.h" -#include "Core/IOS/ES/Formats.h" -#include "DiscIO/DiscScrubber.h" -#include "DiscIO/Volume.h" - -// To be used as follows: -// -// VolumeVerifier verifier(volume, redump_verification, hashes_to_calculate); -// verifier.Start(); -// while (verifier.GetBytesProcessed() != verifier.GetTotalBytes()) -// verifier.Process(); -// verifier.Finish(); -// auto result = verifier.GetResult(); -// -// Start, Process and Finish may take some time to run. -// -// GetResult() can be called before the processing is finished, but the result will be incomplete. - -namespace DiscIO -{ -template -struct Hashes -{ - T crc32; - T md5; - T sha1; -}; - -class RedumpVerifier final -{ -public: - enum class Status - { - Unknown, - GoodDump, - BadDump, - Error, - }; - - struct Result - { - Status status = Status::Unknown; - std::string message; - }; - - RedumpVerifier() = default; - ~RedumpVerifier(); - RedumpVerifier(const RedumpVerifier&) = delete; - RedumpVerifier& operator=(const RedumpVerifier&) = delete; - - void Start(const Volume& volume); - Result Finish(const Hashes>& hashes); - -private: - enum class DownloadStatus - { - NotAttempted, - Success, - Fail, - FailButOldCacheAvailable, - }; - - struct DownloadState - { - std::mutex mutex; - DownloadStatus status = DownloadStatus::NotAttempted; - }; - - struct PotentialMatch - { - u64 size = 0; - Hashes> hashes; - }; - - static DownloadStatus DownloadDatfile(const std::string& system, DownloadStatus old_status); - static std::vector ReadDatfile(const std::string& system); - std::vector ScanDatfile(std::span data, const std::string& system); - - std::string m_game_id; - u16 m_revision = 0; - u8 m_disc_number = 0; - u64 m_size = 0; - - void MatchThread(); - std::vector FindPotentialMatches(Platform platform); - - // Looks the disc up in the Redump datfile while the disc is hashed. - sthread_t* m_thread = nullptr; - Platform m_platform{}; - std::vector m_potential_matches; - Result m_result; - - static DownloadState m_gc_download_state; - static DownloadState m_wii_download_state; -}; - -class VolumeVerifier final -{ -public: - enum class Severity - { - None, // Only used internally - Low, - Medium, - High, - }; - - struct Problem - { - Severity severity; - std::string text; - }; - - struct Result - { - Hashes> hashes; - std::string summary_text; - std::vector problems; - RedumpVerifier::Result redump; - }; - - VolumeVerifier(const Volume& volume, bool redump_verification, Hashes hashes_to_calculate); - ~VolumeVerifier(); - - static Hashes GetDefaultHashesToCalculate(); - void Start(); - void Process(); - u64 GetBytesProcessed() const; - u64 GetTotalBytes() const; - void Finish(); - const Result& GetResult() const; - -private: - struct GroupToVerify - { - Partition partition; - u64 offset; - size_t block_index_start; - size_t block_index_end; - }; - - std::vector CheckPartitions(); - bool CheckPartition(const Partition& partition); // Returns false if partition should be ignored - std::string GetPartitionName(std::optional type) const; - bool IsDebugSigned() const; - bool ShouldHaveChannelPartition() const; - bool ShouldHaveInstallPartition() const; - bool ShouldHaveMasterpiecePartitions() const; - bool ShouldBeDualLayer() const; - void CheckVolumeSize(); - void CheckMisc(); - void CheckSuperPaperMario(); - void SetUpHashing(); - void WaitForAsyncOperations(); - static void RunAsyncTask(void* ctx, size_t index); - bool ReadChunkAndWaitForAsyncOperations(u64 bytes_to_read); - - void AddProblem(Severity severity, std::string text); - - const Volume& m_volume; - Result m_result; - bool m_is_tgc = false; - bool m_is_datel = false; - bool m_is_triforce = false; - bool m_is_not_retail = false; - - bool m_redump_verification; - RedumpVerifier m_redump_verifier; - - bool m_read_errors_occurred = false; - - Hashes m_hashes_to_calculate{}; - bool m_calculating_any_hash = false; - u32 m_crc32_context = 0; - mbedtls_md5_context m_md5_context{}; - std::unique_ptr m_sha1_context; - - u64 m_excess_bytes = 0; - std::vector m_data; - std::vector> m_async_tasks; - - DiscScrubber m_scrubber; - IOS::ES::TicketReader m_ticket; - std::vector m_content_offsets; - u16 m_content_index = 0; - std::vector m_groups; - size_t m_group_index = 0; // Index in m_groups, not index in a specific partition - std::map m_block_errors; - std::map m_unused_block_errors; - - u64 m_biggest_referenced_offset = 0; - u64 m_biggest_verified_offset = 0; - - bool m_started = false; - bool m_done = false; - u64 m_progress = 0; - u64 m_max_progress = 0; - DataSizeType m_data_size_type; - - // Declared last so it stops before anything its tasks touch is destroyed. - Common::AsyncWorkThreadSP m_async_worker; -}; - -} // namespace DiscIO diff --git a/Source/Core/DolphinLibretro/Common/Globals.cpp b/Source/Core/DolphinLibretro/Common/Globals.cpp index f42a07dda1..38c745c454 100644 --- a/Source/Core/DolphinLibretro/Common/Globals.cpp +++ b/Source/Core/DolphinLibretro/Common/Globals.cpp @@ -12,7 +12,8 @@ namespace Input retro_microphone_interface g_microphone_interface{}; bool g_has_microphone_support = false; bool g_gc_mic_button[4]; -std::vector g_active_microphones{}; +retro_atomic_ptr_t g_active_microphones[MAX_ACTIVE_MICROPHONES]; +Common::CallGate g_microphone_poll_gate; } // namespace Input namespace Video diff --git a/Source/Core/DolphinLibretro/Common/Globals.h b/Source/Core/DolphinLibretro/Common/Globals.h index a8a6e265fb..e035a5f2af 100644 --- a/Source/Core/DolphinLibretro/Common/Globals.h +++ b/Source/Core/DolphinLibretro/Common/Globals.h @@ -3,6 +3,10 @@ #include #include +#include + +#include "Common/CallGate.h" + #include "Core/ActionReplay.h" #include "Core/GeckoCode.h" #include "Core/IOS/USB/Emulated/Microphone.h" @@ -20,7 +24,11 @@ namespace Input extern retro_microphone_interface g_microphone_interface; extern bool g_has_microphone_support; extern bool g_gc_mic_button[4]; -extern std::vector g_active_microphones; +// Open emulated USB microphones, polled by the frontend thread inside +// g_microphone_poll_gate; the emulation thread adds and removes them. +constexpr int MAX_ACTIVE_MICROPHONES = 4; +extern retro_atomic_ptr_t g_active_microphones[MAX_ACTIVE_MICROPHONES]; +extern Common::CallGate g_microphone_poll_gate; } // namespace Input namespace Video diff --git a/Source/Core/DolphinLibretro/Input.cpp b/Source/Core/DolphinLibretro/Input.cpp index 4ced7bf525..0928cf44d7 100644 --- a/Source/Core/DolphinLibretro/Input.cpp +++ b/Source/Core/DolphinLibretro/Input.cpp @@ -1967,8 +1967,15 @@ void poll_microphone() if (system.IsWii() && (s_wii_speak_enabled || s_logi_microphone_enabled)) { - for (auto* mic : Libretro::Input::g_active_microphones) - mic->PollRetroArchMic(); + if (Common::CallGateEnter(&Libretro::Input::g_microphone_poll_gate)) + { + for (auto& slot : Libretro::Input::g_active_microphones) + { + if (auto* mic = static_cast(retro_atomic_load_acquire_ptr(&slot))) + mic->PollRetroArchMic(); + } + Common::CallGateExit(&Libretro::Input::g_microphone_poll_gate); + } } else if(s_gc_mic_enabled) { diff --git a/Source/Core/InputCommon/ControlReference/ControlReference.cpp b/Source/Core/InputCommon/ControlReference/ControlReference.cpp index 795204cc05..b9c7f54966 100644 --- a/Source/Core/InputCommon/ControlReference/ControlReference.cpp +++ b/Source/Core/InputCommon/ControlReference/ControlReference.cpp @@ -3,6 +3,10 @@ #include "InputCommon/ControlReference/ControlReference.h" +#include + +#include "Common/CommonTypes.h" + using namespace ciface::ExpressionParser; static thread_local bool tls_input_gate = true; @@ -23,44 +27,101 @@ bool ControlReference::GetInputGate() // Updates a controlreference's binded devices/controls // need to call this to re-bind a control reference after changing its expression // +void ControlReference::FreeBinding(quiesce_node_t* node) +{ + delete reinterpret_cast(node); +} + +void ControlReference::Publish(Binding* binding) +{ + retro_atomic_store_release_int(&m_has_expression, !binding->expression.empty()); + if (void* old = retro_atomic_exchange_ptr(&m_binding, binding)) + quiesce_retire(ciface::InputQuiesce(), &static_cast(old)->node, FreeBinding); +} + void ControlReference::UpdateReference(ciface::ExpressionParser::ControlEnvironment& env) { - if (m_parsed_expression) + ciface::InputReadScope scope; + for (;;) { - m_parsed_expression->UpdateReferences(env); + Binding* const cur = PeekBinding(); + if (!cur || !cur->parsed) + return; + + // A fresh parse to bind, leaving the published one untouched for its readers. + auto parse_result = ParseExpression(cur->expression); + auto* next = new Binding{{}, cur->expression, parse_result.status, std::move(parse_result.expr)}; + if (next->parsed) + next->parsed->UpdateReferences(env); + + // Lost to a SetExpression or another rebind meanwhile: bind what is there now. + if (retro_atomic_cas_ptr(&m_binding, cur, next)) + { + quiesce_retire(ciface::InputQuiesce(), &cur->node, FreeBinding); + return; + } + delete next; } } int ControlReference::BoundCount() const { - if (m_parsed_expression) - return m_parsed_expression->CountNumControls(); - else - return 0; + ciface::InputReadScope scope; + const Binding* const b = PeekBinding(); + return b && b->parsed ? b->parsed->CountNumControls() : 0; } ParseStatus ControlReference::GetParseStatus() const { - return m_parse_status; + ciface::InputReadScope scope; + const Binding* const b = PeekBinding(); + return b ? b->status : ParseStatus::EmptyExpression; } std::string ControlReference::GetExpression() const { - return m_expression; + ciface::InputReadScope scope; + const Binding* const b = PeekBinding(); + return b ? b->expression : std::string{}; +} + +bool ControlReference::HasExpression() const +{ + return retro_atomic_load_acquire_int(&m_has_expression) != 0; } std::optional ControlReference::SetExpression(std::string expr) { - m_expression = std::move(expr); - auto parse_result = ParseExpression(m_expression); - m_parse_status = parse_result.status; - m_parsed_expression = std::move(parse_result.expr); + auto parse_result = ParseExpression(expr); + Publish(new Binding{{}, std::move(expr), parse_result.status, std::move(parse_result.expr)}); return parse_result.description; } -ControlReference::ControlReference() = default; +ControlState ControlReference::GetRange() const +{ + const u64 bits = static_cast(retro_atomic_load_acquire_64(&m_range_bits)); + ControlState range; + std::memcpy(&range, &bits, sizeof(range)); + return range; +} + +void ControlReference::SetRange(ControlState range) +{ + u64 bits; + std::memcpy(&bits, &range, sizeof(bits)); + retro_atomic_store_release_64(&m_range_bits, static_cast(bits)); +} + +ControlReference::ControlReference() +{ + SetRange(1.0); +} + +ControlReference::~ControlReference() +{ + delete static_cast(retro_atomic_exchange_ptr(&m_binding, nullptr)); +} -ControlReference::~ControlReference() = default; InputReference::InputReference() : ControlReference() { @@ -87,9 +148,11 @@ bool OutputReference::IsInput() const // ControlState InputReference::State(const ControlState ignore) { - if (m_parsed_expression && GetInputGate()) - return m_parsed_expression->GetValue() * range; - return 0.0; + if (!GetInputGate()) + return 0.0; + ciface::InputReadScope scope; + const Binding* const b = PeekBinding(); + return b && b->parsed ? b->parsed->GetValue() * GetRange() : 0.0; } // @@ -102,7 +165,9 @@ ControlState InputReference::State(const ControlState ignore) // ControlState OutputReference::State(const ControlState state) { - if (m_parsed_expression) - m_parsed_expression->SetValue(state * range); + ciface::InputReadScope scope; + const Binding* const b = PeekBinding(); + if (b && b->parsed) + b->parsed->SetValue(state * GetRange()); return 0.0; } diff --git a/Source/Core/InputCommon/ControlReference/ControlReference.h b/Source/Core/InputCommon/ControlReference/ControlReference.h index 984ab2cd0f..d8bb50cce8 100644 --- a/Source/Core/InputCommon/ControlReference/ControlReference.h +++ b/Source/Core/InputCommon/ControlReference/ControlReference.h @@ -6,6 +6,9 @@ #include #include +#include + +#include "Common/Quiesce.h" #include "InputCommon/ControlReference/ExpressionParser.h" #include "InputCommon/ControllerInterface/CoreDevice.h" @@ -44,20 +47,44 @@ public: int BoundCount() const; ciface::ExpressionParser::ParseStatus GetParseStatus() const; + // Binds the expression to the devices in env. Builds a new binding and swaps it in, so a + // thread reading the old one is never disturbed. void UpdateReference(ciface::ExpressionParser::ControlEnvironment& env); std::string GetExpression() const; + // Whether there is an expression at all, without copying it. + bool HasExpression() const; // Returns a human-readable error description when the given expression is invalid. std::optional SetExpression(std::string expr); - ControlState range = 1; + ControlState GetRange() const; + void SetRange(ControlState range); protected: ControlReference(); - std::string m_expression; - std::unique_ptr m_parsed_expression; - ciface::ExpressionParser::ParseStatus m_parse_status = - ciface::ExpressionParser::ParseStatus::EmptyExpression; + ControlReference(const ControlReference&) = delete; + ControlReference& operator=(const ControlReference&) = delete; + + // An expression with what it was parsed into. Never changed once published. + struct Binding + { + quiesce_node_t node; // first: the free callback casts it back + std::string expression; + ciface::ExpressionParser::ParseStatus status; + std::unique_ptr parsed; + }; + static void FreeBinding(quiesce_node_t* node); + // Only inside an InputReadScope. + Binding* PeekBinding() const + { + return static_cast(retro_atomic_load_acquire_ptr(&m_binding)); + } + void Publish(Binding* binding); + + mutable retro_atomic_ptr_t m_binding{}; + retro_atomic_int_t m_has_expression{}; + // The range as the bits of a double, 1.0 until set. + retro_atomic_64_t m_range_bits; }; namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Attachments.cpp b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Attachments.cpp index 27304907c5..1bcbbb7e26 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Attachments.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Attachments.cpp @@ -40,7 +40,7 @@ NumericSetting& Attachments::GetSelectionSetting() return m_selection_setting; } -SubscribableSettingValue& Attachments::GetAttachmentSetting() +SettingValue& Attachments::GetAttachmentSetting() { return m_selection_value; } diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Attachments.h b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Attachments.h index 1573b01bca..67138fbb41 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Attachments.h +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Attachments.h @@ -33,7 +33,7 @@ public: void SetSelectedAttachment(u32 val); NumericSetting& GetSelectionSetting(); - SubscribableSettingValue& GetAttachmentSetting(); + SettingValue& GetAttachmentSetting(); const std::vector>& GetAttachmentList() const; @@ -43,7 +43,7 @@ public: void UpdateReferences(ciface::ExpressionParser::ControlEnvironment& env) override; private: - SubscribableSettingValue m_selection_value; + SettingValue m_selection_value; // This is here and not added to the list of numeric_settings because it's serialized differently, // by string (to be independent from the enum), and visualized differently in the UI. // For the rest, it's treated similarly to other numeric_settings in the group. diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/ControlGroup.cpp b/Source/Core/InputCommon/ControllerEmu/ControlGroup/ControlGroup.cpp index b619ae1b08..2673b55dd6 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/ControlGroup.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/ControlGroup.cpp @@ -76,8 +76,9 @@ void ControlGroup::LoadConfig(Common::IniFile::Section* sec, const std::string& } // range - sec->Get(group + c->name + "/Range", &c->control_ref->range, 100.0); - c->control_ref->range /= 100; + double range = 100.0; + sec->Get(group + c->name + "/Range", &range, 100.0); + c->control_ref->SetRange(range / 100); } } @@ -101,7 +102,7 @@ void ControlGroup::SaveConfig(Common::IniFile::Section* sec, const std::string& sec->Set(group + c->name, expression, ""); // range - sec->Set(group + c->name + "/Range", c->control_ref->range * 100.0, 100.0); + sec->Set(group + c->name + "/Range", c->control_ref->GetRange() * 100.0, 100.0); } } diff --git a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp index ce0105b90f..eb438ddfb8 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp @@ -4,7 +4,6 @@ #include "InputCommon/ControllerEmu/ControllerEmu.h" #include -#include #include #include @@ -18,31 +17,52 @@ namespace ControllerEmu { // This should theoretically be per EmulatedController instance, // though no EmulatedController usually run in parallel, so it makes little difference -static std::recursive_mutex s_get_state_mutex; std::string ControlGroupContainer::GetDisplayName() const { return GetName(); } -EmulatedController::~EmulatedController() = default; - -// This should be called before calling GetState() or State() on a control reference -// to prevent a race condition. -// This is a recursive mutex because UpdateReferences is recursive. -std::unique_lock EmulatedController::GetStateLock() +EmulatedController::~EmulatedController() { - std::unique_lock lock(s_get_state_mutex); - return lock; + delete static_cast(retro_atomic_exchange_ptr(&m_default_device, nullptr)); +} + +EmulatedController::EmulatedController() = default; + +void EmulatedController::FreeDefaultDevice(quiesce_node_t* node) +{ + delete reinterpret_cast(node); } void EmulatedController::UpdateReferences(const ControllerInterface& devi) { - std::scoped_lock lk(s_get_state_mutex, devi.GetDevicesMutex()); + retro_atomic_store_release_int(&m_rebind_requested, 1); + // Pairs with the fence below: either this thread finds the role free, or the thread that + // held it sees the request. + retro_atomic_thread_fence_seq_cst(); + for (;;) + { + // Someone else is rebinding: they see the request before they stop. + if (!retro_atomic_cas_int(&m_rebinding, 0, 1)) + return; + while (retro_atomic_exchange_int(&m_rebind_requested, 0)) + RebindNow(devi); + retro_atomic_store_release_int(&m_rebinding, 0); + // A request that came in after the last check, while this thread still held the role. + retro_atomic_thread_fence_seq_cst(); + if (!retro_atomic_load_seq_cst_int(&m_rebind_requested)) + return; + } +} - m_default_device_is_connected = devi.HasConnectedDevice(m_default_device); +void EmulatedController::RebindNow(const ControllerInterface& devi) +{ + const ciface::Core::DeviceQualifier default_device = GetDefaultDevice(); + retro_atomic_store_release_int(&m_default_device_is_connected, + devi.HasConnectedDevice(default_device)); - ciface::ExpressionParser::ControlEnvironment env(devi, GetDefaultDevice(), m_expression_vars); + ciface::ExpressionParser::ControlEnvironment env(devi, default_device, m_expression_vars); UpdateGroupsReferences(env); @@ -52,48 +72,28 @@ void EmulatedController::UpdateReferences(const ControllerInterface& devi) void ControlGroupContainer::UpdateGroupsReferences( ciface::ExpressionParser::ControlEnvironment& env) { - const auto lock = EmulatedController::GetStateLock(); - for (auto& group : groups) group->UpdateReferences(env); } void EmulatedController::UpdateSingleControlReference(const ControllerInterface& devi, - ControlReference* ref) + ControlReference*) { - ciface::ExpressionParser::ControlEnvironment env(devi, GetDefaultDevice(), m_expression_vars); - - const auto lock = GetStateLock(); - ref->UpdateReference(env); - - env.CleanUnusedVariables(); -} - -const ciface::ExpressionParser::ControlEnvironment::VariableContainer& -EmulatedController::GetExpressionVariables() const -{ - return m_expression_vars; -} - -void EmulatedController::ResetExpressionVariables() -{ - for (auto& var : m_expression_vars) - { - if (var.second) - { - *var.second = 0; - } - } + // The environment's variables belong to the thread doing a rebind, so this is one. + UpdateReferences(devi); } bool EmulatedController::IsDefaultDeviceConnected() const { - return m_default_device_is_connected; + return retro_atomic_load_acquire_int( + const_cast(&m_default_device_is_connected)) != 0; } -const ciface::Core::DeviceQualifier& EmulatedController::GetDefaultDevice() const +ciface::Core::DeviceQualifier EmulatedController::GetDefaultDevice() const { - return m_default_device; + ciface::InputReadScope scope; + const auto* d = static_cast(retro_atomic_load_acquire_ptr(&m_default_device)); + return d ? d->qualifier : ciface::Core::DeviceQualifier{}; } void EmulatedController::SetDefaultDevice(const std::string& device) @@ -105,15 +105,16 @@ void EmulatedController::SetDefaultDevice(const std::string& device) void EmulatedController::SetDefaultDevice(ciface::Core::DeviceQualifier devq) { - m_default_device = std::move(devq); + auto* d = new DefaultDevice{{}, std::move(devq)}; + if (void* old = retro_atomic_exchange_ptr(&m_default_device, d)) + quiesce_retire(ciface::InputQuiesce(), &static_cast(old)->node, + FreeDefaultDevice); } ControlGroupContainer::~ControlGroupContainer() = default; void EmulatedController::LoadConfig(Common::IniFile::Section* sec) { - const auto lock = EmulatedController::GetStateLock(); - std::string defdev; if (sec->Get("Device", &defdev, "") && !defdev.empty()) SetDefaultDevice(defdev); @@ -129,8 +130,6 @@ void ControlGroupContainer::LoadGroupsConfig(Common::IniFile::Section* sec, cons void EmulatedController::SaveConfig(Common::IniFile::Section* sec) { - const auto lock = EmulatedController::GetStateLock(); - sec->Set("Device", GetDefaultDevice().ToString(), ""); SaveGroupsConfig(sec, ""); @@ -144,7 +143,6 @@ void ControlGroupContainer::SaveGroupsConfig(Common::IniFile::Section* sec, cons void EmulatedController::LoadDefaults(const ControllerInterface& ciface) { - const auto lock = GetStateLock(); // load an empty inifile section, clears everything Common::IniFile::Section sec; LoadConfig(&sec); @@ -156,14 +154,4 @@ void EmulatedController::LoadDefaults(const ControllerInterface& ciface) } } -void ControlGroupContainer::SetInputOverrideFunction(InputOverrideFunction override_func) -{ - m_input_override_function = std::move(override_func); -} - -void ControlGroupContainer::ClearInputOverrideFunction() -{ - m_input_override_function = {}; -} - } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h index 466906da04..c4260a7c38 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h +++ b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h @@ -5,11 +5,12 @@ #include #include -#include #include #include #include +#include + #include "Common/BitUtils.h" #include "Common/Common.h" #include "Common/IniFile.h" @@ -223,9 +224,6 @@ public: void UpdateGroupsReferences(ciface::ExpressionParser::ControlEnvironment& env); - void SetInputOverrideFunction(InputOverrideFunction override_func); - void ClearInputOverrideFunction(); - std::vector> groups; protected: @@ -244,30 +242,40 @@ public: void LoadConfig(Common::IniFile::Section* sec); void SaveConfig(Common::IniFile::Section* sec); + EmulatedController(); + EmulatedController(const EmulatedController&) = delete; + EmulatedController& operator=(const EmulatedController&) = delete; + bool IsDefaultDeviceConnected() const; - const ciface::Core::DeviceQualifier& GetDefaultDevice() const; + ciface::Core::DeviceQualifier GetDefaultDevice() const; void SetDefaultDevice(const std::string& device); void SetDefaultDevice(ciface::Core::DeviceQualifier devq); + // Rebinds every control to the current devices. Any thread may ask; one thread at a time + // does the work, and one asking while it runs has its request done by that thread before + // it stops, so nobody waits for anybody. void UpdateReferences(const ControllerInterface& devi); void UpdateSingleControlReference(const ControllerInterface& devi, ControlReference* ref); - // This returns a lock that should be held before calling State() on any control - // references and GetState(), by extension. This prevents a race condition - // which happens while handling a hotplug event because a control reference's State() - // could be called before we have finished updating the reference. - [[nodiscard]] static std::unique_lock GetStateLock(); - const ciface::ExpressionParser::ControlEnvironment::VariableContainer& - GetExpressionVariables() const; - - // Resets the values while keeping the list. - void ResetExpressionVariables(); - -protected: - ciface::ExpressionParser::ControlEnvironment::VariableContainer m_expression_vars; + // Reading input takes no lock: bindings are swapped whole and freed only once no reader + // is inside. Kept so callers can mark a whole controller read as one input scope. + [[nodiscard]] static ciface::InputReadScope ReadScope() { return {}; } private: - ciface::Core::DeviceQualifier m_default_device; - bool m_default_device_is_connected{false}; + struct DefaultDevice + { + quiesce_node_t node; // first: the free callback casts it back + ciface::Core::DeviceQualifier qualifier; + }; + static void FreeDefaultDevice(quiesce_node_t* node); + void RebindNow(const ControllerInterface& devi); + + // Only touched by the thread doing a rebind. + ciface::ExpressionParser::ControlEnvironment::VariableContainer m_expression_vars; + + mutable retro_atomic_ptr_t m_default_device{}; // DefaultDevice* + retro_atomic_int_t m_default_device_is_connected{}; + retro_atomic_int_t m_rebind_requested{}; + retro_atomic_int_t m_rebinding{}; }; } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/Setting/NumericSetting.h b/Source/Core/InputCommon/ControllerEmu/Setting/NumericSetting.h index dbe52576e4..839b3f7949 100644 --- a/Source/Core/InputCommon/ControllerEmu/Setting/NumericSetting.h +++ b/Source/Core/InputCommon/ControllerEmu/Setting/NumericSetting.h @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -193,7 +192,7 @@ public: ValueType GetCachedValue() const { return m_value; } - bool IsSimpleValue() const { return m_input.GetExpression().empty(); } + bool IsSimpleValue() const { return !m_input.HasExpression(); } virtual void SetValue(const ValueType value) { @@ -211,78 +210,4 @@ private: mutable InputReference m_input; }; -template -class SubscribableSettingValue final : public SettingValue -{ -public: - using Base = SettingValue; - - ValueType GetValue() const override - { - const ValueType cached_value = GetCachedValue(); - if (IsSimpleValue()) - return cached_value; - - const ValueType updated_value = Base::GetValue(); - if (updated_value != cached_value) - TriggerCallbacks(); - - return updated_value; - } - - void SetValue(const ValueType value) override - { - if (value != GetCachedValue()) - { - Base::SetValue(value); - TriggerCallbacks(); - } - else if (!IsSimpleValue()) - { - // The setting has an expression with a cached value equal to the one currently being set. - // Don't trigger the callbacks (since the value didn't change), but clear the expression and - // make the setting a simple value instead. - Base::SetValue(value); - } - } - - ValueType GetCachedValue() const { return Base::GetCachedValue(); } - bool IsSimpleValue() const { return Base::IsSimpleValue(); } - - using SettingChangedCallback = std::function; - - int AddCallback(const SettingChangedCallback& callback) - { - std::lock_guard lock(m_mutex); - const int callback_id = m_next_callback_id; - ++m_next_callback_id; - m_callback_pairs.emplace_back(callback_id, callback); - - return callback_id; - } - - void RemoveCallback(const int id) - { - std::lock_guard lock(m_mutex); - const auto iter = std::ranges::find(m_callback_pairs, id, &IDCallbackPair::first); - if (iter != m_callback_pairs.end()) - m_callback_pairs.erase(iter); - } - -private: - void TriggerCallbacks() const - { - std::lock_guard lock(m_mutex); - const ValueType value = Base::GetValue(); - for (const auto& pair : m_callback_pairs) - pair.second(value); - } - - using IDCallbackPair = std::pair; - std::vector m_callback_pairs; - int m_next_callback_id = 0; - - mutable std::mutex m_mutex; -}; - } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/StickGate.cpp b/Source/Core/InputCommon/ControllerEmu/StickGate.cpp index 660ee939be..1f5256c090 100644 --- a/Source/Core/InputCommon/ControllerEmu/StickGate.cpp +++ b/Source/Core/InputCommon/ControllerEmu/StickGate.cpp @@ -138,15 +138,55 @@ ControlState ReshapableInput::GetDeadzoneRadiusAtAngle(double angle) const return GetInputRadiusAtAngle(angle) * GetDeadzonePercentage(); } +ReshapableInput::~ReshapableInput() +{ + delete static_cast(retro_atomic_exchange_ptr(&m_shape, nullptr)); +} + +void ReshapableInput::FreeShape(quiesce_node_t* node) +{ + delete reinterpret_cast(node); +} + +const ReshapableInput::Shape& ReshapableInput::PeekShape() const +{ + static const Shape s_default{}; + const auto* shape = static_cast(retro_atomic_load_acquire_ptr(&m_shape)); + return shape ? *shape : s_default; +} + +template +void ReshapableInput::EditShape(Edit&& edit) +{ + ciface::InputReadScope scope; + for (;;) + { + void* const cur = retro_atomic_load_acquire_ptr(&m_shape); + const Shape& from = cur ? *static_cast(cur) : PeekShape(); + auto* next = new Shape{{}, from.calibration, from.center}; + edit(*next); + if (retro_atomic_cas_ptr(&m_shape, cur, next)) + { + if (cur) + quiesce_retire(ciface::InputQuiesce(), &static_cast(cur)->node, FreeShape); + return; + } + delete next; + } +} + ControlState ReshapableInput::GetInputRadiusAtAngle(double angle) const { + ciface::InputReadScope scope; + const CalibrationData& calibration = PeekShape().calibration; + // Handle the "default" state. - if (m_calibration.empty()) + if (calibration.empty()) { return GetDefaultInputRadiusAtAngle(angle); } - return GetCalibrationDataRadiusAtAngle(m_calibration, angle); + return GetCalibrationDataRadiusAtAngle(calibration, angle); } ControlState ReshapableInput::GetDeadzonePercentage() const @@ -182,16 +222,19 @@ ControlState ReshapableInput::GetDefaultInputRadiusAtAngle(double angle) const void ReshapableInput::SetCalibrationToDefault() { - m_calibration.clear(); + EditShape([](Shape& shape) { shape.calibration.clear(); }); } void ReshapableInput::SetCalibrationFromGate(const StickGate& gate) { - m_calibration.resize(gate.GetIdealCalibrationSampleCount().value_or(CALIBRATION_SAMPLE_COUNT)); + CalibrationData calibration( + gate.GetIdealCalibrationSampleCount().value_or(CALIBRATION_SAMPLE_COUNT)); u32 i = 0; - for (auto& val : m_calibration) - val = gate.GetRadiusAtAngle(MathUtil::TAU * i++ / m_calibration.size()); + for (auto& val : calibration) + val = gate.GetRadiusAtAngle(MathUtil::TAU * i++ / calibration.size()); + + SetCalibrationData(std::move(calibration)); } void ReshapableInput::UpdateCalibrationData(CalibrationData& data, Common::DVec2 point1, @@ -207,24 +250,26 @@ void ReshapableInput::UpdateCalibrationData(CalibrationData& data, Common::DVec2 } } -const ReshapableInput::CalibrationData& ReshapableInput::GetCalibrationData() const +ReshapableInput::CalibrationData ReshapableInput::GetCalibrationData() const { - return m_calibration; + ciface::InputReadScope scope; + return PeekShape().calibration; } void ReshapableInput::SetCalibrationData(CalibrationData data) { - m_calibration = std::move(data); + EditShape([&data](Shape& shape) { shape.calibration = data; }); } -const ReshapableInput::ReshapeData& ReshapableInput::GetCenter() const +ReshapableInput::ReshapeData ReshapableInput::GetCenter() const { - return m_center; + ciface::InputReadScope scope; + return PeekShape().center; } void ReshapableInput::SetCenter(ReshapableInput::ReshapeData center) { - m_center = center; + EditShape([¢er](Shape& shape) { shape.center = center; }); } void ReshapableInput::LoadConfig(Common::IniFile::Section* section, const std::string& base_name) @@ -236,19 +281,19 @@ void ReshapableInput::LoadConfig(Common::IniFile::Section* section, const std::s // Special handling for "Modifier" button "Range" settings which default to 50% instead of 100%. if (const auto* modifier_input = GetModifierInput()) { - section->Get(group + modifier_input->name + "/Range", &modifier_input->control_ref->range, - 50.0); - modifier_input->control_ref->range /= 100; + double range = 50.0; + section->Get(group + modifier_input->name + "/Range", &range, 50.0); + modifier_input->control_ref->SetRange(range / 100); } std::string load_str; section->Get(group + CALIBRATION_CONFIG_NAME, &load_str, ""); const auto load_data = SplitString(load_str, ' '); - m_calibration.assign(load_data.size(), CALIBRATION_DEFAULT_VALUE); + CalibrationData calibration(load_data.size(), CALIBRATION_DEFAULT_VALUE); auto it = load_data.begin(); - for (auto& sample : m_calibration) + for (auto& sample : calibration) { if (TryParse(*(it++), &sample)) sample /= CALIBRATION_CONFIG_SCALE; @@ -257,16 +302,21 @@ void ReshapableInput::LoadConfig(Common::IniFile::Section* section, const std::s section->Get(group + CENTER_CONFIG_NAME, &load_str, ""); const auto center_data = SplitString(load_str, ' '); - m_center = Common::DVec2(); + ReshapeData center = Common::DVec2(); if (center_data.size() == 2) { - if (TryParse(center_data[0], &m_center.x)) - m_center.x /= CENTER_CONFIG_SCALE; + if (TryParse(center_data[0], ¢er.x)) + center.x /= CENTER_CONFIG_SCALE; - if (TryParse(center_data[1], &m_center.y)) - m_center.y /= CENTER_CONFIG_SCALE; + if (TryParse(center_data[1], ¢er.y)) + center.y /= CENTER_CONFIG_SCALE; } + + EditShape([&calibration, ¢er](Shape& shape) { + shape.calibration = calibration; + shape.center = center; + }); } void ReshapableInput::SaveConfig(Common::IniFile::Section* section, const std::string& base_name) @@ -278,19 +328,21 @@ void ReshapableInput::SaveConfig(Common::IniFile::Section* section, const std::s // Special handling for "Modifier" button "Range" settings which default to 50% instead of 100%. if (const auto* modifier_input = GetModifierInput()) { - section->Set(group + modifier_input->name + "/Range", modifier_input->control_ref->range * 100, + section->Set(group + modifier_input->name + "/Range", modifier_input->control_ref->GetRange() * 100, 50.0); } + const CalibrationData calibration = GetCalibrationData(); + const ReshapeData center = GetCenter(); const std::ranges::transform_view scaled_calibration( - m_calibration, [](ControlState val) { return val * CALIBRATION_CONFIG_SCALE; }); + calibration, [](ControlState val) { return val * CALIBRATION_CONFIG_SCALE; }); section->Set(group + CALIBRATION_CONFIG_NAME, fmt::format("{:.2f}", fmt::join(scaled_calibration, " ")), ""); // Save center value. static constexpr char center_format[] = "{:.2f} {:.2f}"; - const auto center_data = fmt::format(center_format, m_center.x * CENTER_CONFIG_SCALE, - m_center.y * CENTER_CONFIG_SCALE); + const auto center_data = fmt::format(center_format, center.x * CENTER_CONFIG_SCALE, + center.y * CENTER_CONFIG_SCALE); section->Set(group + CENTER_CONFIG_NAME, center_data, fmt::format(center_format, 0.0, 0.0)); } @@ -298,8 +350,13 @@ ReshapableInput::ReshapeData ReshapableInput::Reshape(ControlState x, ControlSta ControlState modifier, ControlState clamp) const { - x -= m_center.x; - y -= m_center.y; + ReshapeData center; + { + ciface::InputReadScope scope; + center = PeekShape().center; + } + x -= center.x; + y -= center.y; // We run this even if both x and y will be zero. // In that case, std::atan2(0, 0) returns a valid non-NaN value, but the exact value diff --git a/Source/Core/InputCommon/ControllerEmu/StickGate.h b/Source/Core/InputCommon/ControllerEmu/StickGate.h index 919cc08815..e0fb1b0b15 100644 --- a/Source/Core/InputCommon/ControllerEmu/StickGate.h +++ b/Source/Core/InputCommon/ControllerEmu/StickGate.h @@ -6,7 +6,10 @@ #include #include +#include + #include "Common/Matrix.h" +#include "Common/Quiesce.h" #include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h" #include "InputCommon/ControllerEmu/Setting/NumericSetting.h" @@ -99,12 +102,16 @@ public: Common::DVec2 point2); static ControlState GetCalibrationDataRadiusAtAngle(const CalibrationData& data, double angle); - const CalibrationData& GetCalibrationData() const; + CalibrationData GetCalibrationData() const; void SetCalibrationData(CalibrationData data); - const ReshapeData& GetCenter() const; + ReshapeData GetCenter() const; void SetCenter(ReshapeData center); + ~ReshapableInput() override; + ReshapableInput(const ReshapableInput&) = delete; + ReshapableInput& operator=(const ReshapableInput&) = delete; + static constexpr const char* X_INPUT_OVERRIDE = "X"; static constexpr const char* Y_INPUT_OVERRIDE = "Y"; static constexpr const char* Z_INPUT_OVERRIDE = "Z"; @@ -119,9 +126,23 @@ private: void LoadConfig(Common::IniFile::Section*, const std::string& base_name) override; void SaveConfig(Common::IniFile::Section*, const std::string& base_name) override; - CalibrationData m_calibration; + // Calibration and center, read on every poll and set from the configuration: published + // whole, so a poll never sees half of a change. + struct Shape + { + quiesce_node_t node; // first: the free callback casts it back + CalibrationData calibration; + ReshapeData center; + }; + static void FreeShape(quiesce_node_t* node); + // Only inside an InputReadScope. + const Shape& PeekShape() const; + // Swaps in edit's result, made from a copy of the current shape. + template + void EditShape(Edit&& edit); + + mutable retro_atomic_ptr_t m_shape{}; SettingValue m_deadzone_setting; - ReshapeData m_center; }; } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp b/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp index 43ca21e6cb..d5dc313f52 100644 --- a/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp +++ b/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp @@ -34,8 +34,6 @@ void ControllerInterface::Initialize(const WindowSystemInfo& wsi) if (m_is_init) return; - std::lock_guard lk_population(m_devices_population_mutex); - m_wsi = wsi; m_populating_devices_counter = 1; @@ -56,10 +54,12 @@ void ControllerInterface::Initialize(const WindowSystemInfo& wsi) RefreshDevices(); - // Devices writes are already protected by m_devices_population_mutex but this won't hurt - m_devices_mutex.lock(); - const bool devices_empty = m_devices.empty(); - m_devices_mutex.unlock(); + bool devices_empty; + { + ciface::InputReadScope scope; + const DeviceList* const list = PeekDevices(); + devices_empty = !list || list->devices.empty(); + } if (m_populating_devices_counter.fetch_sub(1) == 1 && !devices_empty) InvokeDevicesChangedCallbacks(); @@ -88,13 +88,8 @@ void ControllerInterface::RefreshDevices(RefreshReason reason) if (!m_is_init) return; - // We lock m_devices_population_mutex here to make everything simpler. - // Multiple devices classes have their own "hotplug" thread, and can add/remove devices at any - // time, while actual writes to "m_devices" are safe, the order in which they happen is not. That - // means a thread could be adding devices while we are removing them from a different thread, - // or removing them as we are populating them (causing missing or duplicate devices). - std::lock_guard lk_population(m_devices_population_mutex); - + // Backends with a hotplug thread may add or remove devices meanwhile; every change is + // one swap of the whole list, so each lands complete, before or after this refresh's. // If only the window changed, avoid removing and re-adding all devices. // Instead only refresh devices that require the window handle. if (reason == RefreshReason::WindowChangeOnly) @@ -137,8 +132,6 @@ void ControllerInterface::PlatformPopulateDevices(const std::function& c if (!m_is_init) return; - std::lock_guard lk_population(m_devices_population_mutex); - m_populating_devices_counter.fetch_add(1); callback(); @@ -166,8 +159,7 @@ void ControllerInterface::Shutdown() // Make sure no devices had been added within Shutdown() in the time // between checking they checked atomic m_is_init bool and we changed it. - // We couldn't have locked m_devices_population_mutex for the whole Shutdown() - // as it could cause deadlocks. Note that this is still not 100% safe as some backends are + // Note that this is still not 100% safe as some backends are // shut down in other places, possibly adding devices after we have shut down, but the chances of // that happening are basically zero. ClearDevices(); @@ -175,15 +167,11 @@ void ControllerInterface::Shutdown() void ControllerInterface::ClearDevices() { - std::lock_guard lk_population(m_devices_population_mutex); + const bool cleared = EditDevices([](std::vector>& devices) { + if (devices.empty()) + return false; - { - std::lock_guard lk(m_devices_mutex); - - if (m_devices.empty()) - return; - - for (const auto& d : m_devices) + for (const auto& d : devices) { // Set outputs to ZERO before destroying device for (ciface::Core::Device::Output* o : d->Outputs()) @@ -192,10 +180,12 @@ void ControllerInterface::ClearDevices() // Devices could still be alive after this as there might be shared ptrs around holding them. // The InvokeDevicesChangedCallbacks() underneath should always clean all of them (it needs to). - m_devices.clear(); - } + devices.clear(); + return true; + }); - InvokeDevicesChangedCallbacks(); + if (cleared) + InvokeDevicesChangedCallbacks(); } bool ControllerInterface::AddDevice(std::shared_ptr device) @@ -208,13 +198,9 @@ bool ControllerInterface::AddDevice(std::shared_ptr device "Devices shouldn't be added within input update calls, there is a risk of deadlock " "if another thread was already here"); - std::lock_guard lk_population(m_devices_population_mutex); - - { - std::lock_guard lk(m_devices_mutex); - - const auto is_id_in_use = [&device, this](int id) { - return std::ranges::any_of(m_devices, [&device, &id](const auto& d) { + EditDevices([&device](std::vector>& devices) { + const auto is_id_in_use = [&device, &devices](int id) { + return std::ranges::any_of(devices, [&device, &id](const auto& d) { return d->GetSource() == device->GetSource() && d->GetName() == device->GetName() && d->GetId() == id; }); @@ -236,8 +222,7 @@ bool ControllerInterface::AddDevice(std::shared_ptr device device->SetId(id); } - NOTICE_LOG_FMT(CONTROLLERINTERFACE, "Added device: {}", device->GetQualifiedName()); - m_devices.emplace_back(std::move(device)); + devices.emplace_back(device); // We can't (and don't want) to control the order in which devices are added, but we need // their order to be consistent, and we need the same one to always be the first, where present @@ -245,9 +230,11 @@ bool ControllerInterface::AddDevice(std::shared_ptr device // profile, it will automatically select the first device in the list as its default. It would // be nice to sort devices by Source then Name then ID, but it's better to leave them sorted by // the add order. This also avoids breaking the order on other platforms that are less tested. - std::ranges::stable_sort(m_devices, std::ranges::greater{}, + std::ranges::stable_sort(devices, std::ranges::greater{}, &ciface::Core::Device::GetSortPriority); - } + return true; + }); + NOTICE_LOG_FMT(CONTROLLERINTERFACE, "Added device: {}", device->GetQualifiedName()); if (!m_populating_devices_counter) InvokeDevicesChangedCallbacks(); @@ -265,21 +252,18 @@ void ControllerInterface::RemoveDevice(std::functionGetQualifiedName()); - return true; - } - return false; - }); - any_removed = erased != 0; - } + const bool any_removed = + EditDevices([&callback](std::vector>& devices) { + return std::erase_if(devices, [&callback](const auto& dev) { + if (callback(dev.get())) + { + NOTICE_LOG_FMT(CONTROLLERINTERFACE, "Removed device: {}", + dev->GetQualifiedName()); + return true; + } + return false; + }) != 0; + }); if (any_removed && (!m_populating_devices_counter || force_devices_release)) InvokeDevicesChangedCallbacks(); @@ -293,24 +277,21 @@ void ControllerInterface::UpdateInput() if (!m_is_init) return; - // We add the devices to remove while we still have the "m_devices_mutex" locked. - // This guarantees that: - // -We won't try to lock "m_devices_population_mutex" while it was already locked and waiting - // for "m_devices_mutex", which would result in dead lock. - // -We don't keep shared ptrs on devices and thus unwillingly keep them alive even if somebody - // is currently trying to remove them (and needs them destroyed on the spot). - // -If somebody else destroyed them in the meantime, we'll know which ones have been destroyed. + // The devices to remove are collected as weak pointers: they are not kept alive by this, and + // if somebody else removed them meanwhile, we'll know which ones are gone. std::vector> devices_to_remove; { - std::lock_guard lk_devices(m_devices_mutex); + ciface::InputReadScope scope; tls_is_updating_devices = true; for (auto& backend : m_input_backends) backend->UpdateInput(devices_to_remove); - for (const auto& d : m_devices) + static const std::vector> s_no_devices; + const DeviceList* const list = PeekDevices(); + for (const auto& d : list ? list->devices : s_no_devices) { // Theoretically we could avoid updating input on devices that don't have any references to // them, but in practice a few devices types could break in different ways, so we don't diff --git a/Source/Core/InputCommon/ControllerInterface/ControllerInterface.h b/Source/Core/InputCommon/ControllerInterface/ControllerInterface.h index 3c4f364d80..5fa6440c3b 100644 --- a/Source/Core/InputCommon/ControllerInterface/ControllerInterface.h +++ b/Source/Core/InputCommon/ControllerInterface/ControllerInterface.h @@ -6,7 +6,6 @@ #include #include #include -#include #include "Common/HookableEvent.h" #include "Common/Matrix.h" @@ -76,9 +75,8 @@ public: // set force_devices_release to true. void RemoveDevice(std::function callback, bool force_devices_release = false); - // This is mandatory to use on device populations functions that can be called concurrently by - // more than one thread, or that are called by a single other thread. - // Without this, our devices list might end up in a mixed state. + // Runs a backend's population, holding back the devices-changed callbacks until it is + // done so they run once for the batch. void PlatformPopulateDevices(const std::function& callback); bool IsInit() const { return m_is_init; } void UpdateInput(); @@ -111,11 +109,8 @@ private: Common::HookableEvent<> m_devices_changed_event; - mutable std::recursive_mutex m_devices_population_mutex; std::atomic m_is_init; - // This is now always protected by m_devices_population_mutex, so - // it doesn't really need to be a counter or atomic anymore (it could be a raw bool), - // but we keep it so for simplicity, in case we changed the design. + // Populations in progress on any thread; the last to finish runs the callbacks. std::atomic m_populating_devices_counter; WindowSystemInfo m_wsi; std::atomic m_aspect_ratio_adjustment = 1; diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp index 9f7e708593..539370a06f 100644 --- a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp @@ -15,6 +15,16 @@ #include "Common/MathUtil.h" +namespace ciface +{ +static quiesce_t s_input_quiesce; + +quiesce_t* InputQuiesce() +{ + return &s_input_quiesce; +} +} // namespace ciface + namespace ciface::Core { // Compared to an input's current state (ideally 1.0) minus abs(initial_state) (ideally 0.0). @@ -245,10 +255,28 @@ bool DeviceQualifier::operator==(const DeviceQualifier& devq) const return std::tie(cid, name, source) == std::tie(devq.cid, devq.name, devq.source); } +static const std::vector>& NoDevices() +{ + static const std::vector> s_none; + return s_none; +} + +void DeviceContainer::FreeDeviceList(quiesce_node_t* node) +{ + delete reinterpret_cast(node); +} + +DeviceContainer::~DeviceContainer() +{ + delete static_cast(retro_atomic_exchange_ptr(&m_device_list, nullptr)); +} + std::shared_ptr DeviceContainer::FindDevice(const DeviceQualifier& devq) const { - std::lock_guard lk(m_devices_mutex); - for (const auto& d : m_devices) + InputReadScope scope; + const DeviceList* const list = PeekDevices(); + const auto& devices = list ? list->devices : NoDevices(); + for (const auto& d : devices) { if (devq == d.get()) return d; @@ -259,19 +287,21 @@ std::shared_ptr DeviceContainer::FindDevice(const DeviceQualifier& devq) std::vector> DeviceContainer::GetAllDevices() const { - std::lock_guard lk(m_devices_mutex); - - return m_devices; + InputReadScope scope; + const DeviceList* const list = PeekDevices(); + return list ? list->devices : std::vector>{}; } std::vector DeviceContainer::GetAllDeviceStrings() const { - std::lock_guard lk(m_devices_mutex); + InputReadScope scope; + const DeviceList* const list = PeekDevices(); + const auto& devices = list ? list->devices : NoDevices(); std::vector device_strings; DeviceQualifier device_qualifier; - for (const auto& d : m_devices) + for (const auto& d : devices) { device_qualifier.FromDevice(d.get()); device_strings.emplace_back(device_qualifier.ToString()); @@ -282,20 +312,24 @@ std::vector DeviceContainer::GetAllDeviceStrings() const bool DeviceContainer::HasDefaultDevice() const { - std::lock_guard lk(m_devices_mutex); + InputReadScope scope; + const DeviceList* const list = PeekDevices(); + const auto& devices = list ? list->devices : NoDevices(); // Devices are already sorted by priority - return !m_devices.empty() && m_devices[0]->GetSortPriority() >= 0; + return !devices.empty() && devices[0]->GetSortPriority() >= 0; } std::string DeviceContainer::GetDefaultDeviceString() const { - std::lock_guard lk(m_devices_mutex); + InputReadScope scope; + const DeviceList* const list = PeekDevices(); + const auto& devices = list ? list->devices : NoDevices(); // Devices are already sorted by priority - if (m_devices.empty() || m_devices[0]->GetSortPriority() < 0) + if (devices.empty() || devices[0]->GetSortPriority() < 0) return ""; DeviceQualifier device_qualifier; - device_qualifier.FromDevice(m_devices[0].get()); + device_qualifier.FromDevice(devices[0].get()); return device_qualifier.ToString(); } @@ -308,8 +342,10 @@ Device::Input* DeviceContainer::FindInput(std::string_view name, const Device* d return inp; } - std::lock_guard lk(m_devices_mutex); - for (const auto& d : m_devices) + InputReadScope scope; + const DeviceList* const list = PeekDevices(); + const auto& devices = list ? list->devices : NoDevices(); + for (const auto& d : devices) { Device::Input* const i = d->FindInput(name); diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.h b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h index c8be0aeb65..fbca25358f 100644 --- a/Source/Core/InputCommon/ControllerInterface/CoreDevice.h +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h @@ -6,20 +6,37 @@ #include #include #include -#include #include #include #include #include #include +#include + #include "Common/CommonTypes.h" +#include "Common/Quiesce.h" // idk in case I wanted to change it to double or something, idk what's best typedef double ControlState; namespace ciface { +// Everything the input code shares between threads (the device list, control bindings, +// stick calibration) is published whole and freed through this quiesce, so reading input +// takes no lock and a reconfiguration never waits for a reader. +quiesce_t* InputQuiesce(); + +// Marks the calling thread as reading input for its lifetime. Nests. +class InputReadScope final +{ +public: + InputReadScope() { quiesce_enter(InputQuiesce()); } + ~InputReadScope() { quiesce_exit(InputQuiesce()); } + InputReadScope(const InputReadScope&) = delete; + InputReadScope& operator=(const InputReadScope&) = delete; +}; + // 100Hz which homebrew docs very roughly imply is within WiiMote normal // range, used for periodic haptic effects though often ignored by devices // TODO: Make this configurable. @@ -233,13 +250,58 @@ public: bool HasConnectedDevice(const DeviceQualifier& qualifier) const; - std::recursive_mutex& GetDevicesMutex() const { return m_devices_mutex; } + DeviceContainer() = default; + ~DeviceContainer(); + DeviceContainer(const DeviceContainer&) = delete; + DeviceContainer& operator=(const DeviceContainer&) = delete; protected: - // Exclusively needed when reading/writing the "m_devices" array. - // Not needed when individually reading/writing a single device ptr. - mutable std::recursive_mutex m_devices_mutex; - std::vector> m_devices; + // The devices, sorted by priority. Never changed once published: a writer copies it, + // edits the copy and swaps it in, and the old one is freed once no reader is inside. + struct DeviceList + { + quiesce_node_t node; // first: the free callback casts it back + std::vector> devices; + }; + + // The current list, or null for none. Only inside an InputReadScope. + const DeviceList* PeekDevices() const + { + return static_cast(retro_atomic_load_acquire_ptr(&m_device_list)); + } + + // Swaps in edit's result, made from a copy of the current devices. edit returns false to + // leave the list as it is; it may run more than once if another thread swaps in a list + // meanwhile. Returns whether a list was swapped in. + template + bool EditDevices(Edit&& edit) + { + InputReadScope scope; + for (;;) + { + void* const cur = retro_atomic_load_acquire_ptr(&m_device_list); + auto* next = new DeviceList{{}, {}}; + if (cur) + next->devices = static_cast(cur)->devices; + if (!edit(next->devices)) + { + delete next; + return false; + } + if (retro_atomic_cas_ptr(&m_device_list, cur, next)) + { + if (cur) + quiesce_retire(InputQuiesce(), &static_cast(cur)->node, FreeDeviceList); + return true; + } + delete next; + } + } + +private: + static void FreeDeviceList(quiesce_node_t* node); + + mutable retro_atomic_ptr_t m_device_list{}; }; // Wait for inputs on supplied devices. diff --git a/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.cpp b/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.cpp deleted file mode 100644 index 9ddbaffcd6..0000000000 --- a/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.cpp +++ /dev/null @@ -1,272 +0,0 @@ -// Copyright 2021 Dolphin Emulator Project -// Licensed under GPLv2+ -// Refer to the license.txt file included. - -#include "InputCommon/ControllerInterface/Touch/InputOverrider.h" - -#include -#include -#include -#include -#include -#include - -#include "Common/Assert.h" - -#include "Core/HW/GCPad.h" -#include "Core/HW/GCPadEmu.h" -#include "Core/HW/Wiimote.h" -#include "Core/HW/WiimoteEmu/Extension/Classic.h" -#include "Core/HW/WiimoteEmu/Extension/Nunchuk.h" -#include "Core/HW/WiimoteEmu/WiimoteEmu.h" - -#include "InputCommon/ControllerEmu/ControlGroup/Attachments.h" -#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h" -#include "InputCommon/ControllerEmu/ControllerEmu.h" -#include "InputCommon/ControllerEmu/StickGate.h" -#include "InputCommon/ControllerInterface/CoreDevice.h" -#include "InputCommon/InputConfig.h" - -namespace ciface::Touch -{ -namespace -{ -struct InputState -{ - ControlState normal_state = 0; - ControlState override_state = 0; - bool overriding = false; -}; - -using ControlsMap = std::map, ControlID>; -using StateArray = std::array; - -std::array s_state_arrays; - -const ControlsMap s_gcpad_controls_map = {{ - {{GCPad::BUTTONS_GROUP, GCPad::A_BUTTON}, ControlID::GCPAD_A_BUTTON}, - {{GCPad::BUTTONS_GROUP, GCPad::B_BUTTON}, ControlID::GCPAD_B_BUTTON}, - {{GCPad::BUTTONS_GROUP, GCPad::X_BUTTON}, ControlID::GCPAD_X_BUTTON}, - {{GCPad::BUTTONS_GROUP, GCPad::Y_BUTTON}, ControlID::GCPAD_Y_BUTTON}, - {{GCPad::BUTTONS_GROUP, GCPad::Z_BUTTON}, ControlID::GCPAD_Z_BUTTON}, - {{GCPad::BUTTONS_GROUP, GCPad::START_BUTTON}, ControlID::GCPAD_START_BUTTON}, - {{GCPad::DPAD_GROUP, DIRECTION_UP}, ControlID::GCPAD_DPAD_UP}, - {{GCPad::DPAD_GROUP, DIRECTION_DOWN}, ControlID::GCPAD_DPAD_DOWN}, - {{GCPad::DPAD_GROUP, DIRECTION_LEFT}, ControlID::GCPAD_DPAD_LEFT}, - {{GCPad::DPAD_GROUP, DIRECTION_RIGHT}, ControlID::GCPAD_DPAD_RIGHT}, - {{GCPad::TRIGGERS_GROUP, GCPad::L_DIGITAL}, ControlID::GCPAD_L_DIGITAL}, - {{GCPad::TRIGGERS_GROUP, GCPad::R_DIGITAL}, ControlID::GCPAD_R_DIGITAL}, - {{GCPad::TRIGGERS_GROUP, GCPad::L_ANALOG}, ControlID::GCPAD_L_ANALOG}, - {{GCPad::TRIGGERS_GROUP, GCPad::R_ANALOG}, ControlID::GCPAD_R_ANALOG}, - {{GCPad::MAIN_STICK_GROUP, ControllerEmu::ReshapableInput::X_INPUT_OVERRIDE}, - ControlID::GCPAD_MAIN_STICK_X}, - {{GCPad::MAIN_STICK_GROUP, ControllerEmu::ReshapableInput::Y_INPUT_OVERRIDE}, - ControlID::GCPAD_MAIN_STICK_Y}, - {{GCPad::C_STICK_GROUP, ControllerEmu::ReshapableInput::X_INPUT_OVERRIDE}, - ControlID::GCPAD_C_STICK_X}, - {{GCPad::C_STICK_GROUP, ControllerEmu::ReshapableInput::Y_INPUT_OVERRIDE}, - ControlID::GCPAD_C_STICK_Y}, -}}; - -const ControlsMap s_wiimote_controls_map = {{ - {{WiimoteEmu::Wiimote::BUTTONS_GROUP, WiimoteEmu::Wiimote::A_BUTTON}, - ControlID::WIIMOTE_A_BUTTON}, - {{WiimoteEmu::Wiimote::BUTTONS_GROUP, WiimoteEmu::Wiimote::B_BUTTON}, - ControlID::WIIMOTE_B_BUTTON}, - {{WiimoteEmu::Wiimote::BUTTONS_GROUP, WiimoteEmu::Wiimote::ONE_BUTTON}, - ControlID::WIIMOTE_ONE_BUTTON}, - {{WiimoteEmu::Wiimote::BUTTONS_GROUP, WiimoteEmu::Wiimote::TWO_BUTTON}, - ControlID::WIIMOTE_TWO_BUTTON}, - {{WiimoteEmu::Wiimote::BUTTONS_GROUP, WiimoteEmu::Wiimote::PLUS_BUTTON}, - ControlID::WIIMOTE_PLUS_BUTTON}, - {{WiimoteEmu::Wiimote::BUTTONS_GROUP, WiimoteEmu::Wiimote::MINUS_BUTTON}, - ControlID::WIIMOTE_MINUS_BUTTON}, - {{WiimoteEmu::Wiimote::BUTTONS_GROUP, WiimoteEmu::Wiimote::HOME_BUTTON}, - ControlID::WIIMOTE_HOME_BUTTON}, - {{WiimoteEmu::Wiimote::DPAD_GROUP, DIRECTION_UP}, ControlID::WIIMOTE_DPAD_UP}, - {{WiimoteEmu::Wiimote::DPAD_GROUP, DIRECTION_DOWN}, ControlID::WIIMOTE_DPAD_DOWN}, - {{WiimoteEmu::Wiimote::DPAD_GROUP, DIRECTION_LEFT}, ControlID::WIIMOTE_DPAD_LEFT}, - {{WiimoteEmu::Wiimote::DPAD_GROUP, DIRECTION_RIGHT}, ControlID::WIIMOTE_DPAD_RIGHT}, - {{WiimoteEmu::Wiimote::IR_GROUP, ControllerEmu::ReshapableInput::X_INPUT_OVERRIDE}, - ControlID::WIIMOTE_IR_X}, - {{WiimoteEmu::Wiimote::IR_GROUP, ControllerEmu::ReshapableInput::Y_INPUT_OVERRIDE}, - ControlID::WIIMOTE_IR_Y}, -}}; - -const ControlsMap s_nunchuk_controls_map = {{ - {{WiimoteEmu::Nunchuk::BUTTONS_GROUP, WiimoteEmu::Nunchuk::C_BUTTON}, - ControlID::NUNCHUK_C_BUTTON}, - {{WiimoteEmu::Nunchuk::BUTTONS_GROUP, WiimoteEmu::Nunchuk::Z_BUTTON}, - ControlID::NUNCHUK_Z_BUTTON}, - {{WiimoteEmu::Nunchuk::STICK_GROUP, ControllerEmu::ReshapableInput::X_INPUT_OVERRIDE}, - ControlID::NUNCHUK_STICK_X}, - {{WiimoteEmu::Nunchuk::STICK_GROUP, ControllerEmu::ReshapableInput::Y_INPUT_OVERRIDE}, - ControlID::NUNCHUK_STICK_Y}, -}}; - -const ControlsMap s_classic_controls_map = {{ - {{WiimoteEmu::Classic::BUTTONS_GROUP, WiimoteEmu::Classic::A_BUTTON}, - ControlID::CLASSIC_A_BUTTON}, - {{WiimoteEmu::Classic::BUTTONS_GROUP, WiimoteEmu::Classic::B_BUTTON}, - ControlID::CLASSIC_B_BUTTON}, - {{WiimoteEmu::Classic::BUTTONS_GROUP, WiimoteEmu::Classic::X_BUTTON}, - ControlID::CLASSIC_X_BUTTON}, - {{WiimoteEmu::Classic::BUTTONS_GROUP, WiimoteEmu::Classic::Y_BUTTON}, - ControlID::CLASSIC_Y_BUTTON}, - {{WiimoteEmu::Classic::BUTTONS_GROUP, WiimoteEmu::Classic::ZL_BUTTON}, - ControlID::CLASSIC_ZL_BUTTON}, - {{WiimoteEmu::Classic::BUTTONS_GROUP, WiimoteEmu::Classic::ZR_BUTTON}, - ControlID::CLASSIC_ZR_BUTTON}, - {{WiimoteEmu::Classic::BUTTONS_GROUP, WiimoteEmu::Classic::PLUS_BUTTON}, - ControlID::CLASSIC_PLUS_BUTTON}, - {{WiimoteEmu::Classic::BUTTONS_GROUP, WiimoteEmu::Classic::MINUS_BUTTON}, - ControlID::CLASSIC_MINUS_BUTTON}, - {{WiimoteEmu::Classic::BUTTONS_GROUP, WiimoteEmu::Classic::HOME_BUTTON}, - ControlID::CLASSIC_HOME_BUTTON}, - {{WiimoteEmu::Classic::DPAD_GROUP, DIRECTION_UP}, ControlID::CLASSIC_DPAD_UP}, - {{WiimoteEmu::Classic::DPAD_GROUP, DIRECTION_DOWN}, ControlID::CLASSIC_DPAD_DOWN}, - {{WiimoteEmu::Classic::DPAD_GROUP, DIRECTION_LEFT}, ControlID::CLASSIC_DPAD_LEFT}, - {{WiimoteEmu::Classic::DPAD_GROUP, DIRECTION_RIGHT}, ControlID::CLASSIC_DPAD_RIGHT}, - {{WiimoteEmu::Classic::TRIGGERS_GROUP, WiimoteEmu::Classic::L_DIGITAL}, - ControlID::CLASSIC_L_DIGITAL}, - {{WiimoteEmu::Classic::TRIGGERS_GROUP, WiimoteEmu::Classic::R_DIGITAL}, - ControlID::CLASSIC_R_DIGITAL}, - {{WiimoteEmu::Classic::TRIGGERS_GROUP, WiimoteEmu::Classic::L_ANALOG}, - ControlID::CLASSIC_L_ANALOG}, - {{WiimoteEmu::Classic::TRIGGERS_GROUP, WiimoteEmu::Classic::R_ANALOG}, - ControlID::CLASSIC_R_ANALOG}, - {{WiimoteEmu::Classic::LEFT_STICK_GROUP, ControllerEmu::ReshapableInput::X_INPUT_OVERRIDE}, - ControlID::CLASSIC_LEFT_STICK_X}, - {{WiimoteEmu::Classic::LEFT_STICK_GROUP, ControllerEmu::ReshapableInput::Y_INPUT_OVERRIDE}, - ControlID::CLASSIC_LEFT_STICK_Y}, - {{WiimoteEmu::Classic::RIGHT_STICK_GROUP, ControllerEmu::ReshapableInput::X_INPUT_OVERRIDE}, - ControlID::CLASSIC_RIGHT_STICK_X}, - {{WiimoteEmu::Classic::RIGHT_STICK_GROUP, ControllerEmu::ReshapableInput::Y_INPUT_OVERRIDE}, - ControlID::CLASSIC_RIGHT_STICK_Y}, -}}; - -ControllerEmu::InputOverrideFunction GetInputOverrideFunction(const ControlsMap& controls_map, - size_t i) -{ - StateArray& state_array = s_state_arrays[i]; - - return [&](std::string_view group_name, std::string_view control_name, - ControlState controller_state) -> std::optional { - const auto it = controls_map.find(std::make_pair(group_name, control_name)); - if (it == controls_map.end()) - return std::nullopt; - - const ControlID control = it->second; - InputState& input_state = state_array[control]; - if (input_state.normal_state != controller_state) - { - input_state.normal_state = controller_state; - input_state.overriding = false; - } - - return input_state.overriding ? std::make_optional(input_state.override_state) : std::nullopt; - }; -} - -} // namespace - -void RegisterGameCubeInputOverrider(int controller_index) -{ - Pad::GetConfig() - ->GetController(controller_index) - ->SetInputOverrideFunction(GetInputOverrideFunction(s_gcpad_controls_map, controller_index)); -} - -void RegisterWiiInputOverrider(int controller_index) -{ - auto* wiimote = - static_cast(Wiimote::GetConfig()->GetController(controller_index)); - - wiimote->SetInputOverrideFunction( - GetInputOverrideFunction(s_wiimote_controls_map, controller_index)); - - auto& attachments = static_cast( - wiimote->GetWiimoteGroup(WiimoteEmu::WiimoteGroup::Attachments)) - ->GetAttachmentList(); - - attachments[WiimoteEmu::ExtensionNumber::NUNCHUK]->SetInputOverrideFunction( - GetInputOverrideFunction(s_nunchuk_controls_map, controller_index)); - attachments[WiimoteEmu::ExtensionNumber::CLASSIC]->SetInputOverrideFunction( - GetInputOverrideFunction(s_classic_controls_map, controller_index)); -} - -void UnregisterGameCubeInputOverrider(int controller_index) -{ - Pad::GetConfig()->GetController(controller_index)->ClearInputOverrideFunction(); - - for (size_t i = ControlID::FIRST_GC_CONTROL; i <= ControlID::LAST_GC_CONTROL; ++i) - s_state_arrays[controller_index][i].overriding = false; -} - -void UnregisterWiiInputOverrider(int controller_index) -{ - auto* wiimote = - static_cast(Wiimote::GetConfig()->GetController(controller_index)); - - wiimote->ClearInputOverrideFunction(); - - auto& attachments = static_cast( - wiimote->GetWiimoteGroup(WiimoteEmu::WiimoteGroup::Attachments)) - ->GetAttachmentList(); - - attachments[WiimoteEmu::ExtensionNumber::NUNCHUK]->ClearInputOverrideFunction(); - attachments[WiimoteEmu::ExtensionNumber::CLASSIC]->ClearInputOverrideFunction(); - - for (size_t i = ControlID::FIRST_WII_CONTROL; i <= ControlID::LAST_WII_CONTROL; ++i) - s_state_arrays[controller_index][i].overriding = false; -} - -void SetControlState(int controller_index, ControlID control, double state) -{ - InputState& input_state = s_state_arrays[controller_index][control]; - - input_state.override_state = state; - input_state.overriding = true; -} - -void ClearControlState(int controller_index, ControlID control) -{ - InputState& input_state = s_state_arrays[controller_index][control]; - - input_state.overriding = false; -} - -double GetGateRadiusAtAngle(int controller_index, ControlID stick, double angle) -{ - ControllerEmu::ControlGroup* group; - - switch (stick) - { - case ControlID::GCPAD_MAIN_STICK_X: - case ControlID::GCPAD_MAIN_STICK_Y: - group = Pad::GetGroup(controller_index, PadGroup::MainStick); - break; - case ControlID::GCPAD_C_STICK_X: - case ControlID::GCPAD_C_STICK_Y: - group = Pad::GetGroup(controller_index, PadGroup::CStick); - break; - case ControlID::NUNCHUK_STICK_X: - case ControlID::NUNCHUK_STICK_Y: - group = Wiimote::GetNunchukGroup(controller_index, WiimoteEmu::NunchukGroup::Stick); - break; - case ControlID::CLASSIC_LEFT_STICK_X: - case ControlID::CLASSIC_LEFT_STICK_Y: - group = Wiimote::GetClassicGroup(controller_index, WiimoteEmu::ClassicGroup::LeftStick); - break; - case ControlID::CLASSIC_RIGHT_STICK_X: - case ControlID::CLASSIC_RIGHT_STICK_Y: - group = Wiimote::GetClassicGroup(controller_index, WiimoteEmu::ClassicGroup::RightStick); - break; - default: - ASSERT(false); - return 0; - } - - return static_cast(group)->GetGateRadiusAtAngle(angle); -} -} // namespace ciface::Touch diff --git a/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.h b/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.h deleted file mode 100644 index 6217b44d4e..0000000000 --- a/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.h +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2021 Dolphin Emulator Project -// Licensed under GPLv2+ -// Refer to the license.txt file included. - -#pragma once - -namespace ciface::Touch -{ -enum ControlID -{ - GCPAD_A_BUTTON = 0, - GCPAD_B_BUTTON = 1, - GCPAD_X_BUTTON = 2, - GCPAD_Y_BUTTON = 3, - GCPAD_Z_BUTTON = 4, - GCPAD_START_BUTTON = 5, - GCPAD_DPAD_UP = 6, - GCPAD_DPAD_DOWN = 7, - GCPAD_DPAD_LEFT = 8, - GCPAD_DPAD_RIGHT = 9, - GCPAD_L_DIGITAL = 10, - GCPAD_R_DIGITAL = 11, - GCPAD_L_ANALOG = 12, - GCPAD_R_ANALOG = 13, - GCPAD_MAIN_STICK_X = 14, - GCPAD_MAIN_STICK_Y = 15, - GCPAD_C_STICK_X = 16, - GCPAD_C_STICK_Y = 17, - - WIIMOTE_A_BUTTON = 18, - WIIMOTE_B_BUTTON = 19, - WIIMOTE_ONE_BUTTON = 20, - WIIMOTE_TWO_BUTTON = 21, - WIIMOTE_PLUS_BUTTON = 22, - WIIMOTE_MINUS_BUTTON = 23, - WIIMOTE_HOME_BUTTON = 24, - WIIMOTE_DPAD_UP = 25, - WIIMOTE_DPAD_DOWN = 26, - WIIMOTE_DPAD_LEFT = 27, - WIIMOTE_DPAD_RIGHT = 28, - WIIMOTE_IR_X = 29, - WIIMOTE_IR_Y = 30, - - NUNCHUK_C_BUTTON = 31, - NUNCHUK_Z_BUTTON = 32, - NUNCHUK_STICK_X = 33, - NUNCHUK_STICK_Y = 34, - - CLASSIC_A_BUTTON = 35, - CLASSIC_B_BUTTON = 36, - CLASSIC_X_BUTTON = 37, - CLASSIC_Y_BUTTON = 38, - CLASSIC_ZL_BUTTON = 39, - CLASSIC_ZR_BUTTON = 40, - CLASSIC_PLUS_BUTTON = 41, - CLASSIC_MINUS_BUTTON = 42, - CLASSIC_HOME_BUTTON = 43, - CLASSIC_DPAD_UP = 44, - CLASSIC_DPAD_DOWN = 45, - CLASSIC_DPAD_LEFT = 46, - CLASSIC_DPAD_RIGHT = 47, - CLASSIC_L_DIGITAL = 48, - CLASSIC_R_DIGITAL = 49, - CLASSIC_L_ANALOG = 50, - CLASSIC_R_ANALOG = 51, - CLASSIC_LEFT_STICK_X = 52, - CLASSIC_LEFT_STICK_Y = 53, - CLASSIC_RIGHT_STICK_X = 54, - CLASSIC_RIGHT_STICK_Y = 55, - - NUMBER_OF_CONTROLS, - - FIRST_GC_CONTROL = GCPAD_A_BUTTON, - LAST_GC_CONTROL = GCPAD_C_STICK_Y, - FIRST_WII_CONTROL = WIIMOTE_A_BUTTON, - LAST_WII_CONTROL = CLASSIC_RIGHT_STICK_Y, - -}; -void RegisterGameCubeInputOverrider(int controller_index); -void RegisterWiiInputOverrider(int controller_index); -void UnregisterGameCubeInputOverrider(int controller_index); -void UnregisterWiiInputOverrider(int controller_index); - -void SetControlState(int controller_index, ControlID control, double state); -void ClearControlState(int controller_index, ControlID control); - -// Angle is in radians and should be non-negative -double GetGateRadiusAtAngle(int controller_index, ControlID stick, double angle); -} // namespace ciface::Touch diff --git a/Source/Core/InputCommon/GCAdapter.cpp b/Source/Core/InputCommon/GCAdapter.cpp index 90007ebfa0..980ed6ba33 100644 --- a/Source/Core/InputCommon/GCAdapter.cpp +++ b/Source/Core/InputCommon/GCAdapter.cpp @@ -3,1095 +3,55 @@ #include "InputCommon/GCAdapter.h" -#ifdef ANDROID -#define GCADAPTER_USE_LIBUSB_IMPLEMENTATION false -#define GCADAPTER_USE_ANDROID_IMPLEMENTATION true -#elif defined(__LIBRETRO__) -#define GCADAPTER_USE_LIBUSB_IMPLEMENTATION false -#define GCADAPTER_USE_ANDROID_IMPLEMENTATION false -#elif defined(IPHONEOS) -#define GCADAPTER_USE_LIBUSB_IMPLEMENTATION false -#define GCADAPTER_USE_ANDROID_IMPLEMENTATION false -#else -#define GCADAPTER_USE_LIBUSB_IMPLEMENTATION true -#define GCADAPTER_USE_ANDROID_IMPLEMENTATION false -#endif - -#include -#include -#include -#include -#include - -using namespace std::chrono_literals; - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION -#include -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION -#include -#endif - -#include "Common/BitUtils.h" -#include "Common/Config/Config.h" -#include "Common/Event.h" -#include "Common/Flag.h" -#include "Common/Logging/Log.h" -#include "Common/Thread.h" -#include "Core/Config/MainSettings.h" -#include "Core/Core.h" -#include "Core/CoreTiming.h" -#include "Core/HW/SI/SI.h" -#include "Core/HW/SI/SI_Device.h" -#include "Core/HW/SystemTimers.h" -#include "Core/System.h" #include "InputCommon/GCPadStatus.h" -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION -#include "Common/ScopeGuard.h" -#include "Core/LibusbUtils.h" -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION -#include "jni/AndroidCommon/IDCache.h" -#endif - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION -#if defined(LIBUSB_API_VERSION) -#define LIBUSB_API_VERSION_EXIST 1 -#else -#define LIBUSB_API_VERSION_EXIST 0 -#endif - -#define LIBUSB_API_VERSION_ATLEAST(v) (LIBUSB_API_VERSION_EXIST && LIBUSB_API_VERSION >= (v)) -#define LIBUSB_API_HAS_HOTPLUG LIBUSB_API_VERSION_ATLEAST(0x01000102) -#endif - +// The libretro core reaches no USB adapter: the libusb and Android Java paths belonged to the +// standalone builds. A libretro frontend owns the controllers, so an adapter configured on a +// port simply never reports one connected. namespace GCAdapter { -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - -constexpr unsigned int USB_TIMEOUT_MS = 100; - -static bool CheckDeviceAccess(libusb_device* device); -static void AddGCAdapter(libusb_device* device); -static void ResetRumbleLockNeeded(); -#endif - -static void Reset(); -static void Setup(); -static void ProcessInputPayload(const u8* data, std::size_t size); -static void ReadThreadFunc(void*); -static void WriteThreadFunc(void*); - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION -enum class AdapterStatus +void Init() { - NotDetected, - Detected, - Error, -}; - -static std::atomic s_status = AdapterStatus::NotDetected; -static std::atomic s_adapter_error = LIBUSB_SUCCESS; -static libusb_device_handle* s_handle = nullptr; -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION -// Java classes -static jclass s_adapter_class; - -static bool s_detected = false; -static int s_fd = 0; -#endif - -enum class ControllerType : u8 -{ - None = 0, - Wired = 1, - Wireless = 2, -}; - -static std::array s_controller_rumble; - -constexpr size_t CONTROLLER_INPUT_PAYLOAD_EXPECTED_SIZE = 37; -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION -constexpr size_t CONTROLLER_OUTPUT_INIT_PAYLOAD_SIZE = 1; -#endif -constexpr size_t CONTROLLER_OUTPUT_RUMBLE_PAYLOAD_SIZE = 5; - -struct PortState -{ - GCPadStatus origin = {}; - GCPadStatus status = {}; - - ControllerType controller_type = ControllerType::None; - bool is_new_connection = false; -}; - -// Only access with s_mutex held! -static std::array s_port_states; - -static std::array s_controller_write_payload; -static std::atomic s_controller_write_payload_size{0}; - -static sthread_t* s_read_adapter_thread = nullptr; -static Common::Flag s_read_adapter_thread_running; -static sthread_t* s_write_adapter_thread = nullptr; -static Common::Flag s_write_adapter_thread_running; -static Common::Event s_write_happened; - -static std::mutex s_init_mutex; -static std::mutex s_read_mutex; -#if GCADAPTER_USE_ANDROID_IMPLEMENTATION -static std::mutex s_write_mutex; -#endif - -static sthread_t* s_adapter_detect_thread = nullptr; -static Common::Flag s_adapter_detect_thread_running; - -static Common::Event s_hotplug_event; - -static Common::Flag s_read_thread_has_init; -static Common::Flag s_adapter_reads_failing; - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION -static std::function s_detect_callback; - -#if defined(__FreeBSD__) && __FreeBSD__ >= 11 -static bool s_libusb_hotplug_enabled = true; -#else -static bool s_libusb_hotplug_enabled = false; -#endif -#if LIBUSB_API_HAS_HOTPLUG -static libusb_hotplug_callback_handle s_hotplug_handle; -#endif - -static std::unique_ptr s_libusb_context; - -static u8 s_endpoint_in = 0; -static u8 s_endpoint_out = 0; -#endif - -static u64 s_last_init = 0; - -static std::optional s_config_callback_id = std::nullopt; - -static std::atomic_bool s_is_adapter_wanted = false; -static std::array s_config_rumble_enabled{}; - -static std::atomic s_adapter_poll_rate{}; - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION || GCADAPTER_USE_ANDROID_IMPLEMENTATION -static void ReadThreadFunc(void*) -{ - Common::SetCurrentThreadName("GCAdapter Read Thread"); - NOTICE_LOG_FMT(CONTROLLERINTERFACE, "GCAdapter read thread started"); - -#if GCADAPTER_USE_ANDROID_IMPLEMENTATION - bool first_read = true; - JNIEnv* const env = IDCache::GetEnvForThread(); - - const jfieldID payload_field = env->GetStaticFieldID(s_adapter_class, "controllerPayload", "[B"); - jobject payload_object = env->GetStaticObjectField(s_adapter_class, payload_field); - auto* const java_controller_payload = reinterpret_cast(&payload_object); - - // Get function pointers - const jmethodID getfd_func = env->GetStaticMethodID(s_adapter_class, "getFd", "()I"); - const jmethodID input_func = env->GetStaticMethodID(s_adapter_class, "input", "()I"); - const jmethodID openadapter_func = env->GetStaticMethodID(s_adapter_class, "openAdapter", "()Z"); - - const bool connected = env->CallStaticBooleanMethod(s_adapter_class, openadapter_func); - - if (!connected) - { - s_fd = 0; - s_detected = false; - - NOTICE_LOG_FMT(CONTROLLERINTERFACE, "GC Adapter failed to open!"); - return; - } -#endif - - s_write_adapter_thread_running.Set(true); - s_write_adapter_thread = sthread_create(WriteThreadFunc, nullptr); - - std::chrono::steady_clock::time_point last_successful_read_time = - std::chrono::steady_clock::now(); - s_adapter_reads_failing.Clear(); - s_read_thread_has_init.Set(); - - // Reset rumble once on initial reading - ResetRumble(); - - // Measure poll rate for display in UI. - constexpr int POLL_RATE_MEASUREMENT_SAMPLE_COUNT = 50; - auto poll_rate_measurement_start_time = Clock::now(); - int poll_rate_measurement_count = 0; - - bool last_read_failed = false; - - while (s_read_adapter_thread_running.IsSet() && !s_adapter_reads_failing.IsSet()) - { -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - std::array input_buffer; - - int payload_size = 0; - int transfer_return_code = - libusb_interrupt_transfer(s_handle, s_endpoint_in, input_buffer.data(), - int(input_buffer.size()), &payload_size, USB_TIMEOUT_MS); - if (transfer_return_code == LIBUSB_SUCCESS) - { - ProcessInputPayload(input_buffer.data(), payload_size); - last_successful_read_time = std::chrono::steady_clock::now(); - last_read_failed = false; - } - else - { - if (last_read_failed) - { - INFO_LOG_FMT(CONTROLLERINTERFACE, "Read: libusb_interrupt_transfer failed: {}", - LibusbUtils::ErrorWrap(transfer_return_code)); - - // Prevents busy-looping when transfers return instantly e.g. on disconnect - Common::SleepCurrentThread(1); - } - else - { - ERROR_LOG_FMT(CONTROLLERINTERFACE, "Read: libusb_interrupt_transfer newly failed: {}", - LibusbUtils::ErrorWrap(transfer_return_code)); - } - last_read_failed = true; - - if (std::chrono::steady_clock::now() - last_successful_read_time > 500ms) - { - ERROR_LOG_FMT(CONTROLLERINTERFACE, "Read: continuously failing transfers, resetting."); - s_adapter_reads_failing.Set(); - } - } - -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - const int payload_size = env->CallStaticIntMethod(s_adapter_class, input_func); - jbyte* const java_data = env->GetByteArrayElements(*java_controller_payload, nullptr); - - ProcessInputPayload(reinterpret_cast(java_data), payload_size); - - env->ReleaseByteArrayElements(*java_controller_payload, java_data, 0); - - if (first_read) - { - first_read = false; - s_fd = env->CallStaticIntMethod(s_adapter_class, getfd_func); - } -#endif - - // Update poll rate measurement. - if (++poll_rate_measurement_count == POLL_RATE_MEASUREMENT_SAMPLE_COUNT) - { - const auto now = Clock::now(); - - const auto poll_rate = - POLL_RATE_MEASUREMENT_SAMPLE_COUNT / DT_s(now - poll_rate_measurement_start_time).count(); - s_adapter_poll_rate.store(poll_rate, std::memory_order_relaxed); - - poll_rate_measurement_start_time = now; - poll_rate_measurement_count = 0; - } - } - - // Terminate the write thread on leaving - if (s_write_adapter_thread_running.TestAndClear()) - { - s_controller_write_payload_size.store(0); - // Kick the waiting event - s_write_happened.Set(); - Common::JoinThread(s_write_adapter_thread); - } - -#if GCADAPTER_USE_ANDROID_IMPLEMENTATION - s_fd = 0; - s_detected = false; -#endif - - s_adapter_poll_rate.store(0.0, std::memory_order_relaxed); - - NOTICE_LOG_FMT(CONTROLLERINTERFACE, "GCAdapter read thread stopped"); -} -#endif - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION || GCADAPTER_USE_ANDROID_IMPLEMENTATION -static void WriteThreadFunc(void*) -{ - Common::SetCurrentThreadName("GCAdapter Write Thread"); - NOTICE_LOG_FMT(CONTROLLERINTERFACE, "GCAdapter write thread started"); - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - int size = 0; -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - JNIEnv* const env = IDCache::GetEnvForThread(); - const jmethodID output_func = env->GetStaticMethodID(s_adapter_class, "output", "([B)I"); -#endif - - while (s_write_adapter_thread_running.IsSet()) - { - s_write_happened.Wait(); - - const int write_size = s_controller_write_payload_size.load(); - if (write_size) - { -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - const int error = - libusb_interrupt_transfer(s_handle, s_endpoint_out, s_controller_write_payload.data(), - write_size, &size, USB_TIMEOUT_MS); - if (error != LIBUSB_SUCCESS) - { - ERROR_LOG_FMT(CONTROLLERINTERFACE, "Write: libusb_interrupt_transfer failed: {}", - LibusbUtils::ErrorWrap(error)); - } -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - const jbyteArray jrumble_array = env->NewByteArray(CONTROLLER_OUTPUT_RUMBLE_PAYLOAD_SIZE); - jbyte* const jrumble = env->GetByteArrayElements(jrumble_array, nullptr); - - { - std::lock_guard lk(s_write_mutex); - memcpy(jrumble, s_controller_write_payload.data(), write_size); - } - - env->ReleaseByteArrayElements(jrumble_array, jrumble, 0); - env->CallStaticIntMethod(s_adapter_class, output_func, jrumble_array); -#endif - } - - Common::YieldCPU(); - } - - NOTICE_LOG_FMT(CONTROLLERINTERFACE, "GCAdapter write thread stopped"); -} -#endif - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION -#if LIBUSB_API_HAS_HOTPLUG -static int HotplugCallback(libusb_context* ctx, libusb_device* dev, libusb_hotplug_event event, - void* user_data) -{ - if (event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED) - { - if (s_handle == nullptr) - s_hotplug_event.Set(); - } - else if (event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT) - { - if (s_handle != nullptr && libusb_get_device(s_handle) == dev) - Reset(); - - // Reset a potential error status now that the adapter is unplugged - if (s_status == AdapterStatus::Error) - { - s_status = AdapterStatus::NotDetected; - if (s_detect_callback != nullptr) - s_detect_callback(); - } - } - return 0; -} -#endif -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION -extern "C" { - -JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_utils_GCAdapter_onAdapterConnected(JNIEnv* env, jclass) -{ - INFO_LOG_FMT(CONTROLLERINTERFACE, "GC adapter connected"); - if (!s_detected) - s_hotplug_event.Set(); } -JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_utils_GCAdapter_onAdapterDisconnected(JNIEnv* env, jclass) +void Shutdown() { - INFO_LOG_FMT(CONTROLLERINTERFACE, "GC adapter disconnected"); - if (s_detected) - Reset(); -} -} -#endif - -static void ScanThreadFunc(void*) -{ - Common::SetCurrentThreadName("GC Adapter Scanning Thread"); - NOTICE_LOG_FMT(CONTROLLERINTERFACE, "GC Adapter scanning thread started"); - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION -#if LIBUSB_API_HAS_HOTPLUG -#ifndef __FreeBSD__ - s_libusb_hotplug_enabled = libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG) != 0; - // As of v1.0.23-rc1 (2026/02) libusb has hotplug event capability on Linux & MacOS, but not on - // Windows -#endif - if (s_libusb_hotplug_enabled) - { - const int error = libusb_hotplug_register_callback( - *s_libusb_context, - (libusb_hotplug_event)(LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | - LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT), - LIBUSB_HOTPLUG_ENUMERATE, 0x057e, 0x0337, LIBUSB_HOTPLUG_MATCH_ANY, HotplugCallback, - nullptr, &s_hotplug_handle); - if (error == LIBUSB_SUCCESS) - { - NOTICE_LOG_FMT(CONTROLLERINTERFACE, "Using libUSB hotplug detection"); - } - else - { - s_libusb_hotplug_enabled = false; - ERROR_LOG_FMT(CONTROLLERINTERFACE, "Failed to add libUSB hotplug detection callback: {}", - LibusbUtils::ErrorWrap(error)); - } - } -#endif - - while (s_adapter_detect_thread_running.IsSet()) - { - if (s_handle == nullptr) - { - std::lock_guard lk(s_init_mutex); - Setup(); - } - - if (s_libusb_hotplug_enabled) - s_hotplug_event.WaitFor(1000ms); - else - Common::SleepCurrentThread(500); - - if (s_read_thread_has_init.IsSet() && s_adapter_reads_failing.IsSet()) - Reset(); - } -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - JNIEnv* const env = IDCache::GetEnvForThread(); - - const jmethodID enable_hotplug_callback_func = - env->GetStaticMethodID(s_adapter_class, "enableHotplugCallback", "()V"); - env->CallStaticVoidMethod(s_adapter_class, enable_hotplug_callback_func); - - const jmethodID is_usb_device_available_func = - env->GetStaticMethodID(s_adapter_class, "isUsbDeviceAvailable", "()Z"); - - while (s_adapter_detect_thread_running.IsSet()) - { - if (!s_detected && s_is_adapter_wanted.load(std::memory_order_relaxed) && - env->CallStaticBooleanMethod(s_adapter_class, is_usb_device_available_func)) - { - std::lock_guard lk(s_init_mutex); - Setup(); - } - - s_hotplug_event.Wait(); - } -#endif - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION -#if LIBUSB_API_HAS_HOTPLUG - if (s_libusb_hotplug_enabled) - libusb_hotplug_deregister_callback(*s_libusb_context, s_hotplug_handle); -#endif -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - const jmethodID disable_hotplug_callback_func = - env->GetStaticMethodID(s_adapter_class, "disableHotplugCallback", "()V"); - env->CallStaticVoidMethod(s_adapter_class, disable_hotplug_callback_func); -#endif - - NOTICE_LOG_FMT(CONTROLLERINTERFACE, "GC Adapter scanning thread stopped"); } void SetAdapterCallback(std::function func) { -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - s_detect_callback = std::move(func); -#endif -} - -static void StartScanThread() -{ - if (s_adapter_detect_thread_running.IsSet()) - return; -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - if (!s_libusb_context->IsValid()) - return; -#endif - s_adapter_detect_thread_running.Set(true); - s_adapter_detect_thread = sthread_create(ScanThreadFunc, nullptr); -} - -static void StopScanThread() -{ - if (s_adapter_detect_thread_running.TestAndClear()) - { - s_hotplug_event.Set(); - Common::JoinThread(s_adapter_detect_thread); - } -} - -static void RefreshConfig() -{ - bool is_adapter_wanted = false; - - for (int i = 0; i < SerialInterface::MAX_SI_CHANNELS; ++i) - { - is_adapter_wanted |= Config::Get(Config::GetInfoForSIDevice(i)) == - SerialInterface::SIDevices::SIDEVICE_WIIU_ADAPTER; - s_config_rumble_enabled[i].store(Config::Get(Config::GetInfoForAdapterRumble(i)), - std::memory_order_relaxed); - } - - s_is_adapter_wanted.store(is_adapter_wanted, std::memory_order_relaxed); - - if (is_adapter_wanted) - StartScanThread(); - else - StopScanThread(); -} - -void Init() -{ -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - if (s_handle != nullptr) - return; - - s_libusb_context = std::make_unique(); -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - if (s_fd) - return; -#endif - - auto& system = Core::System::GetInstance(); - if (const Core::State state = Core::GetState(system); - state != Core::State::Uninitialized && state != Core::State::Starting) - { - auto& core_timing = system.GetCoreTiming(); - if ((core_timing.GetTicks() - s_last_init) < system.GetSystemTimers().GetTicksPerSecond()) - return; - - s_last_init = core_timing.GetTicks(); - } - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - s_status = AdapterStatus::NotDetected; - s_adapter_error = LIBUSB_SUCCESS; -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - JNIEnv* const env = IDCache::GetEnvForThread(); - - const jclass adapter_class = env->FindClass("org/dolphinemu/dolphinemu/utils/GCAdapter"); - s_adapter_class = reinterpret_cast(env->NewGlobalRef(adapter_class)); -#endif - - if (!s_config_callback_id) - s_config_callback_id = Config::AddConfigChangedCallback(RefreshConfig); - RefreshConfig(); -} - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION || GCADAPTER_USE_ANDROID_IMPLEMENTATION -static void Setup() -{ -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - const AdapterStatus prev_status = s_status; - - // Reset the error status in case the adapter gets unplugged - if (s_status == AdapterStatus::Error) - s_status = AdapterStatus::NotDetected; - - s_port_states.fill({}); - s_controller_rumble.fill(0); - - const int ret = s_libusb_context->GetDeviceList([](libusb_device* device) { - if (CheckDeviceAccess(device)) - { - // Only connect to a single adapter in case the user has multiple connected - AddGCAdapter(device); - return false; - } - return true; - }); - if (ret != LIBUSB_SUCCESS) - WARN_LOG_FMT(CONTROLLERINTERFACE, "Failed to get device list: {}", LibusbUtils::ErrorWrap(ret)); - - if (s_status != AdapterStatus::Detected && prev_status != s_status && - s_detect_callback != nullptr) - s_detect_callback(); -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - s_fd = 0; - s_detected = true; - - // Make sure the thread isn't in the middle of shutting down while starting a new one - if (s_read_adapter_thread_running.TestAndClear()) - Common::JoinThread(s_read_adapter_thread); - - s_read_adapter_thread_running.Set(true); - s_read_adapter_thread = sthread_create(ReadThreadFunc, nullptr); -#endif -} -#endif - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION -static bool CheckDeviceAccess(libusb_device* device) -{ - libusb_device_descriptor desc; - int ret = libusb_get_device_descriptor(device, &desc); - if (ret != LIBUSB_SUCCESS) - { - // could not acquire the descriptor, no point in trying to use it. - ERROR_LOG_FMT(CONTROLLERINTERFACE, "libusb_get_device_descriptor failed: {}", - LibusbUtils::ErrorWrap(ret)); - return false; - } - - if (desc.idVendor != 0x057e || desc.idProduct != 0x0337) - { - // This isn’t the device we are looking for. - return false; - } - - const u8 bus = libusb_get_bus_number(device); - const u8 port = libusb_get_device_address(device); - - NOTICE_LOG_FMT(CONTROLLERINTERFACE, - "Found GC Adapter [Bus:{:03d}, Address:{:03d}, VID:" - "{:04X}, PID:{:04X}]", - bus, port, desc.idVendor, desc.idProduct); - - // In case of failure, capture the libusb error code into the adapter status - Common::ScopeGuard status_guard([&ret] { - s_adapter_error = static_cast(ret); - s_status = AdapterStatus::Error; - }); - - ret = libusb_open(device, &s_handle); - if (ret != LIBUSB_SUCCESS) - { - ERROR_LOG_FMT(CONTROLLERINTERFACE, - "libusb_open failed to open GC Adapter [Bus:{:03d}, Address:{:03d}, VID:" - "{:04X}, PID:{:04X}] with error {}", - bus, port, desc.idVendor, desc.idProduct, LibusbUtils::ErrorWrap(ret)); - return false; - } - - bool detach_failed = false; - ret = libusb_kernel_driver_active(s_handle, 0); - if (ret == 1) // 1: kernel driver is active - { - // On macos detaching would fail without root or entitlement. - // We assume user is using GCAdapterDriver and therefore don't want to detach anything -#if !defined(__APPLE__) - ret = libusb_detach_kernel_driver(s_handle, 0); - detach_failed = - ret < LIBUSB_SUCCESS && ret != LIBUSB_ERROR_NOT_FOUND && ret != LIBUSB_ERROR_NOT_SUPPORTED; -#endif - if (detach_failed) - { - ERROR_LOG_FMT(CONTROLLERINTERFACE, "libusb_detach_kernel_driver failed: {}", - LibusbUtils::ErrorWrap(ret)); - } - } - else if (ret != 0) // 0: kernel driver is not active, but otherwise no error. - { - // Neither 0 nor 1 means an error occured. - if (ret == LIBUSB_ERROR_NOT_SUPPORTED) - { - // Expected outside Linux - INFO_LOG_FMT(CONTROLLERINTERFACE, "libusb_kernel_driver_active failed: {}", - LibusbUtils::ErrorWrap(ret)); - } - else - { - ERROR_LOG_FMT(CONTROLLERINTERFACE, "libusb_kernel_driver_active failed: {}", - LibusbUtils::ErrorWrap(ret)); - } - } - - // This call makes Nyko-brand (and perhaps other) adapters work. - // However it returns LIBUSB_ERROR_PIPE with Mayflash adapters. - const int transfer = libusb_control_transfer(s_handle, 0x21, 11, 0x0001, 0, nullptr, 0, 1000); - if (transfer < LIBUSB_SUCCESS) - { - WARN_LOG_FMT(CONTROLLERINTERFACE, "libusb_control_transfer failed: {}", - LibusbUtils::ErrorWrap(transfer)); - } - - // this split is needed so that we don't avoid claiming the interface when - // detaching the kernel driver is successful - if (detach_failed) - { - libusb_close(s_handle); - s_handle = nullptr; - return false; - } - - ret = libusb_claim_interface(s_handle, 0); - if (ret != LIBUSB_SUCCESS) - { - ERROR_LOG_FMT(CONTROLLERINTERFACE, "libusb_claim_interface failed: {}", - LibusbUtils::ErrorWrap(ret)); - libusb_close(s_handle); - s_handle = nullptr; - return false; - } - - // Updating the adapter status will be done in AddGCAdapter - status_guard.Dismiss(); - - return true; -} - -static void AddGCAdapter(libusb_device* device) -{ - auto [error, config] = LibusbUtils::MakeConfigDescriptor(device); - if (error != LIBUSB_SUCCESS) - { - WARN_LOG_FMT(CONTROLLERINTERFACE, "libusb_get_config_descriptor failed: {}", - LibusbUtils::ErrorWrap(error)); - } - for (u8 ic = 0; ic < config->bNumInterfaces; ic++) - { - const libusb_interface* interfaceContainer = &config->interface[ic]; - for (int i = 0; i < interfaceContainer->num_altsetting; i++) - { - const libusb_interface_descriptor* interface = &interfaceContainer->altsetting[i]; - for (u8 e = 0; e < interface->bNumEndpoints; e++) - { - const libusb_endpoint_descriptor* endpoint = &interface->endpoint[e]; - if (endpoint->bEndpointAddress & LIBUSB_ENDPOINT_IN) - s_endpoint_in = endpoint->bEndpointAddress; - else - s_endpoint_out = endpoint->bEndpointAddress; - } - } - } - config.reset(); - - int size = 0; - std::array payload = {0x13}; - error = libusb_interrupt_transfer(s_handle, s_endpoint_out, payload.data(), - CONTROLLER_OUTPUT_INIT_PAYLOAD_SIZE, &size, USB_TIMEOUT_MS); - if (error != LIBUSB_SUCCESS) - { - WARN_LOG_FMT(CONTROLLERINTERFACE, "AddGCAdapter: libusb_interrupt_transfer failed: {}", - LibusbUtils::ErrorWrap(error)); - } - - s_read_adapter_thread_running.Set(true); - s_read_adapter_thread = sthread_create(ReadThreadFunc, nullptr); - - s_status = AdapterStatus::Detected; - if (s_detect_callback != nullptr) - s_detect_callback(); - ResetRumbleLockNeeded(); -} -#endif - -void Shutdown() -{ - StopScanThread(); - Reset(); - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - s_libusb_context.reset(); - s_status = AdapterStatus::NotDetected; -#endif - - if (s_config_callback_id) - { - Config::RemoveConfigChangedCallback(*s_config_callback_id); - s_config_callback_id = std::nullopt; - } -} - -static void Reset() -{ - std::unique_lock lock(s_init_mutex, std::defer_lock); - if (!lock.try_lock()) - return; -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - if (s_status != AdapterStatus::Detected) - return; -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - if (!s_detected) - return; -#endif - - if (s_read_adapter_thread_running.TestAndClear()) - Common::JoinThread(s_read_adapter_thread); - // The read thread will close the write thread - - s_read_thread_has_init.Clear(); - - s_port_states.fill({}); - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - s_status = AdapterStatus::NotDetected; - - if (s_handle) - { - const int error = libusb_release_interface(s_handle, 0); - if (error != LIBUSB_SUCCESS) - { - WARN_LOG_FMT(CONTROLLERINTERFACE, "libusb_release_interface failed: {}", - LibusbUtils::ErrorWrap(error)); - } - libusb_close(s_handle); - s_handle = nullptr; - } - if (s_detect_callback != nullptr) - s_detect_callback(); -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - s_detected = false; - s_fd = 0; -#endif - - NOTICE_LOG_FMT(CONTROLLERINTERFACE, "GC Adapter detached"); } GCPadStatus Input(int chan) { - if (!s_is_adapter_wanted.load(std::memory_order_relaxed)) - return {}; - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - if (s_handle == nullptr || s_status != AdapterStatus::Detected) - return {}; -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - if (!s_detected || !s_fd) - return {}; -#endif - - std::lock_guard lk(s_read_mutex); - - auto& pad_state = s_port_states[chan]; - - // Return the "origin" state for the first input on a new connection. - if (pad_state.is_new_connection) - { - pad_state.is_new_connection = false; - return pad_state.origin; - } - - return pad_state.status; + return {}; } -// Get ControllerType from first byte in input payload. -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION || GCADAPTER_USE_ANDROID_IMPLEMENTATION -static ControllerType IdentifyControllerType(u8 data) -{ - if (Common::ExtractBit<4>(data)) - return ControllerType::Wired; - - if (Common::ExtractBit<5>(data)) - return ControllerType::Wireless; - - return ControllerType::None; -} -#endif - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION || GCADAPTER_USE_ANDROID_IMPLEMENTATION -void ProcessInputPayload(const u8* data, std::size_t size) -{ - if (size != CONTROLLER_INPUT_PAYLOAD_EXPECTED_SIZE -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - || data[0] != LIBUSB_DT_HID -#endif - ) - { - // This can occur for a few frames on initialization. - ERROR_LOG_FMT(CONTROLLERINTERFACE, "error reading payload (size: {}, type: {:02x})", size, - data[0]); - } - else - { - std::lock_guard lk(s_read_mutex); - - for (int chan = 0; chan != SerialInterface::MAX_SI_CHANNELS; ++chan) - { - const u8* const channel_data = &data[1 + (9 * chan)]; - - const auto type = IdentifyControllerType(channel_data[0]); - - auto& pad_state = s_port_states[chan]; - - GCPadStatus pad = {}; - - if (type != ControllerType::None) - { - const u8 b1 = channel_data[1]; - const u8 b2 = channel_data[2]; - - if (Common::ExtractBit<0>(b1)) - pad.button |= PAD_BUTTON_A; - if (Common::ExtractBit<1>(b1)) - pad.button |= PAD_BUTTON_B; - if (Common::ExtractBit<2>(b1)) - pad.button |= PAD_BUTTON_X; - if (Common::ExtractBit<3>(b1)) - pad.button |= PAD_BUTTON_Y; - - if (Common::ExtractBit<4>(b1)) - pad.button |= PAD_BUTTON_LEFT; - if (Common::ExtractBit<5>(b1)) - pad.button |= PAD_BUTTON_RIGHT; - if (Common::ExtractBit<6>(b1)) - pad.button |= PAD_BUTTON_DOWN; - if (Common::ExtractBit<7>(b1)) - pad.button |= PAD_BUTTON_UP; - - if (Common::ExtractBit<0>(b2)) - pad.button |= PAD_BUTTON_START; - if (Common::ExtractBit<1>(b2)) - pad.button |= PAD_TRIGGER_Z; - if (Common::ExtractBit<2>(b2)) - pad.button |= PAD_TRIGGER_R; - if (Common::ExtractBit<3>(b2)) - pad.button |= PAD_TRIGGER_L; - - pad.stickX = channel_data[3]; - pad.stickY = channel_data[4]; - pad.substickX = channel_data[5]; - pad.substickY = channel_data[6]; - pad.triggerLeft = channel_data[7]; - pad.triggerRight = channel_data[8]; - } - else if (!Core::WantsDeterminism()) - { - // This is a hack to prevent a desync due to SI devices - // being different and returning different values. - // The corresponding code in DeviceGCAdapter has the same check - pad.button = PAD_ERR_STATUS; - } - - if (type != ControllerType::None && pad_state.controller_type == ControllerType::None) - { - NOTICE_LOG_FMT(CONTROLLERINTERFACE, "New device connected to Port {} of Type: {:02x}", - chan + 1, channel_data[0]); - - pad.button |= PAD_GET_ORIGIN; - pad_state.origin = pad; - pad_state.is_new_connection = true; - } - - pad_state.controller_type = type; - pad_state.status = pad; - } - } -} -#endif - bool DeviceConnected(int chan) { - std::lock_guard lk(s_read_mutex); - return s_port_states[chan].controller_type != ControllerType::None; + return false; } void ResetDeviceType(int chan) { - std::lock_guard lk(s_read_mutex); - s_port_states[chan].controller_type = ControllerType::None; } void ResetRumble() { -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - std::unique_lock lock(s_init_mutex, std::defer_lock); - if (!lock.try_lock()) - return; - ResetRumbleLockNeeded(); -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - std::array rumble = {0x11, 0, 0, 0, 0}; - { - std::lock_guard lk(s_write_mutex); - s_controller_write_payload = rumble; - s_controller_write_payload_size.store(CONTROLLER_OUTPUT_RUMBLE_PAYLOAD_SIZE); - } - s_write_happened.Set(); -#endif } -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION -// Needs to be called when s_init_mutex is locked in order to avoid -// being called while the libusb state is being reset -static void ResetRumbleLockNeeded() -{ - if (s_handle == nullptr || s_status != AdapterStatus::Detected || - !s_is_adapter_wanted.load(std::memory_order_relaxed)) - { - return; - } - - s_controller_rumble.fill(0); - - std::array rumble = { - 0x11, s_controller_rumble[0], s_controller_rumble[1], s_controller_rumble[2], - s_controller_rumble[3]}; - - int size = 0; - const int error = - libusb_interrupt_transfer(s_handle, s_endpoint_out, rumble.data(), - CONTROLLER_OUTPUT_RUMBLE_PAYLOAD_SIZE, &size, USB_TIMEOUT_MS); - if (error != LIBUSB_SUCCESS) - { - WARN_LOG_FMT(CONTROLLERINTERFACE, "ResetRumbleLockNeeded: libusb_interrupt_transfer failed: {}", - LibusbUtils::ErrorWrap(error)); - } - - INFO_LOG_FMT(CONTROLLERINTERFACE, "Rumble state reset"); -} -#endif - void Output(int chan, u8 rumble_command) { - if (!s_is_adapter_wanted.load(std::memory_order_relaxed) || - !s_config_rumble_enabled[chan].load(std::memory_order_relaxed)) - return; - -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - if (s_handle == nullptr) - return; -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - if (!s_detected || !s_fd) - return; -#endif - - // Skip over rumble commands if it has not changed or the controller is wireless - if (rumble_command != s_controller_rumble[chan] && - s_port_states[chan].controller_type != ControllerType::Wireless) - { - s_controller_rumble[chan] = rumble_command; - std::array rumble = { - 0x11, s_controller_rumble[0], s_controller_rumble[1], s_controller_rumble[2], - s_controller_rumble[3]}; - { -#if GCADAPTER_USE_ANDROID_IMPLEMENTATION - std::lock_guard lk(s_write_mutex); -#endif - s_controller_write_payload = rumble; - s_controller_write_payload_size.store(CONTROLLER_OUTPUT_RUMBLE_PAYLOAD_SIZE); - } - s_write_happened.Set(); - } } bool IsDetected(const char** error_message) { -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION - if (s_status != AdapterStatus::Error) - { - if (error_message) - *error_message = nullptr; - - return s_status == AdapterStatus::Detected; - } - - if (error_message) - *error_message = libusb_strerror(s_adapter_error.load()); - -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - return s_detected; -#endif return false; } double GetCurrentPollRate() { - return s_adapter_poll_rate.load(std::memory_order_relaxed); + return 0.0; } } // namespace GCAdapter diff --git a/Source/Core/VideoBackends/D3D/D3DNativeVertexFormat.cpp b/Source/Core/VideoBackends/D3D/D3DNativeVertexFormat.cpp index fbb5611528..631d164847 100644 --- a/Source/Core/VideoBackends/D3D/D3DNativeVertexFormat.cpp +++ b/Source/Core/VideoBackends/D3D/D3DNativeVertexFormat.cpp @@ -15,7 +15,6 @@ namespace DX11 { -std::mutex s_input_layout_lock; std::unique_ptr Gfx::CreateNativeVertexFormat(const PortableVertexDeclaration& vtx_decl) diff --git a/Source/Core/VideoBackends/D3D/D3DState.cpp b/Source/Core/VideoBackends/D3D/D3DState.cpp index 6eb8fc1e94..6b6fc4b888 100644 --- a/Source/Core/VideoBackends/D3D/D3DState.cpp +++ b/Source/Core/VideoBackends/D3D/D3DState.cpp @@ -345,14 +345,35 @@ void StateManager::Restore() #endif } // namespace D3D -StateCache::~StateCache() = default; +// Creating the same state twice is harmless (D3D11 hands back the same object), so a thread +// that loses the race releases its copy and returns the published one. +template +static T* Publish(retro_atomic_ptr_t* slot, ComPtr&& state) +{ + T* const created = state.Detach(); + if (!created || retro_atomic_cas_ptr(slot, nullptr, created)) + return created; + created->Release(); + return static_cast(retro_atomic_load_acquire_ptr(slot)); +} + +StateCache::~StateCache() +{ + const auto release = [](retro_atomic_ptr_t* slot) { + if (auto* state = static_cast(retro_atomic_exchange_ptr(slot, nullptr))) + state->Release(); + }; + m_sampler.ForEach(release); + m_blend.ForEach(release); + m_raster.ForEach(release); + m_depth.ForEach(release); +} ID3D11SamplerState* StateCache::Get(SamplerState state) { - std::lock_guard guard(m_lock); - auto it = m_sampler.find(state); - if (it != m_sampler.end()) - return it->second.Get(); + retro_atomic_ptr_t* const slot = m_sampler.Slot(state); + if (auto* cached = static_cast(retro_atomic_load_acquire_ptr(slot))) + return cached; D3D11_SAMPLER_DESC sampdc = CD3D11_SAMPLER_DESC(CD3D11_DEFAULT()); if (state.tm0.mipmap_filter == FilterMode::Linear) @@ -395,15 +416,14 @@ ID3D11SamplerState* StateCache::Get(SamplerState state) ComPtr res; HRESULT hr = D3D::device->CreateSamplerState(&sampdc, &res); ASSERT_MSG(VIDEO, SUCCEEDED(hr), "Creating D3D sampler state failed: {}", DX11HRWrap(hr)); - return m_sampler.emplace(state, std::move(res)).first->second.Get(); + return Publish(slot, std::move(res)); } ID3D11BlendState* StateCache::Get(BlendingState state) { - std::lock_guard guard(m_lock); - auto it = m_blend.find(state.hex); - if (it != m_blend.end()) - return it->second.Get(); + retro_atomic_ptr_t* const slot = m_blend.Slot(state.hex); + if (auto* cached = static_cast(retro_atomic_load_acquire_ptr(slot))) + return cached; if (state.logic_op_enable && g_backend_info.bSupportsLogicOp) { @@ -430,7 +450,7 @@ ID3D11BlendState* StateCache::Get(BlendingState state) HRESULT hr = D3D::device1->CreateBlendState1(&desc, &res); if (SUCCEEDED(hr)) { - return m_blend.emplace(state.hex, std::move(res)).first->second.Get(); + return Publish(slot, std::move(res)); } WARN_LOG_FMT(VIDEO, "Creating D3D blend state failed with an error: {}", DX11HRWrap(hr)); } @@ -472,15 +492,14 @@ ID3D11BlendState* StateCache::Get(BlendingState state) ComPtr res; HRESULT hr = D3D::device->CreateBlendState(&desc, &res); ASSERT_MSG(VIDEO, SUCCEEDED(hr), "Creating D3D blend state failed: {}", DX11HRWrap(hr)); - return m_blend.emplace(state.hex, std::move(res)).first->second.Get(); + return Publish(slot, std::move(res)); } ID3D11RasterizerState* StateCache::Get(RasterizationState state) { - std::lock_guard guard(m_lock); - auto it = m_raster.find(state.hex); - if (it != m_raster.end()) - return it->second.Get(); + retro_atomic_ptr_t* const slot = m_raster.Slot(state.hex); + if (auto* cached = static_cast(retro_atomic_load_acquire_ptr(slot))) + return cached; static constexpr std::array cull_modes = { {D3D11_CULL_NONE, D3D11_CULL_BACK, D3D11_CULL_FRONT, D3D11_CULL_BACK}}; @@ -493,15 +512,14 @@ ID3D11RasterizerState* StateCache::Get(RasterizationState state) ComPtr res; HRESULT hr = D3D::device->CreateRasterizerState(&desc, &res); ASSERT_MSG(VIDEO, SUCCEEDED(hr), "Creating D3D rasterizer state failed: {}", DX11HRWrap(hr)); - return m_raster.emplace(state.hex, std::move(res)).first->second.Get(); + return Publish(slot, std::move(res)); } ID3D11DepthStencilState* StateCache::Get(DepthState state) { - std::lock_guard guard(m_lock); - auto it = m_depth.find(state.hex); - if (it != m_depth.end()) - return it->second.Get(); + retro_atomic_ptr_t* const slot = m_depth.Slot(state.hex); + if (auto* cached = static_cast(retro_atomic_load_acquire_ptr(slot))) + return cached; D3D11_DEPTH_STENCIL_DESC depthdc = CD3D11_DEPTH_STENCIL_DESC(CD3D11_DEFAULT()); @@ -535,7 +553,7 @@ ID3D11DepthStencilState* StateCache::Get(DepthState state) ComPtr res; HRESULT hr = D3D::device->CreateDepthStencilState(&depthdc, &res); ASSERT_MSG(VIDEO, SUCCEEDED(hr), "Creating D3D depth stencil state failed: {}", DX11HRWrap(hr)); - return m_depth.emplace(state.hex, std::move(res)).first->second.Get(); + return Publish(slot, std::move(res)); } D3D11_PRIMITIVE_TOPOLOGY StateCache::GetPrimitiveTopology(PrimitiveType primitive) diff --git a/Source/Core/VideoBackends/D3D/D3DState.h b/Source/Core/VideoBackends/D3D/D3DState.h index 9c7cca55de..07004bf228 100644 --- a/Source/Core/VideoBackends/D3D/D3DState.h +++ b/Source/Core/VideoBackends/D3D/D3DState.h @@ -7,9 +7,11 @@ #include #include #include -#include #include +#include + +#include "Common/AtomicHashMap.h" #include "Common/BitField.h" #include "Common/CommonTypes.h" #include "VideoBackends/D3D/D3DBase.h" @@ -36,11 +38,12 @@ public: static D3D11_PRIMITIVE_TOPOLOGY GetPrimitiveTopology(PrimitiveType primitive); private: - std::unordered_map> m_depth; - std::unordered_map> m_raster; - std::unordered_map> m_blend; - std::unordered_map> m_sampler; - std::mutex m_lock; + // Filled from the video thread and the shader compile workers alike; each slot holds one + // reference to its state object. + Common::AtomicSlotMap m_depth; + Common::AtomicSlotMap m_raster; + Common::AtomicSlotMap m_blend; + Common::AtomicSlotMap m_sampler; }; namespace D3D diff --git a/Source/Core/VideoBackends/OGL/ProgramShaderCache.cpp b/Source/Core/VideoBackends/OGL/ProgramShaderCache.cpp index c698afce88..18e60bb84f 100644 --- a/Source/Core/VideoBackends/OGL/ProgramShaderCache.cpp +++ b/Source/Core/VideoBackends/OGL/ProgramShaderCache.cpp @@ -50,7 +50,7 @@ static int num_failures = 0; static GLuint CurrentProgram = 0; ProgramShaderCache::PipelineProgramMap ProgramShaderCache::s_pipeline_programs; -std::mutex ProgramShaderCache::s_pipeline_program_lock; +quiesce_t ProgramShaderCache::s_pipeline_program_quiesce; static std::string s_glsl_header; static std::atomic s_shader_counter{0}; static thread_local bool s_is_shared_context = false; @@ -471,8 +471,11 @@ void ProgramShaderCache::Shutdown() s_last_VAO = 0; // All pipeline programs should have been released. - DEBUG_ASSERT(s_pipeline_programs.empty()); - s_pipeline_programs.clear(); + // Every pipeline is gone by now, and with it every program. + s_pipeline_programs.ForEach([](retro_atomic_ptr_t* slot) { + DEBUG_ASSERT(!retro_atomic_load_acquire_ptr(slot)); + }); + quiesce_deinit(&s_pipeline_program_quiesce); } void ProgramShaderCache::CreateAttributelessVAO() @@ -540,14 +543,14 @@ PipelineProgram* ProgramShaderCache::GetPipelineProgram(const GLVertexFormat* ve PipelineProgramKey key = {vertex_shader ? vertex_shader->GetID() : 0, geometry_shader ? geometry_shader->GetID() : 0, pixel_shader ? pixel_shader->GetID() : 0}; + retro_atomic_ptr_t* const slot = s_pipeline_programs.Slot(key); { - std::lock_guard guard{s_pipeline_program_lock}; - auto iter = s_pipeline_programs.find(key); - if (iter != s_pipeline_programs.end()) - { - iter->second->reference_count++; - return iter->second.get(); - } + quiesce_enter(&s_pipeline_program_quiesce); + auto* existing = static_cast(retro_atomic_load_acquire_ptr(slot)); + const bool referenced = existing && TryReference(existing); + quiesce_exit(&s_pipeline_program_quiesce); + if (referenced) + return existing; } std::unique_ptr prog = std::make_unique(); @@ -618,17 +621,6 @@ PipelineProgram* ProgramShaderCache::GetPipelineProgram(const GLVertexFormat* ve } } - // Lock to insert. A duplicate program may have been created in the meantime. - std::lock_guard guard{s_pipeline_program_lock}; - auto iter = s_pipeline_programs.find(key); - if (iter != s_pipeline_programs.end()) - { - // Destroy this program, and use the one which was created first. - prog->shader.Destroy(); - iter->second->reference_count++; - return iter->second.get(); - } - // Set program variables on the shader which will be returned. // This is only needed for drivers which don't support binding layout. prog->shader.SetProgramVariables(); @@ -638,8 +630,40 @@ PipelineProgram* ProgramShaderCache::GetPipelineProgram(const GLVertexFormat* ve if (s_is_shared_context) glFinish(); - auto ip = s_pipeline_programs.emplace(key, std::move(prog)); - return ip.first->second.get(); + // Publish it, unless another thread made the same program meanwhile; a dead program in the + // slot is simply replaced. + for (;;) + { + quiesce_enter(&s_pipeline_program_quiesce); + auto* existing = static_cast(retro_atomic_load_acquire_ptr(slot)); + if (existing && TryReference(existing)) + { + quiesce_exit(&s_pipeline_program_quiesce); + // Destroy this program, and use the one which was created first. + prog->shader.Destroy(); + return existing; + } + const bool published = retro_atomic_cas_ptr(slot, existing, prog.get()); + quiesce_exit(&s_pipeline_program_quiesce); + if (published) + return prog.release(); + } +} + +bool ProgramShaderCache::TryReference(PipelineProgram* prog) +{ + std::size_t count = prog->reference_count.load(); + while (count != 0) + { + if (prog->reference_count.compare_exchange_weak(count, count + 1)) + return true; + } + return false; +} + +void ProgramShaderCache::FreePipelineProgram(quiesce_node_t* node) +{ + delete reinterpret_cast(node); } void ProgramShaderCache::ReleasePipelineProgram(PipelineProgram* prog) @@ -649,10 +673,9 @@ void ProgramShaderCache::ReleasePipelineProgram(PipelineProgram* prog) prog->shader.Destroy(); - std::lock_guard guard{s_pipeline_program_lock}; - const auto iter = s_pipeline_programs.find(prog->key); - ASSERT(iter != s_pipeline_programs.end() && prog == iter->second.get()); - s_pipeline_programs.erase(iter); + // Unless a newer program already took its slot. + retro_atomic_cas_ptr(s_pipeline_programs.Slot(prog->key), prog, nullptr); + quiesce_retire(&s_pipeline_program_quiesce, &prog->node, FreePipelineProgram); } void ProgramShaderCache::CreateHeader() diff --git a/Source/Core/VideoBackends/OGL/ProgramShaderCache.h b/Source/Core/VideoBackends/OGL/ProgramShaderCache.h index 1454385c3b..0f097f82a0 100644 --- a/Source/Core/VideoBackends/OGL/ProgramShaderCache.h +++ b/Source/Core/VideoBackends/OGL/ProgramShaderCache.h @@ -5,10 +5,11 @@ #include #include -#include #include #include +#include "Common/AtomicHashMap.h" +#include "Common/Quiesce.h" #include "Common/GL/GLExtensions/GLExtensions.h" #include "VideoCommon/AsyncShaderCompiler.h" @@ -58,6 +59,7 @@ struct PipelineProgramKeyHash struct PipelineProgram { + quiesce_node_t node{}; // first: the free callback casts it back PipelineProgramKey key{}; SHADER shader; std::atomic_size_t reference_count{1}; @@ -104,14 +106,18 @@ public: static void ReleasePipelineProgram(PipelineProgram* prog); private: + // Programs are made on the video thread and on the compile workers' shared contexts: one + // slot per key, holding the live program. A program whose count fell to 0 is dead and may + // be replaced; it is freed once no thread is looking at it. using PipelineProgramMap = - std::unordered_map, - PipelineProgramKeyHash>; + Common::AtomicSlotMap; + static bool TryReference(PipelineProgram* prog); + static void FreePipelineProgram(quiesce_node_t* node); static void CreateAttributelessVAO(); static PipelineProgramMap s_pipeline_programs; - static std::mutex s_pipeline_program_lock; + static quiesce_t s_pipeline_program_quiesce; static u32 s_ubo_buffer_size; static s32 s_ubo_align; diff --git a/Source/Core/VideoCommon/Assets/CustomAsset.cpp b/Source/Core/VideoCommon/Assets/CustomAsset.cpp index c01fb3b689..3105575d80 100644 --- a/Source/Core/VideoCommon/Assets/CustomAsset.cpp +++ b/Source/Core/VideoCommon/Assets/CustomAsset.cpp @@ -13,9 +13,16 @@ CustomAsset::CustomAsset(std::shared_ptr library, { } +quiesce_t* CustomAsset::DataQuiesce() +{ + static quiesce_t s_quiesce; + return &s_quiesce; +} + std::size_t CustomAsset::Load() { - std::lock_guard lk(m_info_lock); + if (!retro_atomic_cas_int(&m_busy, 0, 1)) + return 0; // The load time needs to come from before the data is actually read. // Using a time point from after the read marks the asset as more up-to-date than it actually is, // and has potential to race (and not be updated) if a change happens immediately after load. @@ -26,16 +33,21 @@ std::size_t CustomAsset::Load() { m_bytes_loaded = load_information.bytes_loaded; m_last_loaded_time = load_time; - return m_bytes_loaded; + retro_atomic_store_release_int(&m_busy, 0); + return load_information.bytes_loaded; } + retro_atomic_store_release_int(&m_busy, 0); return 0; } std::size_t CustomAsset::Unload() { - std::lock_guard lk(m_info_lock); + if (!retro_atomic_cas_int(&m_busy, 0, 1)) + return 0; UnloadImpl(); - return std::exchange(m_bytes_loaded, 0); + const std::size_t unloaded = std::exchange(m_bytes_loaded, 0); + retro_atomic_store_release_int(&m_busy, 0); + return unloaded; } CustomAsset::TimeType CustomAsset::GetLastLoadedTime() const diff --git a/Source/Core/VideoCommon/Assets/CustomAsset.h b/Source/Core/VideoCommon/Assets/CustomAsset.h index 6fe25a7fcf..647f8cfa6b 100644 --- a/Source/Core/VideoCommon/Assets/CustomAsset.h +++ b/Source/Core/VideoCommon/Assets/CustomAsset.h @@ -3,12 +3,14 @@ #pragma once +#include + +#include "Common/Quiesce.h" #include "Common/CommonTypes.h" #include "VideoCommon/Assets/CustomAssetLibrary.h" #include #include -#include namespace VideoCommon { @@ -28,13 +30,17 @@ public: CustomAsset& operator=(const CustomAsset&) = delete; CustomAsset& operator=(CustomAsset&&) = delete; - // Loads the asset from the library returning the number of bytes loaded + // Loads the asset from the library returning the number of bytes loaded. Returns 0 without + // doing anything when another thread is loading or unloading this asset right now. std::size_t Load(); // Unloads the asset data, resets the bytes loaded and - // returns the number of bytes unloaded + // returns the number of bytes unloaded; 0 if another thread is busy with it. std::size_t Unload(); + // Asset data is freed through this once no thread is reading it. + static quiesce_t* DataQuiesce(); + // Returns the time that the data was last loaded TimeType GetLastLoadedTime() const; @@ -55,8 +61,9 @@ private: CustomAssetLibrary::AssetID m_asset_id; std::size_t m_handle; - mutable std::mutex m_info_lock; - std::size_t m_bytes_loaded = 0; + // Held (with a CAS) by the one thread loading or unloading; anyone else backs off. + retro_atomic_int_t m_busy{}; + std::size_t m_bytes_loaded = 0; // only touched while busy std::atomic m_last_loaded_time = {}; }; @@ -78,24 +85,35 @@ public: // they want to handle reloads [[nodiscard]] std::shared_ptr GetData() const { - std::lock_guard lk(m_data_lock); - if (m_loaded) - return m_data; - return nullptr; + quiesce_enter(DataQuiesce()); + const auto* holder = static_cast(retro_atomic_load_acquire_ptr(&m_data)); + std::shared_ptr data = holder ? holder->data : nullptr; + quiesce_exit(DataQuiesce()); + return data; } + ~CustomLoadableAsset() override { delete static_cast(retro_atomic_exchange_ptr(&m_data, nullptr)); } + protected: - bool m_loaded = false; - mutable std::mutex m_data_lock; - std::shared_ptr m_data; + // Publishes freshly loaded data; null unloads. + void SetData(std::shared_ptr data) + { + Holder* const next = data ? new Holder{{}, std::move(data)} : nullptr; + if (void* old = retro_atomic_exchange_ptr(&m_data, next)) + quiesce_retire(DataQuiesce(), &static_cast(old)->node, FreeHolder); + } private: - void UnloadImpl() override + struct Holder { - std::lock_guard lk(m_data_lock); - m_loaded = false; - m_data.reset(); - } + quiesce_node_t node; // first: the free callback casts it back + std::shared_ptr data; + }; + static void FreeHolder(quiesce_node_t* node) { delete reinterpret_cast(node); } + + void UnloadImpl() override { SetData(nullptr); } + + mutable retro_atomic_ptr_t m_data{}; }; // A helper struct that contains diff --git a/Source/Core/VideoCommon/Assets/CustomAssetCache.cpp b/Source/Core/VideoCommon/Assets/CustomAssetCache.cpp index 3b15d5733e..5d04a089e5 100644 --- a/Source/Core/VideoCommon/Assets/CustomAssetCache.cpp +++ b/Source/Core/VideoCommon/Assets/CustomAssetCache.cpp @@ -43,14 +43,19 @@ void CustomAssetCache::Reset() m_pending_assets = {}; m_asset_handle_to_data.clear(); m_asset_id_to_handle.clear(); - m_dirty_assets.clear(); + for (mpsc_stack_node_t* node = mpsc_stack_drain(&m_dirty_assets); node;) + { + auto* dirty = reinterpret_cast(node); + node = node->next; + delete dirty; + } m_ram_used = 0; } void CustomAssetCache::MarkAssetDirty(const CustomAssetLibrary::AssetID& asset_id) { - std::lock_guard guard(m_dirty_mutex); - m_dirty_assets.insert(asset_id); + auto* dirty = new DirtyAsset{{}, asset_id}; + mpsc_stack_push(&m_dirty_assets, &dirty->link); } void CustomAssetCache::MarkAssetPending(CustomAsset* asset) @@ -85,10 +90,14 @@ void CustomAssetCache::Update() void CustomAssetCache::ProcessDirtyAssets() { - decltype(m_dirty_assets) dirty_assets; - - if (const auto lk = std::unique_lock{m_dirty_mutex, std::try_to_lock}) - std::swap(dirty_assets, m_dirty_assets); + std::set dirty_assets; + for (mpsc_stack_node_t* node = mpsc_stack_drain(&m_dirty_assets); node;) + { + auto* dirty = reinterpret_cast(node); + node = node->next; + dirty_assets.insert(std::move(dirty->id)); + delete dirty; + } const auto now = CustomAsset::ClockType::now(); for (const auto& asset_id : dirty_assets) diff --git a/Source/Core/VideoCommon/Assets/CustomAssetCache.h b/Source/Core/VideoCommon/Assets/CustomAssetCache.h index 169c0faaf8..cc5ac55d12 100644 --- a/Source/Core/VideoCommon/Assets/CustomAssetCache.h +++ b/Source/Core/VideoCommon/Assets/CustomAssetCache.h @@ -15,6 +15,7 @@ #include "VideoCommon/Assets/CustomAsset.h" #include "VideoCommon/Assets/CustomAssetLibrary.h" #include "VideoCommon/Assets/CustomAssetLoader.h" +#include namespace VideoCommon { @@ -190,8 +191,13 @@ private: // A calculated amount of memory to avoid exceeding. u64 m_max_ram_available = 0; - std::mutex m_dirty_mutex; - std::set m_dirty_assets; + // Asset IDs the file watcher saw change, taken by ProcessDirtyAssets. + struct DirtyAsset + { + mpsc_stack_node_t link; // first: the drained chain is cast back + CustomAssetLibrary::AssetID id; + }; + mpsc_stack_t m_dirty_assets{}; CustomAssetLoader m_asset_loader; }; diff --git a/Source/Core/VideoCommon/Assets/CustomAssetLoader.cpp b/Source/Core/VideoCommon/Assets/CustomAssetLoader.cpp index d13d9adc93..0a1e1a486c 100644 --- a/Source/Core/VideoCommon/Assets/CustomAssetLoader.cpp +++ b/Source/Core/VideoCommon/Assets/CustomAssetLoader.cpp @@ -60,11 +60,8 @@ void CustomAssetLoader::StopWorkerThreads() return; // Signal worker threads to stop, and wake all of them. - { - std::lock_guard guard(m_assets_to_load_lock); - m_exit_flag.Set(); - m_worker_thread_wake.notify_all(); - } + m_exit_flag.Set(); + retro_eventcount_notify(&m_worker_thread_wake); // Wait for worker threads to exit. for (WorkerThread& worker : m_worker_threads) @@ -79,6 +76,11 @@ void CustomAssetLoader::WorkerThreadEntry(void* data) worker->owner->WorkerThreadRun(worker->index); } +void CustomAssetLoader::FreeBatch(quiesce_node_t* node) +{ + delete reinterpret_cast(node); +} + void CustomAssetLoader::WorkerThreadRun(u32 thread_index) { #if defined(__LIBRETRO__) && defined(SKIP_ASSETLOADER_THREAD) @@ -86,60 +88,69 @@ void CustomAssetLoader::WorkerThreadRun(u32 thread_index) #endif Common::SetCurrentThreadName(fmt::format("Asset Loader {}", thread_index).c_str()); - std::unique_lock load_lock(m_assets_to_load_lock); - while (true) + while (!m_exit_flag.IsSet()) { - m_worker_thread_wake.wait(load_lock, - [&] { return !m_assets_to_load.empty() || m_exit_flag.IsSet(); }); - - if (m_exit_flag.IsSet()) - return; - - // If more memory than allowed has already been loaded, we will load nothing more - // until the next ScheduleAssetsToLoad from Manager. - if (m_change_in_memory > m_allowed_memory) + // Claim the next asset of the current batch, if it has one left. + CustomAsset* item = nullptr; + quiesce_enter(&m_batch_quiesce); + if (auto* batch = static_cast(retro_atomic_load_acquire_ptr(&m_batch))) { - m_assets_to_load.clear(); + // If more memory than allowed has already been loaded, we will load nothing more + // until the next ScheduleAssetsToLoad from Manager. + if (m_change_in_memory > batch->allowed_memory) + retro_atomic_store_release_size(&batch->cursor, batch->assets.size()); + + const size_t index = retro_atomic_fetch_add_size(&batch->cursor, 1); + if (index < batch->assets.size()) + item = batch->assets[index]; + } + quiesce_exit(&m_batch_quiesce); + + if (!item) + { + // Nothing left: park until a new batch or the exit flag. The check follows the + // registration, so a notify in between is not lost. + const int key = retro_eventcount_prepare_wait(&m_worker_thread_wake); + bool has_work = m_exit_flag.IsSet(); + quiesce_enter(&m_batch_quiesce); + if (const auto* batch = static_cast(retro_atomic_load_acquire_ptr(&m_batch))) + has_work |= retro_atomic_load_acquire_size(const_cast(&batch->cursor)) < + batch->assets.size(); + quiesce_exit(&m_batch_quiesce); + if (has_work) + retro_eventcount_cancel_wait(&m_worker_thread_wake); + else + retro_eventcount_commit_wait(&m_worker_thread_wake, key); continue; } - auto* const item = m_assets_to_load.front(); - m_assets_to_load.pop_front(); - - // Make sure another thread isn't loading this handle. - if (!m_handles_in_progress.insert(item->GetHandle()).second) - continue; - - load_lock.unlock(); - - // Unload previously loaded asset. + // Unload previously loaded asset. An asset another thread is busy with comes back 0 + // and is skipped; the Manager schedules it again. m_change_in_memory -= item->Unload(); const std::size_t bytes_loaded = item->Load(); m_change_in_memory += s64(bytes_loaded); - load_lock.lock(); + INFO_LOG_FMT(VIDEO, "CustomAssetLoader thread {} loaded: {} ({})", thread_index, + item->GetAssetId(), UICommon::FormatSize(bytes_loaded)); - { - INFO_LOG_FMT(VIDEO, "CustomAssetLoader thread {} loaded: {} ({})", thread_index, - item->GetAssetId(), UICommon::FormatSize(bytes_loaded)); - - std::lock_guard lk{m_assets_loaded_lock}; - m_asset_handles_loaded.emplace_back(item->GetHandle(), bytes_loaded > 0); - - // Make sure no other threads try to re-process this item. - // Manager will take the handles and re-ScheduleAssetsToLoad based on timestamps if needed. - std::erase(m_assets_to_load, item); - } - - m_handles_in_progress.erase(item->GetHandle()); + auto* loaded = new Loaded{{}, {item->GetHandle(), bytes_loaded > 0}}; + mpsc_stack_push(&m_loaded, &loaded->link); } } auto CustomAssetLoader::TakeLoadResults() -> LoadResults { - std::lock_guard guard(m_assets_loaded_lock); - return {std::move(m_asset_handles_loaded), m_change_in_memory.exchange(0)}; + LoadResults results{{}, 0}; + for (mpsc_stack_node_t* node = mpsc_stack_reverse(mpsc_stack_drain(&m_loaded)); node;) + { + auto* loaded = reinterpret_cast(node); + node = node->next; + results.asset_handles.push_back(loaded->handle); + delete loaded; + } + results.change_in_memory = m_change_in_memory.exchange(0); + return results; } void CustomAssetLoader::ScheduleAssetsToLoad(std::list assets_to_load, @@ -148,11 +159,13 @@ void CustomAssetLoader::ScheduleAssetsToLoad(std::list assets_to_l if (assets_to_load.empty()) [[unlikely]] return; - // There's new assets to process, notify worker threads - std::lock_guard guard(m_assets_to_load_lock); - m_allowed_memory = allowed_memory; - m_assets_to_load = std::move(assets_to_load); - m_worker_thread_wake.notify_all(); + // There's new assets to process, publish them and notify worker threads. + auto* batch = new Batch{{}, {assets_to_load.begin(), assets_to_load.end()}, + static_cast(allowed_memory), {}}; + retro_atomic_size_init(&batch->cursor, 0); + if (void* old = retro_atomic_exchange_ptr(&m_batch, batch)) + quiesce_retire(&m_batch_quiesce, &static_cast(old)->node, FreeBatch); + retro_eventcount_notify(&m_worker_thread_wake); } void CustomAssetLoader::Reset(bool restart_worker_threads) @@ -160,9 +173,10 @@ void CustomAssetLoader::Reset(bool restart_worker_threads) const std::size_t worker_thread_count = m_worker_threads.size(); StopWorkerThreads(); - m_assets_to_load.clear(); - m_asset_handles_loaded.clear(); - m_allowed_memory = 0; + // No worker runs now. + delete static_cast(retro_atomic_exchange_ptr(&m_batch, nullptr)); + quiesce_deinit(&m_batch_quiesce); + TakeLoadResults(); m_change_in_memory = 0; if (restart_worker_threads) diff --git a/Source/Core/VideoCommon/Assets/CustomAssetLoader.h b/Source/Core/VideoCommon/Assets/CustomAssetLoader.h index 6d26e55a84..703bf0f03e 100644 --- a/Source/Core/VideoCommon/Assets/CustomAssetLoader.h +++ b/Source/Core/VideoCommon/Assets/CustomAssetLoader.h @@ -4,14 +4,15 @@ #pragma once #include -#include #include -#include -#include #include +#include +#include +#include #include +#include "Common/Quiesce.h" #include "Common/Flag.h" #include "VideoCommon/Assets/CustomAsset.h" @@ -23,8 +24,8 @@ namespace VideoCommon class CustomAssetLoader { public: - CustomAssetLoader() = default; - ~CustomAssetLoader() = default; + CustomAssetLoader() { retro_eventcount_init(&m_worker_thread_wake); } + ~CustomAssetLoader() { retro_eventcount_free(&m_worker_thread_wake); } CustomAssetLoader(const CustomAssetLoader&) = delete; CustomAssetLoader(CustomAssetLoader&&) = delete; CustomAssetLoader& operator=(const CustomAssetLoader&) = delete; @@ -70,21 +71,31 @@ private: std::vector m_worker_threads; - std::mutex m_assets_to_load_lock; - std::list m_assets_to_load; + // One ScheduleAssetsToLoad's assets, published whole. Workers claim the next one with an + // atomic add on the cursor; a newer batch replaces it, and it is freed once no worker is + // still looking at it. + struct Batch + { + quiesce_node_t node; // first: the free callback casts it back + std::vector assets; + s64 allowed_memory; + retro_atomic_size_t cursor; + }; + static void FreeBatch(quiesce_node_t* node); + quiesce_t m_batch_quiesce{}; + retro_atomic_ptr_t m_batch{}; + // Notified after a batch is published or the exit flag is set. + retro_eventcount_t m_worker_thread_wake; - std::condition_variable m_worker_thread_wake; - - std::vector m_asset_handles_loaded; - - // Memory available to load new assets. - s64 m_allowed_memory = 0; + // Results for TakeLoadResults, pushed by the workers. + struct Loaded + { + mpsc_stack_node_t link; // first: the drained chain is cast back + AssetHandle handle; + }; + mpsc_stack_t m_loaded{}; // Change in memory from just-loaded/unloaded asset results yet to be taken by the Manager. std::atomic m_change_in_memory = 0; - - std::mutex m_assets_loaded_lock; - - std::set m_handles_in_progress; }; } // namespace VideoCommon diff --git a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp index a0b480a109..727a41c1b9 100644 --- a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp +++ b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp @@ -70,51 +70,31 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const Ass void DirectFilesystemAssetLibrary::SetAssetIDMapData(const AssetID& asset_id, VideoCommon::Assets::AssetMap asset_path_map) { - VideoCommon::Assets::AssetMap previous_asset_map; + if (const auto previous_asset_map = m_asset_id_to_asset_map_path.Get(asset_id)) { - std::lock_guard lk(m_asset_map_lock); - previous_asset_map = m_asset_id_to_asset_map_path[asset_id]; + for (const auto& [name, path] : *previous_asset_map) + m_path_to_asset_id.Erase(PathToString(path)); } - { - std::lock_guard lk(m_path_map_lock); - for (const auto& [name, path] : previous_asset_map) - { - m_path_to_asset_id.erase(PathToString(path)); - } + for (const auto& [name, path] : asset_path_map) + m_path_to_asset_id.Set(PathToString(path), asset_id); - for (const auto& [name, path] : asset_path_map) - { - m_path_to_asset_id[PathToString(path)] = asset_id; - } - } - - { - std::lock_guard lk(m_asset_map_lock); - m_asset_id_to_asset_map_path[asset_id] = std::move(asset_path_map); - } + m_asset_id_to_asset_map_path.Set(asset_id, std::move(asset_path_map)); } void DirectFilesystemAssetLibrary::PathModified(std::string_view path) { - std::lock_guard lk(m_path_map_lock); - if (const auto iter = m_path_to_asset_id.find(path); iter != m_path_to_asset_id.end()) + if (const auto asset_id = m_path_to_asset_id.Get(path)) { auto& system = Core::System::GetInstance(); auto& resource_manager = system.GetCustomResourceManager(); - resource_manager.MarkAssetDirty(iter->second); + resource_manager.MarkAssetDirty(*asset_id); } } VideoCommon::Assets::AssetMap DirectFilesystemAssetLibrary::GetAssetMapForID(const AssetID& asset_id) const { - std::lock_guard lk(m_asset_map_lock); - if (auto iter = m_asset_id_to_asset_map_path.find(asset_id); - iter != m_asset_id_to_asset_map_path.end()) - { - return iter->second; - } - return {}; + return m_asset_id_to_asset_map_path.Get(asset_id).value_or(VideoCommon::Assets::AssetMap{}); } } // namespace VideoCommon diff --git a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.h b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.h index e1a4cbd66c..34ae1bb461 100644 --- a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.h +++ b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.h @@ -5,7 +5,6 @@ #include #include #include -#include #include #include "VideoCommon/Assets/CustomTextureData.h" @@ -13,6 +12,8 @@ #include "VideoCommon/Assets/Types.h" #include "VideoCommon/Assets/WatchableFilesystemAssetLibrary.h" +#include "Common/AtomicHashMap.h" + namespace VideoCommon { // This class implements 'CustomAssetLibrary' and loads any assets @@ -33,10 +34,11 @@ private: // Gets the asset map given an asset id Assets::AssetMap GetAssetMapForID(const AssetID& asset_id) const; - mutable std::mutex m_asset_map_lock; - std::map m_asset_id_to_asset_map_path; - - mutable std::mutex m_path_map_lock; - std::map> m_path_to_asset_id; + // Written while mods and texture packs load, read by the loader threads and the file + // watcher: lock-free maps, each value swapped whole. + Common::AtomicHashMap> + m_asset_id_to_asset_map_path; + Common::AtomicHashMap> + m_path_to_asset_id; }; } // namespace VideoCommon diff --git a/Source/Core/VideoCommon/Assets/TextureAsset.cpp b/Source/Core/VideoCommon/Assets/TextureAsset.cpp index 9c2d8bdb13..0d9e2e921f 100644 --- a/Source/Core/VideoCommon/Assets/TextureAsset.cpp +++ b/Source/Core/VideoCommon/Assets/TextureAsset.cpp @@ -13,11 +13,7 @@ CustomAssetLibrary::LoadInfo TextureAsset::LoadImpl(const CustomAssetLibrary::As const auto loaded_info = m_owning_library->LoadTexture(asset_id, potential_data.get()); if (loaded_info.bytes_loaded == 0) return {}; - { - std::lock_guard lk(m_data_lock); - m_loaded = true; - m_data = std::move(potential_data); - } + SetData(std::move(potential_data)); return loaded_info; } } // namespace VideoCommon diff --git a/Source/Core/VideoCommon/BPStructs.cpp b/Source/Core/VideoCommon/BPStructs.cpp index e61e3862e4..67b4fdf5cb 100644 --- a/Source/Core/VideoCommon/BPStructs.cpp +++ b/Source/Core/VideoCommon/BPStructs.cpp @@ -14,7 +14,6 @@ #include "Common/Logging/Log.h" #include "Core/FifoPlayer/FifoPlayer.h" -#include "Core/FifoPlayer/FifoRecorder.h" #include "Core/HW/Memmap.h" #include "Core/HW/VideoInterface.h" #include "Core/System.h" @@ -417,9 +416,6 @@ static void BPWritten(PixelShaderManager& pixel_shader_manager, XFStateManager& auto& memory = system.GetMemory(); memory.CopyFromEmu(s_tex_mem.data() + tmem_addr, addr, tmem_transfer_count); - if (OpcodeDecoder::g_record_fifo_data) - system.GetFifoRecorder().UseMemory(addr, tmem_transfer_count, MemoryUpdate::Type::TMEM); - TMEM::InvalidateAll(); return; @@ -643,12 +639,6 @@ static void BPWritten(PixelShaderManager& pixel_shader_manager, XFStateManager& } } - if (OpcodeDecoder::g_record_fifo_data) - { - Core::System::GetInstance().GetFifoRecorder().UseMemory(src_addr, bytes_read, - MemoryUpdate::Type::TMEM); - } - TMEM::InvalidateAll(); } return; diff --git a/Source/Core/VideoCommon/OnScreenUI.cpp b/Source/Core/VideoCommon/OnScreenUI.cpp index 36499278c4..afa242796c 100644 --- a/Source/Core/VideoCommon/OnScreenUI.cpp +++ b/Source/Core/VideoCommon/OnScreenUI.cpp @@ -6,7 +6,6 @@ #include "Common/CommonPaths.h" #include "Common/EnumMap.h" #include "Common/FileUtil.h" -#include "Common/Profiler.h" #include "Common/Timer.h" #include "Core/Config/GraphicsSettings.h" @@ -305,10 +304,6 @@ void OnScreenUI::DrawDebugText() if (g_ActiveConfig.bOverlayScissorStats) g_stats.DisplayScissor(); - - const std::string profile_output = Common::Profiler::ToString(); - if (!profile_output.empty()) - ImGui::TextUnformatted(profile_output.c_str()); } void OnScreenUI::Finalize() diff --git a/Source/Core/VideoCommon/OpcodeDecoding.cpp b/Source/Core/VideoCommon/OpcodeDecoding.cpp index fc6f896cd7..689e1af822 100644 --- a/Source/Core/VideoCommon/OpcodeDecoding.cpp +++ b/Source/Core/VideoCommon/OpcodeDecoding.cpp @@ -16,7 +16,6 @@ #include "Common/Assert.h" #include "Common/Logging/Log.h" -#include "Core/FifoPlayer/FifoRecorder.h" #include "Core/HW/Memmap.h" #include "Core/System.h" #include "VideoCommon/BPMemory.h" @@ -32,7 +31,6 @@ namespace OpcodeDecoder { -bool g_record_fifo_data = false; template class RunCallback final : public Callback @@ -227,15 +225,6 @@ public: OPCODE_CALLBACK(void OnCommand(const u8* data, u32 size)) { ASSERT(size >= 1); - if constexpr (!is_preprocess) - { - // Display lists get added directly into the FIFO stream since this same callback is used to - // process them. - if (g_record_fifo_data && static_cast(data[0]) != Opcode::GX_CMD_CALL_DL) - { - Core::System::GetInstance().GetFifoRecorder().WriteGPCommand(data, size); - } - } } OPCODE_CALLBACK(CPState& GetCPState()) diff --git a/Source/Core/VideoCommon/OpcodeDecoding.h b/Source/Core/VideoCommon/OpcodeDecoding.h index bf9b94d218..c2d08e15a7 100644 --- a/Source/Core/VideoCommon/OpcodeDecoding.h +++ b/Source/Core/VideoCommon/OpcodeDecoding.h @@ -18,9 +18,6 @@ class DataReader; namespace OpcodeDecoder { -// Global flag to signal if FifoRecorder is active. -extern bool g_record_fifo_data; - enum class Opcode { GX_NOP = 0x00, diff --git a/Source/Core/VideoCommon/TextureCacheBase.cpp b/Source/Core/VideoCommon/TextureCacheBase.cpp index 568abd4e14..8d6ed2acb2 100644 --- a/Source/Core/VideoCommon/TextureCacheBase.cpp +++ b/Source/Core/VideoCommon/TextureCacheBase.cpp @@ -30,7 +30,6 @@ #include "Core/Config/GraphicsSettings.h" #include "Core/ConfigManager.h" #include "Core/FifoPlayer/FifoPlayer.h" -#include "Core/FifoPlayer/FifoRecorder.h" #include "Core/HW/Memmap.h" #include "Core/System.h" @@ -1348,14 +1347,6 @@ RcTcacheEntry TextureCacheBase::GetTexture(const int textureCacheSafetyColorSamp // to have a unique and valid address. This could result in a regular texture and a tmem // texture aliasing onto the same texture cache entry. - // If we are recording a FifoLog, keep track of what memory we read. FifoRecorder does - // its own memory modification tracking independent of the texture hashing below. - if (OpcodeDecoder::g_record_fifo_data && !texture_info.IsFromTmem()) - { - Core::System::GetInstance().GetFifoRecorder().UseMemory(texture_info.GetRawAddress(), - texture_info.GetFullLevelSize(), - MemoryUpdate::Type::TextureMap); - } // TODO: This doesn't hash GB tiles for preloaded RGBA8 textures (instead, it's hashing more data // from the low tmem bank than it should) @@ -2502,18 +2493,6 @@ void TextureCacheBase::CopyRenderTargetToTexture( ++iter.first; } - if (OpcodeDecoder::g_record_fifo_data) - { - // Mark the memory behind this efb copy as dynamically generated for the Fifo log - u32 address = dstAddr; - for (u32 i = 0; i < num_blocks_y; i++) - { - Core::System::GetInstance().GetFifoRecorder().UseMemory(address, bytes_per_row, - MemoryUpdate::Type::TextureMap, true); - address += dstStride; - } - } - // Even if the copy is deferred, still compute the hash. This way if the copy is used as a texture // in a subsequent draw before it is flushed, it will have the same hash. if (entry)