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:
LibretroAdmin authored and libretroadmin committed 2026-11-05 20:11:31 +01:00
1 parent 407a632adc
commit f7d5095739
125 files changed
+2507 -5447

No files matched your search

+1 -5
View File
@@ -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 += \
+226
View File
@@ -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
+13
View File
@@ -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
+5
View File
@@ -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
+37 -32
View File
@@ -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();
+6 -3
View File
@@ -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)
+58 -44
View File
@@ -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
+16 -5
View File
@@ -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
+48 -11
View File
@@ -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,
+9 -9
View File
@@ -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)
-165
View File
@@ -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
-68
View File
@@ -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);
+20 -29
View File
@@ -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;
+2 -4
View File
@@ -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
-12
View File
@@ -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())
+2 -6
View File
@@ -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
+2 -12
View File
@@ -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;
+75 -54
View File
@@ -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";
+27 -11
View File
@@ -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;
};
+1 -1
View File
@@ -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); }};
+2 -11
View File
@@ -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
+6 -5
View File
@@ -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)
{
+2 -2
View File
@@ -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{};
+64 -18
View File
@@ -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
+21 -2
View File
@@ -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{};
+39 -29
View File
@@ -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;
}
+14 -6
View File
@@ -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
+37 -48
View File
@@ -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(&params);
retro_microphone_t* mic = Libretro::Input::g_microphone_interface.open_mic(&params);
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
+14 -11
View File
@@ -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 -21
View File
@@ -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)
+8 -4
View File
@@ -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
+2 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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;
};
+45 -18
View File
@@ -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]);
}
+20 -2
View File
@@ -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;
+3 -3
View File
@@ -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
}
+40 -23
View File
@@ -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()
+1 -1
View File
@@ -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);
}
+2 -2
View File
@@ -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;
+266 -108
View File
@@ -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
+1 -1
View File
@@ -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)
+8 -11
View File
@@ -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();
}
+5 -3
View File
@@ -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;
+17 -10
View File
@@ -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;
+17 -7
View File
@@ -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;
+5 -2
View File
@@ -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
+43 -17
View File
@@ -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)
+27 -4
View File
@@ -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);
-11
View File
@@ -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);
}
-2
View File
@@ -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
+76 -34
View File
@@ -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);
+30 -6
View File
@@ -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;
+1 -11
View File
@@ -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)
{
-3
View File
@@ -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
-4
View File
@@ -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);
-3
View File
@@ -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);
+1 -4
View File
@@ -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
View File
@@ -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();
+14 -3
View File
@@ -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;
};
+40 -5
View File
@@ -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
+15 -2
View File
@@ -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;
};
-5
View File
@@ -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;
-20
View File
@@ -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;
+1 -8
View File
@@ -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;
-2
View File
@@ -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
-223
View File
@@ -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
+9 -1
View File
@@ -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
+9 -2
View File
@@ -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([&center](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], &center.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], &center.y))
center.y /= CENTER_CONFIG_SCALE;
}
EditShape([&calibration, &center](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