mirror of
https://github.com/libretro/parallel-gx1.git
synced 2026-11-07 04:26:01 -10:00
Core, InputCommon, VideoCommon: remove the remaining locks
Every std::mutex, lock_guard, unique_lock, scoped_lock, condition_variable, call_once and semaphore left in Source/Core is replaced by a lock-free design: - Input: the device list, control bindings, stick calibration and default device are published snapshots read under a quiesce; a rebind requested while another thread is rebinding is handed to that thread instead of waited for. - WiimoteReal: the pool thread owns the slots and runs commands from an MPSC queue; the active Wiimotes are atomic slots. - EXI/SI/IOS devices: socket and packet traffic goes through SPSC rings or MPSC handoffs owned by one thread; USB scanning publishes its device list, and libusb transfers drain through a CallGate. - Memory cards: the flush thread applies change records queued by the CPU thread. - Custom assets: loads are scheduled as a published batch with an atomic cursor, results come back over an MPSC stack, and busy assets are skipped and retried. - D3D state and GL program caches use insert-only atomic maps (Common/AtomicHashMap.h). - Logging appends with O_APPEND / FILE_APPEND_DATA; FatFs reports busy instead of waiting; per-socket WSAStartup/WSACleanup. Deleted as unused by the libretro core: the profiler, the FIFO recorder, the volume verifier, the touch InputOverrider and the GameCube adapter's libusb path.
This commit is contained in:
1 parent
407a632adc
commit
f7d5095739
125 files changed
+2507
-5447
No files matched your search
+1
-5
@@ -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 += \
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
|
||||
#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 <typename K, typename V, size_t BUCKETS = 4096, typename Hash = std::hash<K>>
|
||||
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<Node*>(retro_atomic_load_acquire_ptr(&bucket));
|
||||
while (node)
|
||||
{
|
||||
Node* const next = node->next;
|
||||
delete static_cast<Value*>(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 <typename Key>
|
||||
std::optional<V> Get(const Key& key) const
|
||||
{
|
||||
quiesce_enter(&m_quiesce);
|
||||
std::optional<V> result;
|
||||
if (const Node* node = Find(Bucket(key), key, nullptr))
|
||||
{
|
||||
if (const auto* value = static_cast<const Value*>(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 <typename Key>
|
||||
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 <typename Key>
|
||||
retro_atomic_ptr_t* Bucket(const Key& key) const
|
||||
{
|
||||
return &m_buckets[Hash{}(key) % BUCKETS];
|
||||
}
|
||||
|
||||
template <typename Key>
|
||||
static Node* Find(retro_atomic_ptr_t* bucket, const Key& key, const Node* stop)
|
||||
{
|
||||
for (auto* node = static_cast<Node*>(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<Value*>(node); }
|
||||
|
||||
void Retire(void* value)
|
||||
{
|
||||
if (value)
|
||||
quiesce_retire(&m_quiesce, &static_cast<Value*>(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<Node*>(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<Node*>(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 <typename K, size_t BUCKETS = 1024, typename Hash = std::hash<K>>
|
||||
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<Node*>(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<Node*>(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<Node*>(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 <typename Fn>
|
||||
void ForEach(Fn&& fn)
|
||||
{
|
||||
for (auto& bucket : m_buckets)
|
||||
{
|
||||
for (auto* node = static_cast<Node*>(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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -18,6 +17,7 @@
|
||||
#include "diskio.h"
|
||||
// clang-format on
|
||||
|
||||
#include <retro_atomic.h>
|
||||
#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<std::recursive_mutex*>(sobj);
|
||||
m->lock();
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" void ff_rel_grant(FF_SYNC_t sobj)
|
||||
{
|
||||
std::recursive_mutex* m = reinterpret_cast<std::recursive_mutex*>(sobj);
|
||||
m->unlock();
|
||||
}
|
||||
|
||||
extern "C" int ff_del_syncobj(FF_SYNC_t sobj)
|
||||
{
|
||||
delete reinterpret_cast<std::recursive_mutex*>(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<bool()>& 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<bool()>& 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<bool()>& cancelled)
|
||||
|
||||
void RunInFatFsContext(FatFsCallbacks& callbacks, const std::function<void()>& 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();
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
@@ -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<CURL, decltype(&curl_easy_cleanup)> 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)
|
||||
|
||||
@@ -11,84 +11,98 @@ namespace Common
|
||||
{
|
||||
JITMemoryTracker::JITMemoryTracker() = default;
|
||||
|
||||
JITMemoryTracker::~JITMemoryTracker()
|
||||
{
|
||||
for (auto& slot : m_jit_regions)
|
||||
delete static_cast<JITRegionInfo*>(retro_atomic_exchange_ptr(&slot, nullptr));
|
||||
quiesce_deinit(&m_quiesce);
|
||||
}
|
||||
|
||||
void JITMemoryTracker::FreeRegion(quiesce_node_t* node)
|
||||
{
|
||||
delete reinterpret_cast<JITRegionInfo*>(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<JITRegionInfo*>(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<JITRegionInfo*>(retro_atomic_load_acquire_ptr(&slot));
|
||||
if (!info)
|
||||
continue;
|
||||
if (info->start_ptr == ptr)
|
||||
return info;
|
||||
void* region_end = static_cast<void*>(static_cast<u8*>(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<void*>(static_cast<u8*>(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
|
||||
@@ -3,16 +3,23 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
|
||||
#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<void*, JITRegionInfo> m_jit_regions;
|
||||
std::mutex m_mutex;
|
||||
static constexpr size_t MAX_REGIONS = 64;
|
||||
std::array<retro_atomic_ptr_t, MAX_REGIONS> m_jit_regions{};
|
||||
quiesce_t m_quiesce{};
|
||||
};
|
||||
}
|
||||
} // namespace Common
|
||||
@@ -7,14 +7,19 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include <fmt/chrono.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "Common/Config/Config.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/Logging/ConsoleListener.h"
|
||||
@@ -32,32 +37,64 @@ const Config::Info<bool> LOGGER_WRITE_TO_WINDOW{
|
||||
const Config::Info<LogLevel> 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<std::mutex> 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<DWORD>(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<bool> m_enable{false};
|
||||
};
|
||||
|
||||
void GenericLogFmtImpl(LogLevel level, LogType type, const char* file, int line,
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
|
||||
#include "Common/CommonFuncs.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/MsgHandler.h"
|
||||
@@ -35,15 +37,14 @@
|
||||
#include "Common/JITMemoryTracker.h"
|
||||
#ifdef __LIBRETRO__
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#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<void*> 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)
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
// Copyright 2014 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "Common/Profiler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <ios>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
#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*> 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<u32>(s_max_length, u32(m_name.length()));
|
||||
|
||||
std::lock_guard<std::mutex> lk(s_mutex);
|
||||
s_all_profilers.push_back(this);
|
||||
}
|
||||
|
||||
Profiler::~Profiler()
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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
|
||||
@@ -1,68 +0,0 @@
|
||||
// Copyright 2014 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#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<Profiler*> 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);
|
||||
@@ -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<std::mutex> 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<std::mutex> 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;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <mutex>
|
||||
#include <winsock2.h>
|
||||
|
||||
#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
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -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<const Symbol*> SymbolDB::GetSymbolsFromName(std::string_view name) const
|
||||
{
|
||||
std::lock_guard lock(m_mutex);
|
||||
std::vector<const Symbol*> symbols;
|
||||
|
||||
for (auto& func : m_functions)
|
||||
@@ -114,7 +107,6 @@ std::vector<const Symbol*> 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<const Symbol*> 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<const Symbol*> 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())
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -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 <typename F>
|
||||
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 <typename F>
|
||||
void ForEachSymbolWithMutation(F f)
|
||||
{
|
||||
std::lock_guard lock(m_mutex);
|
||||
for (auto& [addr, symbol] : m_functions)
|
||||
{
|
||||
f(symbol);
|
||||
@@ -141,7 +140,6 @@ public:
|
||||
template <typename F>
|
||||
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 <typename F>
|
||||
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
|
||||
@@ -24,7 +24,6 @@
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <iterator>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
@@ -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<ARCode> s_active_codes;
|
||||
static std::vector<ARCode> s_synced_codes;
|
||||
static std::vector<std::string> s_internal_log;
|
||||
@@ -117,8 +116,6 @@ void ApplyCodes(std::span<const ARCode> 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<ARCode> ApplyAndReturnCodes(std::span<const ARCode> 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<std::string> 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");
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
|
||||
#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<bool(const Config::Location&)> 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;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <algorithm>
|
||||
#include <fmt/format.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -79,6 +78,8 @@ void SConfig::Shutdown()
|
||||
SConfig::~SConfig()
|
||||
{
|
||||
SaveSettings();
|
||||
delete static_cast<Metadata*>(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<Metadata*>(node);
|
||||
}
|
||||
|
||||
SConfig::Metadata SConfig::ReadMetadata() const
|
||||
{
|
||||
quiesce_enter(&m_metadata_quiesce);
|
||||
const auto* metadata = static_cast<const Metadata*>(retro_atomic_load_acquire_ptr(&m_metadata));
|
||||
Metadata copy = metadata ? *metadata : Metadata{};
|
||||
quiesce_exit(&m_metadata_quiesce);
|
||||
return copy;
|
||||
}
|
||||
|
||||
template <typename Edit>
|
||||
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<Metadata*>(old)->node, FreeMetadata);
|
||||
}
|
||||
|
||||
const std::string SConfig::GetGameID() const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
|
||||
return m_game_id;
|
||||
return ReadMetadata().game_id;
|
||||
}
|
||||
|
||||
const std::string SConfig::GetGameTDBID() const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
|
||||
return m_gametdb_id;
|
||||
return ReadMetadata().gametdb_id;
|
||||
}
|
||||
|
||||
const std::string SConfig::GetGameIDElfDol() const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> 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<std::string> SConfig::GetGameIDsForTextures() const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> lock(m_metadata_lock);
|
||||
return m_title_name;
|
||||
return ReadMetadata().title_name;
|
||||
}
|
||||
|
||||
const std::string SConfig::GetTitleDescription() const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
|
||||
return m_title_description;
|
||||
return ReadMetadata().title_description;
|
||||
}
|
||||
|
||||
u64 SConfig::GetTitleID() const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
|
||||
return m_title_id;
|
||||
return ReadMetadata().title_id;
|
||||
}
|
||||
|
||||
u16 SConfig::GetRevision() const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
|
||||
return m_revision;
|
||||
return ReadMetadata().revision;
|
||||
}
|
||||
|
||||
u32 SConfig::GetSimulatedMemorySize() const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
|
||||
return m_simulated_memory_size;
|
||||
return ReadMetadata().simulated_memory_size;
|
||||
}
|
||||
|
||||
void SConfig::ResetRunningGameMetadata()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<u16> 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";
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
|
||||
#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 <typename Edit>
|
||||
void EditMetadata(Edit&& edit);
|
||||
|
||||
mutable quiesce_t m_metadata_quiesce{};
|
||||
retro_atomic_ptr_t m_metadata{};
|
||||
};
|
||||
@@ -1,455 +0,0 @@
|
||||
// Copyright 2011 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "Core/FifoPlayer/FifoRecorder.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
#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<FifoDataFile>();
|
||||
|
||||
// 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<const u32*>(&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<const u32*>(&xfmem);
|
||||
const u32* xfregs_ptr = reinterpret_cast<const u32*>(&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<FifoRecordAnalyzer>(this, cpMem);
|
||||
}
|
||||
|
||||
bool FifoRecorder::IsRecording() const
|
||||
{
|
||||
return m_IsRecording;
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright 2011 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/Assert.h"
|
||||
#include "Common/HookableEvent.h"
|
||||
#include "Core/FifoPlayer/FifoDataFile.h"
|
||||
|
||||
namespace Core
|
||||
{
|
||||
class System;
|
||||
}
|
||||
|
||||
class FifoRecorder
|
||||
{
|
||||
public:
|
||||
using CallbackFunc = std::function<void()>;
|
||||
|
||||
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<FifoDataFile> 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<FifoRecordAnalyzer> m_record_analyzer;
|
||||
std::vector<u8> m_FifoData;
|
||||
std::vector<u8> m_Ram;
|
||||
std::vector<u8> m_ExRam;
|
||||
|
||||
Common::EventHook m_end_of_frame_event;
|
||||
|
||||
Core::System& m_system;
|
||||
};
|
||||
@@ -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); }};
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <mutex>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
@@ -47,20 +46,16 @@ static Installation s_code_handler_installed = Installation::Uninstalled;
|
||||
// the currently active codes
|
||||
static std::vector<GeckoCode> s_active_codes;
|
||||
static std::vector<GeckoCode> 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<const GeckoCode> 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<const GeckoCode> gcodes)
|
||||
|
||||
std::vector<GeckoCode> SetAndReturnActiveCodes(std::span<const GeckoCode> gcodes)
|
||||
{
|
||||
std::lock_guard lk(s_active_codes_lock);
|
||||
|
||||
s_active_codes.clear();
|
||||
if (Config::AreCheatsEnabled())
|
||||
@@ -112,7 +106,7 @@ std::vector<GeckoCode> SetAndReturnActiveCodes(std::span<const GeckoCode> 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
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
#include "Core/HW/DSPLLE/DSPLLE.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#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<int>(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)
|
||||
{
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
#include <rthreads/rthreads.h>
|
||||
|
||||
#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<u32> m_cycle_count{};
|
||||
|
||||
@@ -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<u8> 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<Request*>(node);
|
||||
node = node->next;
|
||||
switch (request->kind)
|
||||
{
|
||||
case Request::Kind::Frame:
|
||||
ProcessFrame(request->frame.data(), static_cast<u32>(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<std::mutex> lock(m_mtx);
|
||||
// CPU thread: check the frame and hand it to the read thread, which owns the network stack.
|
||||
const std::optional<u16> 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<u8>(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<u16> 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<std::mutex> 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
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include "SFML/Network/IpAddress.hpp"
|
||||
|
||||
@@ -20,6 +19,7 @@
|
||||
#include <rthreads/rthreads.h>
|
||||
#endif
|
||||
|
||||
#include <queues/mpsc_stack.h>
|
||||
#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<std::vector<u8>, 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<u8> frame;
|
||||
};
|
||||
mpsc_stack_t m_requests{};
|
||||
void Post(Request::Kind kind, std::vector<u8> 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{};
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
|
||||
@@ -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<std::unique_ptr<sf::TcpSocket>> GeckoSockServer::waiting_socks;
|
||||
mpsc_stack_t GeckoSockServer::waiting_socks;
|
||||
std::deque<std::unique_ptr<sf::TcpSocket>> 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<sf::TcpSocket>();
|
||||
}
|
||||
@@ -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<WaitingSock*>(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<u8>();
|
||||
send_fifo = std::deque<u8>();
|
||||
// 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<char, 128> 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<char> packet(send_fifo.begin(), send_fifo.end());
|
||||
send_fifo.clear();
|
||||
std::vector<char> 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<u8>(_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;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
#include <SFML/Network.hpp>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
|
||||
#include <queues/mpsc_stack.h>
|
||||
#include <retro_spsc.h>
|
||||
#include <rthreads/rthreads.h>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
@@ -28,10 +29,10 @@ public:
|
||||
std::unique_ptr<sf::TcpSocket> client;
|
||||
void ClientThread();
|
||||
sthread_t* clientThread = nullptr;
|
||||
std::mutex transfer_lock;
|
||||
|
||||
std::deque<u8> send_fifo;
|
||||
std::deque<u8> 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<std::unique_ptr<sf::TcpSocket>> 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<sf::TcpSocket> socket;
|
||||
};
|
||||
static mpsc_stack_t waiting_socks;
|
||||
// The CPU thread's own queue of them, oldest first.
|
||||
static std::deque<std::unique_ptr<sf::TcpSocket>> taken_socks;
|
||||
};
|
||||
|
||||
class CEXIGecko : public IEXIDevice, private GeckoSockServer
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
#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<retro_microphone_t*>(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_microphone_t*>(retro_atomic_exchange_ptr(&m_retro_mic, nullptr));
|
||||
if (mic)
|
||||
{
|
||||
Libretro::Input::g_microphone_interface.close_mic(
|
||||
static_cast<retro_microphone_t*>(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<size_t>(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<retro_microphone_t*>(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_microphone_t*>(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
|
||||
@@ -3,8 +3,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
#include <retro_spsc.h>
|
||||
|
||||
#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
|
||||
@@ -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<std::mutex> g(m_receive_buffer_lock);
|
||||
const std::size_t bytes_to_copy = std::min<std::size_t>(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<u16>(std::min<std::size_t>(m_receive_buffer.size(), 0x200));
|
||||
const std::size_t received = retro_spsc_read_avail(&m_receive_buffer);
|
||||
const u16 bytes_available = static_cast<u16>(std::min<std::size_t>(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<std::mutex> 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)
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "Core/HW/EXI/BBA/TAPServerConnection.h"
|
||||
#include "Core/HW/EXI/EXI_Device.h"
|
||||
|
||||
#include <retro_spsc.h>
|
||||
|
||||
|
||||
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<u8, 0x20> m_regs{};
|
||||
};
|
||||
} // namespace ExpansionInterface
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
@@ -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<std::unique_ptr<FlushJob>> jobs;
|
||||
for (mpsc_stack_node_t* node = mpsc_stack_reverse(mpsc_stack_drain(&m_flush_jobs)); node;)
|
||||
{
|
||||
auto* job = reinterpret_cast<FlushJob*>(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);
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <rthreads/rthreads.h>
|
||||
|
||||
#include <queues/mpsc_stack.h>
|
||||
|
||||
#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<Memcard::GCMBlock> 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;
|
||||
};
|
||||
@@ -4,9 +4,10 @@
|
||||
#include "Core/HW/GCMemcard/GCMemcardRaw.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -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<u8[]>(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<Change*>(
|
||||
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<Change*>(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]);
|
||||
}
|
||||
@@ -4,10 +4,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include <queues/mpsc_stack.h>
|
||||
#include <rthreads/rthreads.h>
|
||||
|
||||
#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<u64>(address) + static_cast<u64>(length);
|
||||
@@ -38,10 +54,12 @@ private:
|
||||
}
|
||||
|
||||
std::string m_filename;
|
||||
// The card as the emulated side sees it; CPU thread only.
|
||||
std::unique_ptr<u8[]> m_memcard_data;
|
||||
// The flush thread's own copy, brought up to date from m_changes before each write.
|
||||
std::unique_ptr<u8[]> 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;
|
||||
|
||||
@@ -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<bool>();
|
||||
#endif
|
||||
}
|
||||
@@ -6,9 +6,10 @@
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
|
||||
#include <queues/mpsc_stack.h>
|
||||
|
||||
#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<std::unique_ptr<sf::TcpSocket>> s_waiting_socks;
|
||||
std::queue<std::unique_ptr<sf::TcpSocket>> 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<sf::TcpSocket> socket;
|
||||
};
|
||||
mpsc_stack_t s_waiting_socks;
|
||||
mpsc_stack_t s_waiting_clocks;
|
||||
std::queue<std::unique_ptr<sf::TcpSocket>> s_taken_socks;
|
||||
std::queue<std::unique_ptr<sf::TcpSocket>> s_taken_clocks;
|
||||
|
||||
void PushSock(mpsc_stack_t* stack, std::unique_ptr<sf::TcpSocket> socket)
|
||||
{
|
||||
auto* waiting = new WaitingSock{{}, std::move(socket)};
|
||||
mpsc_stack_push(stack, &waiting->link);
|
||||
}
|
||||
|
||||
std::unique_ptr<sf::TcpSocket> TakeSock(mpsc_stack_t* stack,
|
||||
std::queue<std::unique_ptr<sf::TcpSocket>>& taken)
|
||||
{
|
||||
for (mpsc_stack_node_t* node = mpsc_stack_reverse(mpsc_stack_drain(stack)); node;)
|
||||
{
|
||||
auto* waiting = reinterpret_cast<WaitingSock*>(node);
|
||||
node = node->next;
|
||||
taken.push(std::move(waiting->socket));
|
||||
delete waiting;
|
||||
}
|
||||
if (taken.empty())
|
||||
return nullptr;
|
||||
std::unique_ptr<sf::TcpSocket> 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<sf::TcpSocket>();
|
||||
}
|
||||
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<sf::TcpSocket>();
|
||||
}
|
||||
@@ -83,26 +112,14 @@ void GBAConnectionWaiter_Shutdown()
|
||||
Common::JoinThread(s_connection_thread);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static std::unique_ptr<T> MoveFromFront(std::queue<std::unique_ptr<T>>& ptrs)
|
||||
{
|
||||
if (ptrs.empty())
|
||||
return nullptr;
|
||||
std::unique_ptr<T> ptr = std::move(ptrs.front());
|
||||
ptrs.pop();
|
||||
return ptr;
|
||||
}
|
||||
|
||||
static std::unique_ptr<sf::TcpSocket> 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<sf::TcpSocket> GetNextClock()
|
||||
{
|
||||
std::lock_guard lk(s_cs_gba_clk);
|
||||
return MoveFromFront(s_waiting_clocks);
|
||||
return TakeSock(&s_waiting_clocks, s_taken_clocks);
|
||||
}
|
||||
|
||||
GBASockServer::GBASockServer()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -586,7 +586,7 @@ ExtensionNumber Wiimote::GetActiveExtensionNumber() const
|
||||
return m_active_extension;
|
||||
}
|
||||
|
||||
ControllerEmu::SubscribableSettingValue<bool>& Wiimote::GetMotionPlusSetting()
|
||||
ControllerEmu::SettingValue<bool>& Wiimote::GetMotionPlusSetting()
|
||||
{
|
||||
return m_motion_plus_setting;
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ void Shinkansen::Update(const DesiredExtensionState& target_state)
|
||||
ext_data.buttons = desired_state.buttons ^ 0xFFFF;
|
||||
Common::BitCastPtr<DataFormat>(&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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ public:
|
||||
|
||||
// Active extension number is exposed for TAS.
|
||||
ExtensionNumber GetActiveExtensionNumber() const;
|
||||
ControllerEmu::SubscribableSettingValue<bool>& GetMotionPlusSetting();
|
||||
ControllerEmu::SettingValue<bool>& GetMotionPlusSetting();
|
||||
|
||||
static Common::Vec3
|
||||
OverrideVec3(const ControllerEmu::ControlGroup* control_group, Common::Vec3 vec,
|
||||
@@ -305,7 +305,7 @@ private:
|
||||
ControllerEmu::SettingValue<bool> m_sideways_setting;
|
||||
ControllerEmu::SettingValue<bool> m_upright_setting;
|
||||
ControllerEmu::SettingValue<double> m_battery_setting;
|
||||
ControllerEmu::SubscribableSettingValue<bool> m_motion_plus_setting;
|
||||
ControllerEmu::SettingValue<bool> m_motion_plus_setting;
|
||||
ControllerEmu::SettingValue<double> m_fov_x_setting;
|
||||
ControllerEmu::SettingValue<double> m_fov_y_setting;
|
||||
|
||||
|
||||
@@ -4,15 +4,19 @@
|
||||
#include "Core/HW/WiimoteReal/WiimoteReal.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <queues/mpsc_stack.h>
|
||||
#include <retro_atomic.h>
|
||||
#include <rthreads/retro_eventcount.h>
|
||||
|
||||
#include <SFML/Network/UdpSocket.hpp>
|
||||
|
||||
#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<std::string> 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<Wiimote*>(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<Wiimote> 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<std::string> 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<KnownIds*>(node);
|
||||
}
|
||||
|
||||
template <typename Edit>
|
||||
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<const KnownIds*>(cur)->ids :
|
||||
std::unordered_set<std::string>{}};
|
||||
edit(next->ids);
|
||||
if (retro_atomic_cas_ptr(&s_known_ids, cur, next))
|
||||
{
|
||||
if (cur)
|
||||
quiesce_retire(&s_known_ids_quiesce, &static_cast<KnownIds*>(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<WiimotePoolEntry> 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> 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> 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> wiimote)
|
||||
{
|
||||
delete static_cast<Wiimote*>(retro_atomic_exchange_ptr(&g_wiimotes[index], wiimote.release()));
|
||||
}
|
||||
|
||||
static std::unique_ptr<Wiimote> TakeWiimoteFromSlot(unsigned int index)
|
||||
{
|
||||
return std::unique_ptr<Wiimote>(
|
||||
static_cast<Wiimote*>(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> wiimote)
|
||||
static void AddWiimoteToPoolNow(std::unique_ptr<Wiimote> 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)
|
||||
|
||||
wiimote->EmuStop();
|
||||
|
||||
std::lock_guard lk(g_wiimotes_mutex);
|
||||
s_wiimote_pool.emplace_back(WiimotePoolEntry{std::move(wiimote)});
|
||||
}
|
||||
|
||||
void AddWiimoteToPool(std::unique_ptr<Wiimote> 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<std::string>& 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<const std::vector<WiimoteScannerBackend*>*>(
|
||||
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<Wiimote> 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<ManagerCommand*>(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<std::chrono::microseconds>(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<std::unique_ptr<WiimoteScannerBackend>> backends;
|
||||
backends.emplace_back(std::make_unique<WiimoteScannerLinux>());
|
||||
backends.emplace_back(std::make_unique<WiimoteScannerAndroid>());
|
||||
backends.emplace_back(std::make_unique<WiimoteScannerWindows>());
|
||||
backends.emplace_back(std::make_unique<WiimoteScannerHidapi>());
|
||||
|
||||
m_backends.emplace_back(std::make_unique<WiimoteScannerLinux>());
|
||||
m_backends.emplace_back(std::make_unique<WiimoteScannerAndroid>());
|
||||
m_backends.emplace_back(std::make_unique<WiimoteScannerWindows>());
|
||||
m_backends.emplace_back(std::make_unique<WiimoteScannerHidapi>());
|
||||
}
|
||||
std::vector<WiimoteScannerBackend*> 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<std::string>& 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<std::string>& 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<ManagerCommand*>(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<Wiimote>& 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<Wiimote>& 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<Wiimote> 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<const KnownIds*>(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
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -21,6 +20,7 @@
|
||||
#include "Core/HW/WiimoteCommon/WiimoteReport.h"
|
||||
#include "Core/USBUtils.h"
|
||||
|
||||
#include <retro_atomic.h>
|
||||
#include <retro_spsc.h>
|
||||
#include <rthreads/retro_eventcount.h>
|
||||
|
||||
@@ -235,8 +235,9 @@ private:
|
||||
void ThreadFunc();
|
||||
void PoolThreadFunc();
|
||||
|
||||
std::vector<std::unique_ptr<WiimoteScannerBackend>> m_backends;
|
||||
mutable std::mutex m_backends_mutex;
|
||||
// The scanner thread's backends, as a std::vector<WiimoteScannerBackend*>* 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<WiimoteScanMode> 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<Wiimote> 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<Wiimote>);
|
||||
|
||||
@@ -267,5 +268,4 @@ void InitAdapterClass();
|
||||
|
||||
void HandleWiimotesInControllerInterfaceSettingChange();
|
||||
void PopulateDevices();
|
||||
void ProcessWiimotePool();
|
||||
} // namespace WiimoteReal
|
||||
@@ -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<u32>(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)
|
||||
|
||||
@@ -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<IPCReply> 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<AsyncReply*>(node);
|
||||
node = node->next;
|
||||
GetEmulationKernel().EnqueueIPCReply(reply->request, reply->return_value);
|
||||
delete reply;
|
||||
}
|
||||
GetEmulationKernel().GetSocketManager()->Update();
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
|
||||
#include <queues/mpsc_stack.h>
|
||||
|
||||
#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<AsyncTask> m_work_queue;
|
||||
std::mutex m_async_reply_lock;
|
||||
std::queue<AsyncReply> m_async_replies;
|
||||
// Replies from the network worker, handed to the emulation thread by Update.
|
||||
mpsc_stack_t m_async_replies{};
|
||||
};
|
||||
} // namespace IOS::HLE
|
||||
@@ -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<AsyncReply*>(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<AsyncReply*>(node);
|
||||
node = node->next;
|
||||
GetEmulationKernel().EnqueueIPCReply(reply->request, reply->return_value);
|
||||
delete reply;
|
||||
}
|
||||
}
|
||||
|
||||
u32 NetKDRequestDevice::LoadSchedulerWord(size_t index) const
|
||||
{
|
||||
return static_cast<u32>(
|
||||
retro_atomic_load_acquire_int(const_cast<retro_atomic_int_t*>(&m_scheduler_buffer[index])));
|
||||
}
|
||||
|
||||
void NetKDRequestDevice::StoreSchedulerWord(size_t index, u32 swapped)
|
||||
{
|
||||
retro_atomic_store_release_int(&m_scheduler_buffer[index], static_cast<int>(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<int>(Common::swap32(Common::swap32(static_cast<u32>(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<u32>(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<u32>(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<u32>(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<u32>(CurrentFunction::None);
|
||||
StoreSchedulerWord(4, static_cast<u32>(CurrentFunction::None));
|
||||
});
|
||||
|
||||
{
|
||||
std::lock_guard lg(m_scheduler_buffer_lock);
|
||||
m_scheduler_buffer[4] = Common::swap32(static_cast<u32>(CurrentFunction::Check));
|
||||
}
|
||||
StoreSchedulerWord(4, Common::swap32(static_cast<u32>(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<int>(*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<int>(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<u32>(CurrentFunction::None);
|
||||
StoreSchedulerWord(4, static_cast<u32>(CurrentFunction::None));
|
||||
|
||||
m_send_list.WriteSendList();
|
||||
});
|
||||
|
||||
{
|
||||
std::lock_guard lg(m_scheduler_buffer_lock);
|
||||
m_scheduler_buffer[4] = Common::swap32(static_cast<u32>(CurrentFunction::Send));
|
||||
}
|
||||
StoreSchedulerWord(4, Common::swap32(static_cast<u32>(CurrentFunction::Send)));
|
||||
|
||||
m_send_list.ReadSendList();
|
||||
const std::string auth =
|
||||
@@ -1143,9 +1149,11 @@ std::optional<IPCReply> 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<u32, 256> 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<IPCReply> 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<u32>(retro_atomic_load_acquire_int(&m_mail_span)),
|
||||
request.buffer_out + 4);
|
||||
memory.Write_U32(static_cast<u32>(retro_atomic_load_acquire_int(&m_download_span)),
|
||||
request.buffer_out + 8);
|
||||
WriteReturnValue(memory, NWC24::WC24_OK, request.buffer_out);
|
||||
|
||||
return_value = IPC_SUCCESS;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
|
||||
@@ -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 <queues/mpsc_stack.h>
|
||||
#include <retro_atomic.h>
|
||||
|
||||
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 <typename Method, typename Request>
|
||||
std::optional<IPCReply> LaunchAsyncTask(Method method, const Request& request)
|
||||
{
|
||||
@@ -114,19 +122,18 @@ private:
|
||||
NWC24::Mail::WC24FriendList m_friend_list;
|
||||
Common::WorkQueueThreadSP<AsyncTask> m_work_queue;
|
||||
Common::AsyncWorkThreadSP m_scheduler_work_queue;
|
||||
std::mutex m_async_reply_lock;
|
||||
std::mutex m_scheduler_buffer_lock;
|
||||
std::queue<AsyncReply> m_async_replies;
|
||||
u32 m_error_count = 0;
|
||||
std::array<u32, 256> 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<retro_atomic_int_t, 256> m_scheduler_buffer{};
|
||||
std::shared_ptr<NetKDTimeDevice> 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
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
@@ -468,7 +467,6 @@ void InfinityBase::GetPresentFigures(u8 sequence, std::array<u8, 32>& reply_buf)
|
||||
|
||||
void InfinityBase::GetFigureIdentifier(u8 fig_num, u8 sequence, std::array<u8, 32>& 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<u8, 3
|
||||
|
||||
void InfinityBase::QueryBlock(u8 fig_num, u8 block, std::array<u8, 32>& 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<u8, 32>& reply_bu
|
||||
void InfinityBase::WriteBlock(u8 fig_num, u8 block, const u8* to_write_buf,
|
||||
std::array<u8, 32>& 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<u8, INFINITY_NUM_BLOCKS * INFINITY_BLOCK_SIZE>& buf,
|
||||
File::IOFile in_file, FigureUIPosition position)
|
||||
{
|
||||
std::lock_guard lock(m_infinity_mutex);
|
||||
u8 order_added;
|
||||
|
||||
std::vector<u8> sha1_calc = {SHA1_CONSTANT.begin(), SHA1_CONSTANT.end() - 1};
|
||||
@@ -642,7 +637,6 @@ InfinityBase::LoadFigure(const std::array<u8, INFINITY_NUM_BLOCKS * INFINITY_BLO
|
||||
|
||||
void InfinityBase::RemoveFigure(FigureUIPosition position)
|
||||
{
|
||||
std::lock_guard lock(m_infinity_mutex);
|
||||
InfinityFigure& figure = m_figures[static_cast<u8>(position)];
|
||||
|
||||
if (figure.inf_file.IsOpen())
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
@@ -107,7 +106,6 @@ public:
|
||||
std::string FindFigure(u32 character) const;
|
||||
|
||||
protected:
|
||||
std::mutex m_infinity_mutex;
|
||||
std::array<InfinityFigure, 9> m_figures;
|
||||
|
||||
private:
|
||||
|
||||
@@ -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<retro_microphone_t*>(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<retro_microphone_t*>(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<SampleType>(sample * gain);
|
||||
};
|
||||
|
||||
const u32 stream_size = GetStreamSize();
|
||||
const bool are_samples_byte_swapped = AreSamplesByteSwapped();
|
||||
std::vector<SampleType> 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<u16>(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
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
#include <atomic>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include <retro_spsc.h>
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
#include <libretro.h>
|
||||
@@ -62,10 +62,10 @@ private:
|
||||
void StreamStop();
|
||||
|
||||
virtual u32 GetStreamSize() const = 0;
|
||||
std::vector<SampleType> 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<u16> 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;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "Core/IOS/USB/Emulated/Skylanders/Skylander.h"
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "AudioCommon/AudioCommon.h"
|
||||
@@ -1090,7 +1089,6 @@ void SkylanderUSB::ScheduleTransfer(std::unique_ptr<TransferCommand> 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<u8, 64> 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<SkylanderFigure> figure)
|
||||
{
|
||||
std::lock_guard lock(sky_mutex);
|
||||
|
||||
u32 sky_serial = 0;
|
||||
std::array<u8, 0x10> block = {};
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -33,6 +32,7 @@ USBHost::USBHost(EmulationKernel& ios, const std::string& device_name)
|
||||
USBHost::~USBHost()
|
||||
{
|
||||
GetSystem().GetUSBScanner().RemoveClient(this);
|
||||
delete static_cast<USBScanner::DeviceMap*>(retro_atomic_exchange_ptr(&m_pending_devices, nullptr));
|
||||
}
|
||||
|
||||
std::optional<IPCReply> 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<USB::Device> 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<USBScanner::DeviceMap*>(
|
||||
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<USBScanner::DeviceMap*>(
|
||||
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;
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Core/IOS/Device.h"
|
||||
#include "Core/IOS/IOS.h"
|
||||
@@ -49,13 +49,16 @@ protected:
|
||||
std::optional<IPCReply> HandleTransfer(const std::shared_ptr<USB::Device>& device, u32 request,
|
||||
const std::function<s32()>& 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<u64, std::shared_ptr<USB::Device>> 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
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -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<u8, const char*> s_transfer_types = {
|
||||
void LibusbDevice::TransferEndpoint::AddTransfer(std::unique_ptr<TransferCommand> 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<InFlight*>(retro_atomic_exchange_ptr(&slot, nullptr));
|
||||
}
|
||||
|
||||
void LibusbDevice::TransferEndpoint::HandleTransfer(
|
||||
libusb_transfer* transfer, const std::function<s32(const TransferCommand&)>& 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<InFlight> in_flight;
|
||||
for (auto& slot : m_in_flight)
|
||||
{
|
||||
auto* candidate = static_cast<InFlight*>(retro_atomic_load_acquire_ptr(&slot));
|
||||
if (candidate && candidate->transfer == transfer)
|
||||
{
|
||||
in_flight.reset(static_cast<InFlight*>(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<u8[]> buffer(transfer->buffer);
|
||||
const auto& cmd = *iterator->second.get();
|
||||
const auto& cmd = *in_flight->command;
|
||||
const auto* device = static_cast<LibusbDevice*>(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<const InFlight*>(retro_atomic_load_acquire_ptr(&slot)))
|
||||
libusb_cancel_transfer(in_flight->transfer);
|
||||
}
|
||||
Common::CallGateExit(&m_cancel_gate);
|
||||
}
|
||||
|
||||
int LibusbDevice::GetNumberOfAltSettings(const u8 interface_number)
|
||||
|
||||
@@ -7,10 +7,14 @@
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
|
||||
#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<s32(const TransferCommand&)>& 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<libusb_transfer*, std::unique_ptr<TransferCommand>> 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<TransferCommand> command;
|
||||
};
|
||||
static constexpr size_t MAX_IN_FLIGHT = 64;
|
||||
std::array<retro_atomic_ptr_t, MAX_IN_FLIGHT> m_in_flight{};
|
||||
Common::CallGate m_cancel_gate;
|
||||
bool m_used = false; // emulation thread
|
||||
};
|
||||
std::map<u8, TransferEndpoint> m_transfer_endpoints;
|
||||
// Indexed by endpoint address, so the event thread never looks one up in a changing map.
|
||||
std::array<TransferEndpoint, 256> m_transfer_endpoints;
|
||||
static void CtrlTransferCallback(libusb_transfer* transfer);
|
||||
static void TransferCallback(libusb_transfer* transfer);
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ std::optional<IPCReply> 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<IPCReply> 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<IPCReply> 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<IPCReply> 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<IPCReply> 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<USB::Device> 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<USB::Device> d
|
||||
template <typename T>
|
||||
void OH0::TriggerHook(std::map<T, u32>& 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<T, u32>& hooks, T value, const ReturnCode return_
|
||||
|
||||
std::pair<ReturnCode, u64> 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<ReturnCode, u64> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -75,6 +74,5 @@ private:
|
||||
std::map<USB::DeviceInfo, u32> m_insertion_hooks;
|
||||
std::map<u64, u32> m_removal_hooks;
|
||||
std::set<u64> m_opened_devices;
|
||||
std::mutex m_hooks_mutex;
|
||||
};
|
||||
} // namespace IOS::HLE
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <ranges>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
@@ -34,6 +33,26 @@ namespace IOS::HLE
|
||||
USBScanner::~USBScanner()
|
||||
{
|
||||
StopScanning();
|
||||
delete static_cast<Devices*>(retro_atomic_exchange_ptr(&m_devices, nullptr));
|
||||
delete static_cast<Clients*>(retro_atomic_exchange_ptr(&m_clients, nullptr));
|
||||
quiesce_deinit(&m_quiesce);
|
||||
}
|
||||
|
||||
void USBScanner::FreeDevices(quiesce_node_t* node)
|
||||
{
|
||||
delete reinterpret_cast<Devices*>(node);
|
||||
}
|
||||
|
||||
void USBScanner::FreeClients(quiesce_node_t* node)
|
||||
{
|
||||
delete reinterpret_cast<Clients*>(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<Devices*>(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<const Clients*>(retro_atomic_load_acquire_ptr(&m_clients));
|
||||
auto* next = new Clients{{}, cur ? cur->set : std::set<USBHost*>{}};
|
||||
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<Clients*>(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<const Clients*>(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<Clients*>(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<const Devices*>(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<const Devices*>(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<const Devices*>(retro_atomic_load_acquire_ptr(&m_devices));
|
||||
const auto* clients = static_cast<const Clients*>(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);
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
|
||||
#include <rthreads/rthreads.h>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
|
||||
#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<USB::Device> 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<USBHost*> 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<USBHost*> 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;
|
||||
|
||||
@@ -132,8 +132,6 @@ std::optional<IPCReply> 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<IOCtlRequest>(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<IPCReply> 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<USB::Device> 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)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#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<IOCtlRequest> 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<USBV5Device, 32> m_usbv5_devices{};
|
||||
mutable std::mutex m_usbv5_devices_mutex;
|
||||
u16 m_current_device_number = 0x21;
|
||||
};
|
||||
} // namespace IOS::HLE
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "Core/IOS/USB/USB_HID/HIDv4.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -86,7 +85,6 @@ IPCReply USB_HIDv4::CancelInterrupt(const IOCtlRequest& request)
|
||||
|
||||
std::optional<IPCReply> 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<IPCReply> 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::Device> 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::Device> USB_HIDv4::GetDeviceByIOSID(const s32 ios_id) const
|
||||
void USB_HIDv4::OnDeviceChange(ChangeEvent event, std::shared_ptr<USB::Device> 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<USB::Device> 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<u8> GetDescriptors(const USB::Device& device)
|
||||
|
||||
std::vector<u8> 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
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -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<IOCtlRequest> m_devicechange_hook_request;
|
||||
|
||||
mutable std::mutex m_id_map_mutex;
|
||||
// IOS device IDs <=> USB device IDs
|
||||
std::map<s32, u64> m_ios_ids;
|
||||
std::map<u64, s32> m_device_ids;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
@@ -57,8 +56,6 @@ std::optional<IPCReply> 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);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
@@ -68,8 +67,6 @@ std::optional<IPCReply> 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);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "Core/LibusbUtils.h"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#if defined(__LIBUSB__)
|
||||
#include <libusb.h>
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
+34
-13
@@ -9,7 +9,6 @@
|
||||
#include <iterator>
|
||||
#include <locale>
|
||||
#include <mbedtls/md.h>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
@@ -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<DisplayString*>(retro_atomic_exchange_ptr(&slot, nullptr));
|
||||
quiesce_deinit(&m_input_display_quiesce);
|
||||
}
|
||||
|
||||
void MovieManager::FreeDisplayString(quiesce_node_t* node)
|
||||
{
|
||||
delete reinterpret_cast<DisplayString*>(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<DisplayString*>(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<const DisplayString*>(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<int>(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();
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
|
||||
#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<std::string, 8> 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<retro_atomic_ptr_t, 8> m_input_display{};
|
||||
mutable quiesce_t m_input_display_quiesce{};
|
||||
|
||||
Core::System& m_system;
|
||||
};
|
||||
|
||||
@@ -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<Packet*>(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<u8>(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<Packet*>(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<const u8*>(data), length);
|
||||
// CEXIETHERNET's RecvHandlePacket and SendFromDirectFIFO run on different threads.
|
||||
Write(static_cast<const u8*>(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
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
@@ -17,6 +17,8 @@ using socklen_t = int;
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
|
||||
#include <queues/mpsc_stack.h>
|
||||
#include <retro_atomic.h>
|
||||
#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<u8> bytes;
|
||||
};
|
||||
void Write(const u8* data, std::size_t length);
|
||||
|
||||
std::unique_ptr<Common::PCAP> m_file;
|
||||
std::mutex m_io_mutex;
|
||||
mpsc_stack_t m_packets{};
|
||||
retro_atomic_int_t m_writing{};
|
||||
std::map<s32, u32> m_read_sequence_number;
|
||||
std::map<s32, u32> m_write_sequence_number;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <iterator>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
@@ -47,7 +46,6 @@ constexpr std::array<const char*, 3> s_patch_type_strings{{
|
||||
|
||||
static std::vector<Patch> s_on_frame;
|
||||
static std::vector<std::size_t> 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<Pa
|
||||
static void ApplyMemoryPatches(const Core::CPUThreadGuard& guard,
|
||||
std::span<const std::size_t> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
|
||||
#include <fmt/ostream.h>
|
||||
|
||||
#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<AnyCallback, AnyDisassemble>;
|
||||
@@ -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;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,223 +0,0 @@
|
||||
// Copyright 2019 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <mbedtls/md5.h>
|
||||
|
||||
#include <rthreads/rthreads.h>
|
||||
|
||||
#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 <typename T>
|
||||
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<std::vector<u8>>& hashes);
|
||||
|
||||
private:
|
||||
enum class DownloadStatus
|
||||
{
|
||||
NotAttempted,
|
||||
Success,
|
||||
Fail,
|
||||
FailButOldCacheAvailable,
|
||||
};
|
||||
|
||||
struct DownloadState
|
||||
{
|
||||
std::mutex mutex;
|
||||
DownloadStatus status = DownloadStatus::NotAttempted;
|
||||
};
|
||||
|
||||
struct PotentialMatch
|
||||
{
|
||||
u64 size = 0;
|
||||
Hashes<std::vector<u8>> hashes;
|
||||
};
|
||||
|
||||
static DownloadStatus DownloadDatfile(const std::string& system, DownloadStatus old_status);
|
||||
static std::vector<u8> ReadDatfile(const std::string& system);
|
||||
std::vector<PotentialMatch> ScanDatfile(std::span<const u8> 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<PotentialMatch> 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<PotentialMatch> 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<std::vector<u8>> hashes;
|
||||
std::string summary_text;
|
||||
std::vector<Problem> problems;
|
||||
RedumpVerifier::Result redump;
|
||||
};
|
||||
|
||||
VolumeVerifier(const Volume& volume, bool redump_verification, Hashes<bool> hashes_to_calculate);
|
||||
~VolumeVerifier();
|
||||
|
||||
static Hashes<bool> 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<Partition> CheckPartitions();
|
||||
bool CheckPartition(const Partition& partition); // Returns false if partition should be ignored
|
||||
std::string GetPartitionName(std::optional<u32> 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<bool> m_hashes_to_calculate{};
|
||||
bool m_calculating_any_hash = false;
|
||||
u32 m_crc32_context = 0;
|
||||
mbedtls_md5_context m_md5_context{};
|
||||
std::unique_ptr<Common::SHA1::Context> m_sha1_context;
|
||||
|
||||
u64 m_excess_bytes = 0;
|
||||
std::vector<u8> m_data;
|
||||
std::vector<std::function<void()>> m_async_tasks;
|
||||
|
||||
DiscScrubber m_scrubber;
|
||||
IOS::ES::TicketReader m_ticket;
|
||||
std::vector<u64> m_content_offsets;
|
||||
u16 m_content_index = 0;
|
||||
std::vector<GroupToVerify> m_groups;
|
||||
size_t m_group_index = 0; // Index in m_groups, not index in a specific partition
|
||||
std::map<Partition, size_t> m_block_errors;
|
||||
std::map<Partition, size_t> 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
|
||||
@@ -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<IOS::HLE::USB::Microphone*> g_active_microphones{};
|
||||
retro_atomic_ptr_t g_active_microphones[MAX_ACTIVE_MICROPHONES];
|
||||
Common::CallGate g_microphone_poll_gate;
|
||||
} // namespace Input
|
||||
|
||||
namespace Video
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
#include <libretro.h>
|
||||
#include <vector>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
|
||||
#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<IOS::HLE::USB::Microphone*> 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
|
||||
|
||||
@@ -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<IOS::HLE::USB::Microphone*>(retro_atomic_load_acquire_ptr(&slot)))
|
||||
mic->PollRetroArchMic();
|
||||
}
|
||||
Common::CallGateExit(&Libretro::Input::g_microphone_poll_gate);
|
||||
}
|
||||
}
|
||||
else if(s_gc_mic_enabled)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
|
||||
#include "InputCommon/ControlReference/ControlReference.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#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<Binding*>(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<Binding*>(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<std::string> 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<u64>(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<int64_t>(bits));
|
||||
}
|
||||
|
||||
ControlReference::ControlReference()
|
||||
{
|
||||
SetRange(1.0);
|
||||
}
|
||||
|
||||
ControlReference::~ControlReference()
|
||||
{
|
||||
delete static_cast<Binding*>(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;
|
||||
}
|
||||
@@ -6,6 +6,9 @@
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
|
||||
#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<std::string> SetExpression(std::string expr);
|
||||
|
||||
ControlState range = 1;
|
||||
ControlState GetRange() const;
|
||||
void SetRange(ControlState range);
|
||||
|
||||
protected:
|
||||
ControlReference();
|
||||
std::string m_expression;
|
||||
std::unique_ptr<ciface::ExpressionParser::Expression> 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<ciface::ExpressionParser::Expression> parsed;
|
||||
};
|
||||
static void FreeBinding(quiesce_node_t* node);
|
||||
// Only inside an InputReadScope.
|
||||
Binding* PeekBinding() const
|
||||
{
|
||||
return static_cast<Binding*>(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
|
||||
|
||||
@@ -40,7 +40,7 @@ NumericSetting<int>& Attachments::GetSelectionSetting()
|
||||
return m_selection_setting;
|
||||
}
|
||||
|
||||
SubscribableSettingValue<int>& Attachments::GetAttachmentSetting()
|
||||
SettingValue<int>& Attachments::GetAttachmentSetting()
|
||||
{
|
||||
return m_selection_value;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ public:
|
||||
void SetSelectedAttachment(u32 val);
|
||||
|
||||
NumericSetting<int>& GetSelectionSetting();
|
||||
SubscribableSettingValue<int>& GetAttachmentSetting();
|
||||
SettingValue<int>& GetAttachmentSetting();
|
||||
|
||||
const std::vector<std::unique_ptr<AttachedController>>& GetAttachmentList() const;
|
||||
|
||||
@@ -43,7 +43,7 @@ public:
|
||||
void UpdateReferences(ciface::ExpressionParser::ControlEnvironment& env) override;
|
||||
|
||||
private:
|
||||
SubscribableSettingValue<int> m_selection_value;
|
||||
SettingValue<int> 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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "InputCommon/ControllerEmu/ControllerEmu.h"
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -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<std::recursive_mutex> EmulatedController::GetStateLock()
|
||||
EmulatedController::~EmulatedController()
|
||||
{
|
||||
std::unique_lock<std::recursive_mutex> lock(s_get_state_mutex);
|
||||
return lock;
|
||||
delete static_cast<DefaultDevice*>(retro_atomic_exchange_ptr(&m_default_device, nullptr));
|
||||
}
|
||||
|
||||
EmulatedController::EmulatedController() = default;
|
||||
|
||||
void EmulatedController::FreeDefaultDevice(quiesce_node_t* node)
|
||||
{
|
||||
delete reinterpret_cast<DefaultDevice*>(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<retro_atomic_int_t*>(&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<const DefaultDevice*>(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<DefaultDevice*>(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
|
||||
@@ -5,11 +5,12 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
|
||||
#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<std::unique_ptr<ControlGroup>> 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<std::recursive_mutex> 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
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -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 <typename ValueType>
|
||||
class SubscribableSettingValue final : public SettingValue<ValueType>
|
||||
{
|
||||
public:
|
||||
using Base = SettingValue<ValueType>;
|
||||
|
||||
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<void(ValueType)>;
|
||||
|
||||
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<int, SettingChangedCallback>;
|
||||
std::vector<IDCallbackPair> m_callback_pairs;
|
||||
int m_next_callback_id = 0;
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
};
|
||||
|
||||
} // namespace ControllerEmu
|
||||
@@ -138,15 +138,55 @@ ControlState ReshapableInput::GetDeadzoneRadiusAtAngle(double angle) const
|
||||
return GetInputRadiusAtAngle(angle) * GetDeadzonePercentage();
|
||||
}
|
||||
|
||||
ReshapableInput::~ReshapableInput()
|
||||
{
|
||||
delete static_cast<Shape*>(retro_atomic_exchange_ptr(&m_shape, nullptr));
|
||||
}
|
||||
|
||||
void ReshapableInput::FreeShape(quiesce_node_t* node)
|
||||
{
|
||||
delete reinterpret_cast<Shape*>(node);
|
||||
}
|
||||
|
||||
const ReshapableInput::Shape& ReshapableInput::PeekShape() const
|
||||
{
|
||||
static const Shape s_default{};
|
||||
const auto* shape = static_cast<const Shape*>(retro_atomic_load_acquire_ptr(&m_shape));
|
||||
return shape ? *shape : s_default;
|
||||
}
|
||||
|
||||
template <typename Edit>
|
||||
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<const Shape*>(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<Shape*>(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
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <retro_atomic.h>
|
||||
|
||||
#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 <typename Edit>
|
||||
void EditShape(Edit&& edit);
|
||||
|
||||
mutable retro_atomic_ptr_t m_shape{};
|
||||
SettingValue<double> m_deadzone_setting;
|
||||
ReshapeData m_center;
|
||||
};
|
||||
|
||||
} // namespace ControllerEmu
|
||||
@@ -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<void()>& 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<std::shared_ptr<ciface::Core::Device>>& 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<ciface::Core::Device> device)
|
||||
@@ -208,13 +198,9 @@ bool ControllerInterface::AddDevice(std::shared_ptr<ciface::Core::Device> 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<std::shared_ptr<ciface::Core::Device>>& 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<ciface::Core::Device> 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<ciface::Core::Device> 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::function<bool(const ciface::Core::De
|
||||
"Devices shouldn't be removed 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);
|
||||
|
||||
bool any_removed;
|
||||
{
|
||||
std::lock_guard lk(m_devices_mutex);
|
||||
const size_t erased = std::erase_if(m_devices, [&callback](const auto& dev) {
|
||||
if (callback(dev.get()))
|
||||
{
|
||||
NOTICE_LOG_FMT(CONTROLLERINTERFACE, "Removed device: {}", dev->GetQualifiedName());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
any_removed = erased != 0;
|
||||
}
|
||||
const bool any_removed =
|
||||
EditDevices([&callback](std::vector<std::shared_ptr<ciface::Core::Device>>& 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<std::weak_ptr<ciface::Core::Device>> 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<std::shared_ptr<ciface::Core::Device>> 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
|
||||
|
||||
Loaded 100 of 125 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user