diff --git a/AGENTS.md b/AGENTS.md index 2fc6c9cdef..4703abd63b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ for it: |---|---| | [docs/building.md](docs/building.md) | Build commands for the libretro core and the headless tools, unit tests, pspautotests | | [docs/debugging.md](docs/debugging.md) | Driving the WebSocket debugger and PPSSPPHeadless from a script, measuring a commercial game with headless, comparing binaries' speed (`Tools/headless_bench.py`), breakpoint reliability per CPU backend, debugging a game that works on hardware | -| [docs/DebuggerThreading.md](docs/DebuggerThreading.md) | `Core_RunOnCPUThread` / `g_frameMutex` / shutdown-lock rules - required reading before touching debugger code | +| [docs/DebuggerThreading.md](docs/DebuggerThreading.md) | Debugger thread ownership: `Core_RunOnCPUThread`, per-connection mailboxes, no locks - required reading before touching debugger code | | [docs/HLEModules.md](docs/HLEModules.md) | Adding an HLE module or function, and the seven build files a new source file goes in | | [docs/translations.md](docs/translations.md) | Translating UI strings with Tools/langtool | | [docs/pspautotests.md](docs/pspautotests.md) | Workflow for improving PPSSPP using pspautotests | @@ -96,6 +96,13 @@ for it: differently by dialog type, mode or size, cover each of those. A dialog fix once claimed hardware behaviour that the test recorded an hour later contradicted, and the test ran a message dialog, which took a different branch from the savedata dialog that then hung Freak Out. +6. **No locks, no sleeps.** Code built into the core and the headless tools uses no mutexes, + condition variables, spinlocks, semaphores, `std::atomic`, `std::thread` or `volatile` for + synchronization, and never sleeps or polls on a timer. Use `retro_atomic.h`, and in + `Common/Thread/`: `Thread`, `MpscQueue` (any thread pushes, one drains), `ParkingLotWait` / + `ParkingLotNotify` and `EventCounter` for blocking waits, `ReaderGate` for read-mostly data + (copy, swap, drain, free). Prefer giving state a single owning thread and sending it messages. + The PSP filesystem, sceIo and the utility dialogs belong to the emulation thread. ## Build and validation @@ -240,9 +247,9 @@ it when you add a command. - Breakpoints are most reliable on the interpreter (`-i`): under the JITs, memory breakpoints only work for constant addresses and register breakpoints never trip at all. -- **Debugger code that runs off the CPU thread has a lock order that has deadlocked for real** - - `Core_RunOnCPUThread()` for mutations, `g_frameMutex` for hot reads. Read - [docs/DebuggerThreading.md](docs/DebuggerThreading.md) before touching it. +- **Debugger code off the CPU thread touches emulator state only inside `Core_RunOnCPUThread()`**; + events the emulator produces are pushed to per-connection mailboxes, never polled, and there are + no locks. Read [docs/DebuggerThreading.md](docs/DebuggerThreading.md) before touching it. ## Commit message style diff --git a/CMakeLists.txt b/CMakeLists.txt index 70c828f928..6ba8b06931 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -331,6 +331,8 @@ if(APPLE) endif() include_directories("${CMAKE_SOURCE_DIR}") +# Common headers use libretro-common (retro_atomic.h and friends), so every target needs it. +include_directories(libretro/libretro-common/include) if(ANDROID_LEGACY) add_compile_definitions(ANDROID_LEGACY) diff --git a/Common/CMakeLists.txt b/Common/CMakeLists.txt index 81513f8a14..b43fe304f2 100644 --- a/Common/CMakeLists.txt +++ b/Common/CMakeLists.txt @@ -23,8 +23,8 @@ set(CommonVR VR/VRRenderer.h ) -# The HTTP/WebSocket servers, the path browser and the Windows console are for -# the headless tools; libretro/Makefile.common leaves them out of the core too. +# The UI framework, the HTTP/WebSocket servers, the path browser and the Windows console +# are for the headless tools; libretro/Makefile.common leaves them out of the core too. set(CommonNonLibretro) if(NOT LIBRETRO) list(APPEND CommonNonLibretro @@ -141,6 +141,8 @@ add_library(Common STATIC File/Path.h File/Path.cpp File/FileUtil.cpp + File/PositionalFile.cpp + File/PositionalFile.h File/FileUtil.h File/DirListing.cpp File/DirListing.h @@ -279,6 +281,8 @@ add_library(Common STATIC Thread/ParallelLoop.h Thread/ParkingLot.cpp Thread/ParkingLot.h + Thread/Thread.cpp + Thread/Thread.h Thread/Promise.h Thread/ThreadUtil.cpp Thread/ThreadUtil.h @@ -324,6 +328,7 @@ add_library(Common STATIC ${CMAKE_SOURCE_DIR}/ext/jpge/jpgd.h ${CMAKE_SOURCE_DIR}/ext/jpge/jpge.cpp ${CMAKE_SOURCE_DIR}/ext/jpge/jpge.h + ${CommonNonLibretro} ) setup_target_project(Common Common) diff --git a/Common/Data/Text/I18n.cpp b/Common/Data/Text/I18n.cpp index e9f1683047..e94685d300 100644 --- a/Common/Data/Text/I18n.cpp +++ b/Common/Data/Text/I18n.cpp @@ -51,26 +51,75 @@ std::string I18NRepo::LanguageID() { } I18NRepo::I18NRepo() { + for (size_t i = 0; i < (size_t)I18NCat::CATEGORY_COUNT; i++) + retro_atomic_ptr_init(&cats_[i], nullptr); Clear(); } +void I18NRepo::Publish(size_t index, I18NCategory *cat) { + owned_.emplace_back(cat); + retro_atomic_store_release_ptr(&cats_[index], cat); +} + void I18NRepo::Clear() { - std::lock_guard guard(catsLock_); - for (auto &iter : cats_) { + for (size_t i = 0; i < (size_t)I18NCat::CATEGORY_COUNT; i++) { // Initialize with empty categories, so that early lookups don't crash. - iter = std::make_shared(); + Publish(i, new I18NCategory()); } } -I18NCategory::I18NCategory(const Section §ion) { +I18NCategory::I18NCategory() { + for (int i = 0; i < MISSED_SLOTS; i++) + retro_atomic_ptr_init(&missedHashes_[i], nullptr); + mpsc_stack_init(&missed_); +} + +I18NCategory::I18NCategory(const Section §ion) : I18NCategory() { std::map sectionMap = section.ToMap(); SetMap(sectionMap); name_ = section.name().c_str(); } -void I18NCategory::Clear() { - map_.clear(); - missedKeyLog_.clear(); +I18NCategory::~I18NCategory() { + ClearMissed(); +} + +void I18NCategory::ClearMissed() { + mpsc_stack_node_t *node = mpsc_stack_drain(&missed_); + while (node) { + mpsc_stack_node_t *next = node->next; + delete ((MissedLink *)node)->self; + node = next; + } + for (int i = 0; i < MISSED_SLOTS; i++) + retro_atomic_store_relaxed_ptr(&missedHashes_[i], nullptr); +} + +void I18NCategory::NoteMissed(std::string_view key, std::string_view def) { + // FNV-1a, never zero, which marks a free slot. + uintptr_t h = (uintptr_t)14695981039346656037ULL; + for (char c : key) + h = (h ^ (unsigned char)c) * (uintptr_t)1099511628211ULL; + if (h == 0) + h = 1; + for (int probe = 0; probe < MISSED_SLOTS; probe++) { + retro_atomic_ptr_t *slot = &missedHashes_[(h + probe) & (MISSED_SLOTS - 1)]; + const uintptr_t seen = (uintptr_t)retro_atomic_load_acquire_ptr(slot); + if (seen == h) + return; // already recorded + if (seen == 0) { + if (!retro_atomic_cas_ptr(slot, nullptr, (void *)h)) { + if ((uintptr_t)retro_atomic_load_acquire_ptr(slot) == h) + return; + continue; + } + MissedKey *missed = new MissedKey{ {}, std::string(key), std::string(!def.empty() ? def : key) }; + missed->link.self = missed; + mpsc_stack_push(&missed_, &missed->link.node); + return; + } + } + // Full: stop recording. } std::string_view I18NCategory::T(std::string_view key, std::string_view def) { @@ -85,12 +134,7 @@ std::string_view I18NCategory::T(std::string_view key, std::string_view def) { if (key != "Font") { // Font is allowed to be missing. DEBUG_LOG(Log::UI, "Missing translation [%s] %.*s (%.*s)", name_.c_str(), STR_VIEW(key), STR_VIEW(def)); - std::lock_guard guard(missedKeyLock_); - std::string missedKey(key); - if (!def.empty()) - missedKeyLog_[missedKey] = def; - else - missedKeyLog_[missedKey] = missedKey; + NoteMissed(key, def); } return !def.empty() ? def : key; } @@ -105,15 +149,9 @@ const char *I18NCategory::T_cstr(const char *key, const char *def) { // Too early. This is probably in desktop-ui translation. return def ? def : key; } - std::string missedKey(key); - if (missedKey != "Font") { + if (strcmp(key, "Font") != 0) { DEBUG_LOG(Log::UI, "Missing translation %s (%s)", key, def); - - std::lock_guard guard(missedKeyLock_); - if (def) - missedKeyLog_[missedKey] = def; - else - missedKeyLog_[missedKey] = std::string(key); + NoteMissed(key, def ? def : ""); } return def ? def : key; } @@ -130,14 +168,19 @@ void I18NCategory::SetMap(const std::map &m) { } std::map> I18NCategory::Missed() const { - std::lock_guard guard(missedKeyLock_); - return missedKeyLog_; + // Pushes only add in front of what is already there, so the walk is safe. + std::map> missed; + const mpsc_stack_node_t *node = (const mpsc_stack_node_t *)retro_atomic_load_acquire_ptr(const_cast(&missed_.head)); + for (; node; node = node->next) { + const MissedKey *m = ((const MissedLink *)node)->self; + missed[m->key] = m->def; + } + return missed; } -std::shared_ptr I18NRepo::GetCategory(I18NCat category) { - std::lock_guard guard(catsLock_); +I18NCategory *I18NRepo::GetCategory(I18NCat category) { if (category != I18NCat::NONE) - return cats_[(size_t)category]; + return Cat(category); else return nullptr; } @@ -167,38 +210,38 @@ bool I18NRepo::LoadIni(const std::string &languageID, const Path &overridePath) if (!ini.LoadFromVFS(g_VFS, iniPath.ToString())) return false; - Clear(); - + // Build every category first, then publish: a lookup sees either language, whole. const std::vector> §ions = ini.Sections(); - - std::lock_guard guard(catsLock_); + I18NCategory *built[(size_t)I18NCat::CATEGORY_COUNT]{}; for (auto §ion : sections) { for (size_t i = 0; i < (size_t)I18NCat::CATEGORY_COUNT; i++) { if (!strcmp(section->name().c_str(), g_categoryNames[i])) { - cats_[i].reset(new I18NCategory(*section.get())); + delete built[i]; + built[i] = new I18NCategory(*section.get()); } } } + for (size_t i = 0; i < (size_t)I18NCat::CATEGORY_COUNT; i++) + Publish(i, built[i] ? built[i] : new I18NCategory()); languageID_ = languageID; return true; } void I18NRepo::LogMissingKeys() const { - std::lock_guard guard(catsLock_); for (size_t i = 0; i < (size_t)I18NCat::CATEGORY_COUNT; i++) { - auto &cat = cats_[i]; + const I18NCategory *cat = Cat((I18NCat)i); for (auto &key : cat->Missed()) { INFO_LOG(Log::UI, "Missing translation [%s]: %s (%s)", g_categoryNames[i], key.first.c_str(), key.second.c_str()); } } } -std::shared_ptr GetI18NCategory(I18NCat category) { +I18NCategory *GetI18NCategory(I18NCat category) { if (category == I18NCat::NONE) { - return std::shared_ptr(); + return nullptr; } - std::shared_ptr cat = g_i18nrepo.GetCategory(category); + I18NCategory *cat = g_i18nrepo.GetCategory(category); _dbg_assert_(cat); return cat; } diff --git a/Common/Data/Text/I18n.h b/Common/Data/Text/I18n.h index 9d1c98045f..0779e74d0b 100644 --- a/Common/Data/Text/I18n.h +++ b/Common/Data/Text/I18n.h @@ -9,16 +9,19 @@ // As usual, everything is UTF-8. Nothing else allowed. #include -#include #include #include #include #include +#include +#include + #include "Common/Common.h" #include "Common/File/Path.h" -// Reasonably thread safe. +// Lookups may run on any thread while the language is reloaded: a category is immutable once +// published, and a replaced one is kept until the repo goes away. class I18NRepo; class IniFile; @@ -72,8 +75,9 @@ struct I18NEntry { class I18NCategory { public: - I18NCategory() {} + I18NCategory(); explicit I18NCategory(const Section §ion); + ~I18NCategory(); // Faster since the string lengths don't need to be recomputed. std::string_view T(std::string_view key, std::string_view def = ""); @@ -81,20 +85,34 @@ public: // Try to avoid this. Still useful in snprintf. const char *T_cstr(const char *key, const char *def = nullptr); + // Safe alongside lookups. ClearMissed() must not run alongside Missed(). std::map> Missed() const; const std::map> &GetMap() { return map_; } - void ClearMissed() { missedKeyLog_.clear(); } - void Clear(); + void ClearMissed(); private: - I18NCategory(I18NRepo *repo, const char *name) {} void SetMap(const std::map &m); + void NoteMissed(std::string_view key, std::string_view def); // std::less<> is needed to be able to look up string_views in a string-keyed map. std::map> map_; - mutable std::mutex missedKeyLock_; - std::map> missedKeyLog_; + + // Missed keys: a lock-free set of key hashes so each is recorded once, and the + // records themselves on an MPSC stack that is only ever walked, never popped. + struct MissedKey; + struct MissedLink { + mpsc_stack_node_t node; // first, so a node pointer is a link pointer + MissedKey *self; + }; + struct MissedKey { + MissedLink link; + std::string key; + std::string def; + }; + enum { MISSED_SLOTS = 1024 }; + retro_atomic_ptr_t missedHashes_[MISSED_SLOTS]; // hashes, as pointer-sized words + mpsc_stack_t missed_; std::string name_; // Noone else can create these. @@ -109,28 +127,33 @@ public: std::string LanguageID(); - std::shared_ptr GetCategory(I18NCat category); + I18NCategory *GetCategory(I18NCat category); // Translate the string, by looking up "key" in the file, and falling back to either def or key, in that order, if the lookup fails. // def can (and usually is) set to nullptr. std::string_view T(I18NCat category, std::string_view key, std::string_view def = "") { if (category == I18NCat::NONE) return !def.empty() ? def : key; - return cats_[(size_t)category]->T(key, def); + return Cat(category)->T(key, def); } const char *T_cstr(I18NCat category, const char *key, const char *def = nullptr) { if (category == I18NCat::NONE) return def ? def : key; - return cats_[(size_t)category]->T_cstr(key, def); + return Cat(category)->T_cstr(key, def); } void LogMissingKeys() const; private: Path GetIniPath(const std::string &languageID) const; void Clear(); + void Publish(size_t index, I18NCategory *cat); + I18NCategory *Cat(I18NCat category) const { + return (I18NCategory *)retro_atomic_load_acquire_ptr(const_cast(&cats_[(size_t)category])); + } - mutable std::mutex catsLock_; - std::shared_ptr cats_[(size_t)I18NCat::CATEGORY_COUNT]; + retro_atomic_ptr_t cats_[(size_t)I18NCat::CATEGORY_COUNT]; + // Every category ever published, since a lookup may still hold an old one. + std::vector> owned_; std::string languageID_; }; @@ -138,7 +161,7 @@ extern I18NRepo g_i18nrepo; // These are simply talking to the one global instance of I18NRepo. -std::shared_ptr GetI18NCategory(I18NCat cat); +I18NCategory *GetI18NCategory(I18NCat cat); inline std::string_view T(I18NCat category, std::string_view key, std::string_view def = "") { return g_i18nrepo.T(category, key, def); diff --git a/Common/File/PositionalFile.cpp b/Common/File/PositionalFile.cpp new file mode 100644 index 0000000000..191fb04a5c --- /dev/null +++ b/Common/File/PositionalFile.cpp @@ -0,0 +1,95 @@ +#include + +#include + +#include "Common/File/FileUtil.h" +#include "Common/File/PositionalFile.h" + +namespace { + +// Ids are never reused, so a handle cached for a file that has since gone away +// can't be mistaken for one of a newer file. +retro_atomic_int_t g_nextId; + +struct Handle { + uint32_t id; + FILE *file; + int64_t pos; // where file is; -1 when unknown + uint32_t lastUse; +}; + +// This thread's handles: a few files at once, least recently used goes first. +struct HandleCache { + enum { SLOTS = 8 }; + Handle slots[SLOTS]{}; + uint32_t clock = 0; + + ~HandleCache() { + for (Handle &h : slots) { + if (h.file) + fclose(h.file); + } + } + + Handle *Get(uint32_t id, const Path &path) { + Handle *victim = &slots[0]; + for (Handle &h : slots) { + if (h.file && h.id == id) { + h.lastUse = ++clock; + return &h; + } + if (!h.file || h.lastUse < victim->lastUse) + victim = &h; + } + FILE *file = File::OpenCFile(path, "rb"); + if (!file) + return nullptr; + if (victim->file) + fclose(victim->file); + *victim = Handle{ id, file, 0, ++clock }; + return victim; + } + + void Forget(uint32_t id) { + for (Handle &h : slots) { + if (h.file && h.id == id) { + fclose(h.file); + h = Handle{}; + } + } + } +}; + +thread_local HandleCache t_handles; + +} // namespace + +PositionalFile::PositionalFile(const Path &path) : path_(path) { + id_ = (uint32_t)retro_atomic_fetch_add_int(&g_nextId, 1) + 1; + Handle *h = t_handles.Get(id_, path_); + if (h) + size_ = File::GetFileSize(h->file); +} + +PositionalFile::~PositionalFile() { + // Other threads' handles for this file close when they're evicted or the thread ends. + t_handles.Forget(id_); +} + +size_t PositionalFile::ReadAt(uint64_t offset, void *dst, size_t len) { + if (len == 0 || size_ < 0) + return 0; + Handle *h = t_handles.Get(id_, path_); + if (!h) + return 0; + if (h->pos != (int64_t)offset) { + if (File::Fseek(h->file, (int64_t)offset, SEEK_SET) != 0) { + h->pos = -1; + return 0; + } + h->pos = (int64_t)offset; + } + const size_t got = fread(dst, 1, len, h->file); + h->pos = got == len ? h->pos + (int64_t)got : -1; + return got; +} diff --git a/Common/File/PositionalFile.h b/Common/File/PositionalFile.h new file mode 100644 index 0000000000..60869da7af --- /dev/null +++ b/Common/File/PositionalFile.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#include "Common/File/Path.h" + +// Positioned reads of one file from any number of threads, without a lock: each thread +// reads through a handle of its own, opened on first use and cached. A thread reading +// sequentially doesn't seek, which keeps whatever the stream buffers. +class PositionalFile { +public: + explicit PositionalFile(const Path &path); + ~PositionalFile(); + PositionalFile(const PositionalFile &) = delete; + PositionalFile &operator=(const PositionalFile &) = delete; + + bool IsOpen() const { return size_ >= 0; } + int64_t Size() const { return size_; } + const Path &GetPath() const { return path_; } + + // Returns the bytes read, 0 at the end or on error. + size_t ReadAt(uint64_t offset, void *dst, size_t len); + +private: + Path path_; + uint32_t id_; + int64_t size_ = -1; +}; diff --git a/Common/File/VFS/SevenZipFileReader.cpp b/Common/File/VFS/SevenZipFileReader.cpp index a344f6c9c3..3e31c7796a 100644 --- a/Common/File/VFS/SevenZipFileReader.cpp +++ b/Common/File/VFS/SevenZipFileReader.cpp @@ -20,6 +20,7 @@ #include "Common/Log.h" #include "Common/StringUtils.h" #include "Common/File/VFS/SevenZipFileReader.h" +#include "Common/Thread/ParkingLot.h" static constexpr size_t SEVENZIP_LOOKBUF_SIZE = 1 << 14; static constexpr size_t SEVENZIP_STREAM_LOOKAHEAD = 1 << 18; @@ -384,7 +385,6 @@ SevenZipFileReader::SevenZipFileReader(const Path &archivePath, const std::strin } SevenZipFileReader::~SevenZipFileReader() { - std::lock_guard guard(lock_); CloseArchive(); } @@ -398,16 +398,20 @@ SevenZipFileReader *SevenZipFileReader::Create(const Path &archivePath, std::str } bool SevenZipFileReader::OpenArchive(bool logErrors) { - std::lock_guard guard(lock_); - if (valid_) { return true; } - static bool crcTableGenerated = false; - if (!crcTableGenerated) { + // The CRC table is process-wide: 0 = not built, 1 = building, 2 = ready. + static retro_atomic_int_t crcTableState{ 0 }; + if (retro_atomic_cas_int(&crcTableState, 0, 1)) { CrcGenerateTable(); - crcTableGenerated = true; + retro_atomic_store_release_int(&crcTableState, 2); + ParkingLotNotify(&crcTableState); + } else { + ParkingLotWait(&crcTableState, [] { + return retro_atomic_load_acquire_int(&crcTableState) == 2; + }); } if (OpenArchiveStreamForRead(&archiveStream_, archivePath_) != 0) { @@ -532,8 +536,6 @@ bool SevenZipFileReader::FindEntry(std::string_view path, UInt32 *index, bool *i } uint8_t *SevenZipFileReader::ExtractFile(UInt32 fileIndex, size_t *size) { - std::lock_guard guard(lock_); - if (!valid_) { return nullptr; } diff --git a/Common/File/VFS/SevenZipFileReader.h b/Common/File/VFS/SevenZipFileReader.h index 5411587e73..cbbf54823f 100644 --- a/Common/File/VFS/SevenZipFileReader.h +++ b/Common/File/VFS/SevenZipFileReader.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -16,6 +15,7 @@ #include "Common/File/FileUtil.h" #include "Common/File/Path.h" +// Not thread safe: extraction shares one stream and block cache, so use it from one thread at a time. class SevenZipFileReader : public VFSBackend { public: static SevenZipFileReader *Create(const Path &archivePath, std::string_view inArchivePath, bool logErrors = true); @@ -68,7 +68,6 @@ private: Path archivePath_; std::string inArchivePath_; - mutable std::mutex lock_; CFileInStream archiveStream_; CLookToRead2 lookStream_; ISzAlloc allocImp_; diff --git a/Common/File/VFS/ZipFileReader.cpp b/Common/File/VFS/ZipFileReader.cpp index 316d5fd12a..25fedcb49c 100644 --- a/Common/File/VFS/ZipFileReader.cpp +++ b/Common/File/VFS/ZipFileReader.cpp @@ -11,6 +11,7 @@ #include "Common/Common.h" #include "Common/Log.h" +#include "Common/File/PositionalFile.h" #include "Common/File/VFS/ZipFileReader.h" #include "Common/StringUtils.h" @@ -28,20 +29,18 @@ static std::string AsciiLower(std::string_view s) { } // Heap-allocated so the context rzip holds survives moves of the container. +// Reads come from any thread; each reads through a handle of its own. struct ZipFileSource { - FILE *file; + explicit ZipFileSource(const Path &path) : file(path) {} + PositionalFile file; }; ZipContainer::ZipContainer(const Path &path) { - FILE *file = File::OpenCFile(path, "rb"); - if (!file) - return; - const int64_t size = File::GetFileSize(file); - source_ = new ZipFileSource{ file }; + source_ = new ZipFileSource(path); read_ = &FileRead; userdata_ = source_; - if (size > 0) - Open((uint64_t)size); + if (source_->file.Size() > 0) + Open((uint64_t)source_->file.Size()); if (!zip_) close(); } @@ -63,10 +62,7 @@ void ZipContainer::Open(uint64_t size) { } int64_t ZipContainer::FileRead(void *userdata, uint64_t off, void *dst, size_t len) { - FILE *file = ((ZipFileSource *)userdata)->file; - if (File::Fseek(file, (int64_t)off, SEEK_SET) != 0) - return -1; - return (int64_t)fread(dst, 1, len, file); + return (int64_t)((ZipFileSource *)userdata)->file.ReadAt(off, dst, len); } ZipContainer::ZipContainer(ZipContainer &&other) noexcept { @@ -94,11 +90,8 @@ void ZipContainer::close() noexcept { rzip_archive_close(zip_); zip_ = nullptr; } - if (source_) { - fclose(source_->file); - delete source_; - source_ = nullptr; - } + delete source_; + source_ = nullptr; read_ = nullptr; userdata_ = nullptr; lowerIndex_.clear(); @@ -226,7 +219,6 @@ ZipFileReader *ZipFileReader::Create(const Path &zipFile, std::string_view inZip } ZipFileReader::~ZipFileReader() { - std::lock_guard guard(lock_); zip_file_.close(); } @@ -245,12 +237,7 @@ uint8_t *ZipFileReader::ReadFile(std::string_view path, size_t *size) { return 0; } uint8_t *contents = new uint8_t[entrySize + 1]; - bool ok; - { - std::lock_guard guard(lock_); - ok = zip_file_.ExtractInto(index, contents, (size_t)entrySize); - } - if (!ok) { + if (!zip_file_.ExtractInto(index, contents, (size_t)entrySize)) { ERROR_LOG(Log::IO, "Error reading %s from ZIP", temp_path.c_str()); delete[] contents; return 0; @@ -456,7 +443,6 @@ size_t ZipFileReader::Read(VFSOpenFile *vfsOpenFile, void *buffer, size_t length return 0; if (!file->decoded && file->pos == 0 && length >= file->size) { // The whole member in one read: decode it where it's going. - std::lock_guard guard(lock_); if (!zip_file_.ExtractInto(file->reference->zi, (uint8_t *)buffer, file->size)) return 0; file->pos = file->size; @@ -464,7 +450,6 @@ size_t ZipFileReader::Read(VFSOpenFile *vfsOpenFile, void *buffer, size_t length } if (!file->decoded) { file->data.resize(file->size); - std::lock_guard guard(lock_); if (!zip_file_.ExtractInto(file->reference->zi, file->data.data(), file->size)) return 0; file->decoded = true; @@ -481,7 +466,7 @@ void ZipFileReader::CloseFile(VFSOpenFile *vfsOpenFile) { delete file; } -bool ReadSingleFileFromZip(Path zipFile, const char *path, std::string *data, std::mutex *mutex) { +bool ReadSingleFileFromZip(Path zipFile, const char *path, std::string *data) { ZipContainer zip(zipFile); if (!zip) { return false; @@ -501,12 +486,6 @@ bool ReadSingleFileFromZip(Path zipFile, const char *path, std::string *data, st if (!zip.ExtractInto(index, (uint8_t *)&contents[0], contents.size())) { return false; } - if (mutex) { - mutex->lock(); - } data->swap(contents); - if (mutex) { - mutex->unlock(); - } return true; } diff --git a/Common/File/VFS/ZipFileReader.h b/Common/File/VFS/ZipFileReader.h index 10f25a8e50..1a991cef83 100644 --- a/Common/File/VFS/ZipFileReader.h +++ b/Common/File/VFS/ZipFileReader.h @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -94,12 +93,12 @@ private: // Path has to be either an empty string, or a string ending with a /. bool GetZipListings(const std::string &path, std::set &files, std::set &directories); + // The directory is parsed at open and never changes, and every read goes through + // a handle of the reading thread's own, so readers on any thread need no lock. ZipContainer zip_file_; - std::mutex lock_; std::string inZipPath_; Path zipPath_; }; // When you just want a single file from a ZIP, and don't care about accurate error reporting, use this. -// Mutex will be locked while updating data, if non-null. -bool ReadSingleFileFromZip(Path zipFile, const char *path, std::string *data, std::mutex *mutex); +bool ReadSingleFileFromZip(Path zipFile, const char *path, std::string *data); diff --git a/Common/GPU/GPUBackendCommon.cpp b/Common/GPU/GPUBackendCommon.cpp index b53001484d..a02d7bdbc6 100644 --- a/Common/GPU/GPUBackendCommon.cpp +++ b/Common/GPU/GPUBackendCommon.cpp @@ -1,28 +1,34 @@ -#include -#include +#include #include "Common/GPU/GPUBackendCommon.h" // Global push buffer tracker for GPU memory profiling. // Don't want to manually dig up all the active push buffers. -static std::mutex g_pushBufferListMutex; -static std::set g_pushBuffers; +// Slots are claimed and cleared with CAS; the list is only read for the debug overlay. +enum { MAX_PUSH_BUFFERS = 128 }; +static retro_atomic_ptr_t g_pushBuffers[MAX_PUSH_BUFFERS]; std::vector GetActiveGPUMemoryManagers() { std::vector buffers; - std::lock_guard guard(g_pushBufferListMutex); - for (auto iter : g_pushBuffers) { - buffers.push_back(iter); + for (int i = 0; i < MAX_PUSH_BUFFERS; i++) { + GPUMemoryManager *manager = (GPUMemoryManager *)retro_atomic_load_acquire_ptr(&g_pushBuffers[i]); + if (manager) + buffers.push_back(manager); } return buffers; } void RegisterGPUMemoryManager(GPUMemoryManager *manager) { - std::lock_guard guard(g_pushBufferListMutex); - g_pushBuffers.insert(manager); + for (int i = 0; i < MAX_PUSH_BUFFERS; i++) { + if (retro_atomic_cas_ptr(&g_pushBuffers[i], nullptr, (void *)manager)) + return; + } + // Full: this one just won't show in the overlay. } void UnregisterGPUMemoryManager(GPUMemoryManager *manager) { - std::lock_guard guard(g_pushBufferListMutex); - g_pushBuffers.erase(manager); + for (int i = 0; i < MAX_PUSH_BUFFERS; i++) { + if (retro_atomic_cas_ptr(&g_pushBuffers[i], (void *)manager, nullptr)) + return; + } } diff --git a/Common/GPU/OpenGL/GLFrameData.h b/Common/GPU/OpenGL/GLFrameData.h index 16756f1522..77dbb8f182 100644 --- a/Common/GPU/OpenGL/GLFrameData.h +++ b/Common/GPU/OpenGL/GLFrameData.h @@ -1,11 +1,11 @@ #pragma once -#include -#include #include #include #include +#include + #include "Common/GPU/OpenGL/GLCommon.h" class GLRShader; @@ -53,15 +53,15 @@ struct GLFrameData { // Also used for indexing into the frame timing history buffer. uint64_t frameId; - std::mutex fenceMutex; - std::condition_variable fenceCondVar; - bool readyForFence = true; + // Set by the render thread once it has presented this frame, cleared by BeginFrame. + retro_atomic_int_t readyForFence{ 1 }; // Swapchain. bool hasBegun = false; GLDeleter deleter; GLDeleter deleter_prev; + // Render thread only. std::set activePushBuffers; GLQueueProfileContext profile; diff --git a/Common/GPU/OpenGL/GLRenderManager.cpp b/Common/GPU/OpenGL/GLRenderManager.cpp index 2c0346a93f..c0962dd827 100644 --- a/Common/GPU/OpenGL/GLRenderManager.cpp +++ b/Common/GPU/OpenGL/GLRenderManager.cpp @@ -103,6 +103,9 @@ void GLRenderManager::ThreadEnd() { VLOG(" PULL: Quitting"); + // Buffers that never made it over in a task, so the deleters below find them. + AddNewPushBuffers(newPushBuffers_); + // Good time to run all the deleters to get rid of leftover objects. for (int i = 0; i < MAX_INFLIGHT_FRAMES; i++) { // Since we're in shutdown, we should skip the GL calls on Android. @@ -124,21 +127,11 @@ void GLRenderManager::NotifyEmuThreadExit() { exitNotified_ = true; // We need to make sure that the render thread isn't waiting for more work, since the emu thread is gone. // This is a bit of a hack, but it works. - GLRRenderThreadTask *task = new GLRRenderThreadTask(GLRRunType::EXIT); - { - std::unique_lock lock(pushMutex_); - renderThreadQueue_.push(task); - } - pushCondVar_.notify_one(); + PushTask(new GLRRenderThreadTask(GLRRunType::EXIT)); } void GLRenderManager::NotifyEmuThreadPaused() { - GLRRenderThreadTask *task = new GLRRenderThreadTask(GLRRunType::PAUSE); - { - std::unique_lock lock(pushMutex_); - renderThreadQueue_.push(task); - } - pushCondVar_.notify_one(); + PushTask(new GLRRenderThreadTask(GLRRunType::PAUSE)); } // Unlike in Vulkan, this isn't a full independent function, instead it gets called every frame. @@ -164,12 +157,15 @@ bool GLRenderManager::RunQueue(RunUntil until) { while (true) { // Pop a task off the queue and execute it. Exiting this loop is done with a special EXIT task, // to keep things uniform. - { - std::unique_lock lock(pushMutex_); - pushCondVar_.wait(lock, [this] { return !renderThreadQueue_.empty(); }); - task = renderThreadQueue_.front(); - renderThreadQueue_.pop(); + while (renderThreadTasks_.empty()) { + const int seen = renderThreadWork_.Seen(); + renderThreadQueue_.Drain([this](GLRRenderThreadTask *t) { renderThreadTasks_.push_back(t); }); + if (renderThreadTasks_.empty()) { + renderThreadWork_.Wait(seen); + } } + task = renderThreadTasks_.front(); + renderThreadTasks_.pop_front(); if (task->runType == GLRRunType::EXIT) { VLOG(" PULL: Frame %d EXIT (%0.3f)", task->frame, time_now_d()); @@ -374,18 +370,14 @@ void GLRenderManager::BeginFrame(bool enableProfiling) { frameTimeData.afterFenceWait = frameTimeData.frameBegin; GLFrameData &frameData = frameData_[curFrame]; + VLOG("PUSH: BeginFrame (curFrame = %d, time=%0.3f)", curFrame, time_now_d()); + // The render thread may still be presenting this frame slot's last use. + ParkingLotWait(&frameData.readyForFence, [&] { return retro_atomic_load_acquire_int(&frameData.readyForFence) != 0; }); + retro_atomic_store_relaxed_int(&frameData.readyForFence, 0); + frameData.frameId = frameIdGen_; frameData.profile.enabled = enableProfiling; - frameIdGen_++; - { - std::unique_lock lock(frameData.fenceMutex); - VLOG("PUSH: BeginFrame (curFrame = %d, readyForFence = %d, time=%0.3f)", curFrame, (int)frameData.readyForFence, time_now_d()); - while (!frameData.readyForFence) { - frameData.fenceCondVar.wait(lock); - } - frameData.readyForFence = false; - } insideFrame_ = true; } @@ -420,28 +412,14 @@ void GLRenderManager::Finish() { VLOG("PUSH: Finish, pushing task. curFrame = %d", curFrame); GLRRenderThreadTask *task = new GLRRenderThreadTask(GLRRunType::SUBMIT); task->frame = curFrame; - { - std::unique_lock lock(pushMutex_); - { - std::lock_guard initLock(initStepsMutex_); - task->initSteps = std::move(initSteps_); - initSteps_.clear(); - } - task->steps = std::move(steps_); - renderThreadQueue_.push(task); - steps_.clear(); - pushCondVar_.notify_one(); - } + TakeRecordedWork(task); + PushTask(task); } void GLRenderManager::Present() { GLRRenderThreadTask *presentTask = new GLRRenderThreadTask(GLRRunType::PRESENT); presentTask->frame = curFrame_; - { - std::unique_lock lock(pushMutex_); - renderThreadQueue_.push(presentTask); - pushCondVar_.notify_one(); - } + PushTask(presentTask); int newCurFrame = curFrame_ + 1; if (newCurFrame >= inflightFrames_) { @@ -481,15 +459,14 @@ bool GLRenderManager::Run(GLRRenderThreadTask &task) { VLOG(" PULL: Frame %d.readyForFence = true", task.frame); - { - std::lock_guard lock(frameData.fenceMutex); - frameData.readyForFence = true; - frameData.fenceCondVar.notify_one(); - // At this point, we're done with this framedata (for now). - } + // At this point, we're done with this framedata (for now). + retro_atomic_store_release_int(&frameData.readyForFence, 1); + ParkingLotNotify(&frameData.readyForFence); return swapRequest; } + AddNewPushBuffers(task.newPushBuffers); + if (!frameData.hasBegun) { frameData.hasBegun = true; @@ -502,7 +479,6 @@ bool GLRenderManager::Run(GLRRenderThreadTask &task) { // Run this after RunInitSteps so any fresh GLRBuffers for the pushbuffers can get created. if (!skipGLCalls_) { - std::lock_guard lock(pushBuffersMutex_); for (auto iter : frameData.activePushBuffers) { iter->Flush(); iter->UnmapDevice(); @@ -529,7 +505,6 @@ bool GLRenderManager::Run(GLRRenderThreadTask &task) { } if (!skipGLCalls_) { - std::lock_guard lock(pushBuffersMutex_); for (auto iter : frameData.activePushBuffers) { iter->MapDevice(bufferStrategy_); } @@ -544,11 +519,8 @@ bool GLRenderManager::Run(GLRRenderThreadTask &task) { // glFinish is not actually necessary here, and won't be unless we start using // glBufferStorage. Then we need to use fences. - { - std::lock_guard lock(syncMutex_); - syncDone_ = true; - syncCondVar_.notify_one(); - } + retro_atomic_store_release_int(&syncDone_, 1); + ParkingLotNotify(&syncDone_); break; default: @@ -564,26 +536,11 @@ void GLRenderManager::FlushSync() { GLRRenderThreadTask *task = new GLRRenderThreadTask(GLRRunType::SYNC); task->frame = curFrame_; - - std::unique_lock lock(pushMutex_); - renderThreadQueue_.push(task); - { - std::lock_guard initLock(initStepsMutex_); - renderThreadQueue_.back()->initSteps = std::move(initSteps_); - initSteps_.clear(); - } - renderThreadQueue_.back()->steps = std::move(steps_); - pushCondVar_.notify_one(); - steps_.clear(); + TakeRecordedWork(task); + PushTask(task); } - { - std::unique_lock lock(syncMutex_); - // Wait for the flush to be hit, since we're syncing. - while (!syncDone_) { - VLOG("PUSH: Waiting for frame[%d].readyForFence = 1 (sync)", curFrame_); - syncCondVar_.wait(lock); - } - syncDone_ = false; - } + // Wait for the flush to be hit, since we're syncing. + ParkingLotWait(&syncDone_, [this] { return retro_atomic_load_acquire_int(&syncDone_) != 0; }); + retro_atomic_store_relaxed_int(&syncDone_, 0); } diff --git a/Common/GPU/OpenGL/GLRenderManager.h b/Common/GPU/OpenGL/GLRenderManager.h index 240d46ddb5..de04b349d7 100644 --- a/Common/GPU/OpenGL/GLRenderManager.h +++ b/Common/GPU/OpenGL/GLRenderManager.h @@ -1,15 +1,14 @@ #pragma once -#include +#include #include #include #include #include #include #include -#include -#include -#include + +#include #include "Common/GPU/MiscTypes.h" #include "Common/Data/Convert/SmallDataConvert.h" @@ -18,6 +17,8 @@ #include "Common/GPU/OpenGL/GLFrameData.h" #include "Common/GPU/OpenGL/GLCommon.h" #include "Common/GPU/OpenGL/GLMemory.h" +#include "Common/Thread/MpscQueue.h" +#include "Common/Thread/ParkingLot.h" class GLRInputLayout; class GLPushBuffer; @@ -222,6 +223,8 @@ struct GLRRenderThreadTask { std::vector steps; FastVec initSteps; + // Push buffers created since the last task: frame index and buffer. + std::vector> newPushBuffers; int frame = -1; GLRRunType runType; @@ -277,7 +280,6 @@ public: // and then we'll also need formats and stuff. GLRTexture *CreateTexture(GLenum target, int width, int height, int depth, int numMips) { _dbg_assert_(target != 0); - std::lock_guard lock(initStepsMutex_); GLRInitStep &step = initSteps_.push_uninitialized(); step.stepType = GLRInitStepType::CREATE_TEXTURE; step.create_texture.texture = new GLRTexture(caps_, width, height, depth, numMips); @@ -286,7 +288,6 @@ public: } GLRBuffer *CreateBuffer(GLuint target, size_t size, GLuint usage) { - std::lock_guard lock(initStepsMutex_); GLRInitStep &step = initSteps_.push_uninitialized(); step.stepType = GLRInitStepType::CREATE_BUFFER; step.create_buffer.buffer = new GLRBuffer(target, size); @@ -296,7 +297,6 @@ public: } GLRShader *CreateShader(GLuint stage, const std::string &code, std::string_view desc) { - std::lock_guard lock(initStepsMutex_); GLRInitStep &step = initSteps_.push_uninitialized(); step.stepType = GLRInitStepType::CREATE_SHADER; step.create_shader.shader = new GLRShader(desc); @@ -309,7 +309,6 @@ public: GLRFramebuffer *CreateFramebuffer(int width, int height, bool z_stencil, const char *tag) { _dbg_assert_(width > 0 && height > 0 && tag != nullptr); - std::lock_guard lock(initStepsMutex_); GLRInitStep &step = initSteps_.push_uninitialized(); step.stepType = GLRInitStepType::CREATE_FRAMEBUFFER; step.create_framebuffer.framebuffer = new GLRFramebuffer(caps_, width, height, z_stencil, tag); @@ -321,7 +320,6 @@ public: GLRProgram *CreateProgram( std::vector shaders, std::vector semantics, std::vector queries, std::vector initializers, GLRProgramLocData *locData, const GLRProgramFlags &flags) { - std::lock_guard lock(initStepsMutex_); GLRInitStep &step = initSteps_.push_uninitialized(); step.stepType = GLRInitStepType::CREATE_PROGRAM; _assert_(shaders.size() <= ARRAY_SIZE(step.create_program.shaders)); @@ -351,7 +349,6 @@ public: } GLRInputLayout *CreateInputLayout(const std::vector &entries, int stride) { - std::lock_guard lock(initStepsMutex_); GLRInitStep &step = initSteps_.push_uninitialized(); step.stepType = GLRInitStepType::CREATE_INPUT_LAYOUT; step.create_input_layout.inputLayout = new GLRInputLayout(); @@ -431,7 +428,6 @@ public: void BufferSubdata(GLRBuffer *buffer, size_t offset, size_t size, uint8_t *data, bool deleteData = true) { // TODO: Maybe should be a render command instead of an init command? When possible it's better as // an init command, that's for sure. - std::lock_guard lock(initStepsMutex_); GLRInitStep &step = initSteps_.push_uninitialized(); step.stepType = GLRInitStepType::BUFFER_SUBDATA; _dbg_assert_(offset <= buffer->size_ - size); @@ -444,7 +440,6 @@ public: // Takes ownership over the data pointer and delete[]-s it. void TextureImage(GLRTexture *texture, int level, int width, int height, int depth, Draw::DataFormat format, uint8_t *data, GLRAllocType allocType = GLRAllocType::NEW, bool linearFilter = false) { - std::lock_guard lock(initStepsMutex_); GLRInitStep &step = initSteps_.push_uninitialized(); step.stepType = GLRInitStepType::TEXTURE_IMAGE; step.texture_image.texture = texture; @@ -461,7 +456,6 @@ public: // Takes ownership over the data pointer and delete[]-s it. Runs as an init step, so unlike // TextureSubImage below, it doesn't have to happen inside a render pass. void TextureSubImageInit(GLRTexture *texture, int level, int x, int y, int width, int height, Draw::DataFormat format, uint8_t *data, GLRAllocType allocType = GLRAllocType::NEW) { - std::lock_guard lock(initStepsMutex_); GLRInitStep &step = initSteps_.push_uninitialized(); step.stepType = GLRInitStepType::TEXTURE_SUBIMAGE; step.texture_subimage.texture = texture; @@ -492,7 +486,6 @@ public: } void FinalizeTexture(GLRTexture *texture, int loadedLevels, bool genMips) { - std::lock_guard lock(initStepsMutex_); GLRInitStep &step = initSteps_.push_uninitialized(); step.stepType = GLRInitStepType::TEXTURE_FINALIZE; step.texture_finalize.texture = texture; @@ -842,8 +835,8 @@ public: queueRunner_.Resize(width, height); } + // Render thread only (GLDeleter). void UnregisterPushBuffer(GLPushBuffer *buffer) { - std::lock_guard lock(pushBuffersMutex_); int foundCount = 0; for (int i = 0; i < MAX_INFLIGHT_FRAMES; i++) { auto iter = frameData_[i].activePushBuffers.find(buffer); @@ -893,9 +886,32 @@ private: // When using legacy functionality for push buffers (glBufferData), we need to flush them // before actually making the glDraw* calls. It's best if the render manager handles that. + // The render thread owns the active sets, so the buffer travels there with the next task. void RegisterPushBuffer(int frame, GLPushBuffer *buffer) { - std::lock_guard lock(pushBuffersMutex_); - frameData_[frame].activePushBuffers.insert(buffer); + newPushBuffers_.emplace_back(frame, buffer); + } + + // Render thread. + void AddNewPushBuffers(std::vector> &added) { + for (auto &entry : added) { + frameData_[entry.first].activePushBuffers.insert(entry.second); + } + added.clear(); + } + + // Moves what the recording thread has gathered into a task. + void TakeRecordedWork(GLRRenderThreadTask *task) { + task->initSteps = std::move(initSteps_); + initSteps_.clear(); + task->steps = std::move(steps_); + steps_.clear(); + task->newPushBuffers = std::move(newPushBuffers_); + newPushBuffers_.clear(); + } + + void PushTask(GLRRenderThreadTask *task) { + renderThreadQueue_.Push(task); + renderThreadWork_.Notify(); } GLFrameData frameData_[MAX_INFLIGHT_FRAMES]; @@ -905,34 +921,23 @@ private: GLRStep *curRenderStep_ = nullptr; std::vector steps_; - // Guards initSteps_. Recorded into from the emu thread, but also from the loader thread during - // boot (InitGPU runs there, and GL has to record device object creation rather than just doing - // it), and moved out on the emu thread in Finish/FlushSync. Uncontended in practice. - // Lock ordering: taken while pushMutex_ is held, never the other way around. - std::mutex initStepsMutex_; + // Recorded on the thread that records the steps (InitGPU runs there too), and moved into the + // next task with them. FastVec initSteps_; - - // Guards frameData_[].activePushBuffers, which is inserted into from whichever thread creates a - // push buffer, erased from on the render thread (GLDeleter), and walked on the render thread. - // Lock ordering: taken before initStepsMutex_ (Flush() below records init steps), never after. - std::mutex pushBuffersMutex_; + std::vector> newPushBuffers_; // Execution time state // Thread is managed elsewhere, and should call ThreadFrame. GLQueueRunner queueRunner_; - // For pushing data on the queue. - std::mutex pushMutex_; - std::condition_variable pushCondVar_; - - std::queue renderThreadQueue_; + // Tasks for the render thread, oldest first. The render thread drains them into its own list. + MpscQueue renderThreadQueue_; + EventCounter renderThreadWork_; + std::deque renderThreadTasks_; // For readbacks and other reasons we need to sync with the render thread. - std::mutex syncMutex_; - std::condition_variable syncCondVar_; - - bool syncDone_ = false; + retro_atomic_int_t syncDone_{ 0 }; GLDeleter deleter_; bool skipGLCalls_ = false; diff --git a/Common/GPU/Vulkan/VulkanContext.cpp b/Common/GPU/Vulkan/VulkanContext.cpp index bddba4c225..7cb54041ac 100644 --- a/Common/GPU/Vulkan/VulkanContext.cpp +++ b/Common/GPU/Vulkan/VulkanContext.cpp @@ -1809,6 +1809,57 @@ void SaveFixedSPIRVCache() { g_spirvCache.SaveIfDirty(); } +SPIRVCache::SPIRVCache() { + retro_atomic_ptr_init(&table_, (void *)new Table()); + retro_atomic_int_init(&dirty_, 0); +} + +SPIRVCache::~SPIRVCache() { + pending_.Drain([](PendingEntry &&p) { + delete p.entry; + }); + FreeTable((Table *)retro_atomic_exchange_ptr(&table_, nullptr)); +} + +void SPIRVCache::FreeTable(Table *table) { + if (!table) { + return; + } + for (auto &iter : *table) { + delete iter.second; + } + delete table; +} + +const SPIRVCache::Table *SPIRVCache::Current() const { + return (const Table *)retro_atomic_load_acquire_ptr(const_cast(&table_)); +} + +void SPIRVCache::Publish(Table *table, std::vector &retired) { + Table *old = (Table *)retro_atomic_exchange_ptr(&table_, (void *)table); + gate_.Drain(); + for (Entry *entry : retired) { + delete entry; + } + delete old; +} + +void SPIRVCache::Merge() { + if (pending_.Empty()) { + return; + } + Table *table = new Table(*Current()); + std::vector retired; + pending_.Drain([&](PendingEntry &&p) { + Entry *&slot = (*table)[p.key]; + if (slot) { + retired.push_back(slot); + } + slot = p.entry; + }); + Publish(table, retired); +} + SPIRVCache::Key SPIRVCache::MakeKey(VkShaderStageFlagBits stage, GLSLVariant variant, const char *source) { const size_t length = strlen(source); const uint32_t seed = (SPIRV_CACHE_VERSION << 16) | ((uint32_t)stage << 4) | (uint32_t)variant; @@ -1817,42 +1868,51 @@ SPIRVCache::Key SPIRVCache::MakeKey(VkShaderStageFlagBits stage, GLSLVariant var bool SPIRVCache::Lookup(VkShaderStageFlagBits stage, GLSLVariant variant, const char *source, std::vector *spirv) { const Key key = MakeKey(stage, variant, source); - std::lock_guard guard(mutex_); - LoadIfNeededLocked(); - auto iter = entries_.find(key); - if (iter == entries_.end()) { - return false; + bool found = false; + gate_.Enter(); + const Table *table = Current(); + auto iter = table->find(key); + if (iter != table->end()) { + retro_atomic_store_relaxed_int(&iter->second->used, 1); + *spirv = iter->second->spirv; + found = true; } - iter->second.used = true; - *spirv = iter->second.spirv; - return true; + gate_.Exit(); + return found; } void SPIRVCache::Insert(VkShaderStageFlagBits stage, GLSLVariant variant, const char *source, const std::vector &spirv) { - const Key key = MakeKey(stage, variant, source); - std::lock_guard guard(mutex_); - Entry &entry = entries_[key]; - entry.spirv = spirv; - entry.used = true; - dirty_ = true; + Entry *entry = new Entry(); + entry->spirv = spirv; + retro_atomic_store_relaxed_int(&entry->used, 1); + pending_.Push(PendingEntry{ MakeKey(stage, variant, source), entry }); + retro_atomic_store_release_int(&dirty_, 1); } void SPIRVCache::Clear() { - std::lock_guard guard(mutex_); - entries_.clear(); - dirty_ = false; + pending_.Drain([](PendingEntry &&p) { + delete p.entry; + }); + std::vector retired; + for (auto &iter : *Current()) { + retired.push_back(iter.second); + } + Publish(new Table(), retired); + retro_atomic_store_release_int(&dirty_, 0); } bool SPIRVCache::Read(FILE *f) { - std::lock_guard guard(mutex_); - return ReadLocked(f); + Clear(); + Table *table = new Table(); + const bool ok = ReadTable(f, table); + std::vector retired; + Publish(table, retired); + return ok; } // A bad entry is dropped rather than failing the whole read: it's only a cache, and the shader // gets compiled again. A truncated file keeps what came before the damage. -bool SPIRVCache::ReadLocked(FILE *f) { - entries_.clear(); - dirty_ = false; +bool SPIRVCache::ReadTable(FILE *f, Table *table) { SPIRVCacheHeader header{}; if (fread(&header, sizeof(header), 1, f) != 1 || header.magic != SPIRV_CACHE_MAGIC) { return false; @@ -1876,78 +1936,79 @@ bool SPIRVCache::ReadLocked(FILE *f) { WARN_LOG(Log::G3D, "Bad entry in SPIR-V cache, skipping"); continue; } - entries_[Key{ entryHeader.keyHash, entryHeader.keyLength }].spirv = std::move(spirv); + Entry *&slot = (*table)[Key{ entryHeader.keyHash, entryHeader.keyLength }]; + if (!slot) { + slot = new Entry(); + } + slot->spirv = std::move(spirv); } return true; } bool SPIRVCache::Write(FILE *f, bool onlyUsed) { - std::lock_guard guard(mutex_); + Merge(); + const Table *table = Current(); SPIRVCacheHeader header{ SPIRV_CACHE_MAGIC, SPIRV_CACHE_VERSION, 0 }; - for (const auto &[key, entry] : entries_) { - if (entry.used || !onlyUsed) { + for (const auto &[key, entry] : *table) { + if (retro_atomic_load_relaxed_int(&entry->used) || !onlyUsed) { header.count++; } } bool ok = fwrite(&header, sizeof(header), 1, f) == 1; - for (const auto &[key, entry] : entries_) { - if (!ok) { + uint32_t written = 0; + for (const auto &[key, entry] : *table) { + if (!ok || written == header.count) { break; } - if (!entry.used && onlyUsed) { + if (!retro_atomic_load_relaxed_int(&entry->used) && onlyUsed) { continue; } SPIRVCacheEntryHeader entryHeader{}; entryHeader.keyHash = key.hash; entryHeader.keyLength = key.length; - entryHeader.numWords = (uint32_t)entry.spirv.size(); - entryHeader.checksum = (uint32_t)XXH3_64bits(entry.spirv.data(), entry.spirv.size() * sizeof(uint32_t)); + entryHeader.numWords = (uint32_t)entry->spirv.size(); + entryHeader.checksum = (uint32_t)XXH3_64bits(entry->spirv.data(), entry->spirv.size() * sizeof(uint32_t)); ok = fwrite(&entryHeader, sizeof(entryHeader), 1, f) == 1 && - fwrite(entry.spirv.data(), sizeof(uint32_t), entry.spirv.size(), f) == entry.spirv.size(); + fwrite(entry->spirv.data(), sizeof(uint32_t), entry->spirv.size(), f) == entry->spirv.size(); + written++; } if (ok) { - dirty_ = false; + retro_atomic_store_release_int(&dirty_, 0); } return ok; } void SPIRVCache::SetPath(const Path &path, int maxEntries) { - std::lock_guard guard(mutex_); path_ = path; - maxEntries_ = maxEntries; - loaded_ = false; -} - -void SPIRVCache::LoadIfNeededLocked() { - if (loaded_ || path_.empty()) { + Clear(); + if (path_.empty()) { return; } - loaded_ = true; FILE *f = File::OpenCFile(path_, "rb"); - if (f) { - ReadLocked(f); - fclose(f); - INFO_LOG(Log::G3D, "Loaded %d shaders from the SPIR-V cache", (int)entries_.size()); + if (!f) { + return; } + Table *table = new Table(); + ReadTable(f, table); + fclose(f); + INFO_LOG(Log::G3D, "Loaded %d shaders from the SPIR-V cache", (int)table->size()); // Entries for shaders that have since changed pile up, so start over once there are a lot of // them. A few shaders compiling once more doesn't matter. - if (maxEntries_ > 0 && (int)entries_.size() >= maxEntries_) { - INFO_LOG(Log::G3D, "SPIR-V cache has %d entries, flushing it", (int)entries_.size()); - entries_.clear(); - dirty_ = true; + if (maxEntries > 0 && (int)table->size() >= maxEntries) { + INFO_LOG(Log::G3D, "SPIR-V cache has %d entries, flushing it", (int)table->size()); + FreeTable(table); + table = new Table(); + retro_atomic_store_release_int(&dirty_, 1); } + std::vector retired; + Publish(table, retired); } void SPIRVCache::SaveIfDirty() { - Path path; - { - std::lock_guard guard(mutex_); - if (!dirty_ || path_.empty()) { - return; - } - path = path_; + if (!retro_atomic_load_acquire_int(&dirty_) || path_.empty()) { + return; } - FILE *f = File::OpenCFile(path, "wb"); + FILE *f = File::OpenCFile(path_, "wb"); if (!f) { return; } @@ -2048,9 +2109,33 @@ void finalize_glslang() { // NOTE: Every vector in the class has to be listed both here and in PerformDeletes. If one is missing // from Take, the objects in it linger on the global list until device teardown instead of being deleted // a few frames later; if one is missing from PerformDeletes, they leak outright. Both are bugs. +void VulkanDeleteList::Collect() { + pending_.Drain([this](Item &&item) { + switch (item.kind) { + case Kind::CMD_POOL: cmdPools_.push_back(FromBits(item.handle)); break; + case Kind::DESC_POOL: descPools_.push_back(FromBits(item.handle)); break; + case Kind::SHADER_MODULE: modules_.push_back(FromBits(item.handle)); break; + case Kind::BUFFER: buffers_.push_back(FromBits(item.handle)); break; + case Kind::BUFFER_VIEW: bufferViews_.push_back(FromBits(item.handle)); break; + case Kind::IMAGE_VIEW: imageViews_.push_back(FromBits(item.handle)); break; + case Kind::DEVICE_MEMORY: deviceMemory_.push_back(FromBits(item.handle)); break; + case Kind::SAMPLER: samplers_.push_back(FromBits(item.handle)); break; + case Kind::PIPELINE: pipelines_.push_back(FromBits(item.handle)); break; + case Kind::PIPELINE_CACHE: pipelineCaches_.push_back(FromBits(item.handle)); break; + case Kind::RENDER_PASS: renderPasses_.push_back(FromBits(item.handle)); break; + case Kind::FRAMEBUFFER: framebuffers_.push_back(FromBits(item.handle)); break; + case Kind::PIPELINE_LAYOUT: pipelineLayouts_.push_back(FromBits(item.handle)); break; + case Kind::DESC_SET_LAYOUT: descSetLayouts_.push_back(FromBits(item.handle)); break; + case Kind::QUERY_POOL: queryPools_.push_back(FromBits(item.handle)); break; + case Kind::BUFFER_ALLOC: buffersWithAllocs_.push_back(BufferWithAlloc{ FromBits(item.handle), item.alloc }); break; + case Kind::IMAGE_ALLOC: imagesWithAllocs_.push_back(ImageWithAlloc{ FromBits(item.handle), item.alloc }); break; + case Kind::CALLBACK_FN: callbacks_.push_back(std::move(item.callback)); break; + } + }); +} + void VulkanDeleteList::Take(VulkanDeleteList &del) { - // The render thread can be queueing deletes into del (the global list) while we do this. - std::lock_guard lock(del.mutex_); + del.Collect(); _dbg_assert_(cmdPools_.empty()); _dbg_assert_(descPools_.empty()); _dbg_assert_(modules_.empty()); diff --git a/Common/GPU/Vulkan/VulkanContext.h b/Common/GPU/Vulkan/VulkanContext.h index 31a495ec32..d5d4448e1f 100644 --- a/Common/GPU/Vulkan/VulkanContext.h +++ b/Common/GPU/Vulkan/VulkanContext.h @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -20,6 +19,8 @@ #include "Common/GPU/Vulkan/VulkanAlloc.h" #include "Common/GPU/Vulkan/VulkanProfiler.h" #include "Common/GPU/Vulkan/VulkanPresentation.h" +#include "Common/Thread/MpscQueue.h" +#include "Common/Thread/ParkingLot.h" // Enable or disable a simple logging profiler for Vulkan. // Mostly useful for profiling texture uploads currently, but could be useful for @@ -74,18 +75,10 @@ typedef std::function DeleteCallback; // This is a bit repetitive... // -// Thread safety: The queueing functions are locked, because the global delete list gets written from -// more than one thread. Most callers are on the main thread, but not all: -// * VulkanDescSetPool::Recreate, when a descriptor pool has to grow, runs from FlushDescSets on -// the render thread. This one really happens - some games go past the initial 1024 descriptors. -// * VulkanQueueRunner::ResizeReadbackBuffer runs from PerformReadback on the render thread. For -// blocking readbacks the main thread is parked in FlushSync so it can't collide, and the delayed -// ones never actually resize (the readback key contains the dimensions), but it's not worth -// relying on that staying true. -// Meanwhile the main thread moves the global list into the current frame's list in EndFrame(). -// -// PerformDeletes needs no lock of its own: it drains into a private local list (under Take's lock) and -// destroys from that, so the destruction never touches a list another thread can reach. +// Thread safety: the queueing functions can be called from any thread - the render thread queues +// deletes too (VulkanDescSetPool::Recreate from FlushDescSets, ResizeReadbackBuffer). They push onto +// a lock-free MPSC queue. Take() and PerformDeletes() belong to one thread, the main thread, which +// moves the queued items into the per-kind vectors. class VulkanDeleteList { struct BufferWithAlloc { VkBuffer buffer; @@ -96,51 +89,71 @@ class VulkanDeleteList { VmaAllocation alloc; }; - struct Callback { - explicit Callback(void(*f)(VulkanContext *vulkan, void *userdata), void *u) - : func(f), userdata(u) { - } - - void (*func)(VulkanContext *vulkan, void *userdata); - void *userdata; + enum class Kind : uint8_t { + CMD_POOL, DESC_POOL, SHADER_MODULE, BUFFER, BUFFER_VIEW, IMAGE_VIEW, DEVICE_MEMORY, SAMPLER, + PIPELINE, PIPELINE_CACHE, RENDER_PASS, FRAMEBUFFER, PIPELINE_LAYOUT, DESC_SET_LAYOUT, QUERY_POOL, + BUFFER_ALLOC, IMAGE_ALLOC, CALLBACK_FN, }; + struct Item { + Kind kind; + uint64_t handle; + VmaAllocation alloc; + DeleteCallback callback; + }; + + // Handles are pointers or 64-bit integers depending on the platform. + template + static uint64_t Bits(H h) { + uint64_t v = 0; + memcpy(&v, &h, sizeof(h)); + return v; + } + template + static H FromBits(uint64_t v) { + H h; + memcpy(&h, &v, sizeof(h)); + return h; + } + template + void Push(Kind kind, H &handle) { + _dbg_assert_(handle != VK_NULL_HANDLE); + pending_.Push(Item{ kind, Bits(handle), VK_NULL_HANDLE, nullptr }); + handle = VK_NULL_HANDLE; + } public: // NOTE: These all take reference handles so they can zero the input value. - void QueueDeleteCommandPool(VkCommandPool &pool) { _dbg_assert_(pool != VK_NULL_HANDLE); std::lock_guard lock(mutex_); cmdPools_.push_back(pool); pool = VK_NULL_HANDLE; } - void QueueDeleteDescriptorPool(VkDescriptorPool &pool) { _dbg_assert_(pool != VK_NULL_HANDLE); std::lock_guard lock(mutex_); descPools_.push_back(pool); pool = VK_NULL_HANDLE; } - void QueueDeleteShaderModule(VkShaderModule &module) { _dbg_assert_(module != VK_NULL_HANDLE); std::lock_guard lock(mutex_); modules_.push_back(module); module = VK_NULL_HANDLE; } - void QueueDeleteBuffer(VkBuffer &buffer) { _dbg_assert_(buffer != VK_NULL_HANDLE); std::lock_guard lock(mutex_); buffers_.push_back(buffer); buffer = VK_NULL_HANDLE; } - void QueueDeleteBufferView(VkBufferView &bufferView) { _dbg_assert_(bufferView != VK_NULL_HANDLE); std::lock_guard lock(mutex_); bufferViews_.push_back(bufferView); bufferView = VK_NULL_HANDLE; } - void QueueDeleteImageView(VkImageView &imageView) { _dbg_assert_(imageView != VK_NULL_HANDLE); std::lock_guard lock(mutex_); imageViews_.push_back(imageView); imageView = VK_NULL_HANDLE; } - void QueueDeleteDeviceMemory(VkDeviceMemory &deviceMemory) { _dbg_assert_(deviceMemory != VK_NULL_HANDLE); std::lock_guard lock(mutex_); deviceMemory_.push_back(deviceMemory); deviceMemory = VK_NULL_HANDLE; } - void QueueDeleteSampler(VkSampler &sampler) { _dbg_assert_(sampler != VK_NULL_HANDLE); std::lock_guard lock(mutex_); samplers_.push_back(sampler); sampler = VK_NULL_HANDLE; } - void QueueDeletePipeline(VkPipeline &pipeline) { _dbg_assert_(pipeline != VK_NULL_HANDLE); std::lock_guard lock(mutex_); pipelines_.push_back(pipeline); pipeline = VK_NULL_HANDLE; } - void QueueDeletePipelineCache(VkPipelineCache &pipelineCache) { _dbg_assert_(pipelineCache != VK_NULL_HANDLE); std::lock_guard lock(mutex_); pipelineCaches_.push_back(pipelineCache); pipelineCache = VK_NULL_HANDLE; } - void QueueDeleteRenderPass(VkRenderPass &renderPass) { _dbg_assert_(renderPass != VK_NULL_HANDLE); std::lock_guard lock(mutex_); renderPasses_.push_back(renderPass); renderPass = VK_NULL_HANDLE; } - void QueueDeleteFramebuffer(VkFramebuffer &framebuffer) { _dbg_assert_(framebuffer != VK_NULL_HANDLE); std::lock_guard lock(mutex_); framebuffers_.push_back(framebuffer); framebuffer = VK_NULL_HANDLE; } - void QueueDeletePipelineLayout(VkPipelineLayout &pipelineLayout) { _dbg_assert_(pipelineLayout != VK_NULL_HANDLE); std::lock_guard lock(mutex_); pipelineLayouts_.push_back(pipelineLayout); pipelineLayout = VK_NULL_HANDLE; } - void QueueDeleteDescriptorSetLayout(VkDescriptorSetLayout &descSetLayout) { _dbg_assert_(descSetLayout != VK_NULL_HANDLE); std::lock_guard lock(mutex_); descSetLayouts_.push_back(descSetLayout); descSetLayout = VK_NULL_HANDLE; } - void QueueDeleteQueryPool(VkQueryPool &queryPool) { _dbg_assert_(queryPool != VK_NULL_HANDLE); std::lock_guard lock(mutex_); queryPools_.push_back(queryPool); queryPool = VK_NULL_HANDLE; } - void QueueCallback(DeleteCallback func) { std::lock_guard lock(mutex_); callbacks_.push_back(func); } + void QueueDeleteCommandPool(VkCommandPool &pool) { Push(Kind::CMD_POOL, pool); } + void QueueDeleteDescriptorPool(VkDescriptorPool &pool) { Push(Kind::DESC_POOL, pool); } + void QueueDeleteShaderModule(VkShaderModule &module) { Push(Kind::SHADER_MODULE, module); } + void QueueDeleteBuffer(VkBuffer &buffer) { Push(Kind::BUFFER, buffer); } + void QueueDeleteBufferView(VkBufferView &bufferView) { Push(Kind::BUFFER_VIEW, bufferView); } + void QueueDeleteImageView(VkImageView &imageView) { Push(Kind::IMAGE_VIEW, imageView); } + void QueueDeleteDeviceMemory(VkDeviceMemory &deviceMemory) { Push(Kind::DEVICE_MEMORY, deviceMemory); } + void QueueDeleteSampler(VkSampler &sampler) { Push(Kind::SAMPLER, sampler); } + void QueueDeletePipeline(VkPipeline &pipeline) { Push(Kind::PIPELINE, pipeline); } + void QueueDeletePipelineCache(VkPipelineCache &pipelineCache) { Push(Kind::PIPELINE_CACHE, pipelineCache); } + void QueueDeleteRenderPass(VkRenderPass &renderPass) { Push(Kind::RENDER_PASS, renderPass); } + void QueueDeleteFramebuffer(VkFramebuffer &framebuffer) { Push(Kind::FRAMEBUFFER, framebuffer); } + void QueueDeletePipelineLayout(VkPipelineLayout &pipelineLayout) { Push(Kind::PIPELINE_LAYOUT, pipelineLayout); } + void QueueDeleteDescriptorSetLayout(VkDescriptorSetLayout &descSetLayout) { Push(Kind::DESC_SET_LAYOUT, descSetLayout); } + void QueueDeleteQueryPool(VkQueryPool &queryPool) { Push(Kind::QUERY_POOL, queryPool); } + void QueueCallback(DeleteCallback func) { pending_.Push(Item{ Kind::CALLBACK_FN, 0, VK_NULL_HANDLE, std::move(func) }); } void QueueDeleteBufferAllocation(VkBuffer &buffer, VmaAllocation &alloc) { _dbg_assert_(buffer != VK_NULL_HANDLE); - std::lock_guard lock(mutex_); - buffersWithAllocs_.push_back(BufferWithAlloc{ buffer, alloc }); + pending_.Push(Item{ Kind::BUFFER_ALLOC, Bits(buffer), alloc, nullptr }); buffer = VK_NULL_HANDLE; alloc = VK_NULL_HANDLE; } void QueueDeleteImageAllocation(VkImage &image, VmaAllocation &alloc) { _dbg_assert_(image != VK_NULL_HANDLE && alloc != VK_NULL_HANDLE); - std::lock_guard lock(mutex_); - imagesWithAllocs_.push_back(ImageWithAlloc{ image, alloc }); + pending_.Push(Item{ Kind::IMAGE_ALLOC, Bits(image), alloc, nullptr }); image = VK_NULL_HANDLE; alloc = VK_NULL_HANDLE; } - // Moves everything from del into this list. Only the source list is locked - the destination is - // either a frame's own list or a stack local, neither of which another thread can reach. + // Main thread. Moves everything from del into this list. void Take(VulkanDeleteList &del); void PerformDeletes(VulkanContext *vulkan, VmaAllocator allocator); @@ -151,8 +164,10 @@ public: private: // Does the actual destruction, on a list that's been drained out of the shared one. Returns the count. int PerformDeletesInternal(VulkanContext *vulkan, VmaAllocator allocator); + // Main thread. Sorts the queued items into the vectors below. + void Collect(); - std::mutex mutex_; + MpscQueue pending_; std::vector cmdPools_; std::vector descPools_; std::vector modules_; @@ -616,9 +631,16 @@ enum class GLSLVariant { }; // Compiled SPIR-V, keyed on the GLSL source, stage and variant, so that a shader compiled in an earlier -// run doesn't have to go through glslang again. Thread safe. +// run doesn't have to go through glslang again. +// +// Lookup and Insert are lock-free and can run on any thread. Lookups read a published table; inserts +// queue up and join the table at the next Write or SaveIfDirty. The other calls are for one thread +// at a time (the GPU thread that owns the cache). class SPIRVCache { public: + SPIRVCache(); + ~SPIRVCache(); + bool Lookup(VkShaderStageFlagBits stage, GLSLVariant variant, const char *source, std::vector *spirv); void Insert(VkShaderStageFlagBits stage, GLSLVariant variant, const char *source, const std::vector &spirv); void Clear(); @@ -628,8 +650,8 @@ public: bool Read(FILE *f); bool Write(FILE *f, bool onlyUsed); - // For a cache with a file of its own, loaded on the first lookup. If it has grown to maxEntries, - // it's flushed on load and starts over. + // For a cache with a file of its own, loaded here. If it has grown to maxEntries, it's flushed + // and starts over. void SetPath(const Path &path, int maxEntries); void SaveIfDirty(); @@ -645,19 +667,30 @@ private: size_t operator()(const Key &key) const { return key.hash; } }; struct Entry { + Entry() { retro_atomic_int_init(&used, 0); } std::vector spirv; - bool used = false; + retro_atomic_int_t used; + }; + typedef std::unordered_map Table; + struct PendingEntry { + Key key; + Entry *entry; }; - static Key MakeKey(VkShaderStageFlagBits stage, GLSLVariant variant, const char *source); - bool ReadLocked(FILE *f); - void LoadIfNeededLocked(); - std::mutex mutex_; - std::unordered_map entries_; + static Key MakeKey(VkShaderStageFlagBits stage, GLSLVariant variant, const char *source); + static void FreeTable(Table *table); + const Table *Current() const; + // Publishes table, waits out readers of the old one, then frees the entries in retired. + void Publish(Table *table, std::vector &retired); + // Moves pending inserts into the table. + void Merge(); + bool ReadTable(FILE *f, Table *table); + + retro_atomic_ptr_t table_; + ReaderGate gate_; + MpscQueue pending_; + retro_atomic_int_t dirty_; Path path_; - int maxEntries_ = 0; - bool loaded_ = false; - bool dirty_ = false; }; // For thin3d's and other fixed shaders. Game shaders use a cache of their own, stored with the rest of diff --git a/Common/GPU/Vulkan/VulkanDebug.cpp b/Common/GPU/Vulkan/VulkanDebug.cpp index 8867ff0162..1afd6da6e7 100644 --- a/Common/GPU/Vulkan/VulkanDebug.cpp +++ b/Common/GPU/Vulkan/VulkanDebug.cpp @@ -17,8 +17,7 @@ #include #include -#include -#include +#include #include "Common/Log.h" #include "Common/System/System.h" @@ -27,14 +26,43 @@ const int MAX_SAME_ERROR_COUNT = 10; -// Used to stop outputting the same message over and over. -static std::map g_errorCount; -std::mutex g_errorCountMutex; +// Used to stop outputting the same message over and over. Open addressing on the message code, +// stored xored with a tag so that zero means empty. A code that finds the table full, or equals +// the tag, just isn't rate limited. +struct ErrorCountSlot { + retro_atomic_int_t key; + retro_atomic_int_t count; +}; +static constexpr int ERROR_COUNT_SLOTS = 256; +static constexpr int ERROR_KEY_TAG = 0x5A5A5A5A; +static ErrorCountSlot g_errorCount[ERROR_COUNT_SLOTS]; + +static int BumpErrorCount(int code) { + const int key = code ^ ERROR_KEY_TAG; + if (key == 0) { + return 0; + } + const uint32_t start = (uint32_t)code * 2654435761u; + for (int i = 0; i < ERROR_COUNT_SLOTS; i++) { + ErrorCountSlot &slot = g_errorCount[(start + i) % ERROR_COUNT_SLOTS]; + int cur = retro_atomic_load_acquire_int(&slot.key); + if (cur == 0 && retro_atomic_cas_int(&slot.key, 0, key)) { + cur = key; + } else if (cur == 0) { + cur = retro_atomic_load_acquire_int(&slot.key); + } + if (cur == key) { + return retro_atomic_fetch_add_int(&slot.count, 1); + } + } + return 0; +} // TODO: Call this when launching games in some clean way. void VulkanClearValidationErrorCounts() { - std::lock_guard lock(g_errorCountMutex); - g_errorCount.clear(); + for (int i = 0; i < ERROR_COUNT_SLOTS; i++) { + retro_atomic_store_relaxed_int(&g_errorCount[i].count, 0); + } } VKAPI_ATTR VkBool32 VKAPI_CALL VulkanDebugUtilsCallback( @@ -110,11 +138,7 @@ VKAPI_ATTR VkBool32 VKAPI_CALL VulkanDebugUtilsCallback( } */ - int count; - { - std::lock_guard lock(g_errorCountMutex); - count = g_errorCount[messageCode]++; - } + const int count = BumpErrorCount(messageCode); if (count == MAX_SAME_ERROR_COUNT) { WARN_LOG(Log::G3D, "Too many validation messages with message %d, stopping", messageCode); } diff --git a/Common/GPU/Vulkan/VulkanFrameData.cpp b/Common/GPU/Vulkan/VulkanFrameData.cpp index f33d36e0bf..0f63143eda 100644 --- a/Common/GPU/Vulkan/VulkanFrameData.cpp +++ b/Common/GPU/Vulkan/VulkanFrameData.cpp @@ -1,7 +1,6 @@ -#include - #include "VulkanFrameData.h" #include "Common/Log.h" +#include "Common/Thread/ParkingLot.h" #include "Common/StringUtils.h" #if 0 // def _DEBUG @@ -51,7 +50,7 @@ void FrameData::Init(VulkanContext *vulkan, int index) { // Creating the frame fence with true so they can be instantly waited on the first frame fence = vulkan->CreateFence(true); vulkan->SetDebugName(fence, VK_OBJECT_TYPE_FENCE, StringFromFormat("fence%d", index).c_str()); - readyForFence = true; + retro_atomic_store_release_int(&readyForFence, 1); VkQueryPoolCreateInfo query_ci{ VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO }; query_ci.queryCount = MAX_TIMESTAMP_QUERIES; @@ -249,9 +248,8 @@ void FrameData::Submit(VulkanContext *vulkan, FrameSubmitType type, FrameDataSha res = vkQueueSubmit(vulkan->GetGraphicsQueue(), 1, &submit_info, fenceToTrigger); vulkan->UnlockQueue(); if (sharedData.useMultiThreading) { - std::lock_guard lock(fenceMutex); - readyForFence = true; - fenceCondVar.notify_one(); + retro_atomic_store_release_int(&readyForFence, 1); + ParkingLotNotify(&readyForFence); } } else { VLOG("Doing queue submit, fencing something (%p)", fenceToTrigger); @@ -270,7 +268,8 @@ void FrameData::Submit(VulkanContext *vulkan, FrameSubmitType type, FrameDataSha // Hard stall of the GPU, not ideal, but necessary so the CPU has the contents of the readback. vkWaitForFences(vulkan->GetDevice(), 1, &sharedData.readbackFence, true, UINT64_MAX); vkResetFences(vulkan->GetDevice(), 1, &sharedData.readbackFence); - syncDone = true; + retro_atomic_store_release_int(&syncDone, 1); + ParkingLotNotify(&syncDone); } } diff --git a/Common/GPU/Vulkan/VulkanFrameData.h b/Common/GPU/Vulkan/VulkanFrameData.h index dbde815515..f37338a162 100644 --- a/Common/GPU/Vulkan/VulkanFrameData.h +++ b/Common/GPU/Vulkan/VulkanFrameData.h @@ -2,8 +2,7 @@ #include -#include -#include +#include #include "Common/GPU/Vulkan/VulkanContext.h" #include "Common/Data/Collections/Hashmaps.h" @@ -90,9 +89,9 @@ enum class FrameSubmitType { struct FrameData { bool skipSwap = false; - std::mutex fenceMutex; - std::condition_variable fenceCondVar; - bool readyForFence = true; + // With the render thread: set once it has submitted this frame (its fence is then in flight), + // cleared by BeginFrame before it waits on the fence. + retro_atomic_int_t readyForFence{ 1 }; VkFence fence = VK_NULL_HANDLE; VkSemaphore acquireSemaphore = VK_NULL_HANDLE; @@ -112,7 +111,8 @@ struct FrameData { bool hasFencePending = false; bool hasAcquired = false; - bool syncDone = false; + // Set by the render thread once a sync submit has finished, cleared by FlushSync. + retro_atomic_int_t syncDone{ 0 }; // Swapchain. uint32_t curSwapchainImage = -1; diff --git a/Common/GPU/Vulkan/VulkanFramebuffer.cpp b/Common/GPU/Vulkan/VulkanFramebuffer.cpp index da0d06311c..970f663f19 100644 --- a/Common/GPU/Vulkan/VulkanFramebuffer.cpp +++ b/Common/GPU/Vulkan/VulkanFramebuffer.cpp @@ -559,15 +559,34 @@ VkRenderPass VKRRenderPass::Get(VulkanContext *vulkan, RenderPassType rpType, Vk _dbg_assert_(!((rpType & RenderPassType::MULTISAMPLE) && sampleCount == VK_SAMPLE_COUNT_1_BIT)); - // Called from both the main thread and the render thread, see the note on mutex_. - std::lock_guard lock(mutex_); + retro_atomic_ptr_t *slot = &pass_[(int)rpType]; + for (;;) { + Created *cur = (Created *)retro_atomic_load_acquire_ptr(slot); + if (cur && cur->sampleCount == sampleCount) { + return cur->pass; + } + Created *mine = new Created{ CreateRenderPass(vulkan, key_, (RenderPassType)rpType, sampleCount), sampleCount }; + if (retro_atomic_cas_ptr(slot, (void *)cur, (void *)mine)) { + if (cur) { + vulkan->Delete().QueueDeleteRenderPass(cur->pass); + vulkan->Delete().QueueCallback([cur](VulkanContext *) { + delete cur; + }); + } + return mine->pass; + } + // Another thread got there first. Nothing else has seen ours. + vkDestroyRenderPass(vulkan->GetDevice(), mine->pass, nullptr); + delete mine; + } +} - if (!pass[(int)rpType] || sampleCounts[(int)rpType] != sampleCount) { - if (pass[(int)rpType]) { - vulkan->Delete().QueueDeleteRenderPass(pass[(int)rpType]); +void VKRRenderPass::Destroy(VulkanContext *vulkan) { + for (size_t i = 0; i < (size_t)RenderPassType::TYPE_COUNT; i++) { + Created *cur = (Created *)retro_atomic_exchange_ptr(&pass_[i], nullptr); + if (cur) { + vulkan->Delete().QueueDeleteRenderPass(cur->pass); + delete cur; } - pass[(int)rpType] = CreateRenderPass(vulkan, key_, (RenderPassType)rpType, sampleCount); - sampleCounts[(int)rpType] = sampleCount; } - return pass[(int)rpType]; } diff --git a/Common/GPU/Vulkan/VulkanFramebuffer.h b/Common/GPU/Vulkan/VulkanFramebuffer.h index 291609c06c..939336cfc3 100644 --- a/Common/GPU/Vulkan/VulkanFramebuffer.h +++ b/Common/GPU/Vulkan/VulkanFramebuffer.h @@ -1,6 +1,5 @@ #pragma once -#include #include "Common/Common.h" #include "Common/GPU/Vulkan/VulkanContext.h" @@ -141,35 +140,35 @@ struct RPKey { VKRRenderPassStoreAction colorStoreAction; VKRRenderPassStoreAction depthStoreAction; VKRRenderPassStoreAction stencilStoreAction; + + // Dense index over every possible key, for the render pass table. + enum { INDEX_COUNT = 3 * 3 * 3 * 2 * 2 * 2 }; + int Index() const { + return ((((int)colorLoadAction * 3 + (int)depthLoadAction) * 3 + (int)stencilLoadAction) * 8) + + (int)colorStoreAction * 4 + (int)depthStoreAction * 2 + (int)stencilStoreAction; + } }; class VKRRenderPass { public: explicit VKRRenderPass(const RPKey &key) : key_(key) {} + // Any thread: the main thread (EndCurRenderStep) and the render thread (PerformRenderPass) both + // create passes lazily. VkRenderPass Get(VulkanContext *vulkan, RenderPassType rpType, VkSampleCountFlagBits sampleCount); - // Only called from VulkanQueueRunner::DestroyDeviceObjects, with the threads stopped - no lock needed. - void Destroy(VulkanContext *vulkan) { - for (size_t i = 0; i < (size_t)RenderPassType::TYPE_COUNT; i++) { - if (pass[i]) { - vulkan->Delete().QueueDeleteRenderPass(pass[i]); - } - } - } + // Only called from VulkanQueueRunner::DestroyDeviceObjects, with the threads stopped. + void Destroy(VulkanContext *vulkan); + + const RPKey &Key() const { return key_; } private: - // Get() creates the passes lazily, and runs on both the main thread (EndCurRenderStep) and the - // render thread (PerformRenderPass), so the arrays below need guarding. Without it, two threads - // reaching the same empty slot each create a pass and one gets overwritten and leaked - and the - // sample count branch can queue a pass for deletion that the other thread is about to use. - // Lock ordering: taken while VKRGraphicsPipeline::mutex_ is held (VulkanQueueRunner), never the - // other way around. - std::mutex mutex_; - - // TODO: Might be better off with a hashmap once the render pass type count grows really large.. - VkRenderPass pass[(size_t)RenderPassType::TYPE_COUNT]{}; - VkSampleCountFlagBits sampleCounts[(size_t)RenderPassType::TYPE_COUNT]{}; + // Immutable once published. A replaced one is freed from the delete list, after any reader. + struct Created { + VkRenderPass pass; + VkSampleCountFlagBits sampleCount; + }; + retro_atomic_ptr_t pass_[(size_t)RenderPassType::TYPE_COUNT]{}; RPKey key_; }; diff --git a/Common/GPU/Vulkan/VulkanQueueRunner.cpp b/Common/GPU/Vulkan/VulkanQueueRunner.cpp index 1288618c4b..8d4f9fb01c 100644 --- a/Common/GPU/Vulkan/VulkanQueueRunner.cpp +++ b/Common/GPU/Vulkan/VulkanQueueRunner.cpp @@ -71,12 +71,13 @@ void VulkanQueueRunner::DestroyDeviceObjects() { syncReadback_.Destroy(vulkan_); - renderPasses_.IterateMut([&](const RPKey &rpkey, VKRRenderPass *rp) { - _dbg_assert_(rp); - rp->Destroy(vulkan_); - delete rp; - }); - renderPasses_.Clear(); + for (int i = 0; i < RPKey::INDEX_COUNT; i++) { + VKRRenderPass *rp = (VKRRenderPass *)retro_atomic_exchange_ptr(&renderPasses_[i], nullptr); + if (rp) { + rp->Destroy(vulkan_); + delete rp; + } + } } bool VulkanQueueRunner::InitBackbufferFramebuffers(int width, int height, FrameDataShared &frameDataShared) { @@ -200,20 +201,19 @@ VKRRenderPass *VulkanQueueRunner::GetRenderPass(const RPKey &key) { // Called from the main thread (EndCurRenderStep, CreateGraphicsPipeline) and from the render thread // (PerformBindFramebufferAsRenderTarget). The render thread really does insert new keys, not just hit // existing ones - PreprocessSteps rewrites the load actions to CLEAR when it merges a clear-only pass - // into a later one, after the main thread already looked up the pre-merge key. Insert() can Grow(), - // which reallocates the buckets out from under a concurrent Get(). - std::lock_guard lock(renderPassesMutex_); - - VKRRenderPass *foundPass; - if (renderPasses_.Get(key, &foundPass)) { - return foundPass; + // into a later one, after the main thread already looked up the pre-merge key. + retro_atomic_ptr_t *slot = &renderPasses_[key.Index()]; + VKRRenderPass *found = (VKRRenderPass *)retro_atomic_load_acquire_ptr(slot); + if (found) { + return found; } - VKRRenderPass *pass = new VKRRenderPass(key); - renderPasses_.Insert(key, pass); - // Safe to hand out the pointer once the lock is dropped - entries are never erased individually, - // only all at once in DestroyDeviceObjects. - return pass; + if (retro_atomic_cas_ptr(slot, nullptr, (void *)pass)) { + return pass; + } + // Lost the race. Ours never created any Vulkan objects. + delete pass; + return (VKRRenderPass *)retro_atomic_load_acquire_ptr(slot); } void VulkanQueueRunner::PreprocessSteps(std::vector &steps) { @@ -1108,21 +1108,17 @@ void VulkanQueueRunner::PerformRenderPass(const VKRStep &step, VkCommandBuffer c "expected %d sample count, got %d", fbSampleCount, graphicsPipeline->SampleCount()); } - VkPipeline pipeline; - - { - std::lock_guard lock(graphicsPipeline->mutex_); - if (!graphicsPipeline->pipeline[(size_t)rpType]) { - // NOTE: If render steps got merged, it can happen that, as they ended during recording, - // they didn't know their final render pass type so they created the wrong pipelines in EndCurRenderStep(). - // Unfortunately I don't know if we can fix it in any more sensible place than here. - // Maybe a middle pass. But let's try to just block and compile here for now, this doesn't - // happen all that much. - graphicsPipeline->pipeline[(size_t)rpType] = Promise::CreateEmpty(); - graphicsPipeline->Create(vulkan_, renderPass->Get(vulkan_, rpType, fbSampleCount), rpType, fbSampleCount, time_now_d(), -1); - } - pipeline = graphicsPipeline->pipeline[(size_t)rpType]->BlockUntilReady(); + // NOTE: If render steps got merged, it can happen that, as they ended during recording, + // they didn't know their final render pass type so they created the wrong pipelines in EndCurRenderStep(). + // Unfortunately I don't know if we can fix it in any more sensible place than here. + // Maybe a middle pass. But let's try to just block and compile here for now, this doesn't + // happen all that much. + bool created; + Promise *promise = graphicsPipeline->ClaimVariant(rpType, &created); + if (created) { + graphicsPipeline->Create(vulkan_, promise, renderPass->Get(vulkan_, rpType, fbSampleCount), rpType, fbSampleCount, time_now_d(), -1); } + VkPipeline pipeline = promise->BlockUntilReady(); if (pipeline != VK_NULL_HANDLE) { vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); diff --git a/Common/GPU/Vulkan/VulkanQueueRunner.h b/Common/GPU/Vulkan/VulkanQueueRunner.h index 6813b0a8da..39988c97cc 100644 --- a/Common/GPU/Vulkan/VulkanQueueRunner.h +++ b/Common/GPU/Vulkan/VulkanQueueRunner.h @@ -1,8 +1,6 @@ #pragma once #include -#include -#include #include "Common/Thread/Promise.h" #include "Common/Data/Collections/Hashmaps.h" @@ -213,7 +211,7 @@ struct VKRRenderThreadTask { class VulkanQueueRunner { public: - VulkanQueueRunner(VulkanContext *vulkan) : vulkan_(vulkan), renderPasses_(16) {} + VulkanQueueRunner(VulkanContext *vulkan) : vulkan_(vulkan) {} void SetBackbuffer(VkFramebuffer fb, VkImage img) { backbuffer_ = fb; @@ -253,15 +251,13 @@ public: VKRRenderPass *GetRenderPass(const RPKey &key); bool GetRenderPassKey(VKRRenderPass *passToFind, RPKey *outKey) const { - std::lock_guard lock(renderPassesMutex_); - bool found = false; - renderPasses_.Iterate([passToFind, &found, outKey](const RPKey &rpkey, const VKRRenderPass *pass) { - if (pass == passToFind) { - found = true; - *outKey = rpkey; + for (int i = 0; i < RPKey::INDEX_COUNT; i++) { + if (retro_atomic_load_acquire_ptr(const_cast(&renderPasses_[i])) == passToFind) { + *outKey = passToFind->Key(); + return true; } - }); - return found; + } + return false; } void EnableHacks(uint32_t hacks) { @@ -303,9 +299,9 @@ private: // Renderpasses, all combinations of preserving or clearing or dont-care-ing fb contents. // Each VKRRenderPass contains all compatibility classes (which attachments they have, etc). - // Looked up and inserted into from both the main thread and the render thread - see GetRenderPass. - mutable std::mutex renderPassesMutex_; - DenseHashMap renderPasses_; + // VKRRenderPass * by RPKey::Index(). Filled in with CAS from the main and render threads, see + // GetRenderPass, and only emptied in DestroyDeviceObjects. + retro_atomic_ptr_t renderPasses_[RPKey::INDEX_COUNT]{}; // Readback buffer. Currently we only support synchronous readback, so we only really need one. // We size it generously. diff --git a/Common/GPU/Vulkan/VulkanRenderManager.cpp b/Common/GPU/Vulkan/VulkanRenderManager.cpp index df6bed692f..79ec6d2fc8 100644 --- a/Common/GPU/Vulkan/VulkanRenderManager.cpp +++ b/Common/GPU/Vulkan/VulkanRenderManager.cpp @@ -14,6 +14,7 @@ #include "Common/LogReporting.h" #include "Common/Thread/ThreadUtil.h" +#include "Common/Thread/Thread.h" #if 0 // def _DEBUG #define VLOG(...) NOTICE_LOG(Log::G3D, __VA_ARGS__) @@ -28,17 +29,29 @@ using namespace PPSSPP_VK; // renderPass is an example of the "compatibility class" or RenderPassType type. -bool VKRGraphicsPipeline::Create(VulkanContext *vulkan, VkRenderPass compatibleRenderPass, RenderPassType rpType, VkSampleCountFlagBits sampleCount, double scheduleTime, int countToCompile) { - // Good torture test to test the shutdown-while-precompiling-shaders issue on PC where it's normally - // hard to catch because shaders compile so fast. - // sleep_ms(200); +Promise *VKRGraphicsPipeline::ClaimVariant(RenderPassType rpType, bool *created) { + retro_atomic_ptr_t *slot = &pipeline_[(size_t)rpType]; + Promise *existing = (Promise *)retro_atomic_load_acquire_ptr(slot); + if (existing) { + *created = false; + return existing; + } + Promise *mine = Promise::CreateEmpty(); + if (retro_atomic_cas_ptr(slot, nullptr, (void *)mine)) { + *created = true; + return mine; + } + delete mine; + *created = false; + return (Promise *)retro_atomic_load_acquire_ptr(slot); +} +bool VKRGraphicsPipeline::Create(VulkanContext *vulkan, Promise *promise, VkRenderPass compatibleRenderPass, RenderPassType rpType, VkSampleCountFlagBits sampleCount, double scheduleTime, int countToCompile) { bool multisample = RenderPassTypeHasMultisample(rpType); if (multisample) { - if (sampleCount_ != VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM) { - _assert_(sampleCount == sampleCount_); - } else { - sampleCount_ = sampleCount; + const int unset = (int)VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM; + if (!retro_atomic_cas_int(&sampleCount_, unset, (int)sampleCount)) { + _assert_(sampleCount == SampleCount()); } } @@ -46,7 +59,7 @@ bool VKRGraphicsPipeline::Create(VulkanContext *vulkan, VkRenderPass compatibleR // Seen in crash reports from PowerVR GE8320, presumably we failed creating some shader modules. if (!desc->vertexShader || !desc->fragmentShader) { ERROR_LOG(Log::G3D, "Failed creating graphics pipeline - missing vs/fs shader module pointers!"); - pipeline[(size_t)rpType]->Post(VK_NULL_HANDLE); + promise->Post(VK_NULL_HANDLE); return false; } @@ -56,13 +69,13 @@ bool VKRGraphicsPipeline::Create(VulkanContext *vulkan, VkRenderPass compatibleR if (!vs || !fs) { ERROR_LOG(Log::G3D, "Failed creating graphics pipeline - missing shader modules"); - pipeline[(size_t)rpType]->Post(VK_NULL_HANDLE); + promise->Post(VK_NULL_HANDLE); return false; } if (!compatibleRenderPass) { ERROR_LOG(Log::G3D, "Failed creating graphics pipeline - compatible render pass was nullptr"); - pipeline[(size_t)rpType]->Post(VK_NULL_HANDLE); + promise->Post(VK_NULL_HANDLE); return false; } @@ -148,12 +161,12 @@ bool VKRGraphicsPipeline::Create(VulkanContext *vulkan, VkRenderPass compatibleR // Would really like to log more here, we could probably attach more info to desc. // // At least create a null placeholder to avoid creating over and over if something is broken. - pipeline[(size_t)rpType]->Post(VK_NULL_HANDLE); + promise->Post(VK_NULL_HANDLE); ERROR_LOG(Log::G3D, "Failed creating graphics pipeline! VK_INCOMPLETE"); LogCreationFailure(); success = false; } else if (result != VK_SUCCESS) { - pipeline[(size_t)rpType]->Post(VK_NULL_HANDLE); + promise->Post(VK_NULL_HANDLE); ERROR_LOG(Log::G3D, "Failed creating graphics pipeline! result='%s'", VulkanResultToString(result)); LogCreationFailure(); success = false; @@ -162,44 +175,40 @@ bool VKRGraphicsPipeline::Create(VulkanContext *vulkan, VkRenderPass compatibleR if (!tag_.empty()) { vulkan->SetDebugName(vkpipeline, VK_OBJECT_TYPE_PIPELINE, tag_.c_str()); } - pipeline[(size_t)rpType]->Post(vkpipeline); + promise->Post(vkpipeline); } return success; } void VKRGraphicsPipeline::DestroyVariants(VulkanContext *vulkan, bool msaaOnly) { - // Called from InvalidateMSAAPipelines on the main thread, mid-frame, while the render thread may be - // reading and replacing these same slots in PerformRenderPass - so take the lock that's documented - // as protecting the array. It also has to be held across the delete below, or the render thread can - // be left holding a freed Promise. - std::lock_guard lock(mutex_); - + // Main thread, mid-frame. The render thread may still hold a promise it loaded, so each one + // swapped out here is freed from the delete list, after the frames that could use it. for (size_t i = 0; i < (size_t)RenderPassType::TYPE_COUNT; i++) { - if (!this->pipeline[i]) - continue; if (msaaOnly && (i & (int)RenderPassType::MULTISAMPLE) == 0) continue; + Promise *promise = (Promise *)retro_atomic_exchange_ptr(&pipeline_[i], nullptr); + if (!promise) + continue; - VkPipeline pipeline = this->pipeline[i]->BlockUntilReady(); + VkPipeline pipeline = promise->BlockUntilReady(); // pipeline can be nullptr here, if it failed to compile before. if (pipeline) { vulkan->Delete().QueueDeletePipeline(pipeline); } - // The array owns the Promise - DestroyVariantsInstant deletes it too. Forgetting it here leaked - // one per destroyed variant on every MSAA or resolution change. - delete this->pipeline[i]; - this->pipeline[i] = nullptr; + vulkan->Delete().QueueCallback([promise](VulkanContext *) { + delete promise; + }); } - sampleCount_ = VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM; + retro_atomic_store_release_int(&sampleCount_, (int)VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM); } void VKRGraphicsPipeline::DestroyVariantsInstant(VkDevice device) { for (size_t i = 0; i < (size_t)RenderPassType::TYPE_COUNT; i++) { - if (pipeline[i]) { - vkDestroyPipeline(device, pipeline[i]->BlockUntilReady(), nullptr); - delete pipeline[i]; - pipeline[i] = nullptr; + Promise *promise = (Promise *)retro_atomic_exchange_ptr(&pipeline_[i], nullptr); + if (promise) { + vkDestroyPipeline(device, promise->BlockUntilReady(), nullptr); + delete promise; } } } @@ -208,7 +217,7 @@ VKRGraphicsPipeline::~VKRGraphicsPipeline() { // This is called from the callbacked queued in QueueForDeletion. // When we reach here, we should already be empty, so let's assert on that. for (size_t i = 0; i < (size_t)RenderPassType::TYPE_COUNT; i++) { - _assert_(!pipeline[i]); + _assert_(!Variant((RenderPassType)i)); } if (desc) desc->Release(); @@ -216,8 +225,8 @@ VKRGraphicsPipeline::~VKRGraphicsPipeline() { void VKRGraphicsPipeline::BlockUntilCompiled() { for (size_t i = 0; i < (size_t)RenderPassType::TYPE_COUNT; i++) { - if (pipeline[i]) { - pipeline[i]->BlockUntilReady(); + if (Promise *promise = Variant((RenderPassType)i)) { + promise->BlockUntilReady(); } } } @@ -233,7 +242,7 @@ void VKRGraphicsPipeline::QueueForDeletion(VulkanContext *vulkan) { u32 VKRGraphicsPipeline::GetVariantsBitmask() const { u32 bitmask = 0; for (size_t i = 0; i < (size_t)RenderPassType::TYPE_COUNT; i++) { - if (pipeline[i]) { + if (Variant((RenderPassType)i)) { bitmask |= 1 << i; } } @@ -249,6 +258,7 @@ void VKRGraphicsPipeline::LogCreationFailure() const { struct SinglePipelineTask { VKRGraphicsPipeline *pipeline; + Promise *promise; VkRenderPass compatibleRenderPass; RenderPassType rpType; VkSampleCountFlagBits sampleCount; @@ -259,7 +269,7 @@ struct SinglePipelineTask { class CreateMultiPipelinesTask : public Task { public: CreateMultiPipelinesTask(VulkanContext *vulkan, std::vector tasks) : vulkan_(vulkan), tasks_(std::move(tasks)) { - tasksInFlight_.fetch_add(1); + retro_atomic_fetch_add_int(&tasksInFlight_, 1); } ~CreateMultiPipelinesTask() = default; @@ -273,9 +283,9 @@ public: void Run() override { for (auto &task : tasks_) { - task.pipeline->Create(vulkan_, task.compatibleRenderPass, task.rpType, task.sampleCount, task.scheduleTime, task.countToCompile); + task.pipeline->Create(vulkan_, task.promise, task.compatibleRenderPass, task.rpType, task.sampleCount, task.scheduleTime, task.countToCompile); } - if (tasksInFlight_.fetch_sub(1) == 1) + if (retro_atomic_fetch_sub_int(&tasksInFlight_, 1) == 1) ParkingLotNotify(&tasksInFlight_); } @@ -286,16 +296,16 @@ public: // Could probably be done more elegantly. Like waiting for all tasks of a type, or saving pointers to them, or something... // Returns the maximum value of tasks in flight seen during the wait. static int WaitForAll(); - static std::atomic tasksInFlight_; + static retro_atomic_int_t tasksInFlight_; }; int CreateMultiPipelinesTask::WaitForAll() { - const int inFlight = tasksInFlight_.load(); - ParkingLotWait(&tasksInFlight_, [] { return tasksInFlight_.load() <= 0; }); + const int inFlight = retro_atomic_load_acquire_int(&tasksInFlight_); + ParkingLotWait(&tasksInFlight_, [] { return retro_atomic_load_acquire_int(&tasksInFlight_) <= 0; }); return inFlight > 0 ? inFlight : 0; } -std::atomic CreateMultiPipelinesTask::tasksInFlight_; +retro_atomic_int_t CreateMultiPipelinesTask::tasksInFlight_{ 0 }; VulkanRenderManager::VulkanRenderManager(VulkanContext *vulkan, bool useThread, HistoryBuffer &frameTimeHistory) : vulkan_(vulkan), queueRunner_(vulkan), @@ -346,8 +356,7 @@ bool VulkanRenderManager::CreateBackbuffers() { outOfDateFrames_ = 0; for (int i = 0; i < vulkan_->GetInflightFrames(); i++) { - auto &frameData = frameData_[i]; - frameData.readyForFence = true; // Just in case. + retro_atomic_store_release_int(&frameData_[i].readyForFence, 1); // Just in case. } // Start the thread(s). @@ -440,25 +449,27 @@ bool VulkanRenderManager::RecreatePresentationIfNeeded() { return true; } -void VulkanRenderManager::StartThreads() { - { - std::unique_lock lock(compileQueueMutex_); - _assert_(compileQueue_.empty()); - } +void VulkanRenderManager::PushRenderTask(VKRRenderThreadTask *task) { + renderThreadQueue_.Push(task); + renderThreadWork_.Notify(); +} - runCompileThread_ = true; // For controlling the compiler thread's exit +void VulkanRenderManager::QueueCompile(VKRGraphicsPipeline *pipeline, Promise *promise, VkRenderPass compatibleRenderPass, RenderPassType rpType, VkSampleCountFlagBits sampleCount) { + retro_atomic_fetch_add_int(&compilePending_, 1); + compileQueue_.Push(CompileQueueEntry(pipeline, promise, compatibleRenderPass, rpType, sampleCount)); +} + +void VulkanRenderManager::StartThreads() { + _assert_(retro_atomic_load_acquire_int(&compilePending_) == 0); + + retro_atomic_store_release_int(&runCompileThread_, 1); // For controlling the compiler thread's exit if (useRenderThread_) { INFO_LOG(Log::G3D, "Starting Vulkan submission thread"); - renderThread_ = std::thread(&VulkanRenderManager::RenderThreadFunc, this); + renderThread_ = Thread(&VulkanRenderManager::RenderThreadFunc, this); } INFO_LOG(Log::G3D, "Starting Vulkan compiler thread"); - compileThread_ = std::thread(&VulkanRenderManager::CompileThreadFunc, this); - - if (measurePresentTime_ && vulkan_->Extensions().KHR_present_wait && vulkan_->GetPresentMode() == VK_PRESENT_MODE_FIFO_KHR) { - INFO_LOG(Log::G3D, "Starting Vulkan present wait thread"); - presentWaitThread_ = std::thread(&VulkanRenderManager::PresentWaitThreadFunc, this); - } + compileThread_ = Thread(&VulkanRenderManager::CompileThreadFunc, this); } // MUST be called from emuthread! @@ -477,11 +488,7 @@ void VulkanRenderManager::StopThreads() { // Tell the render thread to quit when it's done. VKRRenderThreadTask *task = new VKRRenderThreadTask(VKRRunType::EXIT); task->frame = vulkan_->GetCurFrame(); - { - std::unique_lock lock(pushMutex_); - renderThreadQueue_.push(task); - } - pushCondVar_.notify_one(); + PushRenderTask(task); // Once the render thread encounters the above exit task, it'll exit. renderThread_.join(); INFO_LOG(Log::G3D, "Vulkan submission thread joined. Frame=%d", vulkan_->GetCurFrame()); @@ -493,26 +500,16 @@ void VulkanRenderManager::StopThreads() { frameData.profile.timestampDescriptions.clear(); } - { - std::unique_lock lock(compileQueueMutex_); - runCompileThread_ = false; // Compiler and present thread both look at this bool. - compileCond_.notify_one(); - } + retro_atomic_store_release_int(&runCompileThread_, 0); + compileWork_.Notify(); if (compileThread_.joinable()) { compileThread_.join(); } - if (presentWaitThread_.joinable()) { - presentWaitThread_.join(); - } - - INFO_LOG(Log::G3D, "Vulkan compiler thread joined. Now wait for any straggling compile tasks. runCompileThread_ = %d", (int)runCompileThread_); + INFO_LOG(Log::G3D, "Vulkan compiler thread joined. Now wait for any straggling compile tasks."); CreateMultiPipelinesTask::WaitForAll(); - { - std::unique_lock lock(compileQueueMutex_); - _assert_(compileQueue_.empty()); - } + _assert_(retro_atomic_load_acquire_int(&compilePending_) == 0); } void VulkanRenderManager::DestroyBackbuffers() { @@ -532,30 +529,25 @@ void VulkanRenderManager::DestroyBackbuffers() { // Hm, I'm finding the occasional report of these asserts. void VulkanRenderManager::CheckNothingPending() { _assert_(pipelinesToCheck_.empty()); - { - std::unique_lock lock(compileQueueMutex_); - _assert_(compileQueue_.empty()); - } + _assert_(retro_atomic_load_acquire_int(&compilePending_) == 0); } VulkanRenderManager::~VulkanRenderManager() { INFO_LOG(Log::G3D, "VulkanRenderManager destructor"); - { - std::unique_lock lock(compileQueueMutex_); - _assert_(compileQueue_.empty()); - } + _assert_(retro_atomic_load_acquire_int(&compilePending_) == 0); if (useRenderThread_) { _dbg_assert_(!renderThread_.joinable()); } - _dbg_assert_(!runCompileThread_); // StopThread should already have been called from DestroyBackbuffers. + _dbg_assert_(!CompileThreadRunning()); // StopThread should already have been called from DestroyBackbuffers. vulkan_->WaitUntilQueueIdle(); vulkan_->PerformPendingDeletes(); // Some callbacks can contain a reference to the render manager. - _dbg_assert_(pipelineLayouts_.empty()); + _dbg_assert_(!Layouts() || Layouts()->empty()); + delete (LayoutList *)retro_atomic_exchange_ptr(&pipelineLayouts_, nullptr); VkDevice device = vulkan_->GetDevice(); frameDataShared_.Destroy(vulkan_); @@ -568,19 +560,19 @@ VulkanRenderManager::~VulkanRenderManager() { void VulkanRenderManager::CompileThreadFunc() { SetCurrentThreadName("ShaderCompile"); while (true) { - bool exitAfterCompile = false; + const int seen = compileWork_.Seen(); + // Read before draining, so whatever was queued before the stop still gets scheduled. + const bool exitAfterCompile = !CompileThreadRunning(); std::vector toCompile; - { - std::unique_lock lock(compileQueueMutex_); - while (compileQueue_.empty() && runCompileThread_) { - compileCond_.wait(lock); - } - toCompile = std::move(compileQueue_); - compileQueue_.clear(); - compileScheduling_ = true; - if (!runCompileThread_) { - exitAfterCompile = true; + compileQueue_.Drain([&](CompileQueueEntry &&entry) { + toCompile.push_back(std::move(entry)); + }); + if (toCompile.empty()) { + if (exitAfterCompile) { + break; } + compileWork_.Wait(seen); + continue; } int countToCompile = (int)toCompile.size(); @@ -595,22 +587,17 @@ void VulkanRenderManager::CompileThreadFunc() { // I don't think PowerVR cares though, it doesn't seem to reuse information between the compiles, // so we might want a different splitting algorithm there. for (auto &entry : toCompile) { - switch (entry.type) { - case CompileQueueEntry::Type::GRAPHICS: - { - map[std::make_pair(entry.graphics->desc->vertexShader, entry.graphics->desc->fragmentShader)].push_back( - SinglePipelineTask{ - entry.graphics, - entry.compatibleRenderPass, - entry.renderPassType, - entry.sampleCount, - scheduleTime, // these two are for logging purposes. - countToCompile, - } - ); - break; - } - } + map[std::make_pair(entry.graphics->desc->vertexShader, entry.graphics->desc->fragmentShader)].push_back( + SinglePipelineTask{ + entry.graphics, + entry.promise, + entry.compatibleRenderPass, + entry.renderPassType, + entry.sampleCount, + scheduleTime, // these two are for logging purposes. + countToCompile, + } + ); } for (const auto &iter : map) { @@ -623,40 +610,33 @@ void VulkanRenderManager::CompileThreadFunc() { g_threadManager.EnqueueTask(task); } - { - std::unique_lock lock(compileQueueMutex_); - compileScheduling_ = false; - } - compileProgress_.fetch_add(1); - ParkingLotNotify(&compileProgress_); - - if (exitAfterCompile) { - break; + // The tasks are counted in flight now, so WaitForPipelines() can stop waiting on the queue. + if (retro_atomic_fetch_sub_int(&compilePending_, countToCompile) == countToCompile) { + ParkingLotNotify(&compilePending_); } } - - std::unique_lock lock(compileQueueMutex_); - _assert_(compileQueue_.empty()); } void VulkanRenderManager::RenderThreadFunc() { SetCurrentThreadName("VulkanRenderMan"); + std::deque pendingTasks; while (true) { _dbg_assert_(useRenderThread_); // Pop a task of the queue and execute it. - VKRRenderThreadTask *task = nullptr; - { - std::unique_lock lock(pushMutex_); - while (renderThreadQueue_.empty()) { - pushCondVar_.wait(lock); + if (pendingTasks.empty()) { + const int seen = renderThreadWork_.Seen(); + renderThreadQueue_.Drain([&](VKRRenderThreadTask *&&t) { + pendingTasks.push_back(t); + }); + if (pendingTasks.empty()) { + renderThreadWork_.Wait(seen); + continue; } - task = renderThreadQueue_.front(); - renderThreadQueue_.pop(); } + VKRRenderThreadTask *task = pendingTasks.front(); + pendingTasks.pop_front(); - // Oh, we got a task! We can now have pushMutex_ unlocked, allowing the host to - // push more work when it feels like it, and just start working. if (task->runType == VKRRunType::EXIT) { // Oh, host wanted out. Let's leave. delete task; @@ -674,61 +654,6 @@ void VulkanRenderManager::RenderThreadFunc() { VLOG("PULL: Quitting"); } -void VulkanRenderManager::PresentWaitThreadFunc() { - SetCurrentThreadName("PresentWait"); - -#if !PPSSPP_PLATFORM(IOS_APP_STORE) - _dbg_assert_(vkWaitForPresentKHR != nullptr); - - uint64_t waitedId = frameIdGen_; - while (runCompileThread_) { - const uint64_t timeout = 1000000000ULL; // 1 sec - const VkResult res = vkWaitForPresentKHR(vulkan_->GetDevice(), vulkan_->GetSwapchain(), waitedId, timeout); - if (res == VK_SUCCESS || res == VK_SUBOPTIMAL_KHR) { - frameTimeHistory_[waitedId].actualPresent = time_now_d(); - frameTimeHistory_[waitedId].waitCount++; - waitedId++; - } else if (res == VK_TIMEOUT) { - frameTimeHistory_[waitedId].waitCount++; - } else { - // Lost swapchain or device: every further wait would fail at once. - WARN_LOG(Log::G3D, "vkWaitForPresentKHR failed (%d), stopping present timing", (int)res); - break; - } - _dbg_assert_(waitedId <= frameIdGen_); - } -#endif - - INFO_LOG(Log::G3D, "Leaving PresentWaitThreadFunc()"); -} - -void VulkanRenderManager::PollPresentTiming() { - // For VK_GOOGLE_display_timing, we need to poll. - - // Poll for information about completed frames. - // NOTE: We seem to get the information pretty late! Like after 6 frames, which is quite weird. - // Tested on POCO F4. - // TODO: Getting validation errors that this should be called from the thread doing the presenting. - // Probably a fair point. For now, we turn it off. - if (measurePresentTime_ && vulkan_->Extensions().GOOGLE_display_timing) { - uint32_t count = 0; - vkGetPastPresentationTimingGOOGLE(vulkan_->GetDevice(), vulkan_->GetSwapchain(), &count, nullptr); - if (count > 0) { - VkPastPresentationTimingGOOGLE *timings = new VkPastPresentationTimingGOOGLE[count]; - vkGetPastPresentationTimingGOOGLE(vulkan_->GetDevice(), vulkan_->GetSwapchain(), &count, timings); - for (uint32_t i = 0; i < count; i++) { - uint64_t presentId = timings[i].presentID; - frameTimeHistory_[presentId].actualPresent = from_time_raw(timings[i].actualPresentTime); - frameTimeHistory_[presentId].desiredPresentTime = from_time_raw(timings[i].desiredPresentTime); - frameTimeHistory_[presentId].earliestPresentTime = from_time_raw(timings[i].earliestPresentTime); - double presentMargin = from_time_raw_relative(timings[i].presentMargin); - frameTimeHistory_[presentId].presentMargin = presentMargin; - } - delete[] timings; - } - } -} - void VulkanRenderManager::BeginFrame(bool enableProfiling, bool enableLogProfiler) { double frameBeginTime = time_now_d(); VLOG("BeginFrame"); @@ -741,17 +666,13 @@ void VulkanRenderManager::BeginFrame(bool enableProfiling, bool enableLogProfile // Makes sure the submission from the previous time around has happened. Otherwise // we are not allowed to wait from another thread here.. if (useRenderThread_) { - std::unique_lock lock(frameData.fenceMutex); - while (!frameData.readyForFence) { - frameData.fenceCondVar.wait(lock); - } - frameData.readyForFence = false; + ParkingLotWait(&frameData.readyForFence, [&] { return retro_atomic_load_acquire_int(&frameData.readyForFence) != 0; }); + retro_atomic_store_relaxed_int(&frameData.readyForFence, 0); } auto restoreReadyForFence = [&]() { if (useRenderThread_) { - std::lock_guard lock(frameData.fenceMutex); - frameData.readyForFence = true; - frameData.fenceCondVar.notify_one(); + retro_atomic_store_release_int(&frameData.readyForFence, 1); + ParkingLotNotify(&frameData.readyForFence); } }; @@ -769,15 +690,12 @@ void VulkanRenderManager::BeginFrame(bool enableProfiling, bool enableLogProfile // CreateBackbuffers() resets readyForFence for all frames. Keep the current frame consumed until // its new submission signals the fence. Also, don't reset the fence until recreation succeeded. if (useRenderThread_) { - std::lock_guard lock(frameData.fenceMutex); - frameData.readyForFence = false; + retro_atomic_store_relaxed_int(&frameData.readyForFence, 0); } vkResetFences(device, 1, &frameData.fence); uint64_t frameId = frameIdGen_++; - PollPresentTiming(); - ResetDescriptorLists(curFrame); int validBits = vulkan_->GetQueueFamilyProperties(vulkan_->GetGraphicsQueueFamilyIndex()).timestampValidBits; @@ -908,18 +826,8 @@ void VulkanRenderManager::ReportBadStateForDraw() { } int VulkanRenderManager::WaitForPipelines() { - // Pipelines still in the queue, or taken off it but not yet made into tasks, aren't in flight yet. - // The compile thread bumps compileProgress_ after each batch it turns into tasks. - while (true) { - const int seen = compileProgress_.load(); - { - std::unique_lock lock(compileQueueMutex_); - if (compileQueue_.empty() && !compileScheduling_) { - break; - } - } - ParkingLotWait(&compileProgress_, [&] { return compileProgress_.load() != seen; }); - } + // Queued pipelines aren't in flight until the compile thread has made them into tasks. + ParkingLotWait(&compilePending_, [&] { return retro_atomic_load_acquire_int(&compilePending_) == 0; }); return CreateMultiPipelinesTask::WaitForAll(); } @@ -946,8 +854,7 @@ VKRGraphicsPipeline *VulkanRenderManager::CreateGraphicsPipeline(VKRGraphicsPipe VKRRenderPassStoreAction::STORE, VKRRenderPassStoreAction::DONT_CARE, VKRRenderPassStoreAction::DONT_CARE, }; VKRRenderPass *compatibleRenderPass = queueRunner_.GetRenderPass(key); - std::unique_lock lock(compileQueueMutex_); - _dbg_assert_(runCompileThread_); + _dbg_assert_(CompileThreadRunning()); bool needsCompile = false; for (size_t i = 0; i < (size_t)RenderPassType::TYPE_COUNT; i++) { if (!(variantBitmask & (1 << i))) @@ -970,14 +877,17 @@ VKRGraphicsPipeline *VulkanRenderManager::CreateGraphicsPipeline(VKRGraphicsPipe } // Sanity check - if (runCompileThread_) { - pipeline->pipeline[i] = Promise::CreateEmpty(); - compileQueue_.emplace_back(pipeline, compatibleRenderPass->Get(vulkan_, rpType, sampleCount), rpType, sampleCount); + if (CompileThreadRunning()) { + bool created; + Promise *promise = pipeline->ClaimVariant(rpType, &created); + if (created) { + QueueCompile(pipeline, promise, compatibleRenderPass->Get(vulkan_, rpType, sampleCount), rpType, sampleCount); + } } needsCompile = true; } if (needsCompile) - compileCond_.notify_one(); + compileWork_.Notify(); } return pipeline; } @@ -986,7 +896,7 @@ void VulkanRenderManager::EndCurRenderStep() { if (!curRenderStep_) return; - _dbg_assert_(runCompileThread_); + _dbg_assert_(CompileThreadRunning()); RPKey key{ curRenderStep_->render.colorLoad, curRenderStep_->render.depthLoad, curRenderStep_->render.stencilLoad, @@ -1029,21 +939,17 @@ void VulkanRenderManager::EndCurRenderStep() { // Not good, but let's try not to crash. continue; } - std::unique_lock lock(pipeline->mutex_); - if (!pipeline->pipeline[(size_t)rpType]) { - pipeline->pipeline[(size_t)rpType] = Promise::CreateEmpty(); - lock.unlock(); - + bool created; + Promise *promise = pipeline->ClaimVariant(rpType, &created); + if (created) { _assert_(renderPass); - compileQueueMutex_.lock(); - compileQueue_.emplace_back(pipeline, renderPass->Get(vulkan_, rpType, sampleCount), rpType, sampleCount); - compileQueueMutex_.unlock(); + QueueCompile(pipeline, promise, renderPass->Get(vulkan_, rpType, sampleCount), rpType, sampleCount); needsCompile = true; } } if (needsCompile) { - compileCond_.notify_one(); + compileWork_.Notify(); } pipelinesToCheck_.clear(); @@ -1620,10 +1526,8 @@ void VulkanRenderManager::Finish() { VKRRenderThreadTask *task = new VKRRenderThreadTask(VKRRunType::SUBMIT); task->frame = curFrame; if (useRenderThread_) { - std::unique_lock lock(pushMutex_); - renderThreadQueue_.push(task); - renderThreadQueue_.back()->steps = std::move(steps_); - pushCondVar_.notify_one(); + task->steps = std::move(steps_); + PushRenderTask(task); } else { // Just do it! task->steps = std::move(steps_); @@ -1643,9 +1547,7 @@ void VulkanRenderManager::Present() { VKRRenderThreadTask *task = new VKRRenderThreadTask(VKRRunType::PRESENT); task->frame = curFrame; if (useRenderThread_) { - std::unique_lock lock(pushMutex_); - renderThreadQueue_.push(task); - pushCondVar_.notify_one(); + PushRenderTask(task); } else { // Just do it! Run(*task); @@ -1736,13 +1638,9 @@ void VulkanRenderManager::Run(VKRRenderThreadTask &task) { case VKRRunType::SYNC: // The submit will trigger the readbackFence, and also do the wait for it. + // Submit publishes frameData.syncDone and wakes FlushSync. frameData.Submit(vulkan_, FrameSubmitType::Sync, frameDataShared_); - if (useRenderThread_) { - std::unique_lock lock(syncMutex_); - syncCondVar_.notify_one(); - } - // At this point the GPU is idle, and we can resume filling the command buffers for the // current frame since and thus all previously enqueued command buffers have been // processed. No need to switch to the next frame number, would just be confusing. @@ -1777,24 +1675,14 @@ void VulkanRenderManager::FlushSync() { VLOG("PUSH: Frame[%d]", curFrame); VKRRenderThreadTask *task = new VKRRenderThreadTask(VKRRunType::SYNC); task->frame = curFrame; - { - std::unique_lock lock(pushMutex_); - renderThreadQueue_.push(task); - renderThreadQueue_.back()->steps = std::move(steps_); - pushCondVar_.notify_one(); - } + task->steps = std::move(steps_); + PushRenderTask(task); steps_.clear(); } - { - std::unique_lock lock(syncMutex_); - // Wait for the flush to be hit, since we're syncing. - while (!frameData.syncDone) { - VLOG("PUSH: Waiting for frame[%d].syncDone = 1 (sync)", curFrame); - syncCondVar_.wait(lock); - } - frameData.syncDone = false; - } + // Wait for the flush to be hit, since we're syncing. + ParkingLotWait(&frameData.syncDone, [&] { return retro_atomic_load_acquire_int(&frameData.syncDone) != 0; }); + retro_atomic_store_relaxed_int(&frameData.syncDone, 0); } else { VKRRenderThreadTask task(VKRRunType::SYNC); task.frame = curFrame; @@ -1880,28 +1768,35 @@ VKRPipelineLayout *VulkanRenderManager::CreatePipelineLayout(BindingType *bindin layout->frameData[i].pool.Create(vulkan_, bindingTypes, (uint32_t)bindingTypesCount, 1024); } - { - std::lock_guard lock(pipelineLayoutsMutex_); - pipelineLayouts_.push_back(layout); - } + const LayoutList *old = Layouts(); + LayoutList *list = old ? new LayoutList(*old) : new LayoutList(); + list->push_back(layout); + PublishLayouts(list); return layout; } +// Main thread. Swaps in a new list and frees the old one once the render thread is out of it. +void VulkanRenderManager::PublishLayouts(LayoutList *list) { + LayoutList *old = (LayoutList *)retro_atomic_exchange_ptr(&pipelineLayouts_, (void *)list); + layoutsGate_.Drain(); + delete old; +} + void VulkanRenderManager::DestroyPipelineLayout(VKRPipelineLayout *layout) { // The layout has to stay in pipelineLayouts_ until the frames that were recorded with it have been // flushed by the render thread, otherwise their descriptor sets never get written. So, we can't // remove it here - instead we let it ride along on the delete list, which won't be run until the // fence for the frame it was queued in has been waited on. vulkan_->Delete().QueueCallback([this, layout](VulkanContext *vulkan) { - // Runs on the main thread, while the render thread may be in FlushDescriptors - so both the - // erase and the destruction of the layout itself have to be under the lock. - std::lock_guard lock(pipelineLayoutsMutex_); - for (auto iter = pipelineLayouts_.begin(); iter != pipelineLayouts_.end(); iter++) { + // Main thread. PublishLayouts waits out a FlushDescriptors that still sees the old list. + LayoutList *list = new LayoutList(*Layouts()); + for (auto iter = list->begin(); iter != list->end(); iter++) { if (*iter == layout) { - pipelineLayouts_.erase(iter); + list->erase(iter); break; } } + PublishLayouts(list); for (int i = 0; i < VulkanContext::MAX_INFLIGHT_FRAMES; i++) { layout->frameData[i].pool.DestroyImmediately(); } @@ -1914,16 +1809,22 @@ void VulkanRenderManager::DestroyPipelineLayout(VKRPipelineLayout *layout) { // Called on the render thread. void VulkanRenderManager::FlushDescriptors(int frame) { - std::lock_guard lock(pipelineLayoutsMutex_); - for (VKRPipelineLayout *iter : pipelineLayouts_) { - iter->FlushDescSets(vulkan_, frame, &frameData_[frame].profile); + layoutsGate_.Enter(); + if (const LayoutList *list = Layouts()) { + for (VKRPipelineLayout *iter : *list) { + iter->FlushDescSets(vulkan_, frame, &frameData_[frame].profile); + } } + layoutsGate_.Exit(); } -// Called on the main thread, from BeginFrame. +// Called on the main thread, from BeginFrame. The main thread is the only writer of the list. void VulkanRenderManager::ResetDescriptorLists(int frame) { - std::lock_guard lock(pipelineLayoutsMutex_); - for (VKRPipelineLayout *iter : pipelineLayouts_) { + const LayoutList *list = Layouts(); + if (!list) { + return; + } + for (VKRPipelineLayout *iter : *list) { VKRPipelineLayout::FrameData &data = iter->frameData[frame]; data.flushedDescriptors_ = 0; diff --git a/Common/GPU/Vulkan/VulkanRenderManager.h b/Common/GPU/Vulkan/VulkanRenderManager.h index 65b79e4d9d..81e5257eee 100644 --- a/Common/GPU/Vulkan/VulkanRenderManager.h +++ b/Common/GPU/Vulkan/VulkanRenderManager.h @@ -4,12 +4,10 @@ // Only draws and binds are handled here, resource creation and allocations are handled as normal - // that's the nice thing with Vulkan. -#include -#include #include -#include -#include -#include +#include + +#include #include "Common/Math/Statistics.h" #include "Common/Thread/Promise.h" @@ -25,6 +23,9 @@ #include "Common/GPU/Vulkan/VulkanFramebuffer.h" #include "Common/GPU/Vulkan/VulkanDescSet.h" #include "Common/GPU/thin3d.h" +#include "Common/Thread/Thread.h" +#include "Common/Thread/MpscQueue.h" +#include "Common/Thread/ParkingLot.h" // Forward declaration VK_DEFINE_HANDLE(VmaAllocation); @@ -120,7 +121,8 @@ struct VKRGraphicsPipeline { VKRGraphicsPipeline(PipelineFlags flags, const char *tag) : flags_(flags), tag_(tag) {} ~VKRGraphicsPipeline(); - bool Create(VulkanContext *vulkan, VkRenderPass compatibleRenderPass, RenderPassType rpType, VkSampleCountFlagBits sampleCount, double scheduleTime, int countToCompile); + // Posts the result to promise, which must be the one ClaimVariant() installed for rpType. + bool Create(VulkanContext *vulkan, Promise *promise, VkRenderPass compatibleRenderPass, RenderPassType rpType, VkSampleCountFlagBits sampleCount, double scheduleTime, int countToCompile); void DestroyVariants(VulkanContext *vulkan, bool msaaOnly); // This deletes the whole VKRGraphicsPipeline, you must remove your last pointer to it when doing this. @@ -135,10 +137,18 @@ struct VKRGraphicsPipeline { void LogCreationFailure() const; VKRGraphicsPipelineDesc *desc = nullptr; - Promise *pipeline[(size_t)RenderPassType::TYPE_COUNT]{}; - std::mutex mutex_; // protects the pipeline array - VkSampleCountFlagBits SampleCount() const { return sampleCount_; } + // Any thread. The variant's promise, or null if none was claimed. + Promise *Variant(RenderPassType rpType) const { + return (Promise *)retro_atomic_load_acquire_ptr(const_cast(&pipeline_[(size_t)rpType])); + } + // Any thread. Installs an empty promise if the slot is free. *created says whether this caller + // installed it and so must get it compiled. + Promise *ClaimVariant(RenderPassType rpType, bool *created); + + VkSampleCountFlagBits SampleCount() const { + return (VkSampleCountFlagBits)retro_atomic_load_acquire_int(const_cast(&sampleCount_)); + } const char *Tag() const { return tag_.c_str(); } private: @@ -146,19 +156,19 @@ private: std::string tag_; PipelineFlags flags_; - VkSampleCountFlagBits sampleCount_ = VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM; + // Promise * per render pass type. Slots are claimed with CAS, and a promise that + // was swapped out is freed through the delete list, after any reader is done with it. + retro_atomic_ptr_t pipeline_[(size_t)RenderPassType::TYPE_COUNT]{}; + retro_atomic_int_t sampleCount_{ (int)VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM }; }; struct CompileQueueEntry { - CompileQueueEntry(VKRGraphicsPipeline *p, VkRenderPass _compatibleRenderPass, RenderPassType _renderPassType, VkSampleCountFlagBits _sampleCount) - : type(Type::GRAPHICS), graphics(p), compatibleRenderPass(_compatibleRenderPass), renderPassType(_renderPassType), sampleCount(_sampleCount) {} - enum class Type { - GRAPHICS, - }; - Type type; + CompileQueueEntry(VKRGraphicsPipeline *p, Promise *_promise, VkRenderPass _compatibleRenderPass, RenderPassType _renderPassType, VkSampleCountFlagBits _sampleCount) + : compatibleRenderPass(_compatibleRenderPass), renderPassType(_renderPassType), graphics(p), promise(_promise), sampleCount(_sampleCount) {} VkRenderPass compatibleRenderPass; RenderPassType renderPassType; - VKRGraphicsPipeline* graphics = nullptr; + VKRGraphicsPipeline *graphics = nullptr; + Promise *promise = nullptr; VkSampleCountFlagBits sampleCount; }; @@ -293,9 +303,7 @@ public: int WaitForPipelines(); void NudgeCompilerThread() { - compileQueueMutex_.lock(); - compileCond_.notify_one(); - compileQueueMutex_.unlock(); + compileWork_.Notify(); } void AssertInRenderPass() const { @@ -562,8 +570,11 @@ private: // Bad for performance but sometimes necessary for synchronous CPU readbacks (screenshots and whatnot). void FlushSync(); - void PresentWaitThreadFunc(); - void PollPresentTiming(); + void PushRenderTask(VKRRenderThreadTask *task); + void QueueCompile(VKRGraphicsPipeline *pipeline, Promise *promise, VkRenderPass compatibleRenderPass, RenderPassType rpType, VkSampleCountFlagBits sampleCount); + bool CompileThreadRunning() const { + return retro_atomic_load_acquire_int(const_cast(&runCompileThread_)) != 0; + } void ResetDescriptorLists(int frame); void FlushDescriptors(int frame); @@ -591,8 +602,7 @@ private: int curHeight_ = -1; bool insideFrame_ = false; - // probably doesn't need to be atomic. - std::atomic runCompileThread_{}; + retro_atomic_int_t runCompileThread_{ 0 }; bool useRenderThread_ = true; bool measurePresentTime_ = false; @@ -608,33 +618,19 @@ private: // Execution time state VulkanContext *vulkan_; - std::thread renderThread_; + Thread renderThread_; VulkanQueueRunner queueRunner_; - // For pushing data on the queue. - std::mutex pushMutex_; - std::condition_variable pushCondVar_; - - std::queue renderThreadQueue_; - - // For readbacks and other reasons we need to sync with the render thread. - std::mutex syncMutex_; - std::condition_variable syncCondVar_; + // Tasks for the render thread, which parks on renderThreadWork_. + MpscQueue renderThreadQueue_; + EventCounter renderThreadWork_; // Shader compilation thread to compile while emulating the rest of the frame. - // Only one right now but we could use more. - std::thread compileThread_; - // Sync - std::condition_variable compileCond_; - std::mutex compileQueueMutex_; - std::vector compileQueue_; - // Set while the compile thread turns a batch it took off compileQueue_ into tasks. - bool compileScheduling_ = false; - // Bumped after each batch is scheduled; WaitForPipelines() parks on it. - std::atomic compileProgress_{0}; - - // Thread for measuring presentation delay. - std::thread presentWaitThread_; + Thread compileThread_; + MpscQueue compileQueue_; + EventCounter compileWork_; + // Entries queued but not yet made into tasks. WaitForPipelines() parks on it. + retro_atomic_int_t compilePending_{ 0 }; // pipelines to check and possibly create at the end of the current render pass. std::vector pipelinesToCheck_; @@ -654,11 +650,13 @@ private: VKRPipelineLayout *curPipelineLayout_ = nullptr; - // Guards pipelineLayouts_, both the vector itself and the lifetime of the layouts in it. - // The list is added to by CreatePipelineLayout and erased from by the deferred callback queued by - // DestroyPipelineLayout, both of which run on the main thread, while the render thread walks it - // every frame in FlushDescriptors. Contention is negligible - layouts are only created and destroyed - // when a game starts or stops, and the lock is otherwise taken exactly twice per frame. - std::mutex pipelineLayoutsMutex_; - std::vector pipelineLayouts_; + // Copy-on-write list. The main thread replaces it (create, and the deferred destroy callback), + // the render thread reads it in FlushDescriptors inside layoutsGate_. + typedef std::vector LayoutList; + const LayoutList *Layouts() const { + return (const LayoutList *)retro_atomic_load_acquire_ptr(const_cast(&pipelineLayouts_)); + } + void PublishLayouts(LayoutList *list); + retro_atomic_ptr_t pipelineLayouts_{}; + ReaderGate layoutsGate_; }; diff --git a/Common/GPU/thin3d.cpp b/Common/GPU/thin3d.cpp index 224a26507f..f545e3cef8 100644 --- a/Common/GPU/thin3d.cpp +++ b/Common/GPU/thin3d.cpp @@ -139,21 +139,22 @@ int DataFormatNumChannels(DataFormat fmt) { } RefCountedObject::~RefCountedObject() { - const int rc = refcount_.load(); + const int rc = retro_atomic_load_acquire_int(&refcount_); _dbg_assert_msg_(rc == 0xDEDEDE, "Unexpected refcount %d in object of type '%s'", rc, name_); } bool RefCountedObject::Release() { - if (refcount_ > 0 && refcount_ < 10000) { - if (--refcount_ == 0) { + const int before = retro_atomic_fetch_sub_int(&refcount_, 1); + if (before > 0 && before < 10000) { + if (before == 1) { // Make it very obvious if we try to free this again. - refcount_ = 0xDEDEDE; + retro_atomic_store_relaxed_int(&refcount_, 0xDEDEDE); delete this; return true; } } else { // No point in printing the name here if the object has already been free-d, it'll be corrupt and dangerous to print. - _dbg_assert_msg_(false, "Refcount (%d) invalid for object %p - corrupt?", refcount_.load(), this); + _dbg_assert_msg_(false, "Refcount (%d) invalid for object %p - corrupt?", before, this); } return false; } diff --git a/Common/GPU/thin3d.h b/Common/GPU/thin3d.h index 75edf35f92..984f16ad0f 100644 --- a/Common/GPU/thin3d.h +++ b/Common/GPU/thin3d.h @@ -6,7 +6,7 @@ #pragma once -#include +#include #include #include #include @@ -360,18 +360,18 @@ protected: class RefCountedObject { public: explicit RefCountedObject(const char *name) : name_(name) { - refcount_ = 1; + retro_atomic_int_init(&refcount_, 1); } RefCountedObject(const RefCountedObject &other) = delete; RefCountedObject& operator=(RefCountedObject const&) = delete; virtual ~RefCountedObject(); - void AddRef() { refcount_++; } + void AddRef() { retro_atomic_fetch_add_int(&refcount_, 1); } bool Release(); bool ReleaseAssertLast(); private: - std::atomic refcount_; + retro_atomic_int_t refcount_; const char * const name_; }; diff --git a/Common/Log.cpp b/Common/Log.cpp index 183a1721f7..34e37e3aab 100644 --- a/Common/Log.cpp +++ b/Common/Log.cpp @@ -16,7 +16,7 @@ // http://code.google.com/p/dolphin-emu/ #include -#include +#include #include "ppsspp_config.h" @@ -39,9 +39,15 @@ static HWND g_dialogParent; static bool hitAnyAsserts = false; -static std::mutex g_extraAssertInfoMutex; -static std::string g_extraAssertInfo = "menu"; -static double g_assertInfoTime = 0.0; +// Rotating slots: the writer fills one, then publishes its index. A reader racing a +// writer several updates ahead may see a torn string; it's only assert decoration. +struct AssertInfoSlot { + char text[64]; + double time; +}; +static AssertInfoSlot g_extraAssertInfo[4] = { { "menu", 0.0 } }; +static retro_atomic_int_t g_extraAssertInfoIndex; +static retro_atomic_int_t g_extraAssertInfoNext; static bool g_exitOnAssert; static AssertNoCallbackFunc g_assertCancelCallback = 0; static void *g_assertCancelCallbackUserData = 0; @@ -73,9 +79,11 @@ void SetAssertDialogParent(void *handle) { } void SetExtraAssertInfo(const char *info) { - std::lock_guard guard(g_extraAssertInfoMutex); - g_extraAssertInfo = info ? info : "menu"; - g_assertInfoTime = time_now_d(); + const int idx = retro_atomic_fetch_add_int(&g_extraAssertInfoNext, 1) & 3; + AssertInfoSlot &slot = g_extraAssertInfo[idx]; + truncate_cpy(slot.text, sizeof(slot.text), info ? info : "menu"); + slot.time = time_now_d(); + retro_atomic_store_release_int(&g_extraAssertInfoIndex, idx); } void SetAssertCancelCallback(AssertNoCallbackFunc callback, void *userdata) { @@ -104,13 +112,16 @@ bool HandleAssert(bool isDebugAssert, const char *function, const char *file, in // Secondary formatting. Wonder if this can be combined into the vsnprintf somehow. char formatted[LOG_BUF_SIZE + 128]; { - std::lock_guard guard(g_extraAssertInfoMutex); - double delta = time_now_d() - g_assertInfoTime; + const AssertInfoSlot &slot = g_extraAssertInfo[retro_atomic_load_acquire_int(&g_extraAssertInfoIndex) & 3]; + char info[sizeof(slot.text)]; + memcpy(info, slot.text, sizeof(info)); + info[sizeof(info) - 1] = '\0'; + double delta = time_now_d() - slot.time; u32 debugCounters = 0; for (int i = 0; i < 8; i++) { debugCounters |= g_debugCounters[7 - i] << (i * 4); } - snprintf(formatted, sizeof(formatted), "(%s:%s:%d:%08x): [%s] (%s, %0.1fs) %s", file, function, line, debugCounters, expression, g_extraAssertInfo.c_str(), delta, text); + snprintf(formatted, sizeof(formatted), "(%s:%s:%d:%08x): [%s] (%s, %0.1fs) %s", file, function, line, debugCounters, expression, info, delta, text); } // Normal logging (will also log to Android log) diff --git a/Common/Log.h b/Common/Log.h index 198c765b6a..809be689d4 100644 --- a/Common/Log.h +++ b/Common/Log.h @@ -22,6 +22,8 @@ #include "ppsspp_config.h" #include "CommonFuncs.h" +#include + #define NOTICE_LEVEL 1 // VERY important information that is NOT errors. Like startup and debugprintfs from the game itself. #define ERROR_LEVEL 2 // Important errors. #define WARNING_LEVEL 3 // Something is suspicious. @@ -90,19 +92,44 @@ enum class LogLevel : int { LVERBOSE = VERBOSE_LEVEL, }; +// Level and enabled flag packed in one atomic word: any thread may log while +// another changes the settings. struct LogChannel { -#if defined(_DEBUG) - LogLevel level = LogLevel::LDEBUG; -#else - LogLevel level = LogLevel::LDEBUG; -#endif - bool enabled = true; + enum { DISABLED_BIT = 0x100, LEVEL_MASK = 0xFF }; + + LogChannel() { + retro_atomic_int_init(&state_, (int)LogLevel::LDEBUG); + } + + LogLevel Level() const { + return (LogLevel)(Load() & LEVEL_MASK); + } + bool Enabled() const { + return (Load() & DISABLED_BIT) == 0; + } + void SetLevel(LogLevel level) { + Store(level, Enabled()); + } + void SetEnabled(bool enabled) { + Store(Level(), enabled); + } + void Set(LogLevel level, bool enabled) { + Store(level, enabled); + } bool IsEnabled(LogLevel logLevel) const { - if (logLevel > level || !enabled) - return false; - return true; + const int s = Load(); + return (s & DISABLED_BIT) == 0 && (int)logLevel <= (s & LEVEL_MASK); } + +private: + int Load() const { + return retro_atomic_load_relaxed_int(const_cast(&state_)); + } + void Store(LogLevel level, bool enabled) { + retro_atomic_store_relaxed_int(&state_, ((int)level & LEVEL_MASK) | (enabled ? 0 : DISABLED_BIT)); + } + retro_atomic_int_t state_; }; extern bool *g_bLogEnabledSetting; diff --git a/Common/Log/LogManager.cpp b/Common/Log/LogManager.cpp index 0a574afb96..6821a4b2a1 100644 --- a/Common/Log/LogManager.cpp +++ b/Common/Log/LogManager.cpp @@ -43,7 +43,7 @@ LogChannel g_log[(size_t)Log::NUMBER_OF_LOGS]; LogManager g_logManager; -const char *hleCurrentThreadName = nullptr; +thread_local const char *hleCurrentThreadName = nullptr; bool g_bDummySetting = true; bool *g_bLogEnabledSetting = &g_bDummySetting; @@ -136,8 +136,7 @@ void LogManager::Init(bool *enabledSetting, bool headless) { _dbg_assert_(ARRAY_SIZE(g_logTypeNames) == ARRAY_SIZE(g_log)); for (size_t i = 0; i < ARRAY_SIZE(g_log); i++) { - g_log[i].enabled = true; - g_log[i].level = LogLevel::LINFO; + g_log[i].Set(LogLevel::LINFO, true); } } @@ -147,21 +146,19 @@ void LogManager::Shutdown() { return; } - { - std::lock_guard lk(logFileLock_); - if (fp_) { - fclose(fp_); - fp_ = nullptr; - } - } - - outputs_ = (LogOutput)0; + retro_atomic_store_release_int(&outputs_, 0); + CloseLogFile(); ringLog_.Clear(); initialized_ = false; } LogManager::LogManager() { + retro_atomic_int_init(&outputs_, 0); + retro_atomic_ptr_init(&fp_, nullptr); + retro_atomic_int_init(&externalCount_, 0); + for (int i = 0; i < MAX_EXTERNAL_CALLBACKS; i++) + retro_atomic_ptr_init(&externalCallbacks_[i], nullptr); #if PPSSPP_PLATFORM(IOS) || PPSSPP_PLATFORM(UWP) || PPSSPP_PLATFORM(SWITCH) stdioUseColor_ = false; #elif defined(_MSC_VER) @@ -176,38 +173,45 @@ LogManager::LogManager() { #if PPSSPP_PLATFORM(WINDOWS) if (IsDebuggerPresent()) { - outputs_ |= LogOutput::DebugString; + retro_atomic_fetch_or_int(&outputs_, (int)LogOutput::DebugString); } #endif } LogManager::~LogManager() { Shutdown(); + for (int i = 0; i < MAX_EXTERNAL_CALLBACKS; i++) + delete (ExternalCallbackEntry *)retro_atomic_exchange_ptr(&externalCallbacks_[i], nullptr); +} +void LogManager::CloseLogFile() { + FILE *fp = (FILE *)retro_atomic_exchange_ptr(&fp_, nullptr); + if (fp) { + // Loggers that loaded fp before the exchange are inside the gate. + gate_.Drain(); + fclose(fp); + } } void LogManager::SetFileLogPath(const Path &filename) { - std::lock_guard lk(logFileLock_); - if (fp_ && filename == logFilename_) { + if (retro_atomic_load_acquire_ptr(&fp_) && filename == logFilename_) { // All good return; } - if (fp_) { - fclose(fp_); - fp_ = nullptr; - } + CloseLogFile(); if (!filename.empty()) { logFilename_ = Path(filename); - if (outputs_ & LogOutput::File) { + if (GetOutputsEnabled() & LogOutput::File) { File::CreateFullPath(logFilename_.NavigateUp()); - fp_ = File::OpenCFile(logFilename_, "at"); - logFileOpenFailed_ = fp_ == nullptr; + FILE *fp = File::OpenCFile(logFilename_, "at"); + logFileOpenFailed_ = fp == nullptr; if (logFileOpenFailed_) { printf("Failed to open log file %s\n", logFilename_.c_str()); } + retro_atomic_store_release_ptr(&fp_, fp); } } } @@ -219,8 +223,8 @@ void LogManager::SaveConfig(Section *section) { return; } for (int i = 0; i < (int)Log::NUMBER_OF_LOGS; i++) { - section->Set((std::string(g_logTypeNames[i]) + "Enabled"), g_log[i].enabled); - section->Set((std::string(g_logTypeNames[i]) + "Level"), (int)g_log[i].level); + section->Set((std::string(g_logTypeNames[i]) + "Enabled"), g_log[i].Enabled()); + section->Set((std::string(g_logTypeNames[i]) + "Level"), (int)g_log[i].Level()); } } @@ -231,13 +235,12 @@ void LogManager::LoadConfig(const Section *section) { int level = (int)LogLevel::LERROR; section->Get((std::string(g_logTypeNames[i]) + "Enabled"), &enabled); section->Get((std::string(g_logTypeNames[i]) + "Level"), &level); - g_log[i].enabled = enabled; - g_log[i].level = (LogLevel)level; + g_log[i].Set((LogLevel)level, enabled); } } void LogManager::SetOutputsEnabled(LogOutput outputs) { - outputs_ = outputs; + retro_atomic_store_release_int(&outputs_, (int)outputs); if (outputs & LogOutput::File) { SetFileLogPath(logFilename_); } @@ -246,8 +249,8 @@ void LogManager::SetOutputsEnabled(LogOutput outputs) { void LogManager::LogLine(LogLevel level, Log type, const char *file, int line, const char *format, va_list args) { char msgBuf[1024]; - const LogChannel &log = g_log[(size_t)type]; - if (level > log.level || !log.enabled || outputs_ == (LogOutput)0) { + const LogOutput outputs = GetOutputsEnabled(); + if (!g_log[(size_t)type].IsEnabled(level) || outputs == (LogOutput)0) { // If we get here, it should have been caught earlier. return; } @@ -311,25 +314,37 @@ void LogManager::LogLine(LogLevel level, Log type, const char *file, int line, c message.msg[neededBytes] = '\n'; va_end(args_copy); - if (outputs_ & LogOutput::Stdio) { - // This has its own mutex. + if (outputs & LogOutput::Stdio) { StdioLog(message); } // OK, now go through the possible listeners in order. - if (outputs_ & LogOutput::File) { - // Lock covers the fp_ check too - SetFileLogPath()/Shutdown() can close it concurrently. - std::lock_guard lk(logFileLock_); - if (fp_) { - fprintf(fp_, "%s %s %s", message.timestamp, message.header, message.msg.c_str()); + const bool wantFile = (outputs & LogOutput::File) != 0; + const bool wantExternal = (outputs & LogOutput::ExternalCallback) != 0; + if (wantFile || wantExternal) { + gate_.Enter(); + FILE *fp = wantFile ? (FILE *)retro_atomic_load_acquire_ptr(&fp_) : nullptr; + if (fp) { + // One write per line so concurrent loggers don't interleave mid-line. + std::string line; + line.reserve(strlen(message.timestamp) + strlen(message.header) + message.msg.size() + 2); + line.append(message.timestamp).append(" ").append(message.header).append(" ").append(message.msg); + fwrite(line.data(), 1, line.size(), fp); // Is this really necessary to do every time? I guess to catch the last message before a crash.. - fflush(fp_); + fflush(fp); } + if (wantExternal) { + for (int i = 0; i < MAX_EXTERNAL_CALLBACKS; i++) { + const ExternalCallbackEntry *entry = (const ExternalCallbackEntry *)retro_atomic_load_acquire_ptr(&externalCallbacks_[i]); + if (entry) + entry->callback(message, entry->userdata); + } + } + gate_.Exit(); } #if PPSSPP_PLATFORM(WINDOWS) - if (outputs_ & LogOutput::DebugString) { - // No mutex needed + if (outputs & LogOutput::DebugString) { char buffer[4096]; // We omit the timestamp for easy copy-paste-diffing. snprintf(buffer, sizeof(buffer), "%s %s", message.header, message.msg.c_str()); @@ -337,61 +352,115 @@ void LogManager::LogLine(LogLevel level, Log type, const char *file, int line, c } #endif - if (outputs_ & LogOutput::RingBuffer) { + if (outputs & LogOutput::RingBuffer) { ringLog_.Log(message); } - if (outputs_ & LogOutput::Printf) { + if (outputs & LogOutput::Printf) { PrintfLog(message); } - - - if (outputs_ & LogOutput::ExternalCallback) { - // Held across the dispatch on purpose: RemoveExternalLogCallback() takes the same lock, so a - // listener can't be torn down (and its userdata freed) while we're in the middle of calling it. - std::lock_guard guard(externalLock_); - for (const ExternalCallbackEntry &entry : externalCallbacks_) { - entry.callback(message, entry.userdata); - } - } } int LogManager::AddExternalLogCallback(LogCallback callback, void *userdata) { if (!callback) { return -1; } - std::lock_guard guard(externalLock_); - const int handle = nextExternalHandle_++; - externalCallbacks_.push_back(ExternalCallbackEntry{ handle, callback, userdata }); - EnableOutput(LogOutput::ExternalCallback); - return handle; + ExternalCallbackEntry *entry = new ExternalCallbackEntry{ callback, userdata }; + for (int i = 0; i < MAX_EXTERNAL_CALLBACKS; i++) { + if (retro_atomic_cas_ptr(&externalCallbacks_[i], nullptr, entry)) { + retro_atomic_fetch_add_int(&externalCount_, 1); + EnableOutput(LogOutput::ExternalCallback); + return i; + } + } + delete entry; + return -1; } void LogManager::RemoveExternalLogCallback(int handle) { - if (handle < 0) { + if (handle < 0 || handle >= MAX_EXTERNAL_CALLBACKS) { return; } - std::lock_guard guard(externalLock_); - for (size_t i = 0; i < externalCallbacks_.size(); i++) { - if (externalCallbacks_[i].handle == handle) { - externalCallbacks_.erase(externalCallbacks_.begin() + i); - break; - } - } + ExternalCallbackEntry *entry = (ExternalCallbackEntry *)retro_atomic_exchange_ptr(&externalCallbacks_[handle], nullptr); + if (!entry) + return; // Only when the last one goes away - otherwise removing one connection's callback would stop - // delivery to the ones still attached, which is the bug this list exists to avoid. - if (externalCallbacks_.empty()) { + // delivery to the ones still attached. + if (retro_atomic_fetch_sub_int(&externalCount_, 1) == 1) { DisableOutput(LogOutput::ExternalCallback); } + // Anyone still calling it is inside the gate; after this, userdata may be freed. + gate_.Drain(); + delete entry; +} + +RingbufferLog::RingbufferLog() { + retro_atomic_int_init(&head_, 0); + retro_atomic_int_init(&count_, 0); + for (int i = 0; i < MAX_LOGS; i++) { + retro_atomic_int_init(&slots_[i].seq, 0); + retro_atomic_int_init(&slots_[i].level, 0); + slots_[i].text[0] = '\0'; + } } void RingbufferLog::Log(const LogMessage &message) { - std::lock_guard lock(ringLock_); - messages_[curMessage_] = message; - curMessage_++; - if (curMessage_ >= MAX_LOGS) - curMessage_ -= MAX_LOGS; - count_++; + const int idx = retro_atomic_fetch_add_int(&head_, 1) & (MAX_LOGS - 1); + Slot &slot = slots_[idx]; + const int seq = retro_atomic_load_relaxed_int(&slot.seq); + // Odd: another writer lapped the ring onto this slot. Drop ours. + if ((seq & 1) || !retro_atomic_cas_int(&slot.seq, seq, seq + 1)) + return; + retro_atomic_thread_fence_seq_cst(); + const size_t len = std::min(message.msg.size(), (size_t)MAX_TEXT - 1); + memcpy(slot.text, message.msg.data(), len); + slot.text[len] = '\0'; + retro_atomic_store_relaxed_int(&slot.level, (int)message.level); + retro_atomic_store_release_int(&slot.seq, seq + 2); + if (retro_atomic_load_relaxed_int(&count_) < MAX_LOGS) + retro_atomic_fetch_add_int(&count_, 1); +} + +int RingbufferLog::GetCount() const { + const int count = retro_atomic_load_acquire_int(const_cast(&count_)); + return count < MAX_LOGS ? count : MAX_LOGS; +} + +bool RingbufferLog::Read(int i, std::string *text, LogLevel *level) const { + const int head = retro_atomic_load_acquire_int(const_cast(&head_)); + const Slot &slot = slots_[(head - i - 1) & (MAX_LOGS - 1)]; + retro_atomic_int_t *seqp = const_cast(&slot.seq); + const int seq = retro_atomic_load_acquire_int(seqp); + if (seq & 1) + return false; + char buf[MAX_TEXT]; + memcpy(buf, slot.text, sizeof(buf)); + buf[MAX_TEXT - 1] = '\0'; + const int lvl = retro_atomic_load_relaxed_int(const_cast(&slot.level)); + retro_atomic_thread_fence_seq_cst(); + if (retro_atomic_load_relaxed_int(seqp) != seq) + return false; + if (text) + *text = buf; + if (level) + *level = (LogLevel)lvl; + return true; +} + +std::string RingbufferLog::TextAt(int i) const { + std::string text; + Read(i, &text, nullptr); + return text; +} + +LogLevel RingbufferLog::LevelAt(int i) const { + LogLevel level = LogLevel::LINFO; + Read(i, nullptr, &level); + return level; +} + +void RingbufferLog::Clear() { + retro_atomic_store_release_int(&count_, 0); } #ifdef _WIN32 @@ -492,7 +561,7 @@ void LogManager::StdioLog(const LogMessage &message) { } } - std::lock_guard lock(stdioLock_); + // One call per line; stdio keeps it whole. fprintf(stderr, "%s%s%s", colorAttr, text, resetAttr); #endif } diff --git a/Common/Log/LogManager.h b/Common/Log/LogManager.h index 5390edbd2f..8835fc2f4b 100644 --- a/Common/Log/LogManager.h +++ b/Common/Log/LogManager.h @@ -19,7 +19,6 @@ #include "ppsspp_config.h" -#include #include #include @@ -27,10 +26,12 @@ #include "Common/CommonFuncs.h" #include "Common/Log.h" #include "Common/File/Path.h" +#include "Common/Thread/ParkingLot.h" #define MAX_MESSAGES 8000 -extern const char *hleCurrentThreadName; +// The running PSP thread, for log headers. Thread local: only the thread running the PSP sets it. +extern thread_local const char *hleCurrentThreadName; // Struct that listeners can output how they want. For example, on Android we don't want to add // timestamp or write the level as a string, those already exist. @@ -52,34 +53,33 @@ enum class LogOutput { }; ENUM_CLASS_BITOPS(LogOutput); +// Lock-free ring of recent messages. Writers claim a slot with fetch_add and +// guard it with a per-slot sequence (odd while being written). Readers that +// catch a slot mid-write get an empty string instead of waiting. class RingbufferLog { public: + RingbufferLog(); void Log(const LogMessage &msg); - int GetCount() const { return count_ < MAX_LOGS ? count_ : MAX_LOGS; } - std::string TextAt(int i) const { - std::lock_guard lock(ringLock_); - return messages_[(curMessage_ - i - 1) & (MAX_LOGS - 1)].msg; - } - LogLevel LevelAt(int i) const { - std::lock_guard lock(ringLock_); - return messages_[(curMessage_ - i - 1) & (MAX_LOGS - 1)].level; - } - - void Clear() { - std::lock_guard lock(ringLock_); - curMessage_ = 0; - count_ = 0; - } + int GetCount() const; + std::string TextAt(int i) const; + LogLevel LevelAt(int i) const; + void Clear(); private: - enum { MAX_LOGS = 256 }; - LogMessage messages_[MAX_LOGS]; - int curMessage_ = 0; - int count_ = 0; - mutable std::mutex ringLock_; + enum { MAX_LOGS = 256, MAX_TEXT = 512 }; + struct Slot { + retro_atomic_int_t seq; + retro_atomic_int_t level; + char text[MAX_TEXT]; + }; + bool Read(int i, std::string *text, LogLevel *level) const; + Slot slots_[MAX_LOGS]; + retro_atomic_int_t head_; + retro_atomic_int_t count_; }; class Section; + typedef void (*LogCallback)(const LogMessage &message, void *userdata); extern bool *g_bLogEnabledSetting; @@ -90,15 +90,15 @@ public: void SetOutputsEnabled(LogOutput outputs); LogOutput GetOutputsEnabled() const { - return outputs_; + return (LogOutput)retro_atomic_load_relaxed_int(const_cast(&outputs_)); } void EnableOutput(LogOutput output) { - SetOutputsEnabled(outputs_ | output); + retro_atomic_fetch_or_int(&outputs_, (int)output); + if (output & LogOutput::File) + SetFileLogPath(logFilename_); } void DisableOutput(LogOutput output) { - LogOutput temp = outputs_; - temp &= ~output; - SetOutputsEnabled(temp); + retro_atomic_fetch_and_int(&outputs_, ~(int)output); } void EnableOutput(LogOutput output, bool enabled) { if (enabled) { @@ -119,30 +119,29 @@ public: } void SetLogLevel(Log type, LogLevel level) { - g_log[(size_t)type].level = level; + g_log[(size_t)type].SetLevel(level); } void SetAllLogLevels(LogLevel level) { for (int i = 0; i < (int)Log::NUMBER_OF_LOGS; ++i) { - g_log[i].level = level; + g_log[i].SetLevel(level); } } void SetAllLogEnable(bool enable) { for (int i = 0; i < (int)Log::NUMBER_OF_LOGS; ++i) { - g_log[i].enabled = enable; + g_log[i].SetEnabled(enable); } } void SetEnabled(Log type, bool enable) { - g_log[(size_t)type].enabled = enable; + g_log[(size_t)type].SetEnabled(enable); } LogLevel GetLogLevel(Log type) { - return g_log[(size_t)type].level; + return g_log[(size_t)type].Level(); } - const RingbufferLog &GetRingbuffer() const { return ringLog_; } @@ -157,6 +156,8 @@ public: // first, and then the first one to disconnect cleared the slot and stopped delivery to the // other as well. // Returns a handle to hand back to RemoveExternalLogCallback(), or -1 if it wasn't added. + // Add/Remove/SetFileLogPath/Shutdown are control-side calls: one thread at a time. + // Logging itself may happen from any thread, concurrently with them. int AddExternalLogCallback(LogCallback callback, void *userdata); void RemoveExternalLogCallback(int handle); @@ -195,29 +196,30 @@ private: // Stdio logging void StdioLog(const LogMessage &message); - std::mutex stdioLock_; bool stdioUseColor_ = true; - LogOutput outputs_ = (LogOutput)0; + retro_atomic_int_t outputs_; - // File logging - std::mutex logFileLock_; - FILE *fp_ = nullptr; + // File logging. fp_ is published to loggers, closed only after a drain. + void CloseLogFile(); + retro_atomic_ptr_t fp_; bool logFileOpenFailed_ = false; Path logFilename_; // Ring buffer RingbufferLog ringLog_; - // Callbacks + // Callbacks: published entries, freed only after a drain. struct ExternalCallbackEntry { - int handle; LogCallback callback; void *userdata; }; - std::mutex externalLock_; - std::vector externalCallbacks_; - int nextExternalHandle_ = 1; + enum { MAX_EXTERNAL_CALLBACKS = 8 }; + retro_atomic_ptr_t externalCallbacks_[MAX_EXTERNAL_CALLBACKS]; + retro_atomic_int_t externalCount_; + + // Loggers hold this while touching fp_ or a callback entry. + ReaderGate gate_; }; extern LogManager g_logManager; diff --git a/Common/LogReporting.cpp b/Common/LogReporting.cpp index 24ac63be66..0411be8766 100644 --- a/Common/LogReporting.cpp +++ b/Common/LogReporting.cpp @@ -17,39 +17,61 @@ #include #include -#include -#include +#include + +#include + #include "Common/LogReporting.h" namespace Reporting { // Keeps track of report-only-once identifiers. Since they're always constants, a pointer is okay. -static std::unordered_map logNTimes; -static std::mutex logNTimesLock; +// Fixed open-addressed table: slots are claimed with CAS and never freed. +enum { LOG_N_TIMES_SLOTS = 1024 }; +struct LogNTimesSlot { + retro_atomic_ptr_t key; + retro_atomic_int_t count; +}; +static LogNTimesSlot logNTimes[LOG_N_TIMES_SLOTS]; AllowedCallback allowedCallback = nullptr; MessageCallback messageCallback = nullptr; -bool ShouldLogNTimes(const char *identifier, int count) { - // True if it wasn't there already -> so yes, log. - std::lock_guard lock(logNTimesLock); - auto iter = logNTimes.find(identifier); - if (iter == logNTimes.end()) { - logNTimes.emplace(identifier, 1); - return true; - } else { - if (iter->second >= count) { - return false; - } else { - iter->second++; - return true; +static LogNTimesSlot *FindSlot(const char *identifier) { + uintptr_t h = (uintptr_t)identifier; + h ^= h >> 15; + h *= (uintptr_t)0x9E3779B97F4A7C15ULL; + for (int probe = 0; probe < LOG_N_TIMES_SLOTS; probe++) { + LogNTimesSlot &slot = logNTimes[(h + probe) & (LOG_N_TIMES_SLOTS - 1)]; + void *key = retro_atomic_load_acquire_ptr(&slot.key); + if (key == identifier) + return &slot; + if (!key) { + if (retro_atomic_cas_ptr(&slot.key, nullptr, (void *)identifier)) + return &slot; + if (retro_atomic_load_acquire_ptr(&slot.key) == identifier) + return &slot; } } + return nullptr; +} + +bool ShouldLogNTimes(const char *identifier, int count) { + LogNTimesSlot *slot = FindSlot(identifier); + if (!slot) + return false; + for (;;) { + const int c = retro_atomic_load_relaxed_int(&slot->count); + if (c >= count) + return false; + if (retro_atomic_cas_int(&slot->count, c, c + 1)) + return true; + } } void ResetCounts() { - std::lock_guard lock(logNTimesLock); - logNTimes.clear(); + for (int i = 0; i < LOG_N_TIMES_SLOTS; i++) + retro_atomic_store_relaxed_int(&logNTimes[i].count, 0); } void SetupCallbacks(AllowedCallback allowed, MessageCallback message) { diff --git a/Common/Net/Cancel.cpp b/Common/Net/Cancel.cpp index 90fd1db7b7..3ac9f53116 100644 --- a/Common/Net/Cancel.cpp +++ b/Common/Net/Cancel.cpp @@ -58,7 +58,7 @@ void WakeSocket::Drain() { } void CancelToken::Cancel() { - cancelled_.store(true, std::memory_order_release); + retro_atomic_store_release_int(&cancelled_, 1); // Never drained, so the socket stays readable from here on. wake_.Wake(); } diff --git a/Common/Net/Cancel.h b/Common/Net/Cancel.h index c7f71f64c6..88f04f3937 100644 --- a/Common/Net/Cancel.h +++ b/Common/Net/Cancel.h @@ -1,8 +1,9 @@ #pragma once -#include #include +#include + namespace net { // A socket that another thread can make readable, to wake a select() that @@ -31,11 +32,11 @@ class CancelToken { public: // Any thread. void Cancel(); - bool IsCancelled() const { return cancelled_.load(std::memory_order_acquire); } + bool IsCancelled() const { return retro_atomic_load_acquire_int(const_cast(&cancelled_)) != 0; } intptr_t WakeFd() const { return wake_.Fd(); } private: - std::atomic cancelled_{ false }; + retro_atomic_int_t cancelled_{ 0 }; WakeSocket wake_; }; diff --git a/Common/Net/HTTPClient.cpp b/Common/Net/HTTPClient.cpp index 7befadbdca..602099f142 100644 --- a/Common/Net/HTTPClient.cpp +++ b/Common/Net/HTTPClient.cpp @@ -21,6 +21,7 @@ #include "Common/Data/Encoding/Compression.h" #include "Common/Net/NetBuffer.h" #include "Common/Log.h" +#include "Common/Thread/Thread.h" namespace net { @@ -511,7 +512,7 @@ HTTPRequest::~HTTPRequest() { } void HTTPRequest::Start() { - thread_ = std::thread([this] { Do(); }); + thread_ = Thread([this] { Do(); }); } void HTTPRequest::Join() { @@ -527,7 +528,7 @@ void HTTPRequest::SetFailed(int code) { failed_ = true; progress_.Update(0, 0, true); - completed_ = true; + retro_atomic_store_release_int(&completed_, 1); } int HTTPRequest::Perform(const std::string &url) { @@ -625,7 +626,7 @@ void HTTPRequest::Do() { // Set this last to ensure no race conditions when checking Done. Users must always check // Done before looking at the result code. - completed_ = true; + retro_atomic_store_release_int(&completed_, 1); } std::string RemoveHttpsIfNeeded(std::string_view url) { diff --git a/Common/Net/HTTPClient.h b/Common/Net/HTTPClient.h index a7b0d612f8..46abbf48c9 100644 --- a/Common/Net/HTTPClient.h +++ b/Common/Net/HTTPClient.h @@ -1,9 +1,7 @@ #pragma once -#include #include #include -#include #include #include #include @@ -12,6 +10,7 @@ #include "Common/Net/NetBuffer.h" #include "Common/Net/Resolve.h" #include "Common/Net/HTTPRequest.h" +#include "Common/Thread/Thread.h" namespace net { @@ -113,7 +112,7 @@ public: void Start() override; void Join() override; - bool Done() override { return completed_; } + bool Done() override { return retro_atomic_load_acquire_int(&completed_) != 0; } bool Failed() const override { return failed_; } private: @@ -123,11 +122,11 @@ private: void SetFailed(int code); std::string postData_; - std::thread thread_; + Thread thread_; std::string postMime_; - // Set last on the download thread, after the result code and buffer. Atomic, so that once + // Set last on the download thread, after the result code and buffer (release), so once // Done() sees it, those are visible too. - std::atomic completed_{false}; + retro_atomic_int_t completed_{ 0 }; bool failed_ = false; net::ResolveFunc customResolve_; }; diff --git a/Common/Net/HTTPNaettRequest.cpp b/Common/Net/HTTPNaettRequest.cpp index e3f690c321..c39ca7ae7d 100644 --- a/Common/Net/HTTPNaettRequest.cpp +++ b/Common/Net/HTTPNaettRequest.cpp @@ -1,10 +1,11 @@ #ifndef HTTPS_NOT_AVAILABLE -#include #include #include #include +#include + #include "Common/Net/HTTPRequest.h" #include "Common/Net/HTTPNaettRequest.h" #include "Common/Thread/ThreadUtil.h" @@ -34,7 +35,7 @@ struct NaettBodySink { // naett reads the POST body in place while uploading, so it lives here too. std::string postData; // Written by us, read by the transfer thread. - std::atomic cancelled{false}; + retro_atomic_int_t cancelled{ 0 }; }; // Sinks belonging to requests that hadn't finished when they were torn down, which only happens @@ -54,7 +55,7 @@ void HTTPSShutdown() { int HTTPSRequest::WriteBodyThunk(const void *source, int bytes, void *userData) { NaettBodySink *sink = (NaettBodySink *)userData; - if (sink->cancelled) { + if (retro_atomic_load_acquire_int(&sink->cancelled)) { // Taking less than we were given fails the request, which is how naett lets us stop a // transfer. Without this, cancelling only relabelled the result once it finished anyway. return 0; @@ -70,7 +71,7 @@ int HTTPSRequest::WriteBodyThunk(const void *source, int bytes, void *userData) void HTTPSRequest::Cancel() { Request::Cancel(); if (sink_) { - sink_->cancelled = true; + retro_atomic_store_release_int(&sink_->cancelled, 1); } } @@ -82,7 +83,7 @@ void HTTPSRequest::Start() { // it once it finishes. sink_ = std::make_unique(); // In case someone managed to cancel us between construction and here. - sink_->cancelled = IsCancelled(); + retro_atomic_store_release_int(&sink_->cancelled, IsCancelled() ? 1 : 0); sink_->postData = postData_; std::vector options; @@ -152,7 +153,7 @@ void HTTPSRequest::Join() { // that still exists. The process is on its way out; this is the last word on it. WARN_LOG(Log::HTTP, "Abandoning an unfinished request to '%s' - shutting down", url_.c_str()); if (sink_) { - sink_->cancelled = true; + retro_atomic_store_release_int(&sink_->cancelled, 1); if (!g_abandonedSinks) { g_abandonedSinks = new std::vector>(); } diff --git a/Common/Net/HTTPNaettRequest.h b/Common/Net/HTTPNaettRequest.h index 37ade8521a..e455fb7def 100644 --- a/Common/Net/HTTPNaettRequest.h +++ b/Common/Net/HTTPNaettRequest.h @@ -2,7 +2,6 @@ #include #include -#include #include "Common/Net/HTTPRequest.h" diff --git a/Common/Net/HTTPRequest.h b/Common/Net/HTTPRequest.h index 3485431e7f..ccdd09fcf5 100644 --- a/Common/Net/HTTPRequest.h +++ b/Common/Net/HTTPRequest.h @@ -62,9 +62,6 @@ public: virtual bool HasRunCallback() { return Done() && hasRunCallback_; } - // Returns 1.0 when done. That one value can be compared exactly - or just use Done(). - float Progress() const { return progress_.progress; } - float SpeedKBps() const { return progress_.kBps; } std::string url() const { return url_; } const Path &OutFile() const { return outfile_; } diff --git a/Common/Net/HTTPServer.cpp b/Common/Net/HTTPServer.cpp index 04db0acf9c..13cc23a063 100644 --- a/Common/Net/HTTPServer.cpp +++ b/Common/Net/HTTPServer.cpp @@ -32,8 +32,8 @@ #include #include -#include +#include "Common/Net/Cancel.h" #include "Common/Net/HTTPServer.h" #include "Common/Net/NetBuffer.h" #include "Common/Net/Sinks.h" @@ -45,24 +45,23 @@ void NewThreadExecutor::Run(std::function func) { // Every connection gets a thread, and we only ever joined them at shutdown - so a server that - // had served N connections was still holding N joinable std::threads. Reap the finished ones. + // had served N connections was still holding N joinable threads. Reap the finished ones. Prune(); - auto done = std::make_shared>(false); - Worker worker; - worker.done = done; - worker.thread = std::thread([func, done]() { + std::unique_ptr worker(new Worker()); + Worker *w = worker.get(); + w->thread = Thread([func, w]() { func(); - done->store(true, std::memory_order_release); + retro_atomic_store_release_int(&w->done, 1); }); workers_.push_back(std::move(worker)); } void NewThreadExecutor::Prune() { for (size_t i = 0; i < workers_.size(); ) { - if (workers_[i].done->load(std::memory_order_acquire)) { + if (retro_atomic_load_acquire_int(&workers_[i]->done)) { // Set right at the end of the thread body, so this join returns essentially at once. - workers_[i].thread.join(); + workers_[i]->thread.join(); workers_.erase(workers_.begin() + i); } else { ++i; @@ -72,8 +71,9 @@ void NewThreadExecutor::Prune() { NewThreadExecutor::~NewThreadExecutor() { // If Run was ever called... - for (auto &worker : workers_) - worker.thread.join(); + for (auto &worker : workers_) { + worker->thread.join(); + } workers_.clear(); } @@ -306,15 +306,12 @@ bool Server::Listen6(int port, bool ipv6_only, const char *reason) { #endif } -bool Server::RunSlice(double timeout) { +bool Server::RunSlice(const net::CancelToken *cancel) { if (listenerSock_ < 0 || port_ == 0) { return false; } - if (timeout <= 0.0) { - timeout = 86400.0; - } - if (!fd_util::WaitUntilReady(listenerSock_, timeout, false)) { + if (net::WaitSocket((uintptr_t)listenerSock_, false, -1.0, cancel) != net::WaitResult::READY) { return false; } @@ -343,7 +340,7 @@ bool Server::Run(int port) { } while (true) { - RunSlice(0.0); + RunSlice(nullptr); } // We'll never get here. Ever. diff --git a/Common/Net/HTTPServer.h b/Common/Net/HTTPServer.h index b6d1be7fc9..e65bc91a4d 100644 --- a/Common/Net/HTTPServer.h +++ b/Common/Net/HTTPServer.h @@ -1,38 +1,38 @@ #pragma once -#include #include #include #include -#include #include +#include + #include "Common/Net/HTTPHeaders.h" #include "Common/Net/Resolve.h" +#include "Common/Thread/Thread.h" +// One thread per request. Run() and the destructor belong to one thread (the accept loop's); +// the destructor joins every worker, so handlers must finish on their own or be woken first. class NewThreadExecutor { public: ~NewThreadExecutor(); void Run(std::function func); private: - // Reap threads that have finished. Only called from the thread that calls Run(). + // Joins the workers that have finished. void Prune(); struct Worker { - std::thread thread; - // Set by the worker as its last act, read by whoever calls Run() next. - // It does look a bit fragile with the atomic inside a shared_ptr, but the way - // this is used, it should be fine. The atomic is only used to signal that the thread - // is done, and the shared_ptr is only used to keep the atomic alive until - // the thread is joined. - std::shared_ptr> done; + Thread thread; + // The worker's last act. + retro_atomic_int_t done{ 0 }; }; - std::vector workers_; + std::vector> workers_; }; namespace net { +class CancelToken; class InputSink; class OutputSink; @@ -101,9 +101,9 @@ public: // better put this on a thread. Returns false if failed to start serving, never // returns if successful. bool Run(int port); - // May run for (significantly) longer than timeout, but won't wait longer than that - // for a new connection to handle. - bool RunSlice(double timeout); + // Blocks until a connection arrives and hands it to the executor, or until cancel is set. + // Returns false when cancelled or on error. + bool RunSlice(const net::CancelToken *cancel); bool Listen(int port, const char *reason, net::DNSType type = net::DNSType::ANY); void Stop(); diff --git a/Common/Net/WebsocketServer.cpp b/Common/Net/WebsocketServer.cpp index 0a9323feb0..7ae558de6c 100644 --- a/Common/Net/WebsocketServer.cpp +++ b/Common/Net/WebsocketServer.cpp @@ -226,7 +226,7 @@ void WebSocketServer::CloseAbnormally() { outBufOffset_ = 0; } -bool WebSocketServer::Process(float timeout) { +bool WebSocketServer::Process(const intptr_t *wakeFds, int wakeCount) { if (!open_) { return false; } @@ -248,16 +248,22 @@ bool WebSocketServer::Process(float timeout) { return false; } - struct timeval tv; - tv.tv_sec = floor(timeout); - tv.tv_usec = (timeout - floor(timeout)) * 1000000.0; - fd_set read; FD_ZERO(&read); // In case we closed due to protocol error, don't even try to read. if (!sentClose_) { FD_SET(fd_, &read); } + intptr_t maxfd = (intptr_t)fd_; + for (int i = 0; i < wakeCount; ++i) { + if (wakeFds[i] == -1) { + continue; + } + FD_SET((size_t)wakeFds[i], &read); + if (wakeFds[i] > maxfd) { + maxfd = wakeFds[i]; + } + } fd_set write; FD_ZERO(&write); @@ -266,7 +272,7 @@ bool WebSocketServer::Process(float timeout) { } // First argument to select is the highest socket in the set + 1. - int rval = select((int)fd_ + 1, &read, &write, nullptr, &tv); + int rval = select((int)maxfd + 1, &read, &write, nullptr, nullptr); if (rval < 0) { const int err = socket_errno; #if !PPSSPP_PLATFORM(WINDOWS) @@ -276,17 +282,12 @@ bool WebSocketServer::Process(float timeout) { } #endif // Anything else isn't going to fix itself (a bad fd, say), and returning true on a call - // that fails immediately means the caller busy-loops instead of being paced by the timeout. + // that fails immediately would make the caller busy-loop. ERROR_LOG(Log::IO, "WebSocket select() failed: %d - closing connection", err); CloseAbnormally(); return false; } - if (rval == 0) { - // Timed out. - return true; - } - if (FD_ISSET(fd_, &write)) { SendFlush(); } diff --git a/Common/Net/WebsocketServer.h b/Common/Net/WebsocketServer.h index 398fa32dff..3cd316fb1f 100644 --- a/Common/Net/WebsocketServer.h +++ b/Common/Net/WebsocketServer.h @@ -43,8 +43,10 @@ public: void Pong(const std::vector &payload = {}); void Close(WebSocketClose reason = WebSocketClose::GOING_AWAY); - // Note: may interrupt early. Call in a loop. - bool Process(float timeout = -1.0f); + // Blocks until the socket makes progress or one of wakeFds (any may be -1) becomes readable, + // then returns whether the connection is still open. Call in a loop; the caller drains its + // own wake fds. + bool Process(const intptr_t *wakeFds = nullptr, int wakeCount = 0); void SetTextHandler(std::function func) { text_ = func; diff --git a/Common/Profiler/Profiler.cpp b/Common/Profiler/Profiler.cpp index 0b41fa9c3a..0411ba2b9f 100644 --- a/Common/Profiler/Profiler.cpp +++ b/Common/Profiler/Profiler.cpp @@ -1,9 +1,10 @@ // Ultra-lightweight category profiler with history. -#include #include #include +#include + #include "ppsspp_config.h" #include "Common/Render/DrawBuffer.h" @@ -27,8 +28,9 @@ #define UNIFIED_CONST_STR #endif +// Claimed with a CAS by the first thread to enter that category. struct Category { - const char *name; + retro_atomic_ptr_t name; }; struct CategoryFrame { @@ -48,11 +50,10 @@ struct Profiler { double curFrameStart; }; +// Each thread only touches its own slots of profiler and history. static Profiler profiler; static Category categories[MAX_CATEGORIES]; -static std::mutex categoriesLock; -static int threadIdAfterLast = 0; -static std::mutex threadsLock; +static retro_atomic_int_t threadIdAfterLast{ 0 }; static CategoryFrame *history; #if MAX_THREADS > 1 thread_local int profilerThreadId = -1; @@ -63,7 +64,7 @@ static int profilerThreadId = 0; void internal_profiler_init() { memset(&profiler, 0, sizeof(profiler)); #if MAX_THREADS == 1 - threadIdAfterLast = 1; + retro_atomic_store_release_int(&threadIdAfterLast, 1); #endif for (int i = 0; i < MAX_THREADS; i++) { for (int j = 0; j < MAX_DEPTH; j++) { @@ -79,40 +80,49 @@ static int internal_profiler_find_thread() { return thread_id; } - std::lock_guard guard(threadsLock); - if (threadIdAfterLast < MAX_THREADS) { - thread_id = threadIdAfterLast++; - profilerThreadId = thread_id; - return thread_id; + thread_id = retro_atomic_fetch_add_int(&threadIdAfterLast, 1); + if (thread_id >= MAX_THREADS) { + // Just keep reusing the last one. + thread_id = MAX_THREADS - 1; } + profilerThreadId = thread_id; + return thread_id; +} - // Just keep reusing the last one. - return threadIdAfterLast - 1; +static int NumThreads() { + int n = retro_atomic_load_acquire_int(&threadIdAfterLast); + return n < MAX_THREADS ? n : MAX_THREADS; +} + +static const char *CategoryName(int i) { + return (const char *)retro_atomic_load_acquire_ptr(&categories[i].name); +} + +static bool SameCategory(const char *a, const char *b) { +#ifdef UNIFIED_CONST_STR + return a == b; +#else + return !strcmp(a, b); +#endif } int internal_profiler_find_cat(const char *category_name, bool create_missing) { - int i; - for (i = 0; i < MAX_CATEGORIES; i++) { - const char *catname = categories[i].name; - if (!catname) - break; -#ifdef UNIFIED_CONST_STR - if (catname == category_name) { -#else - if (!strcmp(catname, category_name)) { -#endif + for (int i = 0; i < MAX_CATEGORIES; i++) { + const char *catname = CategoryName(i); + if (!catname) { + if (!category_name || !create_missing) { + return -1; + } + if (retro_atomic_cas_ptr(&categories[i].name, nullptr, (void *)category_name)) { + return i; + } + // Another thread took this slot first, maybe for the same name. + catname = CategoryName(i); + } + if (category_name && SameCategory(catname, category_name)) { return i; } } - - if (i < MAX_CATEGORIES && category_name && create_missing) { - std::lock_guard guard(categoriesLock); - int race_check = internal_profiler_find_cat(category_name, false); - if (race_check == -1) - categories[i].name = category_name; - return i; - } - return -1; } @@ -195,7 +205,7 @@ void internal_profiler_end_frame() { } const char *Profiler_GetCategoryName(int i) { - return i >= 0 ? categories[i].name : "N/A"; + return i >= 0 ? CategoryName(i) : "N/A"; } int Profiler_GetHistoryLength() { @@ -204,14 +214,14 @@ int Profiler_GetHistoryLength() { int Profiler_GetNumCategories() { for (int i = 0; i < MAX_CATEGORIES; i++) { - if (!categories[i].name) + if (!CategoryName(i)) return i; } return 0; } int Profiler_GetNumThreads() { - return threadIdAfterLast; + return NumThreads(); } void Profiler_GetSlowestThreads(int *data, int count) { @@ -225,7 +235,8 @@ void Profiler_GetSlowestThreads(int *data, int count) { float slowestTime = 0.0f; data[i] = 0; - for (int thread = 0; thread < threadIdAfterLast; ++thread) { + const int numThreads = NumThreads(); + for (int thread = 0; thread < numThreads; ++thread) { float sum = 0.0f; for (int c = 0; c < numCategories; ++c) { sum += history[MAX_THREADS * x + thread].time_taken[c]; diff --git a/Common/System/OSD.cpp b/Common/System/OSD.cpp index cb82a271fc..891b7a8e6e 100644 --- a/Common/System/OSD.cpp +++ b/Common/System/OSD.cpp @@ -12,8 +12,47 @@ OnScreenDisplay g_OSD; // Effectively forever. constexpr double forever_s = 10000000000.0; +namespace { +struct OSDCommand; +struct OSDLink { + mpsc_stack_node_t node; // first, so a node pointer is a link pointer + OSDCommand *self; +}; +struct OSDCommand { + OSDLink link; + std::function fn; +}; +} + +OnScreenDisplay::OnScreenDisplay() { + mpsc_stack_init(&pending_); +} + +OnScreenDisplay::~OnScreenDisplay() { + mpsc_stack_node_t *node = mpsc_stack_drain(&pending_); + while (node) { + mpsc_stack_node_t *next = node->next; + delete ((OSDLink *)node)->self; + node = next; + } +} + +void OnScreenDisplay::Post(std::function fn) { + OSDCommand *cmd = new OSDCommand{ {}, std::move(fn) }; + cmd->link.self = cmd; + mpsc_stack_push(&pending_, &cmd->link.node); +} + void OnScreenDisplay::Update() { - std::lock_guard guard(mutex_); + // Apply commands oldest first. + mpsc_stack_node_t *node = mpsc_stack_reverse(mpsc_stack_drain(&pending_)); + while (node) { + mpsc_stack_node_t *next = node->next; + OSDCommand *cmd = ((OSDLink *)node)->self; + cmd->fn(*this); + delete cmd; + node = next; + } double now = time_now_d(); for (auto iter = entries_.begin(); iter != entries_.end(); ) { @@ -26,10 +65,64 @@ void OnScreenDisplay::Update() { } std::vector OnScreenDisplay::Entries() { - std::lock_guard guard(mutex_); return entries_; // makes a copy. } +void OnScreenDisplay::Show(OSDType type, std::string_view text, std::string_view text2, std::string_view icon, float duration_s, const char *id) { + if (text.empty() && type != OSDType::STATUS_ICON) { + // The user hacked the translation files to get rid of the message. Let's reward the dedication + // by skipping it entirely. + return; + } + + INFO_LOG(Log::UI, "OSD: %.*s (%.*s)", STR_VIEW(text), STR_VIEW(text2)); + + const bool hasId = id != nullptr; + Post([type, t = std::string(text), t2 = std::string(text2), ic = std::string(icon), duration_s, hasId, i = std::string(hasId ? id : "")](OnScreenDisplay &osd) { + osd.ShowNow(type, t, t2, ic, duration_s, hasId ? i.c_str() : nullptr); + }); +} + +void OnScreenDisplay::CancelById(std::string_view id) { + Post([i = std::string(id)](OnScreenDisplay &osd) { osd.CancelByIdNow(i); }); +} + +void OnScreenDisplay::ShowAchievementUnlocked(int achievementID) { + Post([=](OnScreenDisplay &osd) { osd.ShowAchievementUnlockedNow(achievementID); }); +} + +void OnScreenDisplay::ShowAchievementProgress(int achievementID, bool show) { + Post([=](OnScreenDisplay &osd) { osd.ShowAchievementProgressNow(achievementID, show); }); +} + +void OnScreenDisplay::ShowChallengeIndicator(int achievementID, bool show) { + Post([=](OnScreenDisplay &osd) { osd.ShowChallengeIndicatorNow(achievementID, show); }); +} + +void OnScreenDisplay::ShowLeaderboardTracker(int leaderboardTrackerID, std::string_view trackerText, bool show) { + Post([=, t = std::string(trackerText)](OnScreenDisplay &osd) { osd.ShowLeaderboardTrackerNow(leaderboardTrackerID, t, show); }); +} + +void OnScreenDisplay::SetProgressBar(std::string_view id, std::string_view message, float minValue, float maxValue, float progress, float delay_s) { + Post([=, i = std::string(id), m = std::string(message)](OnScreenDisplay &osd) { osd.SetProgressBarNow(i, m, minValue, maxValue, progress, delay_s); }); +} + +void OnScreenDisplay::RemoveProgressBar(std::string_view id, bool success, float delay_s) { + Post([=, i = std::string(id)](OnScreenDisplay &osd) { osd.RemoveProgressBarNow(i, success, delay_s); }); +} + +void OnScreenDisplay::ClearAchievementStuff() { + Post([](OnScreenDisplay &osd) { osd.ClearAchievementStuffNow(); }); +} + +void OnScreenDisplay::SetClickCallback(std::string_view id, std::function callback) { + Post([i = std::string(id), cb = std::move(callback)](OnScreenDisplay &osd) { osd.SetClickCallbackNow(i, cb); }); +} + +void OnScreenDisplay::SetFlags(std::string_view id, OSDMessageFlags flags) { + Post([=, i = std::string(id)](OnScreenDisplay &osd) { osd.SetFlagsNow(i, flags); }); +} + void OnScreenDisplay::NudgeIngameNotifications() { sideBarShowTime_ = time_now_d(); } @@ -42,7 +135,6 @@ float OnScreenDisplay::IngameAlpha() const { } void OnScreenDisplay::ClickEntry(size_t index, double now) { - std::lock_guard guard(mutex_); if (index < entries_.size() && entries_[index].type != OSDType::ACHIEVEMENT_CHALLENGE_INDICATOR) { if (entries_[index].clickCallback) { entries_[index].clickCallback(); @@ -52,15 +144,7 @@ void OnScreenDisplay::ClickEntry(size_t index, double now) { } } -void OnScreenDisplay::Show(OSDType type, std::string_view text, std::string_view text2, std::string_view icon, float duration_s, const char *id) { - if (text.empty() && type != OSDType::STATUS_ICON) { - // The user hacked the translation files to get rid of the message. Let's reward the dedication - // by skipping it entirely. - return; - } - - INFO_LOG(Log::UI, "OSD: %.*s (%.*s)", STR_VIEW(text), STR_VIEW(text2)); - +void OnScreenDisplay::ShowNow(OSDType type, std::string_view text, std::string_view text2, std::string_view icon, float duration_s, const char *id) { // Automatic duration based on type. if (duration_s <= 0.0f) { switch (type) { @@ -84,7 +168,6 @@ void OnScreenDisplay::Show(OSDType type, std::string_view text, std::string_view } double now = time_now_d(); - std::lock_guard guard(mutex_); if (id) { for (auto iter = entries_.begin(); iter != entries_.end(); ++iter) { if (iter->id == id) { @@ -115,8 +198,7 @@ void OnScreenDisplay::Show(OSDType type, std::string_view text, std::string_view entries_.insert(entries_.begin(), msg); } -void OnScreenDisplay::CancelById(std::string_view id) { - std::lock_guard guard(mutex_); +void OnScreenDisplay::CancelByIdNow(std::string_view id) { for (auto iter = entries_.begin(); iter != entries_.end();) { if (iter->id == id) { iter = entries_.erase(iter); @@ -134,12 +216,11 @@ void OnScreenDisplay::ShowOnOff(std::string_view message, bool on, float duratio Show(OSDType::MESSAGE_INFO, msg, duration_s); } -void OnScreenDisplay::ShowAchievementUnlocked(int achievementID) { +void OnScreenDisplay::ShowAchievementUnlockedNow(int achievementID) { double now = time_now_d(); double duration_s = 5.0; - std::lock_guard guard(mutex_); Entry msg{}; msg.numericID = achievementID; msg.type = OSDType::ACHIEVEMENT_UNLOCKED; @@ -148,10 +229,9 @@ void OnScreenDisplay::ShowAchievementUnlocked(int achievementID) { entries_.insert(entries_.begin(), msg); } -void OnScreenDisplay::ShowAchievementProgress(int achievementID, bool show) { +void OnScreenDisplay::ShowAchievementProgressNow(int achievementID, bool show) { double now = time_now_d(); - std::lock_guard guard(mutex_); // There can only be one of these at a time. for (auto &entry : entries_) { if (entry.type == OSDType::ACHIEVEMENT_PROGRESS) { @@ -182,10 +262,9 @@ void OnScreenDisplay::ShowAchievementProgress(int achievementID, bool show) { entries_.insert(entries_.begin(), entry); } -void OnScreenDisplay::ShowChallengeIndicator(int achievementID, bool show) { +void OnScreenDisplay::ShowChallengeIndicatorNow(int achievementID, bool show) { double now = time_now_d(); - std::lock_guard guard(mutex_); for (auto &entry : entries_) { if (entry.numericID == achievementID && entry.type == OSDType::ACHIEVEMENT_CHALLENGE_INDICATOR && !show) { // Hide and eventually delete it. @@ -209,10 +288,9 @@ void OnScreenDisplay::ShowChallengeIndicator(int achievementID, bool show) { entries_.insert(entries_.begin(), entry); } -void OnScreenDisplay::ShowLeaderboardTracker(int leaderboardTrackerID, std::string_view trackerText, bool show) { // show=true is used both for create and update. +void OnScreenDisplay::ShowLeaderboardTrackerNow(int leaderboardTrackerID, std::string_view trackerText, bool show) { // show=true is used both for create and update. double now = time_now_d(); - std::lock_guard guard(mutex_); for (auto &entry : entries_) { if (entry.numericID == leaderboardTrackerID && entry.type == OSDType::LEADERBOARD_TRACKER) { if (show) { @@ -252,7 +330,7 @@ void OnScreenDisplay::ShowLeaderboardSubmitted(std::string_view title, std::stri g_OSD.Show(OSDType::LEADERBOARD_SUBMITTED, title, value, 3.0f); } -void OnScreenDisplay::SetProgressBar(std::string_view id, std::string_view message, float minValue, float maxValue, float progress, float delay) { +void OnScreenDisplay::SetProgressBarNow(std::string_view id, std::string_view message, float minValue, float maxValue, float progress, float delay) { _dbg_assert_(!my_isnanorinf(progress)); _dbg_assert_(!my_isnanorinf(minValue)); _dbg_assert_(!my_isnanorinf(maxValue)); @@ -260,7 +338,6 @@ void OnScreenDisplay::SetProgressBar(std::string_view id, std::string_view messa double now = time_now_d(); bool found = false; - std::lock_guard guard(mutex_); for (auto &bar : entries_) { if (bar.type == OSDType::PROGRESS_BAR && bar.id == id) { bar.minValue = minValue; @@ -284,8 +361,7 @@ void OnScreenDisplay::SetProgressBar(std::string_view id, std::string_view messa entries_.push_back(bar); } -void OnScreenDisplay::RemoveProgressBar(std::string_view id, bool success, float delay_s) { - std::lock_guard guard(mutex_); +void OnScreenDisplay::RemoveProgressBarNow(std::string_view id, bool success, float delay_s) { for (auto &ent : entries_) { if (ent.type == OSDType::PROGRESS_BAR && ent.id == id) { if (success) { @@ -306,9 +382,8 @@ void OnScreenDisplay::RemoveProgressBar(std::string_view id, bool success, float } // Fades out everything related to achievements. Should be used on game shutdown. -void OnScreenDisplay::ClearAchievementStuff() { +void OnScreenDisplay::ClearAchievementStuffNow() { double now = time_now_d(); - std::lock_guard guard(mutex_); for (auto &iter : entries_) { switch (iter.type) { case OSDType::ACHIEVEMENT_CHALLENGE_INDICATOR: @@ -325,9 +400,8 @@ void OnScreenDisplay::ClearAchievementStuff() { } } -void OnScreenDisplay::SetClickCallback(std::string_view id, std::function callback) { +void OnScreenDisplay::SetClickCallbackNow(std::string_view id, std::function callback) { _dbg_assert_(callback != nullptr); - std::lock_guard guard(mutex_); for (auto &ent : entries_) { // protect against dupes. if (ent.id == id) { @@ -336,8 +410,7 @@ void OnScreenDisplay::SetClickCallback(std::string_view id, std::function guard(mutex_); +void OnScreenDisplay::SetFlagsNow(std::string_view id, OSDMessageFlags flags) { for (auto &ent : entries_) { // protect against dupes. if (ent.id == id) { diff --git a/Common/System/OSD.h b/Common/System/OSD.h index a229623fc3..6e156f6e8c 100644 --- a/Common/System/OSD.h +++ b/Common/System/OSD.h @@ -3,11 +3,12 @@ #include #include #include -#include #include #include "Common/Common.h" +#include + // Shows a visible message to the user. // The default implementation in NativeApp.cpp uses our "osm" system (on screen messaging). enum class OSDType { @@ -48,8 +49,14 @@ enum class OSDMessageFlags { ENUM_CLASS_BITOPS(OSDMessageFlags); // Data holder for on-screen messages. +// Show/Set/Cancel may be called from any thread: they queue a command. Update(), +// Entries(), IsEmpty(), ClickEntry() and the ingame nudge belong to the one thread +// that drives the display, which applies the queued commands in Update(). class OnScreenDisplay { public: + OnScreenDisplay(); + ~OnScreenDisplay(); + // If you specify 0.0f as duration, a duration will be chosen automatically depending on type. void Show(OSDType type, std::string_view text, float duration_s = 0.0f, const char *id = nullptr) { Show(type, text, "", duration_s, id); @@ -122,8 +129,22 @@ public: static float FadeoutTime() { return 0.25f; } private: - std::vector entries_; - std::mutex mutex_; + void Post(std::function fn); + + void ShowNow(OSDType type, std::string_view text, std::string_view text2, std::string_view icon, float duration_s, const char *id); + void CancelByIdNow(std::string_view id); + void ShowAchievementUnlockedNow(int achievementID); + void ShowAchievementProgressNow(int achievementID, bool show); + void ShowChallengeIndicatorNow(int achievementID, bool show); + void ShowLeaderboardTrackerNow(int leaderboardTrackerID, std::string_view trackerText, bool show); + void SetProgressBarNow(std::string_view id, std::string_view message, float minValue, float maxValue, float progress, float delay_s); + void RemoveProgressBarNow(std::string_view id, bool success, float delay_s); + void ClearAchievementStuffNow(); + void SetClickCallbackNow(std::string_view id, std::function callback); + void SetFlagsNow(std::string_view id, OSDMessageFlags flags); + + std::vector entries_; // display thread only + mpsc_stack_t pending_; double sideBarShowTime_ = 0.0; }; diff --git a/Common/System/Request.cpp b/Common/System/Request.cpp index 4dcbe98b9c..580ad8535e 100644 --- a/Common/System/Request.cpp +++ b/Common/System/Request.cpp @@ -53,6 +53,17 @@ const char *RequestTypeAsString(SystemRequestType type) { } } +RequestManager::RequestManager() { + for (Slot &slot : slots_) { + slot.link.self = &slot; + retro_atomic_int_init(&slot.state, SLOT_FREE); + retro_atomic_int_init(&slot.forgotten, 0); + } + mpsc_stack_init(&responses_); + retro_atomic_int_init(&idCounter_, 10); + retro_atomic_int_init(&tokenGen_, 20000); +} + bool RequestManager::MakeSystemRequest(SystemRequestType type, RequesterToken token, RequestCallback callback, RequestFailedCallback failedCallback, std::string_view param1, std::string_view param2, int64_t param3, int64_t param4) { if (token == NO_REQUESTER_TOKEN) { _dbg_assert_(!callback); @@ -62,97 +73,113 @@ bool RequestManager::MakeSystemRequest(SystemRequestType type, RequesterToken to _dbg_assert_(token != NO_REQUESTER_TOKEN); } - int requestId = idCounter_++; + const int requestId = retro_atomic_fetch_add_int(&idCounter_, 1); // NOTE: We need to register immediately, in order to support synchronous implementations. + Slot *slot = nullptr; if (callback || failedCallback) { - std::lock_guard guard(callbackMutex_); - callbackMap_[requestId] = { callback, failedCallback, token }; + for (Slot &s : slots_) { + if (retro_atomic_cas_int(&s.state, SLOT_FREE, SLOT_FILLING)) { + slot = &s; + break; + } + } + if (!slot) { + ERROR_LOG(Log::System, "Too many outstanding system requests, dropping %s", RequestTypeAsString(type)); + return false; + } + slot->callback = std::move(callback); + slot->failedCallback = std::move(failedCallback); + slot->token = token; + retro_atomic_store_relaxed_int(&slot->forgotten, 0); + retro_atomic_store_release_int(&slot->state, requestId); } VERBOSE_LOG(Log::System, "Making system request %s: id %d", RequestTypeAsString(type), requestId); + std::string p1(param1); std::string p2(param2); // TODO: Convert to string_view if (!System_MakeRequest(type, requestId, p1, p2, param3, param4)) { - if (callback || failedCallback) { - std::lock_guard guard(callbackMutex_); - callbackMap_.erase(requestId); - } + // Not handled. Unless it was answered on the spot, nobody will answer it now. + if (slot && retro_atomic_cas_int(&slot->state, requestId, SLOT_FILLING)) + Release(slot); return false; } return true; } +void RequestManager::Release(Slot *slot) { + slot->callback = nullptr; + slot->failedCallback = nullptr; + slot->responseString.clear(); + retro_atomic_store_release_int(&slot->state, SLOT_FREE); +} + void RequestManager::ForgetRequestsWithToken(RequesterToken token) { - for (auto &iter : callbackMap_) { - if (iter.second.token == token) { + for (Slot &slot : slots_) { + // Only filled slots have a token worth reading. + const int state = retro_atomic_load_acquire_int(&slot.state); + if ((state > 0 || state == SLOT_POSTED) && slot.token == token) { INFO_LOG(Log::System, "Forgetting about requester with token %d", token); - iter.second.callback = nullptr; - iter.second.failedCallback = nullptr; + retro_atomic_store_release_int(&slot.forgotten, 1); } } } +void RequestManager::Post(int requestId, bool success, std::string_view responseString, int responseValue) { + for (Slot &slot : slots_) { + if (retro_atomic_cas_int(&slot.state, requestId, SLOT_POSTING)) { + slot.success = success; + slot.responseString = responseString; + slot.responseValue = responseValue; + retro_atomic_store_release_int(&slot.state, SLOT_POSTED); + mpsc_stack_push(&responses_, &slot.link.node); + return; + } + } + ERROR_LOG(Log::System, "%s: Unexpected request ID %d", success ? "PostSystemSuccess" : "PostSystemFailure", requestId); +} + void RequestManager::PostSystemSuccess(int requestId, std::string_view responseString, int responseValue) { - std::lock_guard guard(callbackMutex_); - auto iter = callbackMap_.find(requestId); - if (iter == callbackMap_.end()) { - ERROR_LOG(Log::System, "PostSystemSuccess: Unexpected request ID %d (responseString=%.*s)", requestId, (int)responseString.size(), responseString.data()); - return; - } - - std::lock_guard responseGuard(responseMutex_); - PendingSuccess response; - response.callback = iter->second.callback; - response.responseString = responseString; - response.responseValue = responseValue; - pendingSuccesses_.push_back(response); DEBUG_LOG(Log::System, "PostSystemSuccess: Request %d (%.*s, %d)", requestId, (int)responseString.size(), responseString.data(), responseValue); - callbackMap_.erase(iter); + Post(requestId, true, responseString, responseValue); } void RequestManager::PostSystemFailure(int requestId, int responseValue) { - std::lock_guard guard(callbackMutex_); - auto iter = callbackMap_.find(requestId); - if (iter == callbackMap_.end()) { - ERROR_LOG(Log::System, "PostSystemFailure: Unexpected request ID %d", requestId); - return; - } - WARN_LOG(Log::System, "PostSystemFailure: Request %d failed", requestId); - - std::lock_guard responseGuard(responseMutex_); - PendingFailure response; - response.failedCallback = iter->second.failedCallback; - response.responseValue = responseValue; - pendingFailures_.push_back(response); - callbackMap_.erase(iter); + Post(requestId, false, "", responseValue); } void RequestManager::ProcessRequests() { - std::lock_guard guard(responseMutex_); - for (auto &iter : pendingSuccesses_) { - if (iter.callback) { - iter.callback(iter.responseString.c_str(), iter.responseValue); + // Oldest response first. + mpsc_stack_node_t *node = mpsc_stack_reverse(mpsc_stack_drain(&responses_)); + while (node) { + mpsc_stack_node_t *next = node->next; + Slot *slot = ((SlotLink *)node)->self; + if (!retro_atomic_load_acquire_int(&slot->forgotten)) { + if (slot->success) { + if (slot->callback) + slot->callback(slot->responseString.c_str(), slot->responseValue); + } else if (slot->failedCallback) { + slot->failedCallback(slot->responseValue); + } } + Release(slot); + node = next; } - pendingSuccesses_.clear(); - for (auto &iter : pendingFailures_) { - if (iter.failedCallback) { - iter.failedCallback(iter.responseValue); - } - } - pendingFailures_.clear(); } void RequestManager::Clear() { - std::lock_guard guard(callbackMutex_); - std::lock_guard responseGuard(responseMutex_); - - pendingSuccesses_.clear(); - pendingFailures_.clear(); - callbackMap_.clear(); + mpsc_stack_node_t *node = mpsc_stack_drain(&responses_); + while (node) { + mpsc_stack_node_t *next = node->next; + Release(((SlotLink *)node)->self); + node = next; + } + // Requests still waiting for an answer won't get one delivered. + for (Slot &slot : slots_) + retro_atomic_store_release_int(&slot.forgotten, 1); } void System_CreateGameShortcut(const Path &path, std::string_view title) { diff --git a/Common/System/Request.h b/Common/System/Request.h index 9ba9bb5b61..d04589f427 100644 --- a/Common/System/Request.h +++ b/Common/System/Request.h @@ -1,8 +1,10 @@ #pragma once #include -#include -#include +#include + +#include +#include #include #include @@ -45,8 +47,7 @@ public: void ProcessRequests(); RequesterToken GenerateRequesterToken() { - int token = tokenGen_++; - return token; + return retro_atomic_fetch_add_int(&tokenGen_, 1); } void ForgetRequestsWithToken(RequesterToken token); @@ -54,34 +55,39 @@ public: // Unclear if we need this... void Clear(); + RequestManager(); + private: - struct CallbackPair { + // One outstanding request with callbacks. state holds the request id while it waits, + // so a response claims its slot with one CAS on the id itself. + enum : int { SLOT_FREE = 0, SLOT_FILLING = -1, SLOT_POSTING = -2, SLOT_POSTED = -3 }; + struct Slot; + struct SlotLink { + mpsc_stack_node_t node; // first, so a node pointer is a link pointer + Slot *self; + }; + struct Slot { + SlotLink link; + retro_atomic_int_t state; + retro_atomic_int_t forgotten; RequestCallback callback; RequestFailedCallback failedCallback; RequesterToken token; - }; - - std::map callbackMap_; - std::mutex callbackMutex_; - - struct PendingSuccess { + bool success; std::string responseString; int responseValue; - RequestCallback callback; }; + enum { MAX_PENDING = 64 }; - struct PendingFailure { - RequestFailedCallback failedCallback; - int responseValue; // can have error codes for example. - }; + void Post(int requestId, bool success, std::string_view responseString, int responseValue); + void Release(Slot *slot); + + Slot slots_[MAX_PENDING]; + mpsc_stack_t responses_; // posted slots, drained by ProcessRequests() // Let's start at 10 to get a recognizably valid ID in logs. - int idCounter_ = 10; - std::vector pendingSuccesses_; - std::vector pendingFailures_; - std::mutex responseMutex_; - - RequesterToken tokenGen_ = 20000; + retro_atomic_int_t idCounter_; + retro_atomic_int_t tokenGen_; }; const char *RequestTypeAsString(SystemRequestType type); diff --git a/Common/System/System.h b/Common/System/System.h index 3016c4d7e6..e071c38c0f 100644 --- a/Common/System/System.h +++ b/Common/System/System.h @@ -5,7 +5,6 @@ #include #include #include -#include // Platform integration diff --git a/Common/Thread/Barrier.h b/Common/Thread/Barrier.h index 698a322661..26c90bf212 100644 --- a/Common/Thread/Barrier.h +++ b/Common/Thread/Barrier.h @@ -1,32 +1,33 @@ #pragma once -#include -#include +#include -// Similar to C++20's std::barrier +#include + +#include "Common/Thread/ParkingLot.h" + +// Similar to C++20's std::barrier. Reusable: each round bumps the generation. class CountingBarrier { public: - CountingBarrier(size_t count) : threadCount_(count) {} + CountingBarrier(size_t count) : threadCount_((int)count) { + retro_atomic_int_init(&arrived_, 0); + retro_atomic_int_init(&generation_, 0); + } void Arrive() { - std::unique_lock lk(m); - counter++; - waiting++; - // notify_all (not notify_one) - every waiter needs to see counter >= threadCount_. - cv.wait(lk, [&] {return counter >= threadCount_; }); - cv.notify_all(); - waiting--; - if (waiting == 0) { - // Reset so it can be re-used. - counter = 0; + const int gen = retro_atomic_load_acquire_int(&generation_); + if (retro_atomic_fetch_add_int(&arrived_, 1) + 1 == threadCount_) { + // Last one in: reset for the next round, then release everyone. + retro_atomic_store_relaxed_int(&arrived_, 0); + retro_atomic_fetch_add_int(&generation_, 1); + ParkingLotNotify(this); + return; } - lk.unlock(); + ParkingLotWait(this, [&] { return retro_atomic_load_acquire_int(&generation_) != gen; }); } private: - std::mutex m; - std::condition_variable cv; - size_t counter = 0; - size_t waiting = 0; - size_t threadCount_; + retro_atomic_int_t arrived_; + retro_atomic_int_t generation_; + const int threadCount_; }; diff --git a/Common/Thread/Channel.h b/Common/Thread/Channel.h index 836290240a..c73f5e84a8 100644 --- a/Common/Thread/Channel.h +++ b/Common/Thread/Channel.h @@ -1,67 +1,75 @@ #pragma once -#include -#include -#include #include +#include + +#include "Common/Thread/ParkingLot.h" + // Named Channel.h because I originally intended to support a multi item channel as // well as a simple blocking mailbox. Let's see if we get there. -// Single item mailbox. +// Single item mailbox. Any number of threads may Wait or Poll; the first Send wins. // T is copyable. Often T will itself just be a pointer or smart pointer of some sort. template struct Mailbox { - Mailbox() : refcount_(1) {} + Mailbox() { + retro_atomic_int_init(&state_, EMPTY); + retro_atomic_int_init(&cancelled_, 0); + retro_atomic_int_init(&refcount_, 1); + } ~Mailbox() { - assert(refcount_ == 0); + assert(retro_atomic_load_acquire_int(&refcount_) == 0); } - std::mutex mutex_; - std::condition_variable condvar_; - T data_{}; - bool dataReceived_ = false; - T Wait() { - std::unique_lock lock(mutex_); - condvar_.wait(lock, [&] {return dataReceived_;}); + ParkingLotWait(this, [this] { return retro_atomic_load_acquire_int(&state_) == FULL; }); return data_; } bool Poll(T *data) { - std::unique_lock lock(mutex_); - if (dataReceived_) { + if (retro_atomic_load_acquire_int(&state_) == FULL) { *data = data_; return true; - } else { - return false; } + return false; } bool Send(T data) { - std::unique_lock lock(mutex_); - if (!dataReceived_) { - data_ = data; - dataReceived_ = true; - condvar_.notify_all(); - return true; - } else { - // Already has value. - return false; - } + // Claim the slot, write it, then publish. + if (!retro_atomic_cas_int(&state_, EMPTY, WRITING)) + return false; // Already has value. + data_ = data; + retro_atomic_store_release_int(&state_, FULL); + ParkingLotNotify(this); + return true; + } + + // Asks the producer to stop and releases waiters with a default value. + void CancelAndRelease() { + retro_atomic_store_release_int(&cancelled_, 1); + Send(T{}); + } + + bool Cancelled() const { + return retro_atomic_load_acquire_int(const_cast(&cancelled_)) != 0; } void AddRef() { - refcount_.fetch_add(1); + retro_atomic_fetch_add_int(&refcount_, 1); } void Release() { - int count = refcount_.fetch_sub(1); - if (count == 1) { // was definitely decreased to 0 + if (retro_atomic_fetch_sub_int(&refcount_, 1) == 1) { // was definitely decreased to 0 delete this; } } private: - std::atomic refcount_; + enum : int { EMPTY = 0, WRITING = 1, FULL = 2 }; + + T data_{}; + retro_atomic_int_t state_; + retro_atomic_int_t cancelled_; + retro_atomic_int_t refcount_; }; diff --git a/Common/Thread/Event.h b/Common/Thread/Event.h index f675cd07d4..1046273188 100644 --- a/Common/Thread/Event.h +++ b/Common/Thread/Event.h @@ -1,37 +1,25 @@ #pragma once -#include "Common/Thread/ThreadManager.h" +#include -#include -#include +#include "Common/Thread/ParkingLot.h" +#include "Common/Thread/ThreadManager.h" struct Event : public Waitable { public: Event() { - triggered_ = false; - } - - ~Event() { - // Make sure no one is still waiting, and any notify lock is released. - Notify(); + retro_atomic_int_init(&triggered_, 0); } void Wait() override { - if (triggered_) { - return; - } - std::unique_lock lock(mutex_); - cond_.wait(lock, [&] { return triggered_.load(); }); + ParkingLotWait(this, [this] { return retro_atomic_load_acquire_int(&triggered_) != 0; }); } void Notify() { - std::unique_lock lock(mutex_); - triggered_ = true; - cond_.notify_one(); + retro_atomic_store_release_int(&triggered_, 1); + ParkingLotNotify(this); } private: - std::condition_variable cond_; - std::mutex mutex_; - std::atomic triggered_; + retro_atomic_int_t triggered_; }; diff --git a/Common/Thread/MpscQueue.h b/Common/Thread/MpscQueue.h new file mode 100644 index 0000000000..48da77eba7 --- /dev/null +++ b/Common/Thread/MpscQueue.h @@ -0,0 +1,80 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include + +#include + +// Unbounded queue on libretro-common's mpsc_stack: any thread pushes, one thread drains, +// oldest first. Every item is one heap node, so keep it off per-sample paths. +template +class MpscQueue { +public: + MpscQueue() { + mpsc_stack_init(&stack_); + } + ~MpscQueue() { + Drain([](T &&) {}); + } + MpscQueue(const MpscQueue &) = delete; + MpscQueue &operator=(const MpscQueue &) = delete; + + // Any thread. + void Push(T value) { + Node *node = new Node(std::move(value)); + mpsc_stack_push(&stack_, &node->link.node); + } + + // Any thread, as a hint; exact for the consumer. + bool Empty() { + return mpsc_stack_empty(&stack_); + } + + // Consumer only. Hands every queued item to f, oldest first. Returns whether there were any. + template + bool Drain(F f) { + mpsc_stack_node_t *node = mpsc_stack_reverse(mpsc_stack_drain(&stack_)); + const bool any = node != nullptr; + while (node) { + mpsc_stack_node_t *next = node->next; + Node *item = (Node *)((Link *)node)->owner; + f(std::move(item->value)); + delete item; + node = next; + } + return any; + } + +private: + // Standard layout with the stack node first, so a drained node converts back to its Link. + struct Link { + mpsc_stack_node_t node; + void *owner; + }; + struct Node { + explicit Node(T &&v) : value(std::move(v)) { + link.node.next = nullptr; + link.owner = this; + } + Link link; + T value; + }; + + mpsc_stack_t stack_; +}; diff --git a/Common/Thread/ParallelLoop.h b/Common/Thread/ParallelLoop.h index a888cc60a3..02b95f69c2 100644 --- a/Common/Thread/ParallelLoop.h +++ b/Common/Thread/ParallelLoop.h @@ -1,38 +1,39 @@ #pragma once #include -#include -#include +#include + +#include "Common/Thread/ParkingLot.h" #include "Common/Thread/ThreadManager.h" -// Same as the latch from C++21. +// Same as the latch from C++20. struct WaitableCounter : public Waitable { public: - WaitableCounter(int count) : count_(count) {} + WaitableCounter(int count) { + retro_atomic_int_init(&count_, count); + } void Count() { - std::unique_lock lock(mutex_); - if (count_ == 0) { - return; - } - count_--; - if (count_ == 0) { - // We were the last one to increment - cond_.notify_all(); + for (;;) { + const int count = retro_atomic_load_acquire_int(&count_); + if (count == 0) + return; + if (retro_atomic_cas_int(&count_, count, count - 1)) { + if (count == 1) { + // We were the last one; the waiter may free us as soon as it sees zero. + ParkingLotNotify(this); + } + return; + } } } void Wait() override { - std::unique_lock lock(mutex_); - while (count_ != 0) { - cond_.wait(lock); - } + ParkingLotWait(this, [this] { return retro_atomic_load_acquire_int(&count_) == 0; }); } - int count_; - std::mutex mutex_; - std::condition_variable cond_; + retro_atomic_int_t count_; }; // Note that upper bounds are non-inclusive: range is [lower, upper) diff --git a/Common/Thread/ParkingLot.h b/Common/Thread/ParkingLot.h index 1ef8f8ab73..5772ab33ea 100644 --- a/Common/Thread/ParkingLot.h +++ b/Common/Thread/ParkingLot.h @@ -37,6 +37,32 @@ inline void ParkingLotWait(const void *addr, Pred pred) { } } +// A count of events, for "wait until something happened since I last looked". A waiter reads +// Seen() before it checks its own state, and Wait(seen) returns once Notify() has run since. +class EventCounter { +public: + EventCounter() { + retro_atomic_int_init(&count_, 0); + } + + int Seen() const { + return retro_atomic_load_acquire_int(const_cast(&count_)); + } + + // Any thread, after publishing whatever the waiters look at. + void Notify() { + retro_atomic_fetch_add_int(&count_, 1); + ParkingLotNotify(this); + } + + void Wait(int seen) const { + ParkingLotWait(this, [&] { return Seen() != seen; }); + } + +private: + retro_atomic_int_t count_; +}; + // Grace period for read-mostly pointers: readers bracket each use with // Enter/Exit, a writer unpublishes and then Drain()s before freeing. // Readers never wait. Drain waits only for readers already inside. diff --git a/Common/Thread/Promise.h b/Common/Thread/Promise.h index 9a98ac1cab..d59d952732 100644 --- a/Common/Thread/Promise.h +++ b/Common/Thread/Promise.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include "Common/Log.h" #include "Common/Thread/Channel.h" @@ -43,10 +42,12 @@ public: } void Run() override { + if (tx_->Cancelled()) { + INFO_LOG(Log::System, "PromiseTask skipped after cancellation"); + return; + } T value = fun_(); - if (!cancelled_) { - tx_->Send(value); - } else { + if (!tx_->Send(value)) { INFO_LOG(Log::System, "PromiseTask ended after cancellation"); } } @@ -57,63 +58,54 @@ public: void Cancel() override { INFO_LOG(Log::System, "PromiseTask cancelled"); - cancelled_ = true; + tx_->CancelAndRelease(); } std::function fun_; Mailbox *tx_; const TaskType type_; const TaskPriority priority_; - - std::atomic cancelled_{}; }; // Represents pending or actual data. // Has ownership over the data. Single use. -// TODO: Split Mailbox (rx_ and tx_) up into separate proxy objects. -// NOTE: Poll/BlockUntilReady should only be used from one thread. +// Poll/BlockUntilReady may be called from any number of threads at once; the value is +// immutable once delivered. Cancel and destruction belong to the owner. // TODO: Make movable? template class Promise { public: // Never fails. static Promise *Spawn(ThreadManager *threadman, std::function fun, TaskType taskType, TaskPriority taskPriority = TaskPriority::NORMAL) { - Mailbox *mailbox = new Mailbox(); - Promise *promise = new Promise(); - promise->rx_ = mailbox; - - PromiseTask *task = new PromiseTask(fun, mailbox, taskType, taskPriority); - threadman->EnqueueTask(task); - promise->task_ = task; + promise->rx_ = new Mailbox(); + threadman->EnqueueTask(new PromiseTask(fun, promise->rx_, taskType, taskPriority)); return promise; } static Promise *AlreadyDone(T data) { Promise *promise = new Promise(); promise->data_ = data; - promise->ready_ = true; return promise; } static Promise *CreateEmpty() { - Mailbox *mailbox = new Mailbox(); Promise *promise = new Promise(); - promise->rx_ = mailbox; + promise->rx_ = new Mailbox(); return promise; } // Allow an empty promise to spawn, too, in case we want to delay it. void SpawnEmpty(ThreadManager *threadman, std::function fun, TaskType taskType, TaskPriority taskPriority = TaskPriority::NORMAL) { - task_ = new PromiseTask(fun, rx_, taskType, taskPriority); - threadman->EnqueueTask(task_); + threadman->EnqueueTask(new PromiseTask(fun, rx_, taskType, taskPriority)); } ~Promise() { - std::lock_guard guard(readyMutex_); - // A promise should have been fulfilled before it's destroyed. - _assert_(ready_); - _assert_(!rx_); + // A promise should have been fulfilled (or cancelled) before it's destroyed. + T unused; + _assert_(!rx_ || rx_->Poll(&unused)); + if (rx_) + rx_->Release(); sentinel_ = 0xeeeeeeee; } @@ -122,38 +114,20 @@ public: T Poll() { uint32_t sentinel = sentinel_; _assert_msg_(sentinel == 0xffc0ffee, "%08x", sentinel); - std::lock_guard guard(readyMutex_); - if (ready_) { + if (!rx_) return data_; - } else { - _dbg_assert_(rx_); - if (rx_->Poll(&data_)) { - rx_->Release(); - rx_ = nullptr; - ready_ = true; - task_ = nullptr; - return data_; - } else { - return nullptr; - } - } + T data; + if (rx_->Poll(&data)) + return data; + return nullptr; } T BlockUntilReady() { uint32_t sentinel = sentinel_; _assert_msg_(sentinel == 0xffc0ffee, "%08x", sentinel); - std::lock_guard guard(readyMutex_); - if (ready_) { + if (!rx_) return data_; - } else { - _dbg_assert_(rx_); - data_ = rx_->Wait(); - rx_->Release(); - rx_ = nullptr; - ready_ = true; - task_ = nullptr; - return data_; - } + return rx_->Wait(); } // For outside injection of data, when not using Spawn. @@ -162,26 +136,15 @@ public: } void Cancel() { - std::lock_guard guard(readyMutex_); - if (!ready_) { - ready_ = true; - _dbg_assert_(task_); - if (task_) { - task_->Cancel(); - } - rx_->Release(); - rx_ = nullptr; - } + if (rx_) + rx_->CancelAndRelease(); } private: Promise() {} // Promise can only be constructed in Spawn (or AlreadyDone). - T data_{}; - bool ready_ = false; - std::mutex readyMutex_; - Mailbox *rx_ = nullptr; + T data_{}; // only for AlreadyDone + Mailbox *rx_ = nullptr; // shared with the task; outlives every waiter uint32_t sentinel_ = 0xffc0ffee; - PromiseTask *task_ = nullptr; }; diff --git a/Common/Thread/Thread.cpp b/Common/Thread/Thread.cpp new file mode 100644 index 0000000000..01c4726887 --- /dev/null +++ b/Common/Thread/Thread.cpp @@ -0,0 +1,43 @@ +#include + +#include "Common/Log.h" +#include "Common/Thread/Thread.h" + +static void ThreadEntry(void *arg) { + std::function *func = (std::function *)arg; + (*func)(); + delete func; +} + +Thread::Thread(std::function func) { + std::function *heapFunc = new std::function(std::move(func)); + thread_ = sthread_create(&ThreadEntry, heapFunc); + _assert_msg_(thread_, "Failed to start a thread"); +} + +Thread::~Thread() { + _assert_msg_(!thread_, "Thread destroyed while still joinable"); +} + +Thread &Thread::operator=(Thread &&other) noexcept { + _assert_msg_(!thread_, "Thread assigned over while still joinable"); + thread_ = other.thread_; + other.thread_ = nullptr; + return *this; +} + +void Thread::join() { + _assert_(thread_); + sthread_join(thread_); + thread_ = nullptr; +} + +void Thread::detach() { + _assert_(thread_); + sthread_detach(thread_); + thread_ = nullptr; +} + +uintptr_t CurrentThreadId() { + return sthread_get_current_thread_id(); +} diff --git a/Common/Thread/Thread.h b/Common/Thread/Thread.h new file mode 100644 index 0000000000..2b4a7306b1 --- /dev/null +++ b/Common/Thread/Thread.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +struct sthread; + +// A joinable thread on libretro-common's rthreads, in place of std::thread. +// Like std::thread, a thread still joinable when destroyed or assigned over is a bug. +class Thread { +public: + Thread() {} + explicit Thread(std::function func); + template + Thread(F &&f, A &&a, Args &&...args) + : Thread(std::function(std::bind(std::forward(f), std::forward(a), std::forward(args)...))) {} + ~Thread(); + + Thread(const Thread &) = delete; + Thread &operator=(const Thread &) = delete; + Thread(Thread &&other) noexcept : thread_(other.thread_) { other.thread_ = nullptr; } + Thread &operator=(Thread &&other) noexcept; + + bool joinable() const { return thread_ != nullptr; } + void join(); + void detach(); + +private: + struct sthread *thread_ = nullptr; +}; + +// An id for the calling thread, comparable with what another thread got from it. +uintptr_t CurrentThreadId(); diff --git a/Common/Thread/ThreadManager.cpp b/Common/Thread/ThreadManager.cpp index ff451f1bae..ed29b30aa6 100644 --- a/Common/Thread/ThreadManager.cpp +++ b/Common/Thread/ThreadManager.cpp @@ -1,11 +1,10 @@ #include #include -#include -#include -#include -#include #include -#include + +#include +#include +#include #include "Common/Log.h" #include "Common/Thread/ThreadUtil.h" @@ -19,89 +18,343 @@ // They should always be scheduled to the first N threads. // * For some tasks, splitting the input values up linearly between the threads // is not fair. However, we ignore that for now. +// +// Nothing here takes a lock: +// * Each pool (compute, I/O) has a bounded MPMC ring per priority that any worker of the +// pool takes from. A full ring spills into an MPSC stack that one worker drains whole. +// * Each worker has an MPSC stack per priority for tasks pinned to it. +// * Idle workers park on their own eventcount. A worker publishes its idle bit before it +// re-checks for work, and a producer claims an idle bit after publishing, with a +// seq_cst fence on both sides, so either the producer finds the bit or the worker finds +// the task. + +#if !defined(RETRO_ATOMIC_HAS_PTR) +#error "The thread manager needs pointer atomics." +#endif const int MIN_IO_BLOCKING_THREADS = 4; -static constexpr size_t TASK_PRIORITY_COUNT = (size_t)TaskPriority::COUNT; +static constexpr int TASK_PRIORITY_COUNT = (int)TaskPriority::COUNT; +static constexpr int RING_SIZE = 1024; +static constexpr int RING_MASK = RING_SIZE - 1; +static constexpr int IDLE_BITS_PER_WORD = 31; ThreadManager g_threadManager; +static inline Task *TaskFromNode(mpsc_stack_node_t *node) { + return ((TaskLink *)node)->self; +} + +static inline int WrapAdd(int a, int b) { + return (int)((unsigned)a + (unsigned)b); +} + +// Bounded multi-producer multi-consumer ring of tasks (per-cell sequence numbers). +struct TaskRing { + struct Cell { + retro_atomic_int_t seq; + Task *task; + }; + + Cell cells[RING_SIZE]; + alignas(64) retro_atomic_int_t enqueuePos; + alignas(64) retro_atomic_int_t dequeuePos; + + TaskRing() { + for (int i = 0; i < RING_SIZE; i++) { + retro_atomic_int_init(&cells[i].seq, i); + cells[i].task = nullptr; + } + retro_atomic_int_init(&enqueuePos, 0); + retro_atomic_int_init(&dequeuePos, 0); + } + + bool Push(Task *task) { + int pos = retro_atomic_load_relaxed_int(&enqueuePos); + Cell *cell; + for (;;) { + cell = &cells[pos & RING_MASK]; + const int seq = retro_atomic_load_acquire_int(&cell->seq); + const int diff = (int)((unsigned)seq - (unsigned)pos); + if (diff == 0) { + if (retro_atomic_cas_int(&enqueuePos, pos, WrapAdd(pos, 1))) + break; + pos = retro_atomic_load_relaxed_int(&enqueuePos); + } else if (diff < 0) { + return false; // full + } else { + pos = retro_atomic_load_relaxed_int(&enqueuePos); + } + } + cell->task = task; + retro_atomic_store_release_int(&cell->seq, WrapAdd(pos, 1)); + return true; + } + + Task *Pop() { + int pos = retro_atomic_load_relaxed_int(&dequeuePos); + Cell *cell; + for (;;) { + cell = &cells[pos & RING_MASK]; + const int seq = retro_atomic_load_acquire_int(&cell->seq); + const int diff = (int)((unsigned)seq - (unsigned)WrapAdd(pos, 1)); + if (diff == 0) { + if (retro_atomic_cas_int(&dequeuePos, pos, WrapAdd(pos, 1))) + break; + pos = retro_atomic_load_relaxed_int(&dequeuePos); + } else if (diff < 0) { + return nullptr; // empty + } else { + pos = retro_atomic_load_relaxed_int(&dequeuePos); + } + } + Task *task = cell->task; + retro_atomic_store_release_int(&cell->seq, WrapAdd(pos, RING_SIZE)); + return task; + } + + bool HasItems() { + const int pos = retro_atomic_load_acquire_int(&dequeuePos); + const int seq = retro_atomic_load_acquire_int(&cells[pos & RING_MASK].seq); + return seq == WrapAdd(pos, 1); + } +}; + +struct TaskPool { + TaskRing ring[TASK_PRIORITY_COUNT]; + mpsc_stack_t overflow[TASK_PRIORITY_COUNT]; + std::vector workers; + // One bit per worker (31 per word), set while the worker is parked or about to park. + retro_atomic_int_t *idle = nullptr; + int idleWords = 0; + + TaskPool() { + for (int p = 0; p < TASK_PRIORITY_COUNT; p++) + mpsc_stack_init(&overflow[p]); + } + ~TaskPool() { + delete[] idle; + } +}; + struct GlobalThreadContext { - std::mutex mutex; - std::deque compute_queue[TASK_PRIORITY_COUNT]; - std::atomic compute_queue_size; - std::deque io_queue[TASK_PRIORITY_COUNT]; - std::atomic io_queue_size; + TaskPool pools[2]; // CPU_COMPUTE, IO_BLOCKING std::vector threads_; - std::atomic roundRobin; + retro_atomic_int_t dedicatedLive; + retro_eventcount_t dedicatedDone; +}; + +struct LocalFifo { + mpsc_stack_node_t *head = nullptr; + mpsc_stack_node_t *tail = nullptr; + + void Append(mpsc_stack_node_t *oldestFirst) { + if (!oldestFirst) + return; + if (tail) + tail->next = oldestFirst; + else + head = oldestFirst; + mpsc_stack_node_t *last = oldestFirst; + while (last->next) + last = last->next; + tail = last; + } + + Task *Pop() { + mpsc_stack_node_t *node = head; + if (!node) + return nullptr; + head = node->next; + if (!head) + tail = nullptr; + node->next = nullptr; + return TaskFromNode(node); + } }; struct TaskThreadContext { - std::atomic queue_size; - std::deque private_queue[TASK_PRIORITY_COUNT]; - std::thread thread; // the worker thread - std::condition_variable cond; // used to signal new work - std::mutex mutex; // protects the local queue. - int index; - TaskType type; - std::atomic cancelled; - char name[16]; + sthread_t *thread = nullptr; + GlobalThreadContext *global = nullptr; + TaskPool *pool = nullptr; + int index = 0; // global index + int poolIndex = 0; // index within its pool + TaskType type = TaskType::CPU_COMPUTE; + char name[16]{}; + + mpsc_stack_t inbox[TASK_PRIORITY_COUNT]; // pinned tasks, any producer + LocalFifo local[TASK_PRIORITY_COUNT]; // worker-private, in order + retro_eventcount_t wake; + retro_atomic_int_t cancelled; }; +static TaskPool &PoolFor(GlobalThreadContext *global, TaskType type) { + return global->pools[type == TaskType::CPU_COMPUTE ? 0 : 1]; +} + ThreadManager::ThreadManager() : global_(new GlobalThreadContext()) { - global_->compute_queue_size = 0; - global_->io_queue_size = 0; - global_->roundRobin = 0; + retro_atomic_int_init(&global_->dedicatedLive, 0); + retro_eventcount_init(&global_->dedicatedDone); } ThreadManager::~ThreadManager() { // Make sure the worker threads are stopped and joined before we free global_ - // otherwise, if Teardown() was never called (e.g. an early return between Init() // and the caller's normal shutdown path), the still-running threads would go on - // touching global_'s mutexes/queues/condition variables after they're freed here. + // touching global_'s queues after they're freed here. if (IsInitialized()) { Teardown(); } + retro_eventcount_free(&global_->dedicatedDone); delete global_; } -void ThreadManager::Teardown() { - for (TaskThreadContext *&threadCtx : global_->threads_) { - std::unique_lock lock(threadCtx->mutex); - threadCtx->cancelled = true; - threadCtx->cond.notify_one(); - } - - // Once cancelled is set, no worker thread will pick up any more tasks from either - // queue, so anything still sitting in the global queues at this point will never - // run. Drain them here so they're properly cancelled/released instead of leaking - // when global_ is destroyed. - { - std::unique_lock lock(global_->mutex); - for (size_t i = 0; i < TASK_PRIORITY_COUNT; ++i) { - for (Task *task : global_->compute_queue[i]) - TeardownTask(task); - global_->compute_queue[i].clear(); - for (Task *task : global_->io_queue[i]) - TeardownTask(task); - global_->io_queue[i].clear(); +static Task *TakeTask(TaskThreadContext *thread) { + TaskPool *pool = thread->pool; + for (int p = 0; p < TASK_PRIORITY_COUNT; ++p) { + Task *task = thread->local[p].Pop(); + if (task) + return task; + if (!mpsc_stack_empty(&thread->inbox[p])) { + thread->local[p].Append(mpsc_stack_reverse(mpsc_stack_drain(&thread->inbox[p]))); + if ((task = thread->local[p].Pop()) != nullptr) + return task; } - global_->compute_queue_size = 0; - global_->io_queue_size = 0; + if ((task = pool->ring[p].Pop()) != nullptr) + return task; + if (!mpsc_stack_empty(&pool->overflow[p])) { + thread->local[p].Append(mpsc_stack_reverse(mpsc_stack_drain(&pool->overflow[p]))); + if ((task = thread->local[p].Pop()) != nullptr) + return task; + } + } + return nullptr; +} + +static bool HasWork(TaskThreadContext *thread) { + TaskPool *pool = thread->pool; + for (int p = 0; p < TASK_PRIORITY_COUNT; ++p) { + if (thread->local[p].head || !mpsc_stack_empty(&thread->inbox[p]) || + pool->ring[p].HasItems() || !mpsc_stack_empty(&pool->overflow[p])) + return true; + } + return false; +} + +static void WorkerThreadFunc(void *arg) { + TaskThreadContext *thread = (TaskThreadContext *)arg; + if (thread->type == TaskType::CPU_COMPUTE) { + snprintf(thread->name, sizeof(thread->name), "PoolW %d", thread->index); + } else { + _assert_(thread->type == TaskType::IO_BLOCKING); + snprintf(thread->name, sizeof(thread->name), "PoolW IO %d", thread->index); + } + SetCurrentThreadName(thread->name); + + + TaskPool *pool = thread->pool; + retro_atomic_int_t *idleWord = &pool->idle[thread->poolIndex / IDLE_BITS_PER_WORD]; + const int idleBit = 1 << (thread->poolIndex % IDLE_BITS_PER_WORD); + + while (!retro_atomic_load_acquire_int(&thread->cancelled)) { + Task *task = TakeTask(thread); + if (task) { + // The task itself takes care of notifying anyone waiting on it. + task->Run(); + task->Release(); + continue; + } + + // Nothing to do: publish idleness, then re-check before sleeping. + retro_atomic_fetch_or_int(idleWord, idleBit); + retro_atomic_thread_fence_seq_cst(); + const int key = retro_eventcount_prepare_wait(&thread->wake); + if (HasWork(thread) || retro_atomic_load_acquire_int(&thread->cancelled)) { + retro_eventcount_cancel_wait(&thread->wake); + } else { + retro_eventcount_commit_wait(&thread->wake, key); + } + retro_atomic_fetch_and_int(idleWord, ~idleBit); } - for (TaskThreadContext *&threadCtx : global_->threads_) { - threadCtx->thread.join(); - // Same reasoning as above - anything left in a thread's private queue at this - // point will never run, since the thread has already exited. - for (size_t i = 0; i < TASK_PRIORITY_COUNT; ++i) { - for (Task *task : threadCtx->private_queue[i]) { +} + +// Wakes one parked worker of the pool, if there is one. Call after publishing the task. +static void WakeOneIdle(TaskPool &pool) { + retro_atomic_thread_fence_seq_cst(); + for (int w = 0; w < pool.idleWords; w++) { + int bits = retro_atomic_load_acquire_int(&pool.idle[w]); + while (bits) { + const int bit = bits & -bits; + const int old = retro_atomic_fetch_and_int(&pool.idle[w], ~bit); + if (old & bit) { + int worker = w * IDLE_BITS_PER_WORD; + for (int b = bit; b > 1; b >>= 1) + worker++; + retro_eventcount_notify(&pool.workers[worker]->wake); + return; + } + bits = old & ~bit; + } + } +} + +void ThreadManager::Teardown() { + for (TaskThreadContext *threadCtx : global_->threads_) { + retro_atomic_store_release_int(&threadCtx->cancelled, 1); + retro_eventcount_notify(&threadCtx->wake); + } + + for (TaskThreadContext *threadCtx : global_->threads_) { + sthread_join(threadCtx->thread); + threadCtx->thread = nullptr; + } + + // No worker picks up anything once cancelled, so whatever is still queued will never + // run. Cancel and release it so it doesn't leak (and waiters on it are released). + for (TaskThreadContext *threadCtx : global_->threads_) { + for (int p = 0; p < TASK_PRIORITY_COUNT; ++p) { + threadCtx->local[p].Append(mpsc_stack_reverse(mpsc_stack_drain(&threadCtx->inbox[p]))); + while (Task *task = threadCtx->local[p].Pop()) TeardownTask(task); + } + } + for (TaskPool &pool : global_->pools) { + for (int p = 0; p < TASK_PRIORITY_COUNT; ++p) { + while (Task *task = pool.ring[p].Pop()) + TeardownTask(task); + mpsc_stack_node_t *node = mpsc_stack_reverse(mpsc_stack_drain(&pool.overflow[p])); + while (node) { + mpsc_stack_node_t *next = node->next; + node->next = nullptr; + TeardownTask(TaskFromNode(node)); + node = next; } } + pool.workers.clear(); + delete[] pool.idle; + pool.idle = nullptr; + pool.idleWords = 0; + } + + for (TaskThreadContext *threadCtx : global_->threads_) { + retro_eventcount_free(&threadCtx->wake); delete threadCtx; } global_->threads_.clear(); + + // Dedicated threads are detached; wait until they're all done with their tasks so + // none of them outlives the core. + for (;;) { + if (retro_atomic_load_acquire_int(&global_->dedicatedLive) == 0) + break; + const int key = retro_eventcount_prepare_wait(&global_->dedicatedDone); + if (retro_atomic_load_acquire_int(&global_->dedicatedLive) == 0) { + retro_eventcount_cancel_wait(&global_->dedicatedDone); + break; + } + retro_eventcount_commit_wait(&global_->dedicatedDone, key); + } } void ThreadManager::TeardownTask(Task *task) { @@ -116,89 +369,11 @@ void ThreadManager::TeardownTask(Task *task) { task->Release(); } -static void WorkerThreadFunc(GlobalThreadContext *global, TaskThreadContext *thread) { - if (thread->type == TaskType::CPU_COMPUTE) { - snprintf(thread->name, sizeof(thread->name), "PoolW %d", thread->index); - } else { - _assert_(thread->type == TaskType::IO_BLOCKING); - snprintf(thread->name, sizeof(thread->name), "PoolW IO %d", thread->index); - } - SetCurrentThreadName(thread->name); - - - const bool isCompute = thread->type == TaskType::CPU_COMPUTE; - const auto global_queue_size = [isCompute, &global]() -> int { - return isCompute ? global->compute_queue_size.load() : global->io_queue_size.load(); - }; - - while (!thread->cancelled) { - Task *task = nullptr; - - // Check the global queue first, then check the private queue and wait if there's nothing to do. - if (global_queue_size() > 0) { - // Grab one from the global queue if there is any. - std::unique_lock lock(global->mutex); - std::deque *queue = isCompute ? global->compute_queue : global->io_queue; - std::atomic &queue_size = isCompute ? global->compute_queue_size : global->io_queue_size; - - for (size_t p = 0; p < TASK_PRIORITY_COUNT; ++p) { - if (!queue[p].empty()) { - task = queue[p].front(); - queue[p].pop_front(); - queue_size--; - - // We are processing one now, so mark that. - thread->queue_size++; - break; - } else if (thread->queue_size != 0) { - // Check the thread, as we prefer a HIGH thread task to a global NORMAL task. - // NOTE: lock order is global->mutex before thread->mutex - never lock them in the opposite order. - std::unique_lock lock(thread->mutex); - if (!thread->private_queue[p].empty()) { - task = thread->private_queue[p].front(); - thread->private_queue[p].pop_front(); - break; - } - } - } - } - - if (!task) { - // We didn't have any global, do we have anything on the thread? - std::unique_lock lock(thread->mutex); - for (size_t p = 0; p < TASK_PRIORITY_COUNT; ++p) { - if (thread->private_queue[p].empty()) - continue; - - task = thread->private_queue[p].front(); - thread->private_queue[p].pop_front(); - break; - } - - // We must check both queue and single again, while locked. - bool wait = !thread->cancelled && !task && global_queue_size() == 0; - - if (wait) - thread->cond.wait(lock); - } - // The task itself takes care of notifying anyone waiting on it. Not the - // responsibility of the ThreadManager (although it could be!). - if (task) { - task->Run(); - task->Release(); - // Reduce the queue size once complete. - thread->queue_size--; - // _dbg_assert_(thread->queue_size == thread->private_queue[0].size() + thread->private_queue[1].size() + thread->private_queue[2].size()); - } - } - -} - void ThreadManager::Init(int numRealCores, int numLogicalCoresPerCpu) { if (IsInitialized()) { Teardown(); } - + numComputeThreads_ = numRealCores * numLogicalCoresPerCpu; // Double it for the IO blocking threads. int numThreads = numComputeThreads_ + std::max(MIN_IO_BLOCKING_THREADS, numComputeThreads_); @@ -208,78 +383,77 @@ void ThreadManager::Init(int numRealCores, int numLogicalCoresPerCpu) { for (int i = 0; i < numThreads; i++) { TaskThreadContext *thread = new TaskThreadContext(); - thread->cancelled.store(false); + thread->global = global_; thread->type = i < numComputeThreads_ ? TaskType::CPU_COMPUTE : TaskType::IO_BLOCKING; thread->index = i; - thread->thread = std::thread(&WorkerThreadFunc, global_, thread); + thread->pool = &PoolFor(global_, thread->type); + thread->poolIndex = (int)thread->pool->workers.size(); + for (int p = 0; p < TASK_PRIORITY_COUNT; ++p) + mpsc_stack_init(&thread->inbox[p]); + retro_eventcount_init(&thread->wake); + retro_atomic_int_init(&thread->cancelled, 0); + thread->pool->workers.push_back(thread); global_->threads_.push_back(thread); } + + for (TaskPool &pool : global_->pools) { + pool.idleWords = ((int)pool.workers.size() + IDLE_BITS_PER_WORD - 1) / IDLE_BITS_PER_WORD; + pool.idle = pool.idleWords ? new retro_atomic_int_t[pool.idleWords] : nullptr; + for (int w = 0; w < pool.idleWords; w++) + retro_atomic_int_init(&pool.idle[w], 0); + } + + // Start the threads only once every context and pool is complete. + for (TaskThreadContext *thread : global_->threads_) { + thread->thread = sthread_create(&WorkerThreadFunc, thread); + _assert_msg_(thread->thread, "ThreadManager: failed to start worker %d", thread->index); + } +} + +struct DedicatedStart { + GlobalThreadContext *global; + Task *task; +}; + +static void DedicatedThreadFunc(void *arg) { + DedicatedStart *start = (DedicatedStart *)arg; + GlobalThreadContext *global = start->global; + Task *task = start->task; + delete start; + + SetCurrentThreadName("DedicatedThreadTask"); + task->Run(); + task->Release(); + + if (retro_atomic_fetch_sub_int(&global->dedicatedLive, 1) == 1) + retro_eventcount_notify(&global->dedicatedDone); } void ThreadManager::EnqueueTask(Task *task) { if (task->Type() == TaskType::DEDICATED_THREAD) { - std::thread th([=](Task *task) { - SetCurrentThreadName("DedicatedThreadTask"); - task->Run(); - task->Release(); - }, task); - th.detach(); + DedicatedStart *start = new DedicatedStart{ global_, task }; + retro_atomic_fetch_add_int(&global_->dedicatedLive, 1); + sthread_t *th = sthread_create(&DedicatedThreadFunc, start); + if (th) { + sthread_detach(th); + } else { + // Couldn't start a thread; run it here rather than lose it. + DedicatedThreadFunc(start); + } return; } _assert_msg_(IsInitialized(), "ThreadManager not initialized"); - size_t queueIndex = (size_t)task->Priority(); - int minThread; - int maxThread; - if (task->Type() == TaskType::CPU_COMPUTE) { - // only the threads reserved for heavy compute. - minThread = 0; - maxThread = numComputeThreads_; - } else { - // Only IO blocking threads (to avoid starving compute threads.) - minThread = numComputeThreads_; - maxThread = numThreads_; + const int queueIndex = (int)task->Priority(); + TaskPool &pool = PoolFor(global_, task->Type()); + _assert_msg_(!pool.workers.empty(), "ThreadManager: no threads available for task type %d (was Init() called with 0 compute threads?)", (int)task->Type()); + + if (!pool.ring[queueIndex].Push(task)) { + // Ring full - never block the producer, spill into the overflow stack instead. + mpsc_stack_push(&pool.overflow[queueIndex], &task->link_.node); } - - _assert_msg_(maxThread > minThread, "ThreadManager: no threads available for task type %d (was Init() called with 0 compute threads?)", (int)task->Type()); - - // Find a thread with no outstanding work. - _assert_(maxThread <= (int)global_->threads_.size()); - for (int threadNum = minThread; threadNum < maxThread; threadNum++) { - TaskThreadContext *thread = global_->threads_[threadNum]; - if (thread->queue_size.load() == 0) { - std::unique_lock lock(thread->mutex); - thread->private_queue[queueIndex].push_back(task); - thread->queue_size++; - thread->cond.notify_one(); - // Found it - done. - return; - } - } - - // Still not scheduled? Put it on the global queue and notify a thread chosen by round-robin. - // Not particularly scientific, but hopefully we should not run into this too much. - { - std::unique_lock lock(global_->mutex); - if (task->Type() == TaskType::CPU_COMPUTE) { - global_->compute_queue[queueIndex].push_back(task); - global_->compute_queue_size++; - } else if (task->Type() == TaskType::IO_BLOCKING) { - global_->io_queue[queueIndex].push_back(task); - global_->io_queue_size++; - } else { - _assert_(false); - } - } - - int chosenIndex = global_->roundRobin++; - chosenIndex = minThread + (chosenIndex % (maxThread - minThread)); - TaskThreadContext *&chosenThread = global_->threads_[chosenIndex]; - - // Lock the thread to ensure it gets the message. - std::unique_lock lock(chosenThread->mutex); - chosenThread->cond.notify_one(); + WakeOneIdle(pool); } void ThreadManager::EnqueueTaskOnThread(int threadNum, Task *task) { @@ -287,13 +461,8 @@ void ThreadManager::EnqueueTaskOnThread(int threadNum, Task *task) { _assert_msg_(threadNum >= 0 && threadNum < (int)global_->threads_.size(), "Bad threadnum %d(/%d) or not initialized", threadNum, (int)global_->threads_.size()); TaskThreadContext *thread = global_->threads_[threadNum]; - size_t queueIndex = (size_t)task->Priority(); - - thread->queue_size++; - - std::unique_lock lock(thread->mutex); - thread->private_queue[queueIndex].push_back(task); - thread->cond.notify_one(); + mpsc_stack_push(&thread->inbox[(int)task->Priority()], &task->link_.node); + retro_eventcount_notify(&thread->wake); } int ThreadManager::GetNumLooperThreads() const { diff --git a/Common/Thread/ThreadManager.h b/Common/Thread/ThreadManager.h index e41b83a446..c0686bda1e 100644 --- a/Common/Thread/ThreadManager.h +++ b/Common/Thread/ThreadManager.h @@ -2,6 +2,8 @@ #include +#include + // The new threadpool. // To help smart scheduling. @@ -19,9 +21,22 @@ enum class TaskPriority { COUNT, }; +class Task; + +// Intrusive link for the thread manager's lock-free queues. node stays the first member, +// so a node drained from an mpsc_stack converts straight back to its TaskLink. +struct TaskLink { + mpsc_stack_node_t node; + Task *self; +}; + // Implement this to make something that you can run on the thread manager. class Task { public: + Task() { + link_.node.next = nullptr; + link_.self = this; + } virtual ~Task() {} virtual TaskType Type() const = 0; virtual TaskPriority Priority() const = 0; @@ -29,6 +44,9 @@ public: virtual bool Cancellable() const { return false; } virtual void Cancel() {} virtual void Release() { delete this; } + + // Belongs to whichever thread manager queue holds the task. + TaskLink link_; }; class Waitable { diff --git a/Common/Thread/ThreadUtil.cpp b/Common/Thread/ThreadUtil.cpp index da69957711..c38d67c4d8 100644 --- a/Common/Thread/ThreadUtil.cpp +++ b/Common/Thread/ThreadUtil.cpp @@ -18,27 +18,13 @@ #include #include +#include +#include + #include "Common/Log.h" #include "Common/Thread/ThreadUtil.h" #include "Common/Data/Encoding/Utf8.h" -#if (PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(LINUX)) && !defined(_GNU_SOURCE) -#define _GNU_SOURCE -#endif - -#if !PPSSPP_PLATFORM(WINDOWS) -#include -#include -#include -#include -#endif - -#if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__OpenBSD__) -#include -#elif defined(__NetBSD__) -#include -#endif - #ifdef TLS_SUPPORTED static thread_local const char *curThreadName; #endif @@ -64,25 +50,22 @@ static EXCEPTION_DISPOSITION NTAPI ignore_handler(EXCEPTION_RECORD *rec, #if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP) typedef HRESULT (WINAPI *TSetThreadDescription)(HANDLE, PCWSTR); -static TSetThreadDescription g_pSetThreadDescription = nullptr; -static bool g_failedToFindSetThreadDescription = false; +// Resolved once by whichever thread gets there first; any number may race to do it. +// Holds the function, or the address of g_noSetThreadDescription when it doesn't exist. +static retro_atomic_ptr_t g_setThreadDescription; +static char g_noSetThreadDescription; -static void InitializeSetThreadDescription() { - if (g_pSetThreadDescription != nullptr || g_failedToFindSetThreadDescription) { - return; - } - - HMODULE hKernel32 = GetModuleHandle(L"kernelbase.dll"); - if (hKernel32 == nullptr) { - // Failed to find the function. Windows version too old, most likely. - g_failedToFindSetThreadDescription = true; - return; - } - g_pSetThreadDescription = reinterpret_cast(GetProcAddress(hKernel32, "SetThreadDescription")); - if (g_pSetThreadDescription == nullptr) { - g_failedToFindSetThreadDescription = true; - return; +static TSetThreadDescription GetSetThreadDescription() { + void *fn = retro_atomic_load_acquire_ptr(&g_setThreadDescription); + if (!fn) { + HMODULE hKernel32 = GetModuleHandle(L"kernelbase.dll"); + // Missing on Windows versions that are too old. + fn = hKernel32 ? (void *)GetProcAddress(hKernel32, "SetThreadDescription") : nullptr; + if (!fn) + fn = &g_noSetThreadDescription; + retro_atomic_store_release_ptr(&g_setThreadDescription, fn); } + return fn == &g_noSetThreadDescription ? nullptr : (TSetThreadDescription)fn; } void SetCurrentThreadNameThroughException(const char *threadName); @@ -98,12 +81,12 @@ const char *GetCurrentThreadName() { void SetCurrentThreadName(const char *threadName) { #if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP) - InitializeSetThreadDescription(); - if (g_pSetThreadDescription) { + TSetThreadDescription setThreadDescription = GetSetThreadDescription(); + if (setThreadDescription) { // Use the modern API wchar_t buffer[256]; ConvertUTF8ToWString(buffer, ARRAY_SIZE(buffer), threadName); - g_pSetThreadDescription(GetCurrentThread(), buffer); + setThreadDescription(GetCurrentThread(), buffer); } else { // Use the old exception hack. SetCurrentThreadNameThroughException(threadName); @@ -112,14 +95,8 @@ void SetCurrentThreadName(const char *threadName) { wchar_t buffer[256]; ConvertUTF8ToWString(buffer, ARRAY_SIZE(buffer), threadName); SetThreadDescription(GetCurrentThread(), buffer); -#elif PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(LINUX) - pthread_setname_np(pthread_self(), threadName); -#elif defined(__APPLE__) - pthread_setname_np(threadName); -#elif defined(__DragonFly__) || defined(__FreeBSD__) || defined(__OpenBSD__) - pthread_set_name_np(pthread_self(), threadName); -#elif defined(__NetBSD__) - pthread_setname_np(pthread_self(), "%s", (void*)threadName); +#else + sthread_setname(threadName); #endif // Set the locally known threadname using a thread local variable. @@ -190,27 +167,5 @@ void AssertCurrentThreadName(const char *threadName) { } int GetCurrentThreadIdForDebug() { -#if __LIBRETRO__ - // Not sure why gettid() would not be available, but it isn't. - // The return value of this function is only used in unit tests anyway... - return 1; -#elif PPSSPP_PLATFORM(WINDOWS) - return (int)GetCurrentThreadId(); -#elif PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS) - uint64_t tid = 0; - pthread_threadid_np(NULL, &tid); - return (int)tid; -#elif PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(LINUX) - // See issue 14545 - return (int)syscall(__NR_gettid); - // return (int)gettid(); -#elif defined(__DragonFly__) || defined(__FreeBSD__) - return pthread_getthreadid_np(); -#elif defined(__NetBSD__) - return _lwp_self(); -#elif defined(__OpenBSD__) - return getthrid(); -#else - return 1; -#endif + return (int)sthread_get_current_thread_id(); } diff --git a/Common/Thread/ThreadUtil.h b/Common/Thread/ThreadUtil.h index f626a12604..8b05c3f216 100644 --- a/Common/Thread/ThreadUtil.h +++ b/Common/Thread/ThreadUtil.h @@ -1,8 +1,5 @@ #pragma once -#include -#include -#include // Note that the string pointed to must have a lifetime until the end of the thread, // for AssertCurrentThreadName to work. @@ -15,32 +12,3 @@ const char *GetCurrentThreadName(); // Just gets a cheap thread identifier so that you can see different threads in debug output, // exactly what it is is badly specified and not useful for anything. int GetCurrentThreadIdForDebug(); - -// Helper to detect if a type is std::atomic -template -struct is_atomic : std::false_type {}; - -template -struct is_atomic> : std::true_type {}; - -template -struct is_atomic> : std::true_type {}; - -template -struct is_atomic> : std::true_type {}; - -template -struct is_atomic> : std::true_type {}; - -// Use on atomics to check if they're equal to one of multiple values. -template -bool equals_any(const T& first, const Ts... rest) { - // Make a single copy (or single load, if atomic) - using BaseType = std::remove_reference_t; - if constexpr (is_atomic::value) { - auto value = first.load(); - return ((value == rest) || ...); - } else { - return ((first == rest) || ...); - } -} diff --git a/Common/Thread/Waitable.h b/Common/Thread/Waitable.h index 52870edcf6..5d73a94111 100644 --- a/Common/Thread/Waitable.h +++ b/Common/Thread/Waitable.h @@ -1,55 +1,30 @@ #pragma once -#include -#include -#include +#include +#include "Common/Thread/ParkingLot.h" #include "Common/Thread/ThreadManager.h" class LimitedWaitable : public Waitable { public: LimitedWaitable() { - triggered_ = false; - } - - ~LimitedWaitable() { - // Make sure no one is still waiting, and any notify lock is released. - Notify(); + retro_atomic_int_init(&triggered_, 0); } void Wait() override { - if (triggered_) - return; - - std::unique_lock lock(mutex_); - cond_.wait(lock, [&] { return triggered_.load(); }); - } - - bool WaitFor(double budget_s) { - if (triggered_) - return true; - - uint32_t us = budget_s > 0 ? (uint32_t)(budget_s * 1000000.0) : 0; - if (us == 0) { - return false; - } - std::unique_lock lock(mutex_); - return cond_.wait_for(lock, std::chrono::microseconds(us), [&] { return triggered_.load(); }); + ParkingLotWait(this, [this] { return Ready(); }); } void Notify() { - std::unique_lock lock(mutex_); - triggered_ = true; - cond_.notify_all(); + retro_atomic_store_release_int(&triggered_, 1); + ParkingLotNotify(this); } // For simple polling. bool Ready() const { - return triggered_; + return retro_atomic_load_acquire_int(const_cast(&triggered_)) != 0; } private: - std::condition_variable cond_; - std::mutex mutex_; - std::atomic triggered_; + retro_atomic_int_t triggered_; }; diff --git a/Core/Config.cpp b/Core/Config.cpp index e7dbae6991..19cdbc9565 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -18,10 +18,8 @@ #include #include #include -#include #include #include -#include #include "ppsspp_config.h" diff --git a/Core/Config.h b/Core/Config.h index dceb04e041..73f66bb3b4 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -21,7 +21,6 @@ #include #include #include -#include #include "ppsspp_config.h" diff --git a/Core/Core.cpp b/Core/Core.cpp index de82c3bcea..8d4aa8b1e9 100644 --- a/Core/Core.cpp +++ b/Core/Core.cpp @@ -17,21 +17,21 @@ #include "ppsspp_config.h" -#include #include #include -#include -#include #include -#include #include -#include + +#include #include "Common/System/System.h" #include "Common/Profiler/Profiler.h" #include "Common/GPU/GraphicsContext.h" #include "Common/Log.h" +#include "Common/Thread/MpscQueue.h" +#include "Common/Thread/ParkingLot.h" +#include "Common/Thread/Thread.h" #include "Common/StringUtils.h" #include "Core/Core.h" #include "Core/Config.h" @@ -53,8 +53,7 @@ #include "GPU/GPU.h" #include "GPU/GPUCommon.h" -// Step command to execute next -static std::mutex g_stepMutex; +// The stepping state below belongs to the CPU thread; other threads go through Core_RunOnCPUThread(). struct CPUStepCommand { CPUStepType type; @@ -96,79 +95,75 @@ static constexpr size_t MAX_PENDING_STEPS = 8; // Task queue for Core_RunOnCPUThread(), see Core.h for the rationale. Drained from Core_RunLoopUntil() // below, so at least once per call to it (i.e. about once per host frame) even while the CPU is fully // running, and continuously (in a tight spin) while it's stepping/paused. +// Lives on the caller's stack: the caller waits until the CPU thread is done with it. struct CPUThreadTask { - std::function func; - bool done = false; + std::function *func; + retro_atomic_int_t done; }; -static std::mutex g_cpuQueueMutex; -static std::condition_variable g_cpuQueueCond; -static std::vector> g_cpuQueue; -static std::once_flag g_cpuThreadIdOnce; -static std::thread::id g_cpuThreadId; -// Published via release/acquire around g_cpuThreadIdOnce, so it's safe to check from other threads -// without taking g_cpuQueueMutex - g_cpuThreadId itself never changes once this becomes true. -static std::atomic g_cpuThreadIdValid{ false }; +static MpscQueue g_cpuQueue; +// Bumped after each push to g_cpuQueue, and by Core_WakeCPUThread(). +static EventCounter g_cpuWork; +// Whichever thread last drained the queue. 0 until the CPU loop has run. +static retro_atomic_size_t g_cpuThreadId; void Core_RunOnCPUThread(std::function func) { - if (g_cpuThreadIdValid.load(std::memory_order_acquire) && std::this_thread::get_id() == g_cpuThreadId) { - // Already on the CPU thread (or called before it's ever run) - just do it now, avoids deadlock. + const size_t cpuThread = retro_atomic_load_acquire_size(&g_cpuThreadId); + if (cpuThread != 0 && cpuThread == (size_t)CurrentThreadId()) { + // Already on the CPU thread - just do it now, avoids deadlock. func(); return; } - auto task = std::make_shared(); - task->func = std::move(func); + CPUThreadTask task; + task.func = &func; + retro_atomic_int_init(&task.done, 0); + g_cpuQueue.Push(&task); + g_cpuWork.Notify(); + ParkingLotWait(&task, [&] { return retro_atomic_load_acquire_int(&task.done) != 0; }); +} - std::unique_lock guard(g_cpuQueueMutex); - g_cpuQueue.push_back(task); - g_cpuQueueCond.wait(guard, [&] { return task->done; }); +int Core_CPUWorkSeen() { + return g_cpuWork.Seen(); +} + +void Core_WaitForCPUWork(int seen) { + g_cpuWork.Wait(seen); +} + +void Core_WakeCPUThread() { + g_cpuWork.Notify(); } // Called from the CPU thread only. void Core_ProcessCPUQueue() { - std::call_once(g_cpuThreadIdOnce, [] { - g_cpuThreadId = std::this_thread::get_id(); - g_cpuThreadIdValid.store(true, std::memory_order_release); - }); + retro_atomic_store_release_size(&g_cpuThreadId, (size_t)CurrentThreadId()); // Piggybacking on the one function that's reliably called on the CPU thread both in game // (Core_RunLoopUntil) and at the menu (NativeFrame) - see WebSocketDebuggerTick(). WebSocketDebuggerTick(); - std::vector> tasks; - { - std::lock_guard guard(g_cpuQueueMutex); - if (g_cpuQueue.empty()) - return; - tasks = std::move(g_cpuQueue); - g_cpuQueue.clear(); - } - - for (auto &task : tasks) - task->func(); - - { - std::lock_guard guard(g_cpuQueueMutex); - for (auto &task : tasks) - task->done = true; - } - g_cpuQueueCond.notify_all(); + g_cpuQueue.Drain([](CPUThreadTask *task) { + (*task->func)(); + // The waiter may return and free the task as soon as it sees this. + retro_atomic_store_release_int(&task->done, 1); + ParkingLotNotify(task); + }); } static int steppingCounter = 0; static std::set lifecycleFuncs; -// This can be read and written from ANYWHERE. -volatile CoreState coreState = CORE_POWERDOWN; +// Written on the CPU thread; see SharedCoreState. +SharedCoreState coreState{ { (int)CORE_POWERDOWN } }; CoreState preGeCoreState = CORE_POWERDOWN; // If true, core state has been changed, but JIT has probably not noticed yet. -volatile bool coreStatePending = false; +retro_atomic_int_t coreStatePending{ 0 }; static bool powerSaving = false; static bool g_breakAfterFrame = false; static BreakReason g_breakReason = BreakReason::None; -// Detail about the breakpoint that caused the current break, if it was one. Guarded by g_stepMutex -// alongside g_cpuStepCommand, which is what it belongs to. +// Detail about the breakpoint that caused the current break, if it was one. Belongs with +// g_cpuStepCommand. static BreakpointHit g_breakHit; static MIPSExceptionInfo g_exceptionInfo; @@ -268,7 +263,7 @@ void Core_NotifyLifecycle(CoreLifecycle stage) { if (stage == CoreLifecycle::STARTING) { Core_ResetException(); // A step queued against the game that just went away must not run against the new one. - std::lock_guard guard(g_stepMutex); + // Boot runs while the CPU loop doesn't, so this doesn't race it. g_cpuStepQueue.clear(); g_cpuStepCommand.clear(); } @@ -286,7 +281,7 @@ void Core_Stop() { void Core_UpdateState(CoreState newState) { const CoreState state = coreState; if ((state == CORE_RUNNING_CPU || state == CORE_NEXTFRAME) && newState != CORE_RUNNING_CPU) - coreStatePending = true; + retro_atomic_store_release_int(&coreStatePending, 1); coreState = newState; } @@ -297,16 +292,16 @@ bool Core_IsStepping() { bool Core_IsActive() { const CoreState state = coreState; - return state == CORE_RUNNING_CPU || state == CORE_NEXTFRAME || coreStatePending; + return state == CORE_RUNNING_CPU || state == CORE_NEXTFRAME || retro_atomic_load_acquire_int(&coreStatePending); } bool Core_IsInactive() { const CoreState state = coreState; - return state != CORE_RUNNING_CPU && state != CORE_NEXTFRAME && !coreStatePending; + return state != CORE_RUNNING_CPU && state != CORE_NEXTFRAME && !retro_atomic_load_acquire_int(&coreStatePending); } void Core_StateProcessed() { - coreStatePending = false; + retro_atomic_store_release_int(&coreStatePending, 0); } void Core_SetPowerSaving(bool mode) { @@ -399,7 +394,6 @@ void Core_SwitchToGe() { } bool Core_RequestCPUStep(CPUStepType type) { - std::lock_guard guard(g_stepMutex); if (g_cpuStepQueue.size() >= MAX_PENDING_STEPS) { ERROR_LOG(Log::CPU, "Too many steps queued (%d), dropping this one", (int)g_cpuStepQueue.size()); return false; @@ -533,9 +527,6 @@ static bool Core_ProcessStepping(MIPSDebugInterface *cpu) { lastSteppingCounter = steppingCounter; } - // Need to check inside the lock to avoid races. - std::lock_guard guard(g_stepMutex); - if (coreState != CORE_STEPPING_CPU) { return true; } @@ -566,7 +557,7 @@ static bool Core_ProcessStepping(MIPSDebugInterface *cpu) { return true; } -// Free-threaded (hm, possibly except tracing). +// CPU thread only. void Core_Break(BreakReason reason, u32 relatedAddress, const BreakpointHit *hit) { const CoreState state = coreState; if (state != CORE_RUNNING_CPU) { @@ -580,7 +571,6 @@ void Core_Break(BreakReason reason, u32 relatedAddress, const BreakpointHit *hit } { - std::lock_guard lock(g_stepMutex); if (!g_cpuStepCommand.empty() && Core_IsStepping()) { // If we're in a failed step that uses a temp breakpoint, we need to be able to override it here. switch (g_cpuStepCommand.type) { @@ -624,7 +614,7 @@ void Core_Break(BreakReason reason, u32 relatedAddress, const BreakpointHit *hit System_Notify(SystemNotification::DEBUG_MODE_CHANGE); } -// Free-threaded (or at least should be) +// CPU thread only. void Core_Resume() { // If the current PC is on a breakpoint, the user doesn't want to do nothing. if (currentMIPS) { @@ -674,7 +664,6 @@ int Core_GetSteppingCounter() { SteppingReason Core_GetSteppingReason() { SteppingReason r{}; - std::lock_guard lock(g_stepMutex); // Deliberately not gated on g_cpuStepCommand.empty(): that's true whenever there's no // pending step *type* to execute, which is also the normal state right after Core_Break() // records a reason (it sets type = CPUStepType::None on purpose - there's no step operation diff --git a/Core/Core.h b/Core/Core.h index 95a79d95ee..d3073c76df 100644 --- a/Core/Core.h +++ b/Core/Core.h @@ -19,10 +19,11 @@ #include #include -#include #include #include +#include + #include "Common/CommonTypes.h" #include "Core/ConfigValues.h" @@ -113,7 +114,7 @@ struct BreakpointHit { std::string source; }; -// Async, called from gui +// The stepping functions below are CPU thread only; other threads use Core_RunOnCPUThread(). // hit is optional detail for the breakpoint kinds, forwarded to the debugger. Only stored when // the break actually takes effect, so a rejected Core_Break() can't leave a stale one behind. void Core_Break(BreakReason reason, u32 relatedAddress = 0, const BreakpointHit *hit = nullptr); @@ -123,8 +124,7 @@ void Core_Resume(); BreakReason Core_BreakReason(); -// This should be called externally. -// Can fail if another step type was requested this frame. +// Can fail if too many steps are queued already. // stepSize is always in instructions (4 bytes each), never bytes - see Core_PerformCPUStep in Core.cpp. bool Core_RequestCPUStep(CPUStepType stepType); @@ -203,18 +203,42 @@ void Core_ReenterDispatcher(); // If you've done things that mess with caches, // Blocks the calling thread until func has actually run, so don't call this from the CPU thread with // something that would itself try to wait on the CPU thread - that'll deadlock. // -// Drained at the top of every Core_RunLoopUntil() iteration, so it's reached continuously (in a tight -// spin) while the CPU is stepping/paused, and at least once per call (i.e. about once per host frame) -// even while it's fully running. +// Drained at the top of every Core_RunLoopUntil() iteration, so at least once per call (about once +// per host frame), and by Core_WaitForCPUWork() while the CPU thread has nothing else to do. void Core_RunOnCPUThread(std::function func); // Drains the queue Core_RunOnCPUThread() feeds. Called from the top of every Core_RunLoopUntil() // iteration, on the CPU thread only. void Core_ProcessCPUQueue(); +// For a CPU thread with nothing to do (stopped in the debugger, or waiting for another thread to +// finish): read the counter, check your condition, then wait. Returns once something was queued +// for the CPU thread or Core_WakeCPUThread() was called since. The caller then drains the queue. +int Core_CPUWorkSeen(); +void Core_WaitForCPUWork(int seen); +// Any thread. Wakes a Core_WaitForCPUWork() so it rechecks its condition. +void Core_WakeCPUThread(); -extern volatile CoreState coreState; -extern volatile bool coreStatePending; + +// Written on the CPU thread, read anywhere. The jit reads it in place, so the value has to be the +// first (and only) member. +struct SharedCoreState { + retro_atomic_int_t value; + + operator CoreState() const { + return (CoreState)retro_atomic_load_acquire_int(const_cast(&value)); + } + SharedCoreState &operator=(CoreState state) { + retro_atomic_store_release_int(&value, (int)state); + return *this; + } +}; + +static_assert(sizeof(SharedCoreState) == 4, "The jit accesses coreState as a 32-bit int"); + +extern SharedCoreState coreState; +// Set by Core_UpdateState until the CPU loop picks the change up. +extern retro_atomic_int_t coreStatePending; void Core_UpdateState(CoreState newState); diff --git a/Core/CoreTiming.cpp b/Core/CoreTiming.cpp index ca20954121..5ccc4440f0 100644 --- a/Core/CoreTiming.cpp +++ b/Core/CoreTiming.cpp @@ -16,11 +16,9 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include -#include #include #include #include -#include #include #include diff --git a/Core/Debugger/Breakpoints.cpp b/Core/Debugger/Breakpoints.cpp index bcdc07ec3d..0eda14fda6 100644 --- a/Core/Debugger/Breakpoints.cpp +++ b/Core/Debugger/Breakpoints.cpp @@ -15,8 +15,6 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include - #include "Common/System/System.h" #include "Common/Log.h" #include "Core/Core.h" @@ -104,7 +102,7 @@ size_t BreakpointManager::FindBreakpoint(u32 addr) { } void BreakpointManager::UpdateAnyBreakPoints() { - anyBreakPoints_ = !breakPoints_.empty() || tempBreakPoint_.valid; + retro_atomic_store_relaxed_int(&anyBreakPoints_, (!breakPoints_.empty() || tempBreakPoint_.valid) ? 1 : 0); } size_t BreakpointManager::FindMemCheck(u32 start, u32 end) { @@ -126,7 +124,7 @@ size_t BreakpointManager::FindRegBreakpoint(int reg) { } bool BreakpointManager::IsAddressBreakPoint(u32 addr) { - if (!anyBreakPoints_) + if (!HasBreakPoints()) return false; size_t bp = FindBreakpoint(addr); if (bp == INVALID_BREAKPOINT) { @@ -136,7 +134,7 @@ bool BreakpointManager::IsAddressBreakPoint(u32 addr) { } bool BreakpointManager::IsAddressBreakPoint(u32 addr, bool* enabled) { - if (!anyBreakPoints_) + if (!HasBreakPoints()) return false; size_t bp = FindBreakpoint(addr); if (bp == INVALID_BREAKPOINT) { @@ -149,7 +147,7 @@ bool BreakpointManager::IsAddressBreakPoint(u32 addr, bool* enabled) { } bool BreakpointManager::NeedsBreakCheckAt(u32 addr) { - if (!anyBreakPoints_) + if (!HasBreakPoints()) return false; if (tempBreakPoint_.valid && tempBreakPoint_.addr == addr) return true; @@ -159,7 +157,7 @@ bool BreakpointManager::NeedsBreakCheckAt(u32 addr) { bool BreakpointManager::RangeContainsBreakPoint(u32 addr, u32 size) { - if (!anyBreakPoints_) + if (!HasBreakPoints()) return false; const u32 end = addr + size; if (tempBreakPoint_.valid && tempBreakPoint_.addr >= addr && tempBreakPoint_.addr < end) @@ -300,7 +298,7 @@ bool BreakpointManager::ChangeBreakPointAddress(u32 oldAddr, u32 newAddr) { // This is not actually called, currently. void BreakpointManager::ClearAllBreakPoints() { - if (!anyBreakPoints_) + if (!HasBreakPoints()) return; if (!breakPoints_.empty()) { for (const auto &bp : breakPoints_) { @@ -353,7 +351,7 @@ void BreakpointManager::ChangeBreakPointLogFormat(u32 addr, const std::string &f // and must still let the step complete. Whichever of them pauses, Core_Break() drops the temporary // breakpoint, so a step that gets interrupted by something else doesn't leave one armed behind it. BreakAction BreakpointManager::ExecBreakPoint(u32 addr) { - if (!anyBreakPoints_) + if (!HasBreakPoints()) return BREAK_ACTION_NONE; BreakAction result = BREAK_ACTION_NONE; @@ -434,7 +432,7 @@ int BreakpointManager::AddMemCheck(u32 start, u32 end, MemCheckCondition cond, B check.action = action; memChecks_.push_back(check); - bool hadAny = anyMemChecks_.exchange(true); + bool hadAny = retro_atomic_exchange_int(&anyMemChecks_, 1) != 0; if (!hadAny) { MemBlockOverrideDetailed(); } @@ -446,7 +444,7 @@ int BreakpointManager::AddMemCheck(u32 start, u32 end, MemCheckCondition cond, B // Update with additional cond and action bits. Not sure if we should OR or override? memChecks_[mc].cond = (MemCheckCondition)(memChecks_[mc].cond | cond); memChecks_[mc].action = memChecks_[mc].action | action; - bool hadAny = anyMemChecks_.exchange(true); + bool hadAny = retro_atomic_exchange_int(&anyMemChecks_, 1) != 0; if (!hadAny) { MemBlockOverrideDetailed(); } @@ -463,7 +461,7 @@ void BreakpointManager::RemoveMemCheck(u32 start, u32 end) if (mc != INVALID_MEMCHECK) { memChecks_.erase(memChecks_.begin() + mc); - bool hadAny = anyMemChecks_.exchange(!memChecks_.empty()); + bool hadAny = retro_atomic_exchange_int(&anyMemChecks_, memChecks_.empty() ? 0 : 1) != 0; if (hadAny) MemBlockReleaseDetailed(); updateMemChecks_ = true; @@ -490,7 +488,7 @@ void BreakpointManager::ClearAllMemChecks() if (!memChecks_.empty()) { memChecks_.clear(); - bool hadAny = anyMemChecks_.exchange(false); + bool hadAny = retro_atomic_exchange_int(&anyMemChecks_, 0) != 0; if (hadAny) MemBlockReleaseDetailed(); updateMemChecks_ = true; @@ -577,7 +575,7 @@ MemCheck *BreakpointManager::FindMemCheckInRange(u32 address, int size) { BreakAction BreakpointManager::ExecMemCheck(u32 address, bool write, int size, u32 pc, const char *reason) { - if (!anyMemChecks_) + if (!HasMemChecks()) return BREAK_ACTION_NONE; MemCheck *check = FindMemCheckInRange(address, size); if (check) { @@ -631,7 +629,7 @@ void BreakpointManager::RecomputeRegBreakpointMask() { if (bp.result != BREAK_ACTION_NONE) mask |= 1u << bp.reg; } - regBreakpointMask_ = mask; + retro_atomic_store_relaxed_int(®BreakpointMask_, (int)mask); } int BreakpointManager::AddRegBreakpoint(int reg) { @@ -684,7 +682,7 @@ void BreakpointManager::ChangeRegBreakpoint(int reg, BreakAction result) { void BreakpointManager::ClearAllRegBreakpoints() { if (!regBreakpoints_.empty()) { regBreakpoints_.clear(); - regBreakpointMask_ = 0; + retro_atomic_store_relaxed_int(®BreakpointMask_, 0); } } @@ -718,7 +716,7 @@ void BreakpointManager::ChangeRegBreakpointLogFormat(int reg, const std::string } bool BreakpointManager::IsRegBreakpoint(int reg) { - return (regBreakpointMask_ & (1u << reg)) != 0; + return (GetRegBreakpointMask() & (1u << reg)) != 0; } bool BreakpointManager::GetRegBreakpoint(int reg, RegBreakpoint *check) { @@ -739,7 +737,7 @@ BreakAction BreakpointManager::ExecRegBreakpoint(int reg, u32 pc) { // the whole point of exposing it - a single shift+and in the hot interpreter loop, skipping // a function call entirely in the overwhelmingly common no-breakpoint case), but check again // here too since this is also reachable directly. - if ((regBreakpointMask_ & (1u << reg)) == 0) + if ((GetRegBreakpointMask() & (1u << reg)) == 0) return BREAK_ACTION_NONE; size_t bp = FindRegBreakpoint(reg); if (bp == INVALID_REG_BREAKPOINT) @@ -878,7 +876,7 @@ std::vector BreakpointManager::GetBreakpoints() { } void BreakpointManager::Frame() { - if (anyMemChecks_ && updateMemChecks_) { + if (HasMemChecks() && updateMemChecks_) { UpdateCachedMemCheckRanges(); updateMemChecks_ = false; } diff --git a/Core/Debugger/Breakpoints.h b/Core/Debugger/Breakpoints.h index 9713d1b1e1..7230588fef 100644 --- a/Core/Debugger/Breakpoints.h +++ b/Core/Debugger/Breakpoints.h @@ -18,7 +18,7 @@ #pragma once #include -#include +#include #include "Core/MIPS/MIPSDebugInterface.h" #include "Common/Math/expression_parser.h" @@ -268,15 +268,15 @@ public: // NOTE: If you edit this array directly, you need to call NotifyChangedMemchecks(). std::vector &GetMemCheckRefs() { return memChecks_; } - bool HasBreakPoints() const { return anyBreakPoints_; } - bool HasMemChecks() const { return anyMemChecks_; } + bool HasBreakPoints() const { return retro_atomic_load_relaxed_int(const_cast(&anyBreakPoints_)) != 0; } + bool HasMemChecks() const { return retro_atomic_load_relaxed_int(const_cast(&anyMemChecks_)) != 0; } void NotifyChangedMemchecks() { updateMemChecks_ = true; } // Bit i set means register i has an active (non-ignored) register breakpoint - a cheap way // for the interpreter's hot per-instruction loop to test "would this write trip anything". - u32 GetRegBreakpointMask() const { return regBreakpointMask_; } - bool HasRegBreakpoints() const { return regBreakpointMask_ != 0; } + u32 GetRegBreakpointMask() const { return (u32)retro_atomic_load_relaxed_int(const_cast(®BreakpointMask_)); } + bool HasRegBreakpoints() const { return GetRegBreakpointMask() != 0; } void Frame(); @@ -297,9 +297,11 @@ private: size_t FindRegBreakpoint(int reg); void RecomputeRegBreakpointMask(); - std::atomic anyBreakPoints_; - std::atomic anyMemChecks_; - std::atomic regBreakpointMask_; + // Everything else is the CPU thread's; these flags are also read from other threads + // (memory tagging on GPU and audio worker threads) as a fast "anything set?" check. + retro_atomic_int_t anyBreakPoints_{ 0 }; + retro_atomic_int_t anyMemChecks_{ 0 }; + retro_atomic_int_t regBreakpointMask_{ 0 }; std::vector breakPoints_; TempBreakPoint tempBreakPoint_; diff --git a/Core/Debugger/DisassemblyManager.cpp b/Core/Debugger/DisassemblyManager.cpp index 893b21c42b..6e3083bdcf 100644 --- a/Core/Debugger/DisassemblyManager.cpp +++ b/Core/Debugger/DisassemblyManager.cpp @@ -205,7 +205,6 @@ void DisassemblyManager::analyze(u32 address, u32 size = 1024) if (PSP_GetBootState() != BootState::Complete) return; - std::lock_guard guard(entriesLock_); auto it = findDisassemblyEntry(entries, address, false); if (it != entries.end()) { @@ -280,7 +279,6 @@ std::vector DisassemblyManager::getBranchLines(u32 start, u32 size) { std::vector result; - std::lock_guard guard(entriesLock_); auto it = findDisassemblyEntry(entries,start,false); if (it != entries.end()) { @@ -296,7 +294,6 @@ std::vector DisassemblyManager::getBranchLines(u32 start, u32 size) void DisassemblyManager::getLine(u32 address, bool insertSymbols, DisassemblyLineInfo &dest, DebugInterface *cpuDebug) { - std::lock_guard guard(entriesLock_); auto it = findDisassemblyEntry(entries,address,false); if (it == entries.end()) { analyze(address); @@ -327,7 +324,6 @@ void DisassemblyManager::getLine(u32 address, bool insertSymbols, DisassemblyLin u32 DisassemblyManager::getStartAddress(u32 address) { - std::lock_guard guard(entriesLock_); auto it = findDisassemblyEntry(entries,address,false); if (it == entries.end()) { @@ -344,7 +340,6 @@ u32 DisassemblyManager::getStartAddress(u32 address) u32 DisassemblyManager::getNthPreviousAddress(u32 address, int n) { - std::lock_guard guard(entriesLock_); while (Memory::IsValidAddress(address)) { auto it = findDisassemblyEntry(entries,address,false); @@ -371,7 +366,6 @@ u32 DisassemblyManager::getNthPreviousAddress(u32 address, int n) u32 DisassemblyManager::getNthNextAddress(u32 address, int n) { - std::lock_guard guard(entriesLock_); while (Memory::IsValidAddress(address)) { auto it = findDisassemblyEntry(entries,address,false); @@ -405,7 +399,6 @@ DisassemblyManager::~DisassemblyManager() { void DisassemblyManager::clear() { - std::lock_guard guard(entriesLock_); for (auto it = entries.begin(); it != entries.end(); it++) { delete it->second; @@ -442,13 +435,11 @@ void DisassemblyFunction::recheck() int DisassemblyFunction::getNumLines() { - std::lock_guard guard(lock_); return (int) lineAddresses.size(); } int DisassemblyFunction::getLineNum(u32 address, bool findStart) { - std::lock_guard guard(lock_); if (findStart) { int last = (int)lineAddresses.size() - 1; @@ -478,7 +469,6 @@ int DisassemblyFunction::getLineNum(u32 address, bool findStart) u32 DisassemblyFunction::getLineAddress(int line) { - std::lock_guard guard(lock_); // A zero-size function leaves lineAddresses empty (e.g. from a symbol added // with size 0 via the WebSocket debugger API or a crafted ELF symtab entry); // fall back to the function's own base address rather than indexing OOB. @@ -489,7 +479,6 @@ u32 DisassemblyFunction::getLineAddress(int line) bool DisassemblyFunction::disassemble(u32 address, DisassemblyLineInfo &dest, bool insertSymbols, DebugInterface *cpuDebug) { - std::lock_guard guard(lock_); auto it = findDisassemblyEntry(entries,address,false); if (it == entries.end()) return false; @@ -501,7 +490,6 @@ void DisassemblyFunction::getBranchLines(u32 start, u32 size, std::vector guard(lock_); for (size_t i = 0; i < lines.size(); i++) { BranchLine& line = lines[i]; @@ -534,7 +522,6 @@ void DisassemblyFunction::generateBranchLines() u32 end = address+size; - std::lock_guard guard(lock_); DebugInterface *cpu = g_disassemblyManager.getCpu(); for (u32 funcPos = address; funcPos < end; funcPos += 4) { @@ -595,7 +582,6 @@ void DisassemblyFunction::generateBranchLines() void DisassemblyFunction::addOpcodeSequence(u32 start, u32 end) { DisassemblyOpcode* opcode = new DisassemblyOpcode(start,(end-start)/4); - std::lock_guard guard(lock_); entries[start] = opcode; lineAddresses.reserve((end - start) / 4); for (u32 pos = start; pos < end; pos += 4) @@ -622,7 +608,6 @@ void DisassemblyFunction::load() addOpcodeSequence(opcodeSequenceStart,funcPos); DisassemblyData* data = new DisassemblyData(funcPos,g_symbolMap->GetDataSize(funcPos),g_symbolMap->GetDataType(funcPos)); - std::lock_guard guard(lock_); entries[funcPos] = data; lineAddresses.push_back(funcPos); funcPos += data->getTotalSize(); @@ -638,7 +623,6 @@ void DisassemblyFunction::load() u32 nextPos = (funcPos+3) & ~3; DisassemblyComment* comment = new DisassemblyComment(funcPos,nextPos-funcPos,".align","4"); - std::lock_guard guard(lock_); entries[funcPos] = comment; lineAddresses.push_back(funcPos); @@ -666,7 +650,6 @@ void DisassemblyFunction::load() void DisassemblyFunction::clear() { - std::lock_guard guard(lock_); for (auto it = entries.begin(); it != entries.end(); it++) { delete it->second; @@ -771,7 +754,6 @@ bool DisassemblyData::disassemble(u32 address, DisassemblyLineInfo &dest, bool i return false; } - std::lock_guard guard(lock_); auto it = lines.find(address); if (it == lines.end()) return false; @@ -783,7 +765,6 @@ bool DisassemblyData::disassemble(u32 address, DisassemblyLineInfo &dest, bool i int DisassemblyData::getLineNum(u32 address, bool findStart) { - std::lock_guard guard(lock_); auto it = lines.upper_bound(address); if (it != lines.end()) { @@ -798,7 +779,6 @@ int DisassemblyData::getLineNum(u32 address, bool findStart) void DisassemblyData::createLines() { - std::lock_guard guard(lock_); lines.clear(); lineAddresses.clear(); diff --git a/Core/Debugger/DisassemblyManager.h b/Core/Debugger/DisassemblyManager.h index 54acf2c87b..72b257c758 100644 --- a/Core/Debugger/DisassemblyManager.h +++ b/Core/Debugger/DisassemblyManager.h @@ -21,7 +21,6 @@ #include #include "ppsspp_config.h" -#include #include "Common/CommonTypes.h" #include "Core/Debugger/SymbolMap.h" #include "Core/MIPS/MIPSAnalyst.h" @@ -97,7 +96,6 @@ private: std::vector lines; std::map entries; std::vector lineAddresses; - std::recursive_mutex lock_; }; class DisassemblyOpcode: public DisassemblyEntry @@ -152,7 +150,6 @@ private: DataType type; std::map lines; std::vector lineAddresses; - std::recursive_mutex lock_; }; class DisassemblyComment: public DisassemblyEntry @@ -176,6 +173,7 @@ private: class DebugInterface; +// All of it is the CPU thread's: the debugger reaches it through Core_RunOnCPUThread(). class DisassemblyManager { public: ~DisassemblyManager(); @@ -197,7 +195,6 @@ public: private: std::map entries; - std::recursive_mutex entriesLock_; DebugInterface *cpu_; int maxParamChars = 29; }; diff --git a/Core/Debugger/MemBlockInfo.cpp b/Core/Debugger/MemBlockInfo.cpp index bd0838e3dc..57e9ab7471 100644 --- a/Core/Debugger/MemBlockInfo.cpp +++ b/Core/Debugger/MemBlockInfo.cpp @@ -16,11 +16,9 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include -#include #include -#include -#include -#include + +#include #include "Common/Log.h" #include "Common/Serialize/Serializer.h" @@ -35,6 +33,8 @@ #include "Core/MIPS/MIPS.h" #include "Common/StringUtils.h" #include "Core/Debugger/SymbolMap.h" +#include "Core/HW/Display.h" +#include "Common/Thread/MpscQueue.h" class MemSlabMap { public: @@ -95,28 +95,15 @@ struct PendingNotifyMem { char tag[127]; }; -// 160 KB. -static constexpr size_t MAX_PENDING_NOTIFIES = 1024; -static constexpr size_t MAX_PENDING_NOTIFIES_THREAD = 1000; +// The slab maps belong to the emulation thread. Notifications come from any thread (the CPU +// thread, software rasterizer and SAS workers), so they queue up in pendingNotifies, which the +// emulation thread drains before every lookup, on each flip, and for savestates. static MemSlabMap allocMap; static MemSlabMap suballocMap; static MemSlabMap writeMap; static MemSlabMap textureMap; -static std::vector pendingNotifies; -static std::atomic pendingNotifyMinAddr1; -static std::atomic pendingNotifyMaxAddr1; -static std::atomic pendingNotifyMinAddr2; -static std::atomic pendingNotifyMaxAddr2; -// To prevent deadlocks, acquire Read before Write if you're going to acquire both. -static std::mutex pendingWriteMutex; -static std::mutex pendingReadMutex; -static int detailedOverride; - -static std::thread flushThread; -static std::atomic flushThreadRunning; -static std::atomic flushThreadPending; -static std::mutex flushLock; -static std::condition_variable flushCond; +static MpscQueue pendingNotifies; +static retro_atomic_int_t detailedOverride{ 0 }; MemSlabMap::MemSlabMap() { Reset(); @@ -412,28 +399,14 @@ void MemSlabMap::FillHeads(Slab *slab) { size_t FormatMemWriteTagAtNoFlush(char *buf, size_t sz, const char *prefix, size_t prefixLen, uint32_t start, uint32_t size); +// Emulation thread. void FlushPendingMemInfo() { - // This lock prevents us from another thread reading while we're busy flushing. - std::lock_guard guard(pendingReadMutex); - std::vector thisBatch; - { - std::lock_guard guard(pendingWriteMutex); - thisBatch = std::move(pendingNotifies); - pendingNotifies.clear(); - pendingNotifies.reserve(MAX_PENDING_NOTIFIES); - - pendingNotifyMinAddr1 = 0xFFFFFFFF; - pendingNotifyMaxAddr1 = 0; - pendingNotifyMinAddr2 = 0xFFFFFFFF; - pendingNotifyMaxAddr2 = 0; - } - - for (const auto &info : thisBatch) { + pendingNotifies.Drain([](PendingNotifyMem &&info) { if (info.copySrc != 0) { char tagData[128]; size_t tagSize = FormatMemWriteTagAtNoFlush(tagData, sizeof(tagData), info.tag, info.tagLen, info.copySrc, info.size); writeMap.Mark(info.start, info.size, info.ticks, info.pc, true, tagData, tagSize); - continue; + return; } if (info.flags & MemBlockFlags::ALLOC) { @@ -455,44 +428,25 @@ void FlushPendingMemInfo() { if (info.flags & MemBlockFlags::WRITE) { writeMap.Mark(info.start, info.size, info.ticks, info.pc, true, info.tag, info.tagLen); } + }); +} + +static void FlushIfPending() { + if (!pendingNotifies.Empty()) { + FlushPendingMemInfo(); } } +static void FlushOnFlip(void *) { + FlushPendingMemInfo(); +} + static inline uint32_t NormalizeAddress(uint32_t addr) { if ((addr & 0x3F000000) == 0x04000000) return addr & 0x041FFFFF; return addr & 0x3FFFFFFF; } -static inline bool MergeRecentMemInfo(const PendingNotifyMem &info, size_t copyLength) { - if (pendingNotifies.size() < 4) - return false; - - for (size_t i = 1; i <= 4; ++i) { - auto &prev = pendingNotifies[pendingNotifies.size() - i]; - if (prev.copySrc != 0) - return false; - - if (prev.flags != info.flags) - continue; - - if (prev.start >= info.start + info.size || prev.start + prev.size <= info.start) - continue; - - // This means there's overlap, but not a match, so we can't combine any. - if (prev.start != info.start || prev.size > info.size) - return false; - - memcpy(prev.tag, info.tag, copyLength + 1); - prev.size = info.size; - prev.ticks = info.ticks; - prev.pc = info.pc; - return true; - } - - return false; -} - void NotifyMemInfoPC(MemBlockFlags flags, uint32_t start, uint32_t size, uint32_t pc, const char *tagStr, size_t strLength) { if (size == 0) { return; @@ -500,7 +454,6 @@ void NotifyMemInfoPC(MemBlockFlags flags, uint32_t start, uint32_t size, uint32_ // Clear the uncached and kernel bits. start = NormalizeAddress(start); - bool needFlush = false; // When the setting is off, we skip smaller info to keep things fast. if (MemBlockInfoDetailed(size) && flags != MemBlockFlags::READ) { PendingNotifyMem info{ flags, start, size }; @@ -515,27 +468,7 @@ void NotifyMemInfoPC(MemBlockFlags flags, uint32_t start, uint32_t size, uint32_ info.tag[copyLength] = 0; info.tagLen = (uint8_t)copyLength; - std::lock_guard guard(pendingWriteMutex); - // Sometimes we get duplicates, quickly check. - if (!MergeRecentMemInfo(info, copyLength)) { - if (start < 0x08000000) { - pendingNotifyMinAddr1 = std::min(pendingNotifyMinAddr1.load(), start); - pendingNotifyMaxAddr1 = std::max(pendingNotifyMaxAddr1.load(), start + size); - } else { - pendingNotifyMinAddr2 = std::min(pendingNotifyMinAddr2.load(), start); - pendingNotifyMaxAddr2 = std::max(pendingNotifyMaxAddr2.load(), start + size); - } - pendingNotifies.push_back(info); - } - needFlush = pendingNotifies.size() > MAX_PENDING_NOTIFIES_THREAD; - } - - if (needFlush) { - { - std::lock_guard guard(flushLock); - flushThreadPending = true; - } - flushCond.notify_one(); + pendingNotifies.Push(info); } if (!(flags & MemBlockFlags::SKIP_MEMCHECK)) { @@ -555,7 +488,6 @@ void NotifyMemInfoCopy(uint32_t destPtr, uint32_t srcPtr, uint32_t size, const c if (size == 0) return; - bool needsFlush = false; if (g_breakpoints.HasMemChecks()) { // This will cause a flush, but it's needed to trigger memchecks with proper data. char tagData[128]; @@ -575,40 +507,14 @@ void NotifyMemInfoCopy(uint32_t destPtr, uint32_t srcPtr, uint32_t size, const c info.tagLen = (uint8_t)std::min(sizeof(info.tag), prefixLen); memcpy(info.tag, prefix, info.tagLen); - std::lock_guard guard(pendingWriteMutex); - if (destPtr < 0x08000000) { - pendingNotifyMinAddr1 = std::min(pendingNotifyMinAddr1.load(), destPtr); - pendingNotifyMaxAddr1 = std::max(pendingNotifyMaxAddr1.load(), destPtr + size); - } else { - pendingNotifyMinAddr2 = std::min(pendingNotifyMinAddr2.load(), destPtr); - pendingNotifyMaxAddr2 = std::max(pendingNotifyMaxAddr2.load(), destPtr + size); - } - pendingNotifies.push_back(info); - needsFlush = pendingNotifies.size() > MAX_PENDING_NOTIFIES_THREAD; - } - - if (needsFlush) { - { - std::lock_guard guard(flushLock); - flushThreadPending = true; - } - flushCond.notify_one(); + pendingNotifies.Push(info); } } std::vector FindMemInfo(uint32_t start, uint32_t size) { start = NormalizeAddress(start); - if (pendingNotifyMinAddr1 < start + size && pendingNotifyMaxAddr1 >= start) - FlushPendingMemInfo(); - if (pendingNotifyMinAddr2 < start + size && pendingNotifyMaxAddr2 >= start) - FlushPendingMemInfo(); - - // pendingReadMutex doesn't just guard the pending queue - it's also what keeps - // the background flush thread's Mark() calls (which mutate the slab maps' - // linked lists via Split()/Merge()/delete) from running concurrently with the - // traversal below, which used to be completely unsynchronized against it. - std::lock_guard guard(pendingReadMutex); + FlushIfPending(); std::vector results; allocMap.Find(MemBlockFlags::ALLOC, start, size, results); suballocMap.Find(MemBlockFlags::SUB_ALLOC, start, size, results); @@ -620,13 +526,7 @@ std::vector FindMemInfo(uint32_t start, uint32_t size) { std::vector FindMemInfoByFlag(MemBlockFlags flags, uint32_t start, uint32_t size) { start = NormalizeAddress(start); - if (pendingNotifyMinAddr1 < start + size && pendingNotifyMaxAddr1 >= start) - FlushPendingMemInfo(); - if (pendingNotifyMinAddr2 < start + size && pendingNotifyMaxAddr2 >= start) - FlushPendingMemInfo(); - - // See the comment in FindMemInfo() above. - std::lock_guard guard(pendingReadMutex); + FlushIfPending(); std::vector results; if (flags & MemBlockFlags::ALLOC) allocMap.Find(MemBlockFlags::ALLOC, start, size, results); @@ -642,22 +542,10 @@ std::vector FindMemInfoByFlag(MemBlockFlags flags, uint32_t start, static const char *FindWriteTagByFlag(MemBlockFlags flags, uint32_t start, uint32_t size, size_t *tagLen, bool flush = true) { start = NormalizeAddress(start); - // See the comment in FindMemInfo() above. Note: the returned tag pointer is - // only valid until the next Mark() call per FastFindWriteTag()'s own contract, - // so callers must treat it as transient exactly as they already do. - // - // flush=false means we're being called from FormatMemWriteTagAtNoFlush(), which - // is only ever called from within FlushPendingMemInfo() - which already holds - // pendingReadMutex for its whole body. Locking it again here would deadlock (or - // be undefined behavior, since it's a plain non-recursive std::mutex), so only - // take the lock ourselves when we might not already be holding it. - std::unique_lock guard(pendingReadMutex, std::defer_lock); + // The returned tag pointer is only valid until the next Mark(). flush=false is for + // FormatMemWriteTagAtNoFlush(), called from inside FlushPendingMemInfo(). if (flush) { - if (pendingNotifyMinAddr1 < start + size && pendingNotifyMaxAddr1 >= start) - FlushPendingMemInfo(); - if (pendingNotifyMinAddr2 < start + size && pendingNotifyMaxAddr2 >= start) - FlushPendingMemInfo(); - guard.lock(); + FlushIfPending(); } if (flags & MemBlockFlags::ALLOC) { const char *tag = allocMap.FastFindWriteTag(MemBlockFlags::ALLOC, start, size, tagLen); @@ -711,52 +599,19 @@ size_t FormatMemWriteTagAtNoFlush(char *buf, size_t sz, const char *prefix, size return snprintf(buf, sz, "%s%08x_size_%08x", prefix, start, size); } -static void FlushMemInfoThread() { - SetCurrentThreadName("FlushMemInfo"); - - while (flushThreadRunning.load()) { - flushThreadPending = false; - FlushPendingMemInfo(); - - std::unique_lock guard(flushLock); - flushCond.wait(guard, [] { - return flushThreadPending.load(); - }); - } -} - +// Emulation thread, like the rest below. void MemBlockInfoInit() { - std::lock_guard guard(pendingReadMutex); - std::lock_guard guardW(pendingWriteMutex); - pendingNotifies.reserve(MAX_PENDING_NOTIFIES); - pendingNotifyMinAddr1 = 0xFFFFFFFF; - pendingNotifyMaxAddr1 = 0; - pendingNotifyMinAddr2 = 0xFFFFFFFF; - pendingNotifyMaxAddr2 = 0; - - flushThreadRunning = true; - flushThreadPending = false; - flushThread = std::thread(&FlushMemInfoThread); + pendingNotifies.Drain([](PendingNotifyMem &&) {}); + __DisplayListenFlip(&FlushOnFlip, nullptr); } void MemBlockInfoShutdown() { - { - std::lock_guard guard(pendingReadMutex); - std::lock_guard guardW(pendingWriteMutex); - allocMap.Reset(); - suballocMap.Reset(); - writeMap.Reset(); - textureMap.Reset(); - pendingNotifies.clear(); - } - - if (flushThreadRunning.load()) { - std::lock_guard guard(flushLock); - flushThreadRunning = false; - flushThreadPending = true; - } - flushCond.notify_one(); - flushThread.join(); + __DisplayForgetFlip(&FlushOnFlip, nullptr); + allocMap.Reset(); + suballocMap.Reset(); + writeMap.Reset(); + textureMap.Reset(); + pendingNotifies.Drain([](PendingNotifyMem &&) {}); } void MemBlockInfoDoState(PointerWrap &p) { @@ -765,8 +620,6 @@ void MemBlockInfoDoState(PointerWrap &p) { return; FlushPendingMemInfo(); - // See the comment in FindMemInfo() above. - std::lock_guard guard(pendingReadMutex); allocMap.DoState(p); suballocMap.DoState(p); writeMap.DoState(p); @@ -775,15 +628,16 @@ void MemBlockInfoDoState(PointerWrap &p) { // Used by the debugger. void MemBlockOverrideDetailed() { - detailedOverride++; + retro_atomic_fetch_add_int(&detailedOverride, 1); } void MemBlockReleaseDetailed() { - detailedOverride--; + retro_atomic_fetch_sub_int(&detailedOverride, 1); } +// Any thread. bool MemBlockInfoDetailed() { - return g_Config.bDebugMemInfoDetailed || detailedOverride != 0; + return g_Config.bDebugMemInfoDetailed || retro_atomic_load_relaxed_int(&detailedOverride) != 0; } void DescribeAddress(const MIPSDebugInterface *mips, u32 address, char *buffer, size_t bufferSize) { diff --git a/Core/Debugger/WebSocket.cpp b/Core/Debugger/WebSocket.cpp index e0d9ecf999..528104f7f6 100644 --- a/Core/Debugger/WebSocket.cpp +++ b/Core/Debugger/WebSocket.cpp @@ -15,17 +15,18 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include -#include -#include -#include #include +#include + +#include "Common/Net/Cancel.h" +#include "Common/Thread/MpscQueue.h" #include "Common/Thread/ThreadUtil.h" #include "Common/TimeUtil.h" #include "Core/Core.h" #include "Core/Debugger/WebSocket.h" #include "Core/Debugger/WebSocket/WebSocketUtils.h" +#include "Core/HLE/sceCtrl.h" // This WebSocket (connected through the same port as disc sharing) allows API/debugger access to PPSSPP. // Currently, the only subprotocol "debugger.ppsspp.org" uses a simple JSON based interface. @@ -93,91 +94,160 @@ static const std::vector subscribers({ &WebSocketClientConfigInit, }); -// To handle webserver restart, keep track of how many running. -static volatile int debuggersConnected = 0; -static volatile bool stopRequested = false; -static std::mutex stopLock; -static std::condition_variable stopCond; +// Threads: each connection runs on its own thread (see NewThreadExecutor). Handlers do all +// emulator-state access inside Core_RunOnCPUThread(); anything the emulator produces on its own +// reaches the connection through its DebuggerEventSink. See docs/DebuggerThreading.md. -// There is deliberately no lock guarding debugger handlers against the core being started or torn -// down under them: every handler either does its emulator-state access inside Core_RunOnCPUThread() -// (so it's serialized with startup/shutdown, which also run on the CPU thread), or only touches -// state that carries its own lock - the log ring buffer, ctrlMutex, GPUStepping's rendezvous. -// The lock that used to be here had to be held across a whole handler, including the blocking wait -// inside Core_RunOnCPUThread(), which deadlocked against the CPU thread taking it on STOPPING. +// A log-only breakpoint in a hot loop can produce events far faster than a connection sends them. +// Past this many queued, new ones are dropped; cpu.breakpoint.hit carries a sequence number so a +// client can tell exactly how many it missed. +static constexpr int MAX_PENDING_EVENTS = 4096; -static void UpdateConnected(int delta) { - std::lock_guard guard(stopLock); - debuggersConnected += delta; - stopCond.notify_all(); -} - -// Per-connection mailbox for events the CPU thread produces (cpu.stepping, game.start, ...). -// -// These used to be polled per connection from the WebSocket thread, which meant every connected -// debugger was reading pc, the tick count, the UI state and the param SFO out from under the CPU -// thread on every lap of its loop. Now the CPU thread notices the transition once, formats the -// event, and drops it in here; the connection's own thread just drains and sends. -// A log-only breakpoint in a hot loop can produce events far faster than a connection drains them -// (the drain runs once per lap of ws->Process, so at best a few hundred times a second). Without a -// cap the queue grows without bound and the connection falls further and further behind. Dropping -// is the only sane answer; cpu.breakpoint.hit carries a sequence number so a client can tell -// exactly how many it missed rather than silently believing it saw everything. -static constexpr size_t MAX_PENDING_EVENTS = 4096; - -struct DebuggerEventSink { - std::mutex lock; - std::vector> pending; - // A debugger that connects while the CPU is already stopped still wants to hear about it. - bool needsSteppingPrime = true; - - void Push(const char *category, std::string json) { - std::lock_guard guard(lock); - if (pending.size() >= MAX_PENDING_EVENTS) +// Per-connection mailbox. Events the CPU thread produces (cpu.stepping, game.start, input, ...) are +// formatted there and posted here, so a connection's own thread never reads emulator state. +// Refcounted: the connection and the CPU thread's g_sinks each hold one reference. +class DebuggerEventSink final : public DebuggerMailbox { +public: + void Post(const char *category, std::string json) override { + if (retro_atomic_fetch_add_int(&pendingCount_, 1) >= MAX_PENDING_EVENTS) { + retro_atomic_fetch_sub_int(&pendingCount_, 1); return; - pending.emplace_back(category, std::move(json)); + } + pending_.Push(PendingEvent{ category, std::move(json) }); + Wake(); } - void Take(std::vector> *out) { - std::lock_guard guard(lock); - out->swap(pending); - pending.clear(); + void Wake() override { + // Only the first wake since the connection last looked touches the socket. + if (retro_atomic_exchange_int(&wakePending_, 1) == 0) { + wake_.Wake(); + } } + + void WatchPress(int pressId, std::string json) override { + pressInbox_.Push(PendingPress{ pressId, std::move(json) }); + } + + // Connection thread: right after waking, before looking at anything that wakes it. + void Rearm() { + wake_.Drain(); + retro_atomic_exchange_int(&wakePending_, 0); + } + + // Connection thread. + template + void TakeEvents(F f) { + int n = 0; + pending_.Drain([&](PendingEvent &&ev) { + n++; + f(ev.category, ev.json); + }); + if (n != 0) { + retro_atomic_fetch_sub_int(&pendingCount_, n); + } + } + + intptr_t WakeFd() const { return wake_.Fd(); } + + // CPU thread: posts the timed presses that are over. + void CheckPresses() { + pressInbox_.Drain([&](PendingPress &&press) { + presses_.push_back(std::move(press)); + }); + for (size_t i = 0; i < presses_.size(); ) { + if (!__CtrlPressActive(presses_[i].id)) { + Post(nullptr, std::move(presses_[i].json)); + presses_.erase(presses_.begin() + i); + } else { + ++i; + } + } + } + + void Release() { + if (retro_atomic_fetch_sub_int(&refs_, 1) == 1) { + delete this; + } + } + + // Set by the connection when it leaves. + retro_atomic_int_t closed{ 0 }; + + // CPU thread only. A debugger that connects while the CPU is already stopped still wants to + // hear about it. + bool needsSteppingPrime = true; + InputBroadcaster input; + +private: + struct PendingEvent { + const char *category; + std::string json; + }; + struct PendingPress { + int id; + std::string json; + }; + + net::WakeSocket wake_; + retro_atomic_int_t wakePending_{ 0 }; + retro_atomic_int_t refs_{ 2 }; + MpscQueue pending_; + retro_atomic_int_t pendingCount_{ 0 }; + MpscQueue pressInbox_; + // CPU thread only. + std::vector presses_; }; -static std::mutex g_sinkLock; +// The CPU thread's list of live connections; new ones arrive through g_newSinks. static std::vector g_sinks; -// Mirrors g_sinks.size() so the breakpoint path can check "is anyone listening" with one relaxed -// load, instead of taking g_sinkLock on every single breakpoint hit. -static std::atomic g_sinkCount{ 0 }; +static MpscQueue g_newSinks; +// Connected debuggers, so the breakpoint path can check "is anyone listening" with one load. +static retro_atomic_int_t g_sinkCount{ 0 }; static void RegisterSink(DebuggerEventSink *sink) { - std::lock_guard guard(g_sinkLock); - g_sinks.push_back(sink); - g_sinkCount.store((int)g_sinks.size(), std::memory_order_relaxed); + retro_atomic_fetch_add_int(&g_sinkCount, 1); + g_newSinks.Push(sink); } +// The connection's last use of sink. static void UnregisterSink(DebuggerEventSink *sink) { - std::lock_guard guard(g_sinkLock); - g_sinks.erase(std::remove(g_sinks.begin(), g_sinks.end(), sink), g_sinks.end()); - g_sinkCount.store((int)g_sinks.size(), std::memory_order_relaxed); + retro_atomic_store_release_int(&sink->closed, 1); + retro_atomic_fetch_sub_int(&g_sinkCount, 1); + sink->Release(); + // A CPU thread stopped in the debugger may want to know nobody's left to resume it. + Core_WakeCPUThread(); +} + +// CPU thread. +static void UpdateSinks() { + g_newSinks.Drain([](DebuggerEventSink *sink) { + g_sinks.push_back(sink); + }); + for (size_t i = 0; i < g_sinks.size(); ) { + if (retro_atomic_load_acquire_int(&g_sinks[i]->closed)) { + g_sinks[i]->Release(); + g_sinks.erase(g_sinks.begin() + i); + } else { + ++i; + } + } } bool WebSocketDebuggerHasClients() { - return g_sinkCount.load(std::memory_order_relaxed) != 0; + return retro_atomic_load_relaxed_int(&g_sinkCount) != 0; } void WebSocketNotifyBreakpointHit(const BreakpointHit &hit) { // Counts hits produced, not hits delivered, so a gap in what a client receives tells it how - // many were dropped by the cap in Push(). + // many were dropped by the cap in Post(). static uint64_t g_hitSequence = 0; - std::lock_guard guard(g_sinkLock); + if (!WebSocketDebuggerHasClients()) + return; + UpdateSinks(); if (g_sinks.empty()) return; - // Formatted once here on the CPU thread, then shared - same rule as the other pushed events: - // a connection's own thread must never be the one reading emulator state. JsonWriter j; j.begin(); j.writeString("event", "cpu.breakpoint.hit"); @@ -187,7 +257,7 @@ void WebSocketNotifyBreakpointHit(const BreakpointHit &hit) { const std::string json = j.str(); for (DebuggerEventSink *sink : g_sinks) - sink->Push("breakpoint", json); + sink->Post("breakpoint", json); } void WebSocketDebuggerTick() { @@ -196,29 +266,37 @@ void WebSocketDebuggerTick() { const std::string gameEvent = GameBroadcaster::PollChange(); const std::string steppingEvent = SteppingBroadcaster::PollChange(); - std::lock_guard guard(g_sinkLock); + UpdateSinks(); if (g_sinks.empty()) return; std::string steppingPrime; + std::vector inputEvents; for (DebuggerEventSink *sink : g_sinks) { + sink->CheckPresses(); + + inputEvents.clear(); + sink->input.Poll(&inputEvents); + for (std::string &ev : inputEvents) + sink->Post("input", std::move(ev)); + if (sink->needsSteppingPrime) { sink->needsSteppingPrime = false; // Only format it if somebody actually needs it. if (steppingPrime.empty()) steppingPrime = SteppingBroadcaster::CurrentState(); if (!steppingPrime.empty()) - sink->Push("stepping", steppingPrime); + sink->Post("stepping", steppingPrime); continue; } if (!gameEvent.empty()) - sink->Push("game", gameEvent); + sink->Post("game", gameEvent); if (!steppingEvent.empty()) - sink->Push("stepping", steppingEvent); + sink->Post("stepping", steppingEvent); } } -void HandleDebuggerRequest(const http::ServerRequest &request) { +void HandleDebuggerRequest(const http::ServerRequest &request, const net::CancelToken *stop) { SetCurrentThreadName("WebSocketDebugger"); net::WebSocketServer *ws = net::WebSocketServer::CreateAsUpgrade(request, "debugger.ppsspp.org"); @@ -226,128 +304,111 @@ void HandleDebuggerRequest(const http::ServerRequest &request) { return; } - UpdateConnected(1); - WebSocketClientInfo client_info; auto& disallowed_config = client_info.disallowed; // Seed every broadcaster category. broadcast.config.set only accepts keys that already exist // here (so a typo is rejected rather than silently ignored), and these otherwise only appear // as a side effect of operator[] the first time each category actually broadcasts - which // meant "game" and "stepping" were rejected as unsupported until one happened to fire, even - // though they're documented and valid. Keep in sync with the Broadcast calls further down. + // though they're documented and valid. Keep in sync with the categories posted to the sink. for (const char *category : { "logger", "input", "game", "stepping", "breakpoint" }) disallowed_config[category] = false; - LogBroadcaster logger; - InputBroadcaster input; + DebuggerEventSink *sink = new DebuggerEventSink(); + RegisterSink(sink); - DebuggerEventSink sink; - RegisterSink(&sink); + { + LogBroadcaster logger(sink); - DebuggerEventHandlerMap eventHandlers; - std::vector subscriberData; - for (auto init : subscribers) { - subscriberData.push_back(init(eventHandlers)); - } - - // There's a tradeoff between responsiveness to incoming events, and polling for changes. - int highActivity = 0; - ws->SetTextHandler([&](const std::string &t) { - JsonReader reader(t.c_str(), t.size()); - if (!reader.ok()) { - ws->Send(DebuggerErrorEvent("Bad message: invalid JSON", LogLevel::LERROR)); - return; - } - - const JsonGet root = reader.root(); - const char *event = root ? root.getStringOr("event", nullptr) : nullptr; - if (!event) { - ws->Send(DebuggerErrorEvent("Bad message: no event property", LogLevel::LERROR, root)); - return; - } - - DEBUG_LOG(Log::Debugger, "WS: Handling '%s'", event); - - DebuggerRequest req(event, ws, root, &client_info); - auto eventFunc = eventHandlers.find(event); - if (eventFunc != eventHandlers.end()) { - eventFunc->second(req); - if (!req.Finish()) { - // The handler arranged something that finishes later - a step, a resume, a stats - // feed - rather than answering now. A client that asked for it gets told so, so it - // can tell "accepted, wait for the event" from "dropped on the floor" without - // carrying a hardcoded list of the events that don't answer. Everyone else sees - // exactly what they saw before; see client.config.set for why it can't be the - // default. - if (client_info.acknowledgeDeferred) - ws->Send(DebuggerDeferredEvent(event, root)); - // Poll more frequently for a second in case this triggers something. - highActivity = 1000; + DebuggerEventHandlerMap eventHandlers; + std::vector subscriberData; + for (auto init : subscribers) { + DebuggerSubscriber *sub = init(eventHandlers); + if (sub) { + sub->mailbox = sink; } - } else { - req.Fail("Bad message: unknown event"); + subscriberData.push_back(sub); } - }); - ws->SetBinaryHandler([&](const std::vector &d) { - ERROR_LOG(Log::Debugger, "Received binary WebSocket frame, not supported"); - ws->Send(DebuggerErrorEvent("Bad message: binary WebSocket frames are not supported", LogLevel::LERROR)); - }); + ws->SetTextHandler([&](const std::string &t) { + JsonReader reader(t.c_str(), t.size()); + if (!reader.ok()) { + ws->Send(DebuggerErrorEvent("Bad message: invalid JSON", LogLevel::LERROR)); + return; + } - // Don't out-line the highActivity check, it needs to recompute on every lap. - constexpr float lowActivityPollTimeStep = 1.0f / 60.0f; - constexpr float highActivityPollTimeStep = 1.0f / 1000.0f; - while (ws->Process(highActivity ? highActivityPollTimeStep : lowActivityPollTimeStep)) { - // These send events that aren't just responses to requests + const JsonGet root = reader.root(); + const char *event = root ? root.getStringOr("event", nullptr) : nullptr; + if (!event) { + ws->Send(DebuggerErrorEvent("Bad message: no event property", LogLevel::LERROR, root)); + return; + } - // The client can explicitly ask not to be notified about some events - // so we check the client settings first - if (!disallowed_config["logger"]) - logger.Broadcast(ws); - if (!disallowed_config["input"]) - input.Broadcast(ws); + DEBUG_LOG(Log::Debugger, "WS: Handling '%s'", event); - // Whatever the CPU thread queued up for us since last lap. - std::vector> events; - sink.Take(&events); - for (const auto &ev : events) { - if (!disallowed_config[ev.first]) - ws->Send(ev.second); + DebuggerRequest req(event, ws, root, &client_info); + auto eventFunc = eventHandlers.find(event); + if (eventFunc != eventHandlers.end()) { + eventFunc->second(req); + if (!req.Finish()) { + // The handler arranged something that finishes later - a step, a resume, a stats + // feed - rather than answering now. A client that asked for it gets told so, so it + // can tell "accepted, wait for the event" from "dropped on the floor" without + // carrying a hardcoded list of the events that don't answer. Everyone else sees + // exactly what they saw before; see client.config.set for why it can't be the + // default. + if (client_info.acknowledgeDeferred) + ws->Send(DebuggerDeferredEvent(event, root)); + } + } else { + req.Fail("Bad message: unknown event"); + } + }); + + ws->SetBinaryHandler([&](const std::vector &d) { + ERROR_LOG(Log::Debugger, "Received binary WebSocket frame, not supported"); + ws->Send(DebuggerErrorEvent("Bad message: binary WebSocket frames are not supported", LogLevel::LERROR)); + }); + + // Blocks until the client sends something, the socket drains, the sink is woken, or the + // server stops. Once we've reacted to stop, its fd stays readable, so leave it out. + const intptr_t wakeFds[2] = { sink->WakeFd(), stop ? stop->WakeFd() : -1 }; + bool stopping = false; + while (ws->Process(wakeFds, stopping ? 1 : 2)) { + sink->Rearm(); + + // The client can explicitly ask not to be notified about some events. + if (!disallowed_config["logger"]) { + logger.Broadcast(ws); + } else { + logger.Discard(); + } + + sink->TakeEvents([&](const char *category, const std::string &json) { + if (!category || !disallowed_config[category]) { + ws->Send(json); + } + }); + + for (size_t i = 0; i < subscribers.size(); ++i) { + if (subscriberData[i]) { + subscriberData[i]->Broadcast(ws); + } + } + + if (!stopping && stop && stop->IsCancelled()) { + stopping = true; + ws->Close(net::WebSocketClose::GOING_AWAY); + } } for (size_t i = 0; i < subscribers.size(); ++i) { - if (subscriberData[i]) { - subscriberData[i]->Broadcast(ws); - } - } - - if (stopRequested) { - ws->Close(net::WebSocketClose::GOING_AWAY); - } - - if (highActivity > 0) { - highActivity--; + delete subscriberData[i]; } } - UnregisterSink(&sink); - - for (size_t i = 0; i < subscribers.size(); ++i) { - delete subscriberData[i]; - } + UnregisterSink(sink); delete ws; request.In()->Discard(); - UpdateConnected(-1); -} - -void StopAllDebuggers() { - std::unique_lock guard(stopLock); - while (debuggersConnected != 0) { - stopRequested = true; - stopCond.wait(guard); - } - - // Reset it back for next time. - stopRequested = false; } diff --git a/Core/Debugger/WebSocket.h b/Core/Debugger/WebSocket.h index 2c59d762d0..dfbfc0002b 100644 --- a/Core/Debugger/WebSocket.h +++ b/Core/Debugger/WebSocket.h @@ -20,10 +20,12 @@ namespace http { class ServerRequest; } +namespace net { +class CancelToken; +} -void HandleDebuggerRequest(const http::ServerRequest &request); -// Note: blocks. -void StopAllDebuggers(); +// Runs one debugger connection on the calling thread until the client leaves or stop is set. +void HandleDebuggerRequest(const http::ServerRequest &request, const net::CancelToken *stop); struct BreakpointHit; diff --git a/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp b/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp index 7ddd8414a0..d5f157cc2f 100644 --- a/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp +++ b/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp @@ -73,10 +73,12 @@ void WebSocketCPUStepping(DebuggerRequest &req) { return req.Fail("CPU not started"); } if (!Core_IsStepping() && Core_IsActive()) { - // Core_Break() is explicitly free-threaded (see Core.cpp), so no need to bounce this to the CPU - // thread - and we can't anyway, since queuing to it only makes sense once the CPU actually *is* - // stepping, which this call is what triggers in the first place. - Core_Break(BreakReason::DebugStep, 0); + // The CPU thread drains its queue at least once a frame while running, too. + Core_RunOnCPUThread([] { + if (!Core_IsStepping() && Core_IsActive()) { + Core_Break(BreakReason::DebugStep, 0); + } + }); } } diff --git a/Core/Debugger/WebSocket/DisasmSubscriber.cpp b/Core/Debugger/WebSocket/DisasmSubscriber.cpp index f343b69252..4263b4d6ae 100644 --- a/Core/Debugger/WebSocket/DisasmSubscriber.cpp +++ b/Core/Debugger/WebSocket/DisasmSubscriber.cpp @@ -34,10 +34,6 @@ class WebSocketDisasmState : public DebuggerSubscriber { public: - WebSocketDisasmState() { - g_disassemblyManager.setCpu(currentDebugMIPS); - } - void Base(DebuggerRequest &req); void Disasm(DebuggerRequest &req); void SearchDisasm(DebuggerRequest &req); @@ -347,6 +343,7 @@ void WebSocketDisasmState::Disasm(DebuggerRequest &req) { // Route the disassembly manager/symbol reads to the CPU thread instead of poking at them directly // from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h. Core_RunOnCPUThread([&] { + g_disassemblyManager.setCpu(currentDebugMIPS); DebugInterface *cpuDebug = CPUFromRequest(req); if (!cpuDebug) return; @@ -492,6 +489,7 @@ void WebSocketDisasmState::SearchDisasm(DebuggerRequest &req) { // Note: for a very large [start, end) range, this scan itself can take a while - unlike memory.search // there's no size cap here, so a huge range will block the CPU thread's own frame pump for its duration. Core_RunOnCPUThread([&] { + g_disassemblyManager.setCpu(currentDebugMIPS); DebugInterface *cpuDebug = CPUFromRequest(req); if (!cpuDebug) return; diff --git a/Core/Debugger/WebSocket/GPURecordSubscriber.cpp b/Core/Debugger/WebSocket/GPURecordSubscriber.cpp index bad43f8175..ff5e281d10 100644 --- a/Core/Debugger/WebSocket/GPURecordSubscriber.cpp +++ b/Core/Debugger/WebSocket/GPURecordSubscriber.cpp @@ -17,6 +17,7 @@ #include "Common/Data/Encoding/Base64.h" #include "Common/File/FileUtil.h" +#include "Common/Thread/MpscQueue.h" #include "Core/Core.h" #include "Core/Debugger/WebSocket/GPURecordSubscriber.h" #include "Core/Debugger/WebSocket/WebSocketUtils.h" @@ -32,9 +33,11 @@ struct WebSocketGPURecordState : public DebuggerSubscriber { void Broadcast(net::WebSocketServer *ws) override; protected: + // The connection thread's: whether the CPU thread may still call our callback. bool pending_ = false; std::string lastTicket_; - Path lastFilename_; + // Filled on the CPU thread when the recording is written. + MpscQueue finished_; }; DebuggerSubscriber *WebSocketGPURecordInit(DebuggerEventHandlerMap &map) { @@ -72,9 +75,9 @@ void WebSocketGPURecordState::Dump(DebuggerRequest &req) { haveGPU = PSP_GetBootState() == BootState::Complete && gpu != nullptr; if (!haveGPU) return; - started = gpu->GetRecorder()->RecordNextFrame([=](const Path &filename) { - lastFilename_ = filename; - pending_ = false; + started = gpu->GetRecorder()->RecordNextFrame([this](const Path &filename) { + finished_.Push(filename); + mailbox->Wake(); }); }); @@ -93,10 +96,14 @@ void WebSocketGPURecordState::Dump(DebuggerRequest &req) { // This handles the asynchronous gpu.record.dump response. void WebSocketGPURecordState::Broadcast(net::WebSocketServer *ws) { - if (!lastFilename_.empty()) { - FILE *fp = File::OpenCFile(lastFilename_, "rb"); + Path lastFilename; + finished_.Drain([&](Path &&filename) { + lastFilename = std::move(filename); + }); + if (!lastFilename.empty()) { + pending_ = false; + FILE *fp = File::OpenCFile(lastFilename, "rb"); if (!fp) { - lastFilename_.clear(); return; } @@ -120,7 +127,6 @@ void WebSocketGPURecordState::Broadcast(net::WebSocketServer *ws) { ws->AddFragment(true, R"("})"); - lastFilename_.clear(); lastTicket_.clear(); } } diff --git a/Core/Debugger/WebSocket/GPUStatsSubscriber.cpp b/Core/Debugger/WebSocket/GPUStatsSubscriber.cpp index 03e2d3c588..126c2515b1 100644 --- a/Core/Debugger/WebSocket/GPUStatsSubscriber.cpp +++ b/Core/Debugger/WebSocket/GPUStatsSubscriber.cpp @@ -15,10 +15,13 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include +#include #include +#include + #include "Common/Data/Text/StringWriter.h" +#include "Common/Thread/MpscQueue.h" #include "Core/Debugger/WebSocket/GPUStatsSubscriber.h" #include "Core/Core.h" #include "Core/HW/Display.h" @@ -81,13 +84,14 @@ struct WebSocketGPUStatsState : public DebuggerSubscriber { void FlipListener(); protected: + // The WebSocket thread's. bool forced_ = false; - bool sendNext_ = false; - bool sendFeed_ = false; - std::string lastTicket_; - std::mutex pendingLock_; - std::vector pendingStats_; + // Set by the WebSocket thread, read on the emu thread at each flip. + retro_atomic_int_t sendNext_{ 0 }; + retro_atomic_int_t sendFeed_{ 0 }; + // Collected on the emu thread, sent from the WebSocket thread. + MpscQueue pendingStats_; }; DebuggerSubscriber *WebSocketGPUStatsInit(DebuggerEventHandlerMap &map) { @@ -99,15 +103,20 @@ DebuggerSubscriber *WebSocketGPUStatsInit(DebuggerEventHandlerMap &map) { } WebSocketGPUStatsState::WebSocketGPUStatsState() { - __DisplayListenFlip(&WebSocketGPUStatsState::FlipForwarder, this); + // The flip listeners belong to the CPU thread. + Core_RunOnCPUThread([this] { __DisplayListenFlip(&WebSocketGPUStatsState::FlipForwarder, this); }); } WebSocketGPUStatsState::~WebSocketGPUStatsState() { // PSP_ForceDebugStats bumps a plain counter, so do it on the CPU thread that owns it - see // Core_RunOnCPUThread() in Core.h. - if (forced_) - Core_RunOnCPUThread([] { PSP_ForceDebugStats(false); }); - __DisplayForgetFlip(&WebSocketGPUStatsState::FlipForwarder, this); + const bool forced = forced_; + Core_RunOnCPUThread([this, forced] { + if (forced) { + PSP_ForceDebugStats(false); + } + __DisplayForgetFlip(&WebSocketGPUStatsState::FlipForwarder, this); + }); } void WebSocketGPUStatsState::FlipForwarder(void *thiz) { @@ -116,13 +125,11 @@ void WebSocketGPUStatsState::FlipForwarder(void *thiz) { } void WebSocketGPUStatsState::FlipListener() { - if (!sendNext_ && !sendFeed_) + if (!retro_atomic_load_acquire_int(&sendNext_) && !retro_atomic_load_acquire_int(&sendFeed_)) return; // Okay, collect the data (we'll actually send at next Broadcast.) - std::lock_guard guard(pendingLock_); - pendingStats_.resize(pendingStats_.size() + 1); - CollectedStats &stats = pendingStats_[pendingStats_.size() - 1]; + CollectedStats stats; __DisplayGetFPS(&stats.vps, &stats.fps, &stats.actual_fps); @@ -140,7 +147,9 @@ void WebSocketGPUStatsState::FlipListener() { memcpy(&stats.sleepTimes[0], sleepHistory, sizeof(float) * valid); } - sendNext_ = false; + pendingStats_.Push(std::move(stats)); + retro_atomic_store_release_int(&sendNext_, 0); + mailbox->Wake(); } // Get next GPU stats (gpu.stats.get) @@ -163,11 +172,9 @@ void WebSocketGPUStatsState::Get(DebuggerRequest &req) { if (PSP_GetBootState() != BootState::Complete) return req.Fail("CPU not started"); - std::lock_guard guard(pendingLock_); - sendNext_ = true; - const JsonNode *value = req.data.get("ticket"); lastTicket_ = value ? json_stringify(value) : ""; + retro_atomic_store_release_int(&sendNext_, 1); } // Setup GPU stats feed (gpu.stats.feed) @@ -186,8 +193,7 @@ void WebSocketGPUStatsState::Feed(DebuggerRequest &req) { if (!req.ParamBool("enable", &enable, DebuggerParamType::OPTIONAL)) return; - std::lock_guard guard(pendingLock_); - sendFeed_ = enable; + retro_atomic_store_release_int(&sendFeed_, enable ? 1 : 0); if (forced_ != enable) { Core_RunOnCPUThread([enable] { PSP_ForceDebugStats(enable); }); forced_ = enable; @@ -195,19 +201,25 @@ void WebSocketGPUStatsState::Feed(DebuggerRequest &req) { } void WebSocketGPUStatsState::Broadcast(net::WebSocketServer *ws) { - std::lock_guard guard(pendingLock_); - if (lastTicket_.empty() && !sendFeed_) { - pendingStats_.clear(); + if (pendingStats_.Empty()) { + return; + } + std::vector pending; + pendingStats_.Drain([&](CollectedStats &&stats) { + pending.push_back(std::move(stats)); + }); + + const bool sendFeed = retro_atomic_load_acquire_int(&sendFeed_) != 0; + if (lastTicket_.empty() && !sendFeed) { return; } // To be safe, make sure we only send one if we're doing a get. - if (!sendFeed_ && pendingStats_.size() > 1) - pendingStats_.resize(1); + if (!sendFeed && pending.size() > 1) + pending.resize(1); - for (size_t i = 0; i < pendingStats_.size(); ++i) { - ws->Send(DebuggerGPUStatsEvent{ pendingStats_[i], lastTicket_ }); + for (size_t i = 0; i < pending.size(); ++i) { + ws->Send(DebuggerGPUStatsEvent{ pending[i], lastTicket_ }); lastTicket_.clear(); } - pendingStats_.clear(); } diff --git a/Core/Debugger/WebSocket/HLESubscriber.cpp b/Core/Debugger/WebSocket/HLESubscriber.cpp index 9a54963d85..3f33feb2eb 100644 --- a/Core/Debugger/WebSocket/HLESubscriber.cpp +++ b/Core/Debugger/WebSocket/HLESubscriber.cpp @@ -835,6 +835,7 @@ void WebSocketHLEBacktrace(DebuggerRequest &req) { // Route the actual CPU/symbol/disassembly reads to the CPU thread instead of poking at them directly // from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h. Core_RunOnCPUThread([&] { + g_disassemblyManager.setCpu(currentDebugMIPS); uint32_t threadID = -1; DebugInterface *cpuDebug = currentDebugMIPS; if (req.HasParam("thread")) { diff --git a/Core/Debugger/WebSocket/InputBroadcaster.cpp b/Core/Debugger/WebSocket/InputBroadcaster.cpp index 16fdc1cbe6..d9841d61a2 100644 --- a/Core/Debugger/WebSocket/InputBroadcaster.cpp +++ b/Core/Debugger/WebSocket/InputBroadcaster.cpp @@ -73,7 +73,7 @@ static std::string ButtonsEvent(uint32_t lastButtons, uint32_t newButtons) { return j.str(); } -void InputBroadcaster::Broadcast(net::WebSocketServer *ws) { +void InputBroadcaster::Poll(std::vector *events) { int counter = __DisplayGetNumVblanks(); if (lastCounter_ == counter) return; @@ -81,21 +81,21 @@ void InputBroadcaster::Broadcast(net::WebSocketServer *ws) { uint32_t newButtons = __CtrlPeekButtons(); if (newButtons != lastButtons_) { - ws->Send(ButtonsEvent(lastButtons_, newButtons)); + events->push_back(ButtonsEvent(lastButtons_, newButtons)); lastButtons_ = newButtons; } Analog newAnalog; __CtrlPeekAnalog(CTRL_STICK_LEFT, &newAnalog.x, &newAnalog.y); if (!lastAnalog_[0].Equals(newAnalog)) { - ws->Send(newAnalog.Event("left")); + events->push_back(newAnalog.Event("left")); lastAnalog_[0].x = newAnalog.x; lastAnalog_[0].y = newAnalog.y; } __CtrlPeekAnalog(CTRL_STICK_RIGHT, &newAnalog.x, &newAnalog.y); if (!lastAnalog_[1].Equals(newAnalog)) { - ws->Send(newAnalog.Event("right")); + events->push_back(newAnalog.Event("right")); lastAnalog_[1].x = newAnalog.x; lastAnalog_[1].y = newAnalog.y; } diff --git a/Core/Debugger/WebSocket/InputBroadcaster.h b/Core/Debugger/WebSocket/InputBroadcaster.h index dd8031d902..c150b4b0e3 100644 --- a/Core/Debugger/WebSocket/InputBroadcaster.h +++ b/Core/Debugger/WebSocket/InputBroadcaster.h @@ -19,17 +19,16 @@ #include #include +#include -namespace net { -class WebSocketServer; -} - +// Notices button/analog changes for one connection. CPU thread only. struct InputBroadcaster { public: InputBroadcaster() { } - void Broadcast(net::WebSocketServer *ws); + // Appends the events for whatever changed since the last vblank looked at. + void Poll(std::vector *events); private: struct Analog { diff --git a/Core/Debugger/WebSocket/InputSubscriber.cpp b/Core/Debugger/WebSocket/InputSubscriber.cpp index 4eb324656a..6212a06d60 100644 --- a/Core/Debugger/WebSocket/InputSubscriber.cpp +++ b/Core/Debugger/WebSocket/InputSubscriber.cpp @@ -63,8 +63,6 @@ struct WebSocketInputState : public DebuggerSubscriber { void ButtonsPress(DebuggerRequest &req); void AnalogSend(DebuggerRequest &req); - void Broadcast(net::WebSocketServer *ws) override; - protected: struct PressInfo { std::string ticket; @@ -74,8 +72,6 @@ protected: std::string Event(); }; - - std::vector pressTickets_; }; std::string WebSocketInputState::PressInfo::Event() { @@ -195,23 +191,9 @@ void WebSocketInputState::ButtonsPress(DebuggerRequest &req) { press.button = info->second; // Released by sceCtrl after that many vblanks of emulated time, so a scripted press lasts the - // same however fast the emulator runs. This thread only reports when it's over. + // same however fast the emulator runs. The CPU thread posts the answer once it's over. press.pressId = __CtrlPressFor(press.button, (int)press.duration); - pressTickets_.push_back(press); -} - -void WebSocketInputState::Broadcast(net::WebSocketServer *ws) { - if (pressTickets_.empty()) - return; - auto done = [](const PressInfo &press) -> bool { - return !__CtrlPressActive(press.pressId); - }; - for (PressInfo &press : pressTickets_) { - if (done(press)) { - ws->Send(press.Event()); - } - } - pressTickets_.erase(std::remove_if(pressTickets_.begin(), pressTickets_.end(), done), pressTickets_.end()); + mailbox->WatchPress(press.pressId, press.Event()); } static bool AnalogValue(DebuggerRequest &req, float *value, const char *name) { diff --git a/Core/Debugger/WebSocket/LogBroadcaster.cpp b/Core/Debugger/WebSocket/LogBroadcaster.cpp index a96fcaea12..83d7245a23 100644 --- a/Core/Debugger/WebSocket/LogBroadcaster.cpp +++ b/Core/Debugger/WebSocket/LogBroadcaster.cpp @@ -15,90 +15,63 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include -#include +#include + #include "Common/Log/LogManager.h" #include "Common/StringUtils.h" +#include "Common/Thread/MpscQueue.h" #include "Common/TimeUtil.h" #include "Core/Debugger/WebSocket/LogBroadcaster.h" #include "Core/Debugger/WebSocket/WebSocketUtils.h" +// Log() runs on whichever thread logged; Take() on the connection's thread. class DebuggerLogListener { public: + explicit DebuggerLogListener(DebuggerMailbox *mailbox) : mailbox_(mailbox) {} + void Log(const LogMessage &msg) { - std::lock_guard guard(lock_); - messages_[nextMessage_] = msg; - nextMessage_++; - if (nextMessage_ >= BUFFER_SIZE) - nextMessage_ -= BUFFER_SIZE; - count_++; + // A source that logs faster than the connection sends - a log-only breakpoint hit thousands + // of times in a tight loop is a real example, see docs/VSHBootInvestigation.md - would grow + // the queue without bound. Past the cap new messages are counted and reported as dropped, + // so it doesn't just look like "my breakpoint only logged a few hits". + if (retro_atomic_fetch_add_int(&count_, 1) >= BUFFER_SIZE) { + retro_atomic_fetch_sub_int(&count_, 1); + retro_atomic_fetch_add_int(&dropped_, 1); + } else { + messages_.Push(msg); + } + mailbox_->Wake(); } - std::vector GetMessages() { - std::lock_guard guard(lock_); - int splitPoint; - int readCount; - // A source that logs faster than this listener gets polled (WebSocket.cpp's event loop, - // up to 1000Hz under high activity) - a log-only breakpoint hit thousands of times in a - // tight loop is a real example, see docs/VSHBootInvestigation.md - can wrap the ring - // buffer before GetMessages() ever reads the oldest entries, silently losing them. That - // used to just look like "my breakpoint only logged a few hits" from the client's side, - // indistinguishable from the breakpoint genuinely not firing - synthesize a warning - // message reporting exactly how many were lost instead of staying silent about it. - int droppedCount = 0; - if (read_ + BUFFER_SIZE < count_) { - // We'll start with our oldest then. - droppedCount = (count_ - BUFFER_SIZE) - read_; - splitPoint = nextMessage_; - readCount = Count(); - } else { - // read_ counts messages ever read, so it has to be wrapped to index the ring - the - // overflow branch above starts from nextMessage_, which already is an index. Without - // the modulo this went wrong the moment a session logged BUFFER_SIZE messages: with - // splitPoint >= BUFFER_SIZE the first copy loop below is empty, so the second one - // handed back messages_[0..readCount-1] - the oldest entries in the buffer, not the - // new ones - and every later poll stayed that far out of step. High-volume sources - // (a log-only breakpoint in a hot loop) hit it within seconds, and the result looked - // like the tail of the log going missing rather than being wrong. - splitPoint = read_ % BUFFER_SIZE; - readCount = count_ - read_; - } - - read_ = count_; - + std::vector Take() { std::vector results; + const int droppedCount = retro_atomic_exchange_int(&dropped_, 0); if (droppedCount > 0) { LogMessage dropped; dropped.level = LogLevel::LWARNING; dropped.log = "Debugger"; GetCurrentTimeFormatted(dropped.timestamp); - truncate_cpy(dropped.header, "LogBroadcaster: ring buffer overflow"); - dropped.msg = StringFromFormat("%d log message(s) dropped - loop polling too slow for this volume\n", droppedCount); + truncate_cpy(dropped.header, "LogBroadcaster: queue overflow"); + dropped.msg = StringFromFormat("%d log message(s) dropped - connection too slow for this volume\n", droppedCount); results.push_back(dropped); } - int splitEnd = std::min(splitPoint + readCount, (int)BUFFER_SIZE); - for (int i = splitPoint; i < splitEnd; ++i) { - results.push_back(messages_[i]); - readCount--; + int n = 0; + messages_.Drain([&](LogMessage &&msg) { + results.push_back(std::move(msg)); + n++; + }); + if (n != 0) { + retro_atomic_fetch_sub_int(&count_, n); } - for (int i = 0; i < readCount; ++i) { - results.push_back(messages_[i]); - } - return results; } - int Count() const { - return count_ < BUFFER_SIZE ? count_ : BUFFER_SIZE; - } - private: enum { BUFFER_SIZE = 1024 }; - LogMessage messages_[BUFFER_SIZE]; - std::mutex lock_; - int nextMessage_ = 0; - int count_ = 0; - int read_ = 0; + DebuggerMailbox *mailbox_; + MpscQueue messages_; + retro_atomic_int_t count_{ 0 }; + retro_atomic_int_t dropped_{ 0 }; }; static void BroadcastCallback(const LogMessage &message, void *userdata) { @@ -106,8 +79,8 @@ static void BroadcastCallback(const LogMessage &message, void *userdata) { listener->Log(message); } -LogBroadcaster::LogBroadcaster() { - listener_ = new DebuggerLogListener(); +LogBroadcaster::LogBroadcaster(DebuggerMailbox *mailbox) { + listener_ = new DebuggerLogListener(mailbox); // One of these exists per open connection, so it registers alongside any other client's // rather than replacing it - see AddExternalLogCallback(). callbackHandle_ = g_logManager.AddExternalLogCallback(&BroadcastCallback, (void *)listener_); @@ -145,8 +118,12 @@ struct DebuggerLogEvent { // - level: number severity level (1 = highest.) // - channel: string describing log channel / grouping. void LogBroadcaster::Broadcast(net::WebSocketServer *ws) { - auto messages = listener_->GetMessages(); - for (auto msg : messages) { + std::vector messages = listener_->Take(); + for (const LogMessage &msg : messages) { ws->Send(DebuggerLogEvent{msg}); } } + +void LogBroadcaster::Discard() { + listener_->Take(); +} diff --git a/Core/Debugger/WebSocket/LogBroadcaster.h b/Core/Debugger/WebSocket/LogBroadcaster.h index 543b168a46..85a1260a6e 100644 --- a/Core/Debugger/WebSocket/LogBroadcaster.h +++ b/Core/Debugger/WebSocket/LogBroadcaster.h @@ -22,13 +22,17 @@ class WebSocketServer; } class DebuggerLogListener; +class DebuggerMailbox; +// Collects log messages from any thread and wakes mailbox; the connection's thread sends them. struct LogBroadcaster { public: - LogBroadcaster(); + explicit LogBroadcaster(DebuggerMailbox *mailbox); ~LogBroadcaster(); void Broadcast(net::WebSocketServer *ws); + // Drops what has queued up, for a client that disallowed log events. + void Discard(); private: DebuggerLogListener *listener_; diff --git a/Core/Debugger/WebSocket/LogConfigSubscriber.cpp b/Core/Debugger/WebSocket/LogConfigSubscriber.cpp index f3c6d4abe7..503de560d8 100644 --- a/Core/Debugger/WebSocket/LogConfigSubscriber.cpp +++ b/Core/Debugger/WebSocket/LogConfigSubscriber.cpp @@ -71,8 +71,8 @@ void WebSocketLogChannelsList(DebuggerRequest &req) { const LogChannel *chan = g_logManager.GetLogChannel(type); json.pushDict(); json.writeString("name", LogManager::GetLogTypeName(type)); - json.writeString("level", LogLevelToString(chan->level)); - json.writeBool("enabled", chan->enabled); + json.writeString("level", LogLevelToString(chan->Level())); + json.writeBool("enabled", chan->Enabled()); json.pop(); } json.pop(); @@ -130,8 +130,8 @@ void WebSocketLogChannelSet(DebuggerRequest &req) { const LogChannel *chan = g_logManager.GetLogChannel(type); JsonWriter &json = req.Respond(); json.writeString("name", channelName); - json.writeString("level", LogLevelToString(chan->level)); - json.writeBool("enabled", chan->enabled); + json.writeString("level", LogLevelToString(chan->Level())); + json.writeBool("enabled", chan->Enabled()); return; } diff --git a/Core/Debugger/WebSocket/MemorySubscriber.cpp b/Core/Debugger/WebSocket/MemorySubscriber.cpp index 835f56cb97..a5bb9dd765 100644 --- a/Core/Debugger/WebSocket/MemorySubscriber.cpp +++ b/Core/Debugger/WebSocket/MemorySubscriber.cpp @@ -17,7 +17,6 @@ #include #include -#include #include "Common/Data/Encoding/Base64.h" #include "Common/Data/Encoding/Utf8.h" #include "Common/StringUtils.h" diff --git a/Core/Debugger/WebSocket/SteppingSubscriber.cpp b/Core/Debugger/WebSocket/SteppingSubscriber.cpp index e9f5493e99..3e987d4fe6 100644 --- a/Core/Debugger/WebSocket/SteppingSubscriber.cpp +++ b/Core/Debugger/WebSocket/SteppingSubscriber.cpp @@ -30,10 +30,6 @@ using namespace MIPSAnalyst; struct WebSocketSteppingState : public DebuggerSubscriber { - WebSocketSteppingState() { - g_disassemblyManager.setCpu(currentDebugMIPS); - } - void Into(DebuggerRequest &req); void Over(DebuggerRequest &req); void Out(DebuggerRequest &req); @@ -92,10 +88,12 @@ void WebSocketSteppingState::Into(DebuggerRequest &req) { if (!currentDebugMIPS->isAlive()) return req.Fail("CPU not started"); if (!Core_IsStepping()) { - // Core_Break() is explicitly free-threaded (see Core.cpp), so no need to bounce this to the CPU - // thread - and we can't anyway, since queuing to it only makes sense once the CPU actually *is* - // stepping, which this call is what triggers in the first place. - Core_Break(BreakReason::DebugStepInto, 0); + // The CPU thread drains its queue at least once a frame while running, too. + Core_RunOnCPUThread([] { + if (!Core_IsStepping()) { + Core_Break(BreakReason::DebugStepInto, 0); + } + }); return; } @@ -353,7 +351,9 @@ void WebSocketSteppingState::HLE(DebuggerRequest &req) { }); } +// CPU thread. uint32_t WebSocketSteppingState::GetNextAddress(DebugInterface *cpuDebug) { + g_disassemblyManager.setCpu(currentDebugMIPS); uint32_t current = g_disassemblyManager.getStartAddress(cpuDebug->GetPC()); return g_disassemblyManager.getNthNextAddress(current, 1); } diff --git a/Core/Debugger/WebSocket/WebSocketUtils.h b/Core/Debugger/WebSocket/WebSocketUtils.h index 54f33026c4..2c1ea4b1c3 100644 --- a/Core/Debugger/WebSocket/WebSocketUtils.h +++ b/Core/Debugger/WebSocket/WebSocketUtils.h @@ -143,12 +143,32 @@ private: bool responsePartial_ = false; }; +// A connection's inbox for things other threads produce. It outlives the connection's +// subscribers, and stays valid for callbacks running on the CPU thread until that thread lets go. +class DebuggerMailbox { +public: + // Any thread. Queues json for the connection to send (unless the client disallowed category; + // nullptr means always send) and wakes it. + virtual void Post(const char *category, std::string json) = 0; + // Any thread. Wakes the connection so its subscribers' Broadcast() runs. + virtual void Wake() = 0; + // Any thread. Posts json once the sceCtrl timed press pressId is over. + virtual void WatchPress(int pressId, std::string json) = 0; + +protected: + ~DebuggerMailbox() {} +}; + class DebuggerSubscriber { public: virtual ~DebuggerSubscriber() {} - // Subscribers can also broadcast if they have simple cases to. + // Runs on the connection's thread each time it wakes. Whatever feeds it from another thread + // calls mailbox->Wake() after publishing. virtual void Broadcast(net::WebSocketServer *ws) {} + + // Set by the connection right after the subscriber is created. + DebuggerMailbox *mailbox = nullptr; }; typedef std::function DebuggerEventHandler; diff --git a/Core/Dialog/PSPOskDialog.cpp b/Core/Dialog/PSPOskDialog.cpp index f19bd41faf..9c03176dc9 100755 --- a/Core/Dialog/PSPOskDialog.cpp +++ b/Core/Dialog/PSPOskDialog.cpp @@ -799,15 +799,15 @@ int PSPOskDialog::NativeKeyboard() { std::shared_ptr native = native_; PSPOskNativeStatus status; std::string value; - { - std::lock_guard guard(native->mutex); - status = native->status; - if (status == PSPOskNativeStatus::IDLE) { - native->status = PSPOskNativeStatus::WAITING; - } else if (status == PSPOskNativeStatus::SUCCESS || status == PSPOskNativeStatus::FAILURE) { - value = std::move(native->value); - native->status = PSPOskNativeStatus::DONE; - } + status = native->Status(); + if (status == PSPOskNativeStatus::IDLE) { + native->SetStatus(PSPOskNativeStatus::WAITING); + } else if (status == PSPOskNativeStatus::SUCCESS || status == PSPOskNativeStatus::FAILURE) { + value = std::move(native->value); + native->SetStatus(PSPOskNativeStatus::DONE); + } else if (status == PSPOskNativeStatus::FILLING) { + // A callback is writing the answer; treat it as still waiting. + status = PSPOskNativeStatus::WAITING; } if (status == PSPOskNativeStatus::IDLE) { @@ -821,21 +821,11 @@ int PSPOskDialog::NativeKeyboard() { System_InputBoxGetString(NON_EPHEMERAL_TOKEN, ::ConvertUCS2ToUTF8(titleText), ::ConvertUCS2ToUTF8(defaultText), false, [native](std::string_view value, int) { // Success callback - std::lock_guard guard(native->mutex); - if (native->status != PSPOskNativeStatus::WAITING) { - return; - } - native->value = value; - native->status = PSPOskNativeStatus::SUCCESS; + native->Answer(PSPOskNativeStatus::SUCCESS, value); }, [native](int responseValue) { // Failure callback - std::lock_guard guard(native->mutex); - if (native->status != PSPOskNativeStatus::WAITING) { - return; - } - native->value.clear(); - native->status = PSPOskNativeStatus::FAILURE; + native->Answer(PSPOskNativeStatus::FAILURE, std::string_view()); } ); } else if (status == PSPOskNativeStatus::SUCCESS) { @@ -1099,10 +1089,8 @@ void PSPOskDialog::DoState(PointerWrap &p) if (p.mode == p.MODE_READ) { // A box still open can answer into the loaded state (the next Update would only open // another). One that finished before mustn't block the next. - // Hold a reference, so the guard doesn't outlive the mutex it unlocks when native_ is replaced. - std::shared_ptr native = native_; - std::lock_guard guard(native->mutex); - if (native->status != PSPOskNativeStatus::WAITING) { + const PSPOskNativeStatus status = native_->Status(); + if (status != PSPOskNativeStatus::WAITING && status != PSPOskNativeStatus::FILLING) { native_ = std::make_shared(); } } diff --git a/Core/Dialog/PSPOskDialog.h b/Core/Dialog/PSPOskDialog.h index 2ace6792eb..7c96093580 100644 --- a/Core/Dialog/PSPOskDialog.h +++ b/Core/Dialog/PSPOskDialog.h @@ -18,7 +18,9 @@ #pragma once #include -#include +#include + +#include #include #include "Core/Dialog/PSPDialog.h" @@ -184,6 +186,7 @@ enum class PSPOskNativeStatus { WAITING, SUCCESS, FAILURE, + FILLING, }; class PSPOskDialog: public PSPDialog { @@ -227,11 +230,25 @@ private: OskKeyboardLanguage currentKeyboardLanguage = OSK_LANGUAGE_ENGLISH; bool isCombinated = false; - // Shared with the native input box's callbacks, which can come after the dialog is gone. + // Shared with the native input box's callbacks, which can come after the dialog is gone and + // on another thread. A callback claims WAITING with a CAS (to FILLING), writes value, then + // publishes SUCCESS or FAILURE. struct NativeInput { - std::mutex mutex; - PSPOskNativeStatus status = PSPOskNativeStatus::IDLE; + retro_atomic_int_t status{ (int)PSPOskNativeStatus::IDLE }; std::string value; + + PSPOskNativeStatus Status() { + return (PSPOskNativeStatus)retro_atomic_load_acquire_int(&status); + } + void SetStatus(PSPOskNativeStatus s) { + retro_atomic_store_release_int(&status, (int)s); + } + void Answer(PSPOskNativeStatus result, std::string_view v) { + if (retro_atomic_cas_int(&status, (int)PSPOskNativeStatus::WAITING, (int)PSPOskNativeStatus::FILLING)) { + value = v; + SetStatus(result); + } + } }; std::shared_ptr native_ = std::make_shared(); diff --git a/Core/Dialog/PSPSaveDialog.cpp b/Core/Dialog/PSPSaveDialog.cpp index 4f702503f5..02cc0d4246 100755 --- a/Core/Dialog/PSPSaveDialog.cpp +++ b/Core/Dialog/PSPSaveDialog.cpp @@ -17,7 +17,6 @@ #include #include -#include #include "Common/Data/Encoding/Utf8.h" #include "Common/Data/Text/I18n.h" @@ -26,7 +25,6 @@ #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" #include "Common/StringUtils.h" -#include "Common/Thread/ThreadUtil.h" #include "Core/Dialog/PSPSaveDialog.h" #include "Core/FileSystems/MetaFileSystem.h" #include "Core/Util/PathUtil.h" @@ -107,9 +105,6 @@ PSPSaveDialog::PSPSaveDialog(UtilityDialogType type) : PSPDialog(type) { } PSPSaveDialog::~PSPSaveDialog() { - if (ioThread.joinable()) { - ioThread.join(); - } } int PSPSaveDialog::Init(int paramAddr) { @@ -119,12 +114,7 @@ int PSPSaveDialog::Init(int paramAddr) { return SCE_ERROR_UTILITY_INVALID_STATUS; } - if (ioThread.joinable()) { - // Normally shouldn't be the case here. - ioThread.join(); - } - - ioThreadStatus = SAVEIO_NONE; + ioStatus_ = SAVEIO_NONE; requestAddr = 0; const int check = CheckRequest(paramAddr, { SAVEDATA_DIALOG_SIZE_V1, SAVEDATA_DIALOG_SIZE_V2, SAVEDATA_DIALOG_SIZE_V3 }); @@ -406,7 +396,6 @@ void PSPSaveDialog::DisplayBanner(int which) } void PSPSaveDialog::DisplaySaveList(bool canMove) { - std::lock_guard guard(paramLock); static int upFramesHeld = 0; static int downFramesHeld = 0; @@ -475,7 +464,6 @@ void PSPSaveDialog::DisplaySaveList(bool canMove) { } void PSPSaveDialog::DisplaySaveIcon(bool checkExists) { - std::lock_guard guard(paramLock); PPGeImageStyle imageStyle = FadedImageStyle(); auto curSave = param.GetFileInfo(currentSelectedSave); @@ -547,7 +535,6 @@ static void FormatSaveDate(char *date, size_t sz, const tm &t) { } void PSPSaveDialog::DisplaySaveDataInfo1() { - std::lock_guard guard(paramLock); const SaveFileInfo &saveInfo = param.GetFileInfo(currentSelectedSave); PPGeStyle saveTitleStyle = FadedStyle(PPGeAlign::BOX_LEFT, 0.55f); @@ -587,7 +574,6 @@ void PSPSaveDialog::DisplaySaveDataInfo1() { } void PSPSaveDialog::DisplaySaveDataInfo2(bool showNewData) { - std::lock_guard guard(paramLock); tm modif_time; const char *save_title; @@ -686,7 +672,6 @@ int PSPSaveDialog::Update(int animSpeed) { memset(&request, 0, sizeof(request)); Memory::Memcpy(&request, requestAddr, size); Memory::Memcpy(&originalRequest, requestAddr, size); - std::lock_guard guard(paramLock); param.SetPspParam(&request); } @@ -719,7 +704,7 @@ int PSPSaveDialog::Update(int animSpeed) { display = DS_SAVE_CONFIRM_OVERWRITE; } else { display = DS_SAVE_SAVING; - StartIOThread(); + StartIO(); } } EndDraw(); @@ -740,7 +725,7 @@ int PSPSaveDialog::Update(int animSpeed) { StartFade(false); } else if (IsButtonPressed(okButtonFlag)) { display = DS_SAVE_SAVING; - StartIOThread(); + StartIO(); } EndDraw(); @@ -765,14 +750,14 @@ int PSPSaveDialog::Update(int animSpeed) { } } else if (IsButtonPressed(okButtonFlag)) { display = DS_SAVE_SAVING; - StartIOThread(); + StartIO(); } EndDraw(); break; case DS_SAVE_SAVING: // The dialog keeps drawing while the IO runs, and takes the results once it's done. - FinishIO(false); + FinishIO(); StartDraw(); @@ -843,7 +828,7 @@ int PSPSaveDialog::Update(int animSpeed) { StartFade(false); } else if (IsButtonPressed(okButtonFlag)) { display = DS_LOAD_LOADING; - StartIOThread(); + StartIO(); } EndDraw(); @@ -864,14 +849,14 @@ int PSPSaveDialog::Update(int animSpeed) { StartFade(false); } else if (IsButtonPressed(okButtonFlag)) { display = DS_LOAD_LOADING; - StartIOThread(); + StartIO(); } EndDraw(); break; case DS_LOAD_LOADING: // The dialog keeps drawing while the IO runs, and takes the results once it's done. - FinishIO(false); + FinishIO(); StartDraw(); @@ -986,14 +971,14 @@ int PSPSaveDialog::Update(int animSpeed) { } } else if (IsButtonPressed(okButtonFlag)) { display = DS_DELETE_DELETING; - StartIOThread(); + StartIO(); } EndDraw(); break; case DS_DELETE_DELETING: // The dialog keeps drawing while the IO runs, and takes the results once it's done. - FinishIO(false); + FinishIO(); StartDraw(); @@ -1060,9 +1045,9 @@ int PSPSaveDialog::Update(int animSpeed) { break; case DS_NONE: // For action which display nothing - if (ioThreadStatus == SAVEIO_NONE) { - StartIOThread(); - } else if (FinishIO(g_Config.iIOTimingMethod != IOTIMING_HOST)) { + if (ioStatus_ == SAVEIO_NONE) { + StartIO(); + } else if (FinishIO()) { ChangeStatus(SCE_UTILITY_STATUS_FINISHED, 0); } break; @@ -1079,7 +1064,7 @@ int PSPSaveDialog::Update(int animSpeed) { return 0; } -// Runs on the IO thread. Of the dialog, it only touches the io* members (see StartIOThread and FinishIO). +// Of the dialog, this only touches the io* members (see StartIO and FinishIO). void PSPSaveDialog::ExecuteIOAction() { ioParam_.ClearSFOCache(); auto &result = ioRequest_.common.result; @@ -1111,7 +1096,7 @@ void PSPSaveDialog::ExecuteIOAction() { } ioParam_.ClearSFOCache(); - ioThreadStatus = SAVEIO_READY; + ioStatus_ = SAVEIO_READY; } void PSPSaveDialog::ExecuteNotVisibleIOAction() { @@ -1195,10 +1180,9 @@ void PSPSaveDialog::ExecuteNotVisibleIOAction() { } } -void PSPSaveDialog::StartIOThread() { - if (ioThread.joinable()) { - WARN_LOG_REPORT(Log::sceUtility, "Starting a save io thread when one already pending, uh oh."); - ioThread.join(); +void PSPSaveDialog::StartIO() { + if (ioStatus_ == SAVEIO_READY) { + WARN_LOG_REPORT(Log::sceUtility, "Starting save io while the last results weren't taken, uh oh."); } // Show save indicator. It's strange how "display" is just as much an action as what to display. @@ -1208,7 +1192,7 @@ void PSPSaveDialog::StartIOThread() { ShowSaveLoadIndicator(save); } - // Everything the IO thread needs from the dialog, taken now: it doesn't look at the dialog's state. + // Everything the IO needs from the dialog, taken now: it doesn't look at the dialog's state. const SceUtilitySavedataType mode = (SceUtilitySavedataType)(u32)request.mode; ioAction_ = display; ioRequest_ = request; @@ -1226,29 +1210,18 @@ void PSPSaveDialog::StartIOThread() { ioDeleteDir_ = param.GetSaveDir(param.GetSelectedSave()); } - ioThreadStatus = SAVEIO_PENDING; - ioThread = std::thread([this]() { - SetCurrentThreadName("SaveIO"); - - this->ExecuteIOAction(); - }); + ioStatus_ = SAVEIO_PENDING; + ExecuteIOAction(); } -// Takes the IO thread's results back. Without wait, only if it's done: returns whether it was. -bool PSPSaveDialog::FinishIO(bool wait) { - if (ioThreadStatus == SAVEIO_PENDING && !wait) { - return false; - } - if (ioThread.joinable()) { - ioThread.join(); - } - if (ioThreadStatus != SAVEIO_READY) { +// Takes the IO's results back. Returns whether there's no IO left pending. +bool PSPSaveDialog::FinishIO() { + if (ioStatus_ != SAVEIO_READY) { return true; } { // Only what the IO changed: the game may have changed its request meanwhile (Update reloads it). - std::lock_guard guard(paramLock); const u8 *before = (const u8 *)&ioRequestStart_; const u8 *after = (const u8 *)&ioRequest_; u8 *live = (u8 *)&request; @@ -1306,29 +1279,19 @@ bool PSPSaveDialog::FinishIO(bool wait) { display = ioDisplay_; // Show what was saved or deleted in the list. if (display == DS_SAVE_DONE || display == DS_DELETE_DONE) { - std::lock_guard guard(paramLock); param.SetPspParam(param.GetPspParam()); } } - ioThreadStatus = SAVEIO_DONE; + ioStatus_ = SAVEIO_DONE; return true; } -void PSPSaveDialog::WaitForIO() { - if (ioThread.joinable()) { - ioThread.join(); - } -} - int PSPSaveDialog::Shutdown(bool force) { if (GetStatus() != SCE_UTILITY_STATUS_FINISHED && !force) return SCE_ERROR_UTILITY_INVALID_STATUS; - if (ioThread.joinable()) { - ioThread.join(); - } - ioThreadStatus = SAVEIO_NONE; + ioStatus_ = SAVEIO_NONE; PSPDialog::Shutdown(force); if (!force) { @@ -1363,14 +1326,11 @@ static void DoStateOldPendingWrites(PointerWrap &p) { } void PSPSaveDialog::DoState(PointerWrap &p) { - if (ioThread.joinable()) { - ioThread.join(); - } PSPDialog::DoState(p); - // Version 3 activates the s > 2 branch below, so ioThreadStatus survives - // a savestate. Safe to restore: the IO thread was joined above, so the - // value is never SAVEIO_PENDING. Without this, loading a state taken + // Version 3 activates the s > 2 branch below, so ioStatus_ survives + // a savestate. The IO runs to completion when it starts, so the value is + // never SAVEIO_PENDING. Without this, loading a state taken // while a savedata operation was in flight would restart the operation // instead of resuming from its recorded status. Version 4 keeps the // results of a finished operation that haven't been taken yet. Version 6 keeps originalRequest, @@ -1392,13 +1352,13 @@ void PSPSaveDialog::DoState(PointerWrap &p) { Do(p, requestAddr); Do(p, currentSelectedSave); Do(p, yesnoChoice); - SaveIOStatus ioStatus = ioThreadStatus; + SaveIOStatus ioStatus = ioStatus_; if (s > 2) { Do(p, ioStatus); } else { ioStatus = SAVEIO_NONE; } - ioThreadStatus = ioStatus; + ioStatus_ = ioStatus; if (s >= 4) { Do(p, ioAction_); Do(p, ioDisplay_); diff --git a/Core/Dialog/PSPSaveDialog.h b/Core/Dialog/PSPSaveDialog.h index 0dbaf84467..854daef476 100644 --- a/Core/Dialog/PSPSaveDialog.h +++ b/Core/Dialog/PSPSaveDialog.h @@ -17,10 +17,7 @@ #pragma once -#include #include -#include -#include #include "Core/Dialog/PSPDialog.h" #include "Core/Dialog/SavedataParam.h" @@ -35,8 +32,6 @@ public: int Shutdown(bool force = false) override; void DoState(PointerWrap &p) override; pspUtilityDialogCommon *GetCommonParam() override; - // Waits for the IO thread, if any, to be done with PSP memory. Its results are still taken as usual. - void WaitForIO(); protected: bool UseAutoStatus() override { @@ -52,8 +47,8 @@ private: void DisplayMessage(std::string_view text, bool hasYesNo = false); std::string GetSelectedSaveDirName() const; - void StartIOThread(); - bool FinishIO(bool wait); + void StartIO(); + bool FinishIO(); void ExecuteIOAction(); void ExecuteNotVisibleIOAction(); @@ -108,13 +103,11 @@ private: SAVEIO_READY, }; - std::thread ioThread; - std::mutex paramLock; - std::atomic ioThreadStatus{ SAVEIO_NONE }; + SaveIOStatus ioStatus_ = SAVEIO_NONE; - // The IO thread uses these instead of the dialog's own state (it writes its results to PSP - // memory directly). StartIOThread sets them up and FinishIO takes the results back, both on the - // emulator thread. + // The IO runs on these instead of the dialog's own state (it writes its results to PSP memory + // directly). StartIO sets them up and runs it, and FinishIO takes the results back on a later + // update, as the game would see it finish. DisplayState ioAction_ = DS_NONE; DisplayState ioDisplay_ = DS_NONE; SceUtilitySavedataParam ioRequest_{}; diff --git a/Core/Dialog/SavedataParam.cpp b/Core/Dialog/SavedataParam.cpp index d5ded0b5a5..c2419fe368 100644 --- a/Core/Dialog/SavedataParam.cpp +++ b/Core/Dialog/SavedataParam.cpp @@ -2017,12 +2017,10 @@ void SavedataParam::DoState(PointerWrap &p) { } void SavedataParam::ClearSFOCache() { - std::lock_guard guard(cacheLock_); sfoCache_.clear(); } std::shared_ptr SavedataParam::LoadCachedSFO(const std::string &path, bool orCreate) { - std::lock_guard guard(cacheLock_); if (sfoCache_.find(path) == sfoCache_.end()) { std::vector data; if (pspFileSystem.ReadEntireFile(path, data, true) < 0) { diff --git a/Core/Dialog/SavedataParam.h b/Core/Dialog/SavedataParam.h index 5689b51954..b31384aeb7 100644 --- a/Core/Dialog/SavedataParam.h +++ b/Core/Dialog/SavedataParam.h @@ -18,7 +18,6 @@ #pragma once #include -#include #include #include #include @@ -399,6 +398,5 @@ private: bool ignoreTextures_ = false; // Cleared before returning to PSP, no need to save state. - std::mutex cacheLock_; std::unordered_map> sfoCache_; }; diff --git a/Core/EmuThread.cpp b/Core/EmuThread.cpp index 46260e6b1a..03ea7135d3 100644 --- a/Core/EmuThread.cpp +++ b/Core/EmuThread.cpp @@ -1,8 +1,6 @@ #include "ppsspp_config.h" -#include -#include -#include +#include #include "Common/System/System.h" #include "Common/System/Request.h" @@ -13,6 +11,7 @@ #include "Common/Log.h" #include "Common/StringUtils.h" #include "Common/GPU/GraphicsContext.h" +#include "Common/Thread/Thread.h" #include "Common/Thread/ThreadUtil.h" #include "Core/EmuThread.h" @@ -27,37 +26,38 @@ enum class EmuThreadState { STOPPED, }; -static std::atomic g_emuThreadState(EmuThreadState::STOPPED); -static std::atomic g_inLoop; +static retro_atomic_int_t g_emuThreadState{ (int)EmuThreadState::STOPPED }; -class GraphicsContext; +static EmuThreadState GetEmuThreadState() { + return (EmuThreadState)retro_atomic_load_acquire_int(&g_emuThreadState); +} -bool MainThread_Ready() { - return g_inLoop; +static void SetEmuThreadState(EmuThreadState state) { + retro_atomic_store_release_int(&g_emuThreadState, (int)state); } static void EmuThreadFunc(GraphicsContext *graphicsContext, Application *application, std::function frame) { INFO_LOG(Log::G3D, "Entering separate emu thread"); SetCurrentThreadName("EmuThread"); - g_emuThreadState = EmuThreadState::RUNNING; + SetEmuThreadState(EmuThreadState::RUNNING); // This normally calls NativeInitGraphics() if (!application->InitGraphics(graphicsContext)) { _assert_msg_(false, "NativeInitGraphics failed, might as well bail"); // If this fails, which it normally shouldn't, let's bail. - g_emuThreadState = EmuThreadState::QUIT_REQUESTED; + SetEmuThreadState(EmuThreadState::QUIT_REQUESTED); } else { INFO_LOG(Log::G3D, "EmuThread: Entering loop"); } - while (g_emuThreadState != EmuThreadState::QUIT_REQUESTED) { + while (GetEmuThreadState() != EmuThreadState::QUIT_REQUESTED) { // We're here again, so the game quit. Restart Run() which controls the UI. // This way they can load a new game. // This normally calls NativeFrame() if (!frame(graphicsContext)) { - g_emuThreadState = EmuThreadState::QUIT_REQUESTED; + SetEmuThreadState(EmuThreadState::QUIT_REQUESTED); } } @@ -69,46 +69,43 @@ static void EmuThreadFunc(GraphicsContext *graphicsContext, Application *applica INFO_LOG(Log::System, "Leaving separate emu thread"); - g_emuThreadState = EmuThreadState::STOPPED; + SetEmuThreadState(EmuThreadState::STOPPED); } -std::thread EmuThread_Start(GraphicsContext *graphicsContext, Application *application, std::function frame) { +static Thread EmuThread_Start(GraphicsContext *graphicsContext, Application *application, std::function frame) { INFO_LOG(Log::System, "EmuTread_Start"); - _dbg_assert_(g_emuThreadState == EmuThreadState::STOPPED); - std::thread emuThread = std::thread(&EmuThreadFunc, graphicsContext, application, frame); + _dbg_assert_(GetEmuThreadState() == EmuThreadState::STOPPED); + Thread emuThread(&EmuThreadFunc, graphicsContext, application, frame); graphicsContext->ThreadStart(); return emuThread; } // This is useful when the render thread is in control. -void EmuThread_RequestExit() { +static void EmuThread_RequestExit() { INFO_LOG(Log::System, "EmuTread_RequestExit"); - if (g_emuThreadState == EmuThreadState::RUNNING) { - g_emuThreadState = EmuThreadState::QUIT_REQUESTED; + if (GetEmuThreadState() == EmuThreadState::RUNNING) { + SetEmuThreadState(EmuThreadState::QUIT_REQUESTED); } else { INFO_LOG(Log::System, "EmuTread_RequestExit: g_emuThreadState was not RUNNING, so not requesting exit."); } } -void EmuThread_Join(GraphicsContext *graphicsContext, std::thread &emuThread) { +static void EmuThread_Join(GraphicsContext *graphicsContext, Thread &emuThread) { INFO_LOG(Log::System, "EmuTread_Join"); if (graphicsContext->NeedsSeparateEmuThread()) { EmuThread_RequestExit(); while (graphicsContext->ThreadFrame()) {} } emuThread.join(); - emuThread = std::thread(); graphicsContext->ThreadEnd(); } -bool RunMainLoop(GraphicsContext *graphicsContext, Application *application, std::function frame) { +static bool RunMainLoop(GraphicsContext *graphicsContext, Application *application, std::function frame) { // This is the main loop for graphics context that handle their own threading. // InitFromRenderThread/ShutdownFromRenderThread are not used. application->InitGraphics(graphicsContext); - g_inLoop = true; - while (frame(graphicsContext)) {} // NOTE: Don't call stuff like Core_Stop here. On Android, we fully shut down graphics when you switch away from the app, @@ -117,8 +114,6 @@ bool RunMainLoop(GraphicsContext *graphicsContext, Application *application, std // Process the shutdown. Without this, non-GL delays 800ms on shutdown. TODO: is this still an issue? Core_StateProcessed(); - g_inLoop = false; - application->ShutdownGraphics(graphicsContext); delete application; return true; @@ -135,15 +130,12 @@ bool MainThreadFunc(GraphicsContext *graphicsContext, Application *application, if (graphicsContext->NeedsSeparateEmuThread()) { SetCurrentThreadName("RenderThread"); - g_inLoop = true; - std::thread emuThread = EmuThread_Start(graphicsContext, application, frame); + Thread emuThread = EmuThread_Start(graphicsContext, application, frame); graphicsContext->ThreadStart(); // This thread becomes the render thread. EmuThread will tell it when to quit by sending a message. while (graphicsContext->ThreadFrame()) {} EmuThread_Join(graphicsContext, emuThread); - g_inLoop = false; - - graphicsContext->ThreadEnd(); + graphicsContext->ThreadEnd(); INFO_LOG(Log::System, "RenderThread - joined"); diff --git a/Core/EmuThread.h b/Core/EmuThread.h index b220b0c5fe..f83feccd41 100644 --- a/Core/EmuThread.h +++ b/Core/EmuThread.h @@ -19,34 +19,17 @@ #include #include -#include #include "Common/System/Application.h" -// Utilities to manage Emu and Render threads. -// TODO: Use across platforms, currently Windows-only. - -bool MainThread_Ready(); +// Runs the headless tools' frame loop, on a separate emu thread when the graphics context needs +// one (this thread then becomes its render thread). class GraphicsContext; struct WindowDesc; // Doesn't take ownership of the graphicsContext, you have to delete it. -// This should be used by platforms that launch a separate thread and doesn't -// need to run a polling loop in it. // NOTE: Does take ownership over Application (which is just a wrapper for NativeInitGraphics/NativeShutdownGraphics/NativeFrame). // On failure (which currently only means the graphics surface couldn't be initialized), errorMessage // gets the reason - pass it on to the user, it's the only place it's available. bool MainThreadFunc(GraphicsContext * graphicsContext, Application *application, const WindowDesc &windowDesc, std::function frame, std::string *errorMessage); - -// If you're not using MainThreadFunc, you can at least use these to manage a spinning EmuThread (that calls NativeFrame), -// whether your graphics context requires multithreading or not. Then use RunMainLoop to implement your main loop for -// the case where a separate EmuThread is not needed. -// NOTE: Does take ownership over Application (which is just a wrapper for NativeInitGraphics/NativeShutdownGraphics/NativeFrame). -std::thread EmuThread_Start(GraphicsContext *graphicsContext, Application *application, std::function frame); -void EmuThread_RequestExit(); // Useful when the render thread is in control like on Android. -void EmuThread_Join(GraphicsContext *graphicsContext, std::thread &emuThread); - -// Call from the main thread. -// NOTE: Does take ownership over Application (which is just a wrapper for NativeInitGraphics/NativeShutdownGraphics/NativeFrame). -bool RunMainLoop(GraphicsContext *graphicsContext, Application *application, std::function frame); diff --git a/Core/FileLoaders/CachingFileLoader.cpp b/Core/FileLoaders/CachingFileLoader.cpp index f55be9f30e..5d6135a270 100644 --- a/Core/FileLoaders/CachingFileLoader.cpp +++ b/Core/FileLoaders/CachingFileLoader.cpp @@ -17,10 +17,8 @@ #include #include -#include #include "Common/Thread/ThreadUtil.h" -#include "Common/TimeUtil.h" #include "Core/FileLoaders/CachingFileLoader.h" // Takes ownership of backend. @@ -29,12 +27,14 @@ CachingFileLoader::CachingFileLoader(FileLoader *backend) } void CachingFileLoader::Prepare() { - std::call_once(preparedFlag_, [this](){ - filesize_ = ProxiedFileLoader::FileSize(); - if (filesize_ > 0) { - InitCache(); - } - }); + if (prepared_) { + return; + } + prepared_ = true; + filesize_ = ProxiedFileLoader::FileSize(); + if (filesize_ > 0) { + InitCache(); + } } CachingFileLoader::~CachingFileLoader() { @@ -77,14 +77,19 @@ size_t CachingFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Flag bytes = (size_t)(filesize_ - absolutePos); } + if (!slots_) { + // Empty or unknown size: no cache and no worker. + return (flags & Flags::HINT_UNCACHED) != 0 ? backend_->ReadAt(absolutePos, bytes, data, flags) : 0; + } + size_t readSize = 0; if ((flags & Flags::HINT_UNCACHED) != 0) { - readSize = backend_->ReadAt(absolutePos, bytes, data, flags); + readSize = RequestUncached(absolutePos, bytes, data, flags); } else { readSize = ReadFromCache(absolutePos, bytes, data); // While in case the cache size is too small for the entire read. while (readSize < bytes) { - SaveIntoCache(absolutePos + readSize, bytes - readSize, flags); + RequestIntoCache(absolutePos + readSize, bytes - readSize, flags); size_t bytesFromCache = ReadFromCache(absolutePos + readSize, bytes - readSize, (u8 *)data + readSize); readSize += bytesFromCache; if (bytesFromCache == 0) { @@ -102,21 +107,35 @@ size_t CachingFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Flag void CachingFileLoader::InitCache() { oldestGeneration_ = 0; generation_ = 0; + numBlocks_ = (filesize_ + BLOCK_SIZE - 1) >> BLOCK_SHIFT; + slots_.reset(new retro_atomic_ptr_t[(size_t)numBlocks_]); + for (s64 i = 0; i < numBlocks_; ++i) { + retro_atomic_ptr_init(&slots_[(size_t)i], nullptr); + } + generations_.reset(new u64[(size_t)numBlocks_]()); + worker_ = Thread([this] { + WorkerLoop(); + }); } void CachingFileLoader::ShutdownCache() { - // We can't delete while the read-ahead thread is running; the join waits for it. - if (aheadThread_.joinable()) - aheadThread_.join(); - - std::lock_guard guard(blocksMutex_); - for (const auto &block : blocks_) { - delete [] block.second.ptr; + retro_atomic_store_release_int(&quit_, 1); + wake_.Notify(); + // Waits out any backend read in progress. + if (worker_.joinable()) { + worker_.join(); } - blocks_.clear(); + + for (s64 i = 0; i < numBlocks_; ++i) { + delete [] BlockAt(i); + } + slots_.reset(); } size_t CachingFileLoader::ReadFromCache(s64 pos, size_t bytes, void *data) { + if (bytes == 0) { + return 0; + } s64 cacheStartPos = pos >> BLOCK_SHIFT; s64 cacheEndPos = (pos + bytes - 1) >> BLOCK_SHIFT; // TODO: Smarter. @@ -124,16 +143,15 @@ size_t CachingFileLoader::ReadFromCache(s64 pos, size_t bytes, void *data) { size_t offset = (size_t)(pos - (cacheStartPos << BLOCK_SHIFT)); u8 *p = (u8 *)data; - std::lock_guard guard(blocksMutex_); for (s64 i = cacheStartPos; i <= cacheEndPos; ++i) { - auto block = blocks_.find(i); - if (block == blocks_.end()) { + const u8 *block = BlockAt(i); + if (!block) { return readSize; } - block->second.generation = generation_; + generations_[(size_t)i] = generation_; size_t toRead = std::min(bytes - readSize, (size_t)BLOCK_SIZE - offset); - memcpy(p + readSize, block->second.ptr + offset, toRead); + memcpy(p + readSize, block + offset, toRead); readSize += toRead; // Don't need an offset after the first read. @@ -142,17 +160,105 @@ size_t CachingFileLoader::ReadFromCache(s64 pos, size_t bytes, void *data) { return readSize; } +size_t CachingFileLoader::RunRequest() { + const int seq = (int)((unsigned)reqSeqLocal_ + 1); + reqSeqLocal_ = seq; + retro_atomic_store_release_int(&reqSeq_, seq); + wake_.Notify(); + ParkingLotWait(&doneSeq_, [&] { + return retro_atomic_load_acquire_int(&doneSeq_) == seq; + }); + return reqResult_; +} + +size_t CachingFileLoader::RequestUncached(s64 pos, size_t bytes, void *data, Flags flags) { + reqPos_ = pos; + reqBytes_ = bytes; + reqData_ = data; + reqFlags_ = flags; + return RunRequest(); +} + +void CachingFileLoader::RequestIntoCache(s64 pos, size_t bytes, Flags flags) { + s64 cacheStartPos = pos >> BLOCK_SHIFT; + s64 cacheEndPos = std::min((pos + (s64)bytes - 1) >> BLOCK_SHIFT, numBlocks_ - 1); + + size_t blocksToRead = 0; + for (s64 i = cacheStartPos; i <= cacheEndPos; ++i) { + if (BlockAt(i)) { + break; + } + ++blocksToRead; + if (blocksToRead >= MAX_BLOCKS_PER_READ) { + break; + } + } + if (blocksToRead == 0) { + return; + } + + MakeCacheSpaceFor(blocksToRead); + reqPos_ = pos; + reqBytes_ = bytes; + reqData_ = nullptr; + reqFlags_ = flags; + RunRequest(); + ++generation_; +} + +void CachingFileLoader::WorkerLoop() { + SetCurrentThreadName("FileLoaderReadAhead"); + + int done = 0; + for (;;) { + const int seen = wake_.Seen(); + if (retro_atomic_load_acquire_int(&quit_)) { + break; + } + + const int seq = retro_atomic_load_acquire_int(&reqSeq_); + if (seq != done) { + if (reqData_) { + reqResult_ = backend_->ReadAt(reqPos_, reqBytes_, reqData_, reqFlags_); + } else { + SaveIntoCache(reqPos_, reqBytes_, reqFlags_); + } + done = seq; + retro_atomic_store_release_int(&doneSeq_, seq); + ParkingLotNotify(&doneSeq_); + continue; + } + + const int ahead = retro_atomic_exchange_int(&aheadBlock_, -1); + if (ahead >= 0) { + ReadAhead(ahead); + continue; + } + + wake_.Wait(seen); + } +} + +void CachingFileLoader::ReadAhead(s64 cacheStartPos) { + s64 cacheEndPos = std::min(cacheStartPos + BLOCK_READAHEAD - 1, numBlocks_ - 1); + for (s64 i = cacheStartPos; i <= cacheEndPos; ++i) { + if (!BlockAt(i)) { + SaveIntoCache(i << BLOCK_SHIFT, BLOCK_SIZE * BLOCK_READAHEAD, Flags::NONE, true); + break; + } + } +} + void CachingFileLoader::SaveIntoCache(s64 pos, size_t bytes, Flags flags, bool readingAhead) { s64 cacheStartPos = pos >> BLOCK_SHIFT; s64 cacheEndPos = (pos + bytes - 1) >> BLOCK_SHIFT; // Read-ahead can ask for blocks past the end of the file. - cacheEndPos = std::min(cacheEndPos, (filesize_ - 1) >> BLOCK_SHIFT); + cacheEndPos = std::min(cacheEndPos, numBlocks_ - 1); - std::lock_guard guard(blocksMutex_); + // Only this thread fills slots, so the empty ones counted here stay empty until we fill them. size_t blocksToRead = 0; for (s64 i = cacheStartPos; i <= cacheEndPos; ++i) { - auto block = blocks_.find(i); - if (block != blocks_.end()) { + if (BlockAt(i)) { break; } ++blocksToRead; @@ -161,36 +267,30 @@ void CachingFileLoader::SaveIntoCache(s64 pos, size_t bytes, Flags flags, bool r } } - if (!MakeCacheSpaceFor(blocksToRead, readingAhead) || blocksToRead == 0) { + if (blocksToRead == 0) { + return; + } + // Read-ahead never evicts; the reader makes room before a demand read. + if (readingAhead && (size_t)retro_atomic_load_acquire_int(&cachedCount_) + blocksToRead > MAX_BLOCKS_CACHED) { return; } if (blocksToRead == 1) { - blocksMutex_.unlock(); - u8 *buf = new u8[BLOCK_SIZE]; size_t readBytes = backend_->ReadAt(cacheStartPos << BLOCK_SHIFT, BLOCK_SIZE, buf, flags); - blocksMutex_.lock(); - // While blocksMutex_ was unlocked, another thread may have read. - // If so, free the one we just read. - if (blocks_.find(cacheStartPos) == blocks_.end()) { - // Only cache a block we actually fully read - a short/failed read (e.g. a - // dropped connection on a Remote ISO) must not be cached as if valid, or - // every later read of this block would silently return the uninitialized - // tail of `buf` as if it were real file data. The last block of the file - // is shorter, and complete if the read reached the end. - if (readBytes == BLOCK_SIZE || (readBytes > 0 && (cacheStartPos << BLOCK_SHIFT) + (s64)readBytes == filesize_)) { - blocks_[cacheStartPos] = BlockInfo{buf}; - } else { - delete [] buf; - } + // Only cache a block we actually fully read - a short/failed read (e.g. a + // dropped connection on a Remote ISO) must not be cached as if valid, or + // every later read of this block would silently return the uninitialized + // tail of `buf` as if it were real file data. The last block of the file + // is shorter, and complete if the read reached the end. + if (readBytes == BLOCK_SIZE || (readBytes > 0 && (cacheStartPos << BLOCK_SHIFT) + (s64)readBytes == filesize_)) { + retro_atomic_store_release_ptr(&slots_[(size_t)cacheStartPos], buf); + retro_atomic_fetch_add_int(&cachedCount_, 1); } else { delete [] buf; } } else { - blocksMutex_.unlock(); - u8 *wholeRead = new u8[blocksToRead << BLOCK_SHIFT]; size_t readBytes = backend_->ReadAt(cacheStartPos << BLOCK_SHIFT, blocksToRead << BLOCK_SHIFT, wholeRead, flags); size_t wholeBlocksRead = readBytes >> BLOCK_SHIFT; @@ -199,97 +299,67 @@ void CachingFileLoader::SaveIntoCache(s64 pos, size_t bytes, Flags flags, bool r wholeBlocksRead++; } - blocksMutex_.lock(); for (size_t i = 0; i < wholeBlocksRead; ++i) { - if (blocks_.find(cacheStartPos + i) != blocks_.end()) { - // Written while we were busy, just skip it. Keep the existing block. - continue; - } u8 *buf = new u8[BLOCK_SIZE]; memcpy(buf, wholeRead + (i << BLOCK_SHIFT), BLOCK_SIZE); - blocks_[cacheStartPos + i] = BlockInfo{buf}; + retro_atomic_store_release_ptr(&slots_[(size_t)cacheStartPos + i], buf); + retro_atomic_fetch_add_int(&cachedCount_, 1); } delete[] wholeRead; } - - ++generation_; } -bool CachingFileLoader::MakeCacheSpaceFor(size_t blocks, bool readingAhead) { +void CachingFileLoader::MakeCacheSpaceFor(size_t blocks) { size_t goal = MAX_BLOCKS_CACHED - blocks; - if (readingAhead && blocks_.size() > goal) { - return false; - } - - std::lock_guard guard(blocksMutex_); - while (blocks_.size() > goal) { + while ((size_t)retro_atomic_load_acquire_int(&cachedCount_) > goal) { u64 minGeneration = generation_; - // We increment the iterator inside because we delete things inside. - for (auto it = blocks_.begin(); it != blocks_.end(); ) { + for (s64 i = 0; i < numBlocks_; ++i) { + u8 *block = BlockAt(i); + if (!block) { + continue; + } + const u64 generation = generations_[(size_t)i]; // Check for the minimum seen generation. // TODO: Do this smarter? - if (it->second.generation != 0 && it->second.generation < minGeneration) { - minGeneration = it->second.generation; + if (generation != 0 && generation < minGeneration) { + minGeneration = generation; } // 0 means it was never used yet or was the first read (e.g. block descriptor.) - if (it->second.generation == oldestGeneration_ || it->second.generation == 0) { - s64 pos = it->first; - delete [] it->second.ptr; - blocks_.erase(it); - - // Our iterator is invalid now. Keep going? - if (blocks_.size() > goal) { - // This finds the one at that position. - it = blocks_.lower_bound(pos); - } else { + if (generation == oldestGeneration_ || generation == 0) { + // The worker never reads a published block, so it can go right away. + retro_atomic_store_release_ptr(&slots_[(size_t)i], nullptr); + generations_[(size_t)i] = 0; + delete [] block; + if ((size_t)(retro_atomic_fetch_sub_int(&cachedCount_, 1) - 1) <= goal) { break; } - } else { - ++it; } } // If we didn't find any, update to the lowest we did find. oldestGeneration_ = minGeneration; } - - return true; } void CachingFileLoader::StartReadAhead(s64 pos) { - std::lock_guard guard(blocksMutex_); - if (aheadThreadRunning_) { - // Already going. + s64 cacheStartPos = pos >> BLOCK_SHIFT; + if (cacheStartPos >= numBlocks_) { return; } - if (blocks_.size() + BLOCK_READAHEAD > MAX_BLOCKS_CACHED) { + if ((size_t)retro_atomic_load_acquire_int(&cachedCount_) + BLOCK_READAHEAD > MAX_BLOCKS_CACHED) { // Not enough space to readahead. return; } - - aheadThreadRunning_ = true; - if (aheadThread_.joinable()) - aheadThread_.join(); - aheadThread_ = std::thread([this, pos] { - SetCurrentThreadName("FileLoaderReadAhead"); - - - std::unique_lock guard(blocksMutex_); - s64 cacheStartPos = pos >> BLOCK_SHIFT; - s64 cacheEndPos = cacheStartPos + BLOCK_READAHEAD - 1; - - for (s64 i = cacheStartPos; i <= cacheEndPos; ++i) { - auto block = blocks_.find(i); - if (block == blocks_.end()) { - guard.unlock(); - SaveIntoCache(i << BLOCK_SHIFT, BLOCK_SIZE * BLOCK_READAHEAD, Flags::NONE, true); - break; - } + s64 cacheEndPos = std::min(cacheStartPos + BLOCK_READAHEAD - 1, numBlocks_ - 1); + for (s64 i = cacheStartPos; i <= cacheEndPos; ++i) { + if (!BlockAt(i)) { + // A newer position replaces one the worker hasn't picked up yet. + retro_atomic_store_release_int(&aheadBlock_, (int)cacheStartPos); + wake_.Notify(); + return; } - - aheadThreadRunning_ = false; - }); + } } diff --git a/Core/FileLoaders/CachingFileLoader.h b/Core/FileLoaders/CachingFileLoader.h index 491fcbdafb..6ae10641a3 100644 --- a/Core/FileLoaders/CachingFileLoader.h +++ b/Core/FileLoaders/CachingFileLoader.h @@ -17,13 +17,17 @@ #pragma once -#include -#include -#include +#include + +#include #include "Common/CommonTypes.h" #include "Core/Loaders.h" +#include "Common/Thread/ParkingLot.h" +#include "Common/Thread/Thread.h" +// ReadAt from one thread at a time. Every backend read happens on the loader's own worker +// thread, so the backend is only ever read from that one thread. class CachingFileLoader : public ProxiedFileLoader { public: CachingFileLoader(FileLoader *backend); @@ -44,11 +48,21 @@ private: void InitCache(); void ShutdownCache(); size_t ReadFromCache(s64 pos, size_t bytes, void *data); - // Guaranteed to read at least one block into the cache. + // Reader side: makes room, then has the worker read at least one block into the cache. + void RequestIntoCache(s64 pos, size_t bytes, Flags flags); + size_t RequestUncached(s64 pos, size_t bytes, void *data, Flags flags); + size_t RunRequest(); + // Worker only. void SaveIntoCache(s64 pos, size_t bytes, Flags flags, bool readingAhead = false); - bool MakeCacheSpaceFor(size_t blocks, bool readingAhead); + void ReadAhead(s64 block); + void WorkerLoop(); + void MakeCacheSpaceFor(size_t blocks); void StartReadAhead(s64 pos); + u8 *BlockAt(s64 block) { + return (u8 *)retro_atomic_load_acquire_ptr(&slots_[(size_t)block]); + } + enum { BLOCK_SIZE = 65536, BLOCK_SHIFT = 16, @@ -57,20 +71,33 @@ private: BLOCK_READAHEAD = 4, }; + // Reader thread only, except filesize_ and numBlocks_, fixed before the worker starts. + bool prepared_ = false; s64 filesize_ = 0; + s64 numBlocks_ = 0; int exists_ = -1; int isDirectory_ = -1; u64 generation_ = 0; u64 oldestGeneration_ = 0; + std::unique_ptr generations_; - struct BlockInfo { - u8 *ptr; - u64 generation; - }; + // One slot per block. Only the worker fills an empty slot, only the reader empties one, + // and the worker never touches a block's data once published. + std::unique_ptr slots_; + retro_atomic_int_t cachedCount_{ 0 }; - std::map blocks_; - std::recursive_mutex blocksMutex_; - bool aheadThreadRunning_ = false; - std::thread aheadThread_; - std::once_flag preparedFlag_; + // One request in flight at a time: the reader fills req*, publishes reqSeq_, waits for doneSeq_. + s64 reqPos_ = 0; + size_t reqBytes_ = 0; + void *reqData_ = nullptr; + Flags reqFlags_ = Flags::NONE; + size_t reqResult_ = 0; + int reqSeqLocal_ = 0; + retro_atomic_int_t reqSeq_{ 0 }; + retro_atomic_int_t doneSeq_{ 0 }; + // Next block to read ahead from, or -1. + retro_atomic_int_t aheadBlock_{ -1 }; + retro_atomic_int_t quit_{ 0 }; + EventCounter wake_; + Thread worker_; }; diff --git a/Core/FileLoaders/DiskCachingFileLoader.cpp b/Core/FileLoaders/DiskCachingFileLoader.cpp index 2e81f191f6..98fd4f7379 100644 --- a/Core/FileLoaders/DiskCachingFileLoader.cpp +++ b/Core/FileLoaders/DiskCachingFileLoader.cpp @@ -19,10 +19,10 @@ #include #include -#include -#include #include +#include + #include "Common/Data/Encoding/Utf8.h" #include "Common/File/DiskFree.h" #include "Common/File/DirListing.h" @@ -44,8 +44,47 @@ static const u32 CACHE_SPACE_FLEX = 4; Path DiskCachingFileLoaderCache::cacheDir_; -std::map DiskCachingFileLoader::caches_; -std::mutex DiskCachingFileLoader::cachesMutex_; +// Cache files open in this process, as a nonzero hash of the file name; 0 is a free slot. +// A hash collision only means a file goes uncached or is kept by garbage collection. +static const int MAX_OPEN_CACHES = 32; +static retro_atomic_int_t g_openCaches[MAX_OPEN_CACHES]; + +static int CacheNameHash(const std::string &name) { + u32 hash = 2166136261U; + for (char c : name) { + hash ^= (u8)c; + hash *= 16777619U; + } + return hash == 0 ? 1 : (int)hash; +} + +static bool IsCacheOpen(int hash, int exceptSlot) { + for (int i = 0; i < MAX_OPEN_CACHES; ++i) { + if (i != exceptSlot && retro_atomic_load_acquire_int(&g_openCaches[i]) == hash) { + return true; + } + } + return false; +} + +// Returns the claimed slot, or -1 if the file is already open (or too many are). +static int ClaimCacheFile(int hash) { + if (IsCacheOpen(hash, -1)) { + return -1; + } + for (int i = 0; i < MAX_OPEN_CACHES; ++i) { + if (retro_atomic_cas_int(&g_openCaches[i], 0, hash)) { + // Two loaders claiming the same file at once see each other here at worst, and both back off. + retro_atomic_thread_fence_seq_cst(); + if (IsCacheOpen(hash, i)) { + retro_atomic_store_release_int(&g_openCaches[i], 0); + return -1; + } + return i; + } + } + return -1; +} // Takes ownership of backend. DiskCachingFileLoader::DiskCachingFileLoader(FileLoader *backend) @@ -53,12 +92,14 @@ DiskCachingFileLoader::DiskCachingFileLoader(FileLoader *backend) } void DiskCachingFileLoader::Prepare() { - std::call_once(preparedFlag_, [this]() { - filesize_ = ProxiedFileLoader::FileSize(); - if (filesize_ > 0) { - InitCache(); - } - }); + if (prepared_) { + return; + } + prepared_ = true; + filesize_ = ProxiedFileLoader::FileSize(); + if (filesize_ > 0) { + InitCache(); + } } DiskCachingFileLoader::~DiskCachingFileLoader() { @@ -117,42 +158,29 @@ size_t DiskCachingFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, return readSize; } -std::vector DiskCachingFileLoader::GetCachedPathsInUse() { - std::lock_guard guard(cachesMutex_); - - // This is on the file loader so that it can manage the caches_. - std::vector files; - files.reserve(caches_.size()); - - for (const auto &it : caches_) { - files.push_back(it.first); - } - - return files; +bool DiskCachingFileLoader::IsCacheFileInUse(const std::string &cacheFilename) { + return IsCacheOpen(CacheNameHash(cacheFilename), -1); } void DiskCachingFileLoader::InitCache() { - std::lock_guard guard(cachesMutex_); - Path path = ProxiedFileLoader::GetPath(); - auto &entry = caches_[path]; - if (!entry) { - entry = new DiskCachingFileLoaderCache(path, filesize_); + claimSlot_ = ClaimCacheFile(CacheNameHash(DiskCachingFileLoaderCache::MakeCacheFilename(path))); + if (claimSlot_ < 0) { + // Another loader has this file's cache open; read straight through. + WARN_LOG(Log::Loader, "Disk cache for %s already in use, not caching", path.c_str()); + return; } - - cache_ = entry; - cache_->AddRef(); + cache_ = new DiskCachingFileLoaderCache(path, filesize_); } void DiskCachingFileLoader::ShutdownCache() { - std::lock_guard guard(cachesMutex_); - - if (cache_->Release()) { - // If it ran out of counts, delete it. - delete cache_; - caches_.erase(ProxiedFileLoader::GetPath()); - } + // Flush and close the file before anyone else may open it. + delete cache_; cache_ = nullptr; + if (claimSlot_ >= 0) { + retro_atomic_store_release_int(&g_openCaches[claimSlot_], 0); + claimSlot_ = -1; + } } DiskCachingFileLoaderCache::DiskCachingFileLoaderCache(const Path &path, u64 filesize) @@ -220,8 +248,6 @@ void DiskCachingFileLoaderCache::ShutdownCache() { } size_t DiskCachingFileLoaderCache::ReadFromCache(s64 pos, size_t bytes, void *data) { - std::lock_guard guard(lock_); - if (!f_) { return 0; } @@ -255,8 +281,6 @@ size_t DiskCachingFileLoaderCache::ReadFromCache(s64 pos, size_t bytes, void *da } size_t DiskCachingFileLoaderCache::SaveIntoCache(FileLoader *backend, s64 pos, size_t bytes, void *data, FileLoader::Flags flags) { - std::lock_guard guard(lock_); - if (!f_) { // Just to keep things working. return backend->ReadAt(pos, bytes, data, flags); @@ -802,12 +826,6 @@ u32 DiskCachingFileLoaderCache::CountCachedFiles() { void DiskCachingFileLoaderCache::GarbageCollectCacheFiles(u64 goalBytes) { // We attempt to free up at least enough files from the cache to get goalBytes more space. - const std::vector usedPaths = DiskCachingFileLoader::GetCachedPathsInUse(); - std::set used; - for (const Path &path : usedPaths) { - used.insert(MakeCacheFilename(path)); - } - Path dir = cacheDir_; if (dir.empty()) { dir = GetSysDirectory(DIRECTORY_CACHE); @@ -822,7 +840,7 @@ void DiskCachingFileLoaderCache::GarbageCollectCacheFiles(u64 goalBytes) { if (file.isDirectory) { continue; } - if (used.find(file.name) != used.end()) { + if (DiskCachingFileLoader::IsCacheFileInUse(file.name)) { // In use, must leave alone. continue; } diff --git a/Core/FileLoaders/DiskCachingFileLoader.h b/Core/FileLoaders/DiskCachingFileLoader.h index 242c28ab5d..ad96d79140 100644 --- a/Core/FileLoaders/DiskCachingFileLoader.h +++ b/Core/FileLoaders/DiskCachingFileLoader.h @@ -17,9 +17,8 @@ #pragma once +#include #include -#include -#include #include "Common/CommonTypes.h" #include "Common/File/Path.h" @@ -28,6 +27,8 @@ class DiskCachingFileLoaderCache; +// Used from one thread at a time (the CachingFileLoader worker that wraps it, or the thread +// that prepares and destroys it), so it and its cache need no locks. class DiskCachingFileLoader : public ProxiedFileLoader { public: DiskCachingFileLoader(FileLoader *backend); @@ -42,21 +43,20 @@ public: } size_t ReadAt(s64 absolutePos, size_t bytes, void *data, Flags flags = Flags::NONE) override; - static std::vector GetCachedPathsInUse(); + // Whether a cache file (by name, as MakeCacheFilename gives it) is open in this process. + static bool IsCacheFileInUse(const std::string &cacheFilename); private: void Prepare(); void InitCache(); void ShutdownCache(); - std::once_flag preparedFlag_; + bool prepared_ = false; s64 filesize_ = 0; DiskCachingFileLoaderCache *cache_ = nullptr; - // We don't support concurrent disk cache access (we use memory cached indexes.) - // So we have to ensure there's only one of these per. - static std::map caches_; - static std::mutex cachesMutex_; + // So a cache file is open in one loader at most; this is our claim on it, or -1. + int claimSlot_ = -1; }; class DiskCachingFileLoaderCache { @@ -68,17 +68,10 @@ public: return f_ != nullptr; } - void AddRef() { - ++refCount_; - } - - bool Release() { - return --refCount_ == 0; - } - static void SetCacheDir(const Path &path) { cacheDir_ = path; } + static std::string MakeCacheFilename(const Path &path); size_t ReadFromCache(s64 pos, size_t bytes, void *data); // Guaranteed to read at least one block into the cache. @@ -100,7 +93,6 @@ private: s64 GetBlockOffset(u32 block); Path MakeCacheFilePath(const Path &filename); - std::string MakeCacheFilename(const Path &path); bool LoadCacheFile(const Path &path); void LoadCacheIndex(); void CreateCacheFile(const Path &path); @@ -137,7 +129,6 @@ private: INVALID_INDEX = 0xFFFFFFFF, }; - int refCount_ = 0; s64 filesize_; u32 blockSize_; u16 generation_; @@ -146,7 +137,6 @@ private: u32 flags_; size_t cacheSize_; size_t indexCount_; - std::mutex lock_; Path origPath_; struct FileHeader { diff --git a/Core/FileLoaders/HTTPFileLoader.cpp b/Core/FileLoaders/HTTPFileLoader.cpp index 882e500826..be61ca407d 100644 --- a/Core/FileLoaders/HTTPFileLoader.cpp +++ b/Core/FileLoaders/HTTPFileLoader.cpp @@ -27,104 +27,106 @@ HTTPFileLoader::HTTPFileLoader(const ::Path &filename) } void HTTPFileLoader::Prepare() { - std::call_once(preparedFlag_, [this](){ - client_.SetUserAgent(StringFromFormat("PPSSPP/%s", PPSSPP_GIT_VERSION)); + if (prepared_) { + return; + } + prepared_ = true; + client_.SetUserAgent(StringFromFormat("PPSSPP/%s", PPSSPP_GIT_VERSION)); - std::vector responseHeaders; - Url resourceURL = url_; - int redirectsLeft = 20; - while (redirectsLeft > 0) { - responseHeaders.clear(); - int code = SendHEAD(resourceURL, responseHeaders); - if (code == -400) { - // Already reported the error. - return; - } - - if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) { - Disconnect(); - - std::string redirectURL; - if (http::GetHeaderValue(responseHeaders, "Location", &redirectURL)) { - Url url(resourceURL); - url = url.Relative(redirectURL); - - if (url.ToString() == url_.ToString() || url.ToString() == resourceURL.ToString()) { - ERROR_LOG(Log::Loader, "HTTP request failed, hit a redirect loop"); - latestError_ = "Could not connect (redirect loop)"; - return; - } - - resourceURL = url; - redirectsLeft--; - continue; - } - - // No Location header? - ERROR_LOG(Log::Loader, "HTTP request failed, invalid redirect"); - latestError_ = "Could not connect (invalid response)"; - return; - } - - if (code != 200) { - // Leave size at 0, invalid. - ERROR_LOG(Log::Loader, "HTTP request failed, got %03d for %s", code, filename_.c_str()); - latestError_ = "Could not connect (invalid response)"; - Disconnect(); - return; - } - - // We got a good, non-redirect response. - redirectsLeft = 0; - url_ = resourceURL; + std::vector responseHeaders; + Url resourceURL = url_; + int redirectsLeft = 20; + while (redirectsLeft > 0) { + responseHeaders.clear(); + int code = SendHEAD(resourceURL, responseHeaders); + if (code == -400) { + // Already reported the error. + return; } - // TODO: Expire cache via ETag, etc. - bool acceptsRange = false; - for (std::string header : responseHeaders) { - if (startsWithNoCase(header, "Content-Length:")) { - size_t size_pos = header.find_first_of(' '); - if (size_pos != header.npos) { - size_pos = header.find_first_not_of(' ', size_pos); - } - if (size_pos != header.npos) { - filesize_ = atoll(&header[size_pos]); + if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) { + Disconnect(); + + std::string redirectURL; + if (http::GetHeaderValue(responseHeaders, "Location", &redirectURL)) { + Url url(resourceURL); + url = url.Relative(redirectURL); + + if (url.ToString() == url_.ToString() || url.ToString() == resourceURL.ToString()) { + ERROR_LOG(Log::Loader, "HTTP request failed, hit a redirect loop"); + SetError("Could not connect (redirect loop)"); + return; } + + resourceURL = url; + redirectsLeft--; + continue; } - if (startsWithNoCase(header, "Accept-Ranges:")) { - std::string lowerHeader = header; - std::transform(lowerHeader.begin(), lowerHeader.end(), lowerHeader.begin(), tolower); - // TODO: Delimited. - if (lowerHeader.find("bytes") != lowerHeader.npos) { - acceptsRange = true; - } + + // No Location header? + ERROR_LOG(Log::Loader, "HTTP request failed, invalid redirect"); + SetError("Could not connect (invalid response)"); + return; + } + + if (code != 200) { + // Leave size at 0, invalid. + ERROR_LOG(Log::Loader, "HTTP request failed, got %03d for %s", code, filename_.c_str()); + SetError("Could not connect (invalid response)"); + Disconnect(); + return; + } + + // We got a good, non-redirect response. + redirectsLeft = 0; + url_ = resourceURL; + } + + // TODO: Expire cache via ETag, etc. + bool acceptsRange = false; + for (std::string header : responseHeaders) { + if (startsWithNoCase(header, "Content-Length:")) { + size_t size_pos = header.find_first_of(' '); + if (size_pos != header.npos) { + size_pos = header.find_first_not_of(' ', size_pos); + } + if (size_pos != header.npos) { + filesize_ = atoll(&header[size_pos]); } } - - // TODO: Keepalive instead. - Disconnect(); - - if (!acceptsRange) { - WARN_LOG(Log::Loader, "HTTP server did not advertise support for range requests."); - } - if (filesize_ == 0) { - ERROR_LOG(Log::Loader, "Could not determine file size for %s", filename_.c_str()); + if (startsWithNoCase(header, "Accept-Ranges:")) { + std::string lowerHeader = header; + std::transform(lowerHeader.begin(), lowerHeader.end(), lowerHeader.begin(), tolower); + // TODO: Delimited. + if (lowerHeader.find("bytes") != lowerHeader.npos) { + acceptsRange = true; + } } + } - // If we didn't end up with a filesize_ (e.g. chunked response), give up. File invalid. - }); + // TODO: Keepalive instead. + Disconnect(); + + if (!acceptsRange) { + WARN_LOG(Log::Loader, "HTTP server did not advertise support for range requests."); + } + if (filesize_ == 0) { + ERROR_LOG(Log::Loader, "Could not determine file size for %s", filename_.c_str()); + } + + // If we didn't end up with a filesize_ (e.g. chunked response), give up. File invalid. } int HTTPFileLoader::SendHEAD(const Url &url, std::vector &responseHeaders) { if (!url.Valid()) { ERROR_LOG(Log::Loader, "HTTP request failed, invalid URL: '%s'", url.ToString().c_str()); - latestError_ = "Invalid URL"; + SetError("Invalid URL"); return -400; } if (!client_.Resolve(url.Host().c_str(), url.Port())) { ERROR_LOG(Log::Loader, "HTTP request failed, unable to resolve: |%s| port %d", url.Host().c_str(), url.Port()); - latestError_ = "Could not connect (name not resolved)"; + SetError("Could not connect (name not resolved)"); return -400; } @@ -134,7 +136,7 @@ int HTTPFileLoader::SendHEAD(const Url &url, std::vector &responseH Connect(10.0); if (!connected_) { ERROR_LOG(Log::Loader, "HTTP request failed, failed to connect: %s port %d (resource: '%s')", url.Host().c_str(), url.Port(), url.Resource().c_str()); - latestError_ = "Could not connect (refused to connect)"; + SetError("Could not connect (refused to connect)"); return -400; } @@ -142,7 +144,7 @@ int HTTPFileLoader::SendHEAD(const Url &url, std::vector &responseH int err = client_.SendRequest("HEAD", req, nullptr, &progress_); if (err < 0) { ERROR_LOG(Log::Loader, "HTTP request failed, failed to send request: %s port %d", url.Host().c_str(), url.Port()); - latestError_ = "Could not connect (could not request data)"; + SetError("Could not connect (could not request data)"); Disconnect(); return -400; } @@ -180,7 +182,6 @@ Path HTTPFileLoader::GetPath() const { size_t HTTPFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Flags flags) { Prepare(); - std::lock_guard guard(readAtMutex_); s64 absoluteEnd = std::min(absolutePos + (s64)bytes, filesize_); if (absolutePos >= filesize_ || bytes == 0) { @@ -201,7 +202,7 @@ size_t HTTPFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Flags f http::RequestParams req(url_.Resource(), "*/*"); int err = client_.SendRequest("GET", req, requestHeaders, &progress_); if (err < 0) { - latestError_ = "Invalid response reading data"; + SetError("Invalid response reading data"); Disconnect(); return 0; } @@ -211,7 +212,7 @@ size_t HTTPFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Flags f int code = client_.ReadResponseHeaders(&readbuf, responseHeaders, &progress_); if (code != 206) { ERROR_LOG(Log::Loader, "HTTP server did not respond with range, received code=%03d", code); - latestError_ = "Invalid response reading data"; + SetError("Invalid response reading data"); Disconnect(); return 0; } @@ -250,7 +251,7 @@ size_t HTTPFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Flags f if (!supportedResponse) { ERROR_LOG(Log::Loader, "HTTP server did not respond with the range we wanted."); - latestError_ = "Invalid response reading data"; + SetError("Invalid response reading data"); return 0; } diff --git a/Core/FileLoaders/HTTPFileLoader.h b/Core/FileLoaders/HTTPFileLoader.h index 1be05f5fd1..1702a683c2 100644 --- a/Core/FileLoaders/HTTPFileLoader.h +++ b/Core/FileLoaders/HTTPFileLoader.h @@ -17,9 +17,10 @@ #pragma once -#include #include +#include + #include "Common/File/Path.h" #include "Common/Net/Cancel.h" #include "Common/Net/HTTPClient.h" @@ -28,6 +29,8 @@ #include "Common/CommonTypes.h" #include "Core/Loaders.h" +// Prepared (FileSize/Exists) before reads start, then read from one thread at a time: the worker +// of the CachingFileLoader that ConstructFileLoader wraps it in. Cancel and LatestError are any thread. class HTTPFileLoader : public FileLoader { public: HTTPFileLoader(const ::Path &filename); @@ -52,11 +55,14 @@ public: } std::string LatestError() const override { - return latestError_; + return (const char *)retro_atomic_load_acquire_ptr(const_cast(&latestError_)); } private: void Prepare(); + void SetError(const char *error) { + retro_atomic_store_release_ptr(&latestError_, (void *)error); + } int SendHEAD(const Url &url, std::vector &responseHeaders); void Connect(double timeout); @@ -76,8 +82,8 @@ private: ::Path filename_; bool connected_ = false; net::CancelToken cancel_; - const char *latestError_ = ""; + // Always a string literal. + retro_atomic_ptr_t latestError_{ (void *)"" }; - std::once_flag preparedFlag_; - std::mutex readAtMutex_; + bool prepared_ = false; }; diff --git a/Core/FileLoaders/LocalFileLoader.cpp b/Core/FileLoaders/LocalFileLoader.cpp index 5a19084330..d75d624464 100644 --- a/Core/FileLoaders/LocalFileLoader.cpp +++ b/Core/FileLoaders/LocalFileLoader.cpp @@ -38,7 +38,7 @@ #endif #ifdef HAVE_LIBRETRO_VFS -#include +#include "Common/File/PositionalFile.h" #endif #if !defined(_WIN32) && !defined(HAVE_LIBRETRO_VFS) @@ -85,12 +85,13 @@ LocalFileLoader::LocalFileLoader(const Path &filename) #endif #if defined(HAVE_LIBRETRO_VFS) - file_ = File::OpenCFile(filename, "rb"); - if (!file_) { + file_.reset(new PositionalFile(filename)); + if (!file_->IsOpen()) { ERROR_LOG(Log::FileSystem, "LocalFileLoader: failed to open file: '%s'", filename.c_str()); + file_.reset(); return; } - filesize_ = File::GetFileSize(file_); + filesize_ = file_->Size(); #elif PPSSPP_PLATFORM(IOS) if (!File::Exists(filename)) { // Try to "unlock" the path before the file loader hits it @@ -138,9 +139,7 @@ LocalFileLoader::LocalFileLoader(const Path &filename) LocalFileLoader::~LocalFileLoader() { #if defined(HAVE_LIBRETRO_VFS) - if (file_ != nullptr) { - fclose(file_); - } + file_.reset(); #elif PPSSPP_PLATFORM(IOS) close(fd_); DarwinFileSystemServices::stopAccessingPath(filename_); @@ -186,22 +185,10 @@ size_t LocalFileLoader::ReadAt(s64 absolutePos, size_t bytes, size_t count, void } #if defined(HAVE_LIBRETRO_VFS) - std::lock_guard guard(readLock_); - // The VFS has no positioned read. Only seek when the read isn't sequential: - // a seek also throws away whatever the frontend's stream had buffered. - if (filePos_ != absolutePos) { - if (File::Fseek(file_, absolutePos, SEEK_SET) != 0) { - filePos_ = -1; - return 0; - } - filePos_ = absolutePos; - } - const size_t done = fread(data, 1, bytes * count, file_); - filePos_ = done == bytes * count ? filePos_ + (s64)done : -1; - return done / bytes; + // The VFS has no positioned read; each thread reads through its own handle. + return file_->ReadAt((u64)absolutePos, data, bytes * count) / bytes; #elif PPSSPP_PLATFORM(SWITCH) - // Toolchain has no fancy IO API. We must lock. - std::lock_guard guard(readLock_); + // Toolchain has no fancy IO API. Seek and read is fine with one reader at a time. lseek(fd_, absolutePos, SEEK_SET); // read() returns -1 on error, not a short count. Dividing that (implicitly // converted to a huge size_t) by bytes would otherwise report a huge bogus @@ -218,8 +205,7 @@ size_t LocalFileLoader::ReadAt(s64 absolutePos, size_t bytes, size_t count, void #endif return retval < 0 ? 0 : (size_t)retval / bytes; } else { - // Since pread64 doesn't change the file offset, it should be safe to avoid the lock in the common case. - std::lock_guard guard(readLock_); + // Seek and read is fine with one reader at a time. lseek64(fd_, absolutePos, SEEK_SET); ssize_t retval = read(fd_, data, bytes * count); return retval < 0 ? 0 : (size_t)retval / bytes; diff --git a/Core/FileLoaders/LocalFileLoader.h b/Core/FileLoaders/LocalFileLoader.h index 785d5b5917..efab3a5a47 100644 --- a/Core/FileLoaders/LocalFileLoader.h +++ b/Core/FileLoaders/LocalFileLoader.h @@ -17,19 +17,22 @@ #pragma once -#include +#include #include "Common/CommonTypes.h" #include "Common/File/Path.h" #include "Common/StringUtils.h" #include "Core/Loaders.h" +class PositionalFile; + #if defined(_WIN32) && !defined(HAVE_LIBRETRO_VFS) typedef void *HANDLE; #endif // This one accepts directories as well as files. // Later we handle the discrepancy in MountGameISO. +// ReadAt from one thread at a time (a caching wrapper reads it from its own worker only). class LocalFileLoader : public FileLoader { public: LocalFileLoader(const Path &filename); @@ -45,8 +48,7 @@ public: private: #ifdef HAVE_LIBRETRO_VFS - FILE *file_ = nullptr; - s64 filePos_ = -1; // where file_ is; -1 when unknown + std::unique_ptr file_; #elif !defined(_WIN32) void DetectSizeFd(); int fd_ = -1; @@ -56,5 +58,4 @@ private: u64 filesize_ = 0; Path filename_; bool isDirectory_ = false; - std::mutex readLock_; }; diff --git a/Core/FileLoaders/RamCachingFileLoader.cpp b/Core/FileLoaders/RamCachingFileLoader.cpp index 8c339bfd5c..a09935b67d 100644 --- a/Core/FileLoaders/RamCachingFileLoader.cpp +++ b/Core/FileLoaders/RamCachingFileLoader.cpp @@ -16,11 +16,9 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include -#include #include #include "Common/Thread/ThreadUtil.h" -#include "Common/TimeUtil.h" #include "Common/Log.h" #include "Core/FileLoaders/RamCachingFileLoader.h" @@ -65,29 +63,31 @@ s64 RamCachingFileLoader::FileSize() { } size_t RamCachingFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Flags flags) { - size_t readSize = 0; - if (cache_ == nullptr || (flags & Flags::HINT_UNCACHED) != 0) { - readSize = backend_->ReadAt(absolutePos, bytes, data, flags); - } else { - readSize = ReadFromCache(absolutePos, bytes, data); - // While in case the cache size is too small for the entire read. - while (readSize < bytes) { - SaveIntoCache(absolutePos + readSize, bytes - readSize, flags); - size_t bytesFromCache = ReadFromCache(absolutePos + readSize, bytes - readSize, (u8 *)data + readSize); - readSize += bytesFromCache; - if (bytesFromCache == 0) { - // We can't read any more. - break; - } - } - - StartReadAhead(absolutePos + readSize); + if (cache_ == nullptr) { + // No cache, so no worker: the backend is ours. + return backend_->ReadAt(absolutePos, bytes, data, flags); } + if ((flags & Flags::HINT_UNCACHED) != 0) { + return RunRequest(absolutePos, bytes, data, flags); + } + + size_t readSize = ReadFromCache(absolutePos, bytes, data); + // While in case the cache size is too small for the entire read. + while (readSize < bytes) { + RunRequest(absolutePos + readSize, bytes - readSize, nullptr, flags); + size_t bytesFromCache = ReadFromCache(absolutePos + readSize, bytes - readSize, (u8 *)data + readSize); + readSize += bytesFromCache; + if (bytesFromCache == 0) { + // We can't read any more. + break; + } + } + + StartReadAhead(absolutePos + readSize); return readSize; } void RamCachingFileLoader::InitCache() { - std::lock_guard guard(blocksMutex_); u32 blockCount = (u32)((filesize_ + BLOCK_SIZE - 1) >> BLOCK_SHIFT); // Overallocate for the last block. cache_ = (u8 *)malloc((size_t)blockCount << BLOCK_SHIFT); @@ -96,21 +96,28 @@ void RamCachingFileLoader::InitCache() { return; } aheadRemaining_ = blockCount; - blocks_.resize(blockCount); + blockCount_ = blockCount; + blocks_.reset(new retro_atomic_int_t[blockCount_]); + for (size_t i = 0; i < blockCount_; ++i) { + retro_atomic_int_init(&blocks_[i], 0); + } + worker_ = Thread([this] { + WorkerLoop(); + }); } void RamCachingFileLoader::ShutdownCache() { Cancel(); - // We can't delete while the thread is running, so have to wait. - // This should only happen from the menu. - if (aheadThread_.joinable()) - aheadThread_.join(); + retro_atomic_store_release_int(&quit_, 1); + wake_.Notify(); + // Waits out any backend read in progress. + if (worker_.joinable()) { + worker_.join(); + } - _dbg_assert_(!aheadThreadRunning_); - - std::lock_guard guard(blocksMutex_); - blocks_.clear(); + blocks_.reset(); + blockCount_ = 0; if (cache_ != nullptr) { free(cache_); cache_ = nullptr; @@ -118,37 +125,31 @@ void RamCachingFileLoader::ShutdownCache() { } void RamCachingFileLoader::Cancel() { - if (aheadThreadRunning_) { - std::lock_guard guard(blocksMutex_); - aheadCancel_ = true; - } - + retro_atomic_store_release_int(&aheadCancel_, 1); ProxiedFileLoader::Cancel(); } size_t RamCachingFileLoader::ReadFromCache(s64 pos, size_t bytes, void *data) { + // Clamp bytes to what's actually available. + if (pos >= filesize_ || bytes == 0) { + return 0; + } + if (pos + (s64)bytes > filesize_) { + bytes = (size_t)(filesize_ - pos); + } + s64 cacheStartPos = pos >> BLOCK_SHIFT; s64 cacheEndPos = (pos + bytes - 1) >> BLOCK_SHIFT; - if ((size_t)cacheEndPos >= blocks_.size()) { - cacheEndPos = blocks_.size() - 1; + if ((size_t)cacheEndPos >= blockCount_) { + cacheEndPos = blockCount_ - 1; } size_t readSize = 0; size_t offset = (size_t)(pos - (cacheStartPos << BLOCK_SHIFT)); u8 *p = (u8 *)data; - // Clamp bytes to what's actually available. - if (pos + (s64)bytes > filesize_) { - // Should've been caught above, but just in case. - if (pos >= filesize_) { - return 0; - } - bytes = (size_t)(filesize_ - pos); - } - - std::lock_guard guard(blocksMutex_); for (s64 i = cacheStartPos; i <= cacheEndPos; ++i) { - if (blocks_[(size_t)i] == 0) { + if (!BlockReady((size_t)i)) { return readSize; } @@ -163,26 +164,44 @@ size_t RamCachingFileLoader::ReadFromCache(s64 pos, size_t bytes, void *data) { return readSize; } -void RamCachingFileLoader::SaveIntoCache(s64 pos, size_t bytes, Flags flags) { +size_t RamCachingFileLoader::RunRequest(s64 pos, size_t bytes, void *data, Flags flags) { + reqPos_ = pos; + reqBytes_ = bytes; + reqData_ = data; + reqFlags_ = flags; + const int seq = (int)((unsigned)reqSeqLocal_ + 1); + reqSeqLocal_ = seq; + retro_atomic_store_release_int(&reqSeq_, seq); + wake_.Notify(); + ParkingLotWait(&doneSeq_, [&] { + return retro_atomic_load_acquire_int(&doneSeq_) == seq; + }); + return reqResult_; +} + +u32 RamCachingFileLoader::SaveIntoCache(s64 pos, size_t bytes, Flags flags) { s64 cacheStartPos = pos >> BLOCK_SHIFT; + if (bytes == 0 || (size_t)cacheStartPos >= blockCount_) { + return 0; + } s64 cacheEndPos = (pos + bytes - 1) >> BLOCK_SHIFT; - if ((size_t)cacheEndPos >= blocks_.size()) { - cacheEndPos = blocks_.size() - 1; + if ((size_t)cacheEndPos >= blockCount_) { + cacheEndPos = blockCount_ - 1; } + // Stop at the first loaded block: the reader may be copying it, so it is never rewritten. size_t blocksToRead = 0; - { - std::lock_guard guard(blocksMutex_); - for (s64 i = cacheStartPos; i <= cacheEndPos; ++i) { - if (blocks_[(size_t)i] == 0) { - ++blocksToRead; - if (blocksToRead >= MAX_BLOCKS_PER_READ) { - break; - } - - // TODO: Shouldn't we break as soon as we see a 1? - } + for (s64 i = cacheStartPos; i <= cacheEndPos; ++i) { + if (BlockReady((size_t)i)) { + break; } + ++blocksToRead; + if (blocksToRead >= MAX_BLOCKS_PER_READ) { + break; + } + } + if (blocksToRead == 0) { + return 0; } s64 cacheFilePos = cacheStartPos << BLOCK_SHIFT; @@ -199,81 +218,79 @@ void RamCachingFileLoader::SaveIntoCache(s64 pos, size_t bytes, Flags flags) { if ((bytesRead & (BLOCK_SIZE - 1)) != 0 && cacheFilePos + (s64)bytesRead == filesize_) { ++blocksActuallyRead; } - { - std::lock_guard guard(blocksMutex_); - // In case they were simultaneously read. - u32 blocksRead = 0; - for (size_t i = 0; i < blocksActuallyRead; ++i) { - if (blocks_[(size_t)cacheStartPos + i] == 0) { - blocks_[(size_t)cacheStartPos + i] = 1; - ++blocksRead; - } - } - - if (aheadRemaining_ != 0) { - aheadRemaining_ -= blocksRead; + for (size_t i = 0; i < blocksActuallyRead; ++i) { + retro_atomic_store_release_int(&blocks_[(size_t)cacheStartPos + i], 1); + } + if (aheadRemaining_ != 0) { + aheadRemaining_ -= blocksActuallyRead; + if (aheadRemaining_ == 0) { + retro_atomic_store_release_int(&allLoaded_, 1); } } + return blocksActuallyRead; } void RamCachingFileLoader::StartReadAhead(s64 pos) { - if (cache_ == nullptr) { + if (retro_atomic_load_acquire_int(&allLoaded_)) { return; } - - std::lock_guard guard(blocksMutex_); - aheadPos_ = pos; - if (aheadThreadRunning_) { - // Already going. - return; + s64 block = pos >> BLOCK_SHIFT; + if ((size_t)block > blockCount_) { + block = (s64)blockCount_; } - - aheadThreadRunning_ = true; - aheadCancel_ = false; - if (aheadThread_.joinable()) - aheadThread_.join(); - aheadThread_ = std::thread([this] { - SetCurrentThreadName("FileLoaderReadAhead"); - - - while (aheadRemaining_ != 0 && !aheadCancel_) { - // Where should we look? - const u32 cacheStartPos = NextAheadBlock(); - if (cacheStartPos == 0xFFFFFFFF) { - // Must be full. - break; - } - u32 cacheEndPos = cacheStartPos + BLOCK_READAHEAD - 1; - if (cacheEndPos >= blocks_.size()) { - cacheEndPos = (u32)blocks_.size() - 1; - } - - for (u32 i = cacheStartPos; i <= cacheEndPos; ++i) { - if (blocks_[i] == 0) { - SaveIntoCache((u64)i << BLOCK_SHIFT, BLOCK_SIZE * BLOCK_READAHEAD, Flags::NONE); - break; - } - } - } - - aheadThreadRunning_ = false; - }); + retro_atomic_store_release_int(&aheadCancel_, 0); + retro_atomic_store_release_int(&aheadBlock_, (int)block); + wake_.Notify(); } -u32 RamCachingFileLoader::NextAheadBlock() { - std::lock_guard guard(blocksMutex_); +void RamCachingFileLoader::WorkerLoop() { + SetCurrentThreadName("FileLoaderReadAhead"); - // If we had an aheadPos_ set, start reading from there and go forward. - u32 startFrom = (u32)(aheadPos_ >> BLOCK_SHIFT); - // But next time, start from the beginning again. - aheadPos_ = 0; + int done = 0; + bool readingAhead = false; + for (;;) { + const int seen = wake_.Seen(); + if (retro_atomic_load_acquire_int(&quit_)) { + break; + } - for (u32 i = startFrom; i < blocks_.size(); ++i) { - if (blocks_[i] == 0) { + const int seq = retro_atomic_load_acquire_int(&reqSeq_); + if (seq != done) { + if (reqData_) { + reqResult_ = backend_->ReadAt(reqPos_, reqBytes_, reqData_, reqFlags_); + } else { + SaveIntoCache(reqPos_, reqBytes_, reqFlags_); + } + done = seq; + retro_atomic_store_release_int(&doneSeq_, seq); + ParkingLotNotify(&doneSeq_); + continue; + } + + // If a read posted a position, go forward from there, otherwise from the start. + const int ahead = retro_atomic_exchange_int(&aheadBlock_, -1); + if (ahead >= 0) { + readingAhead = true; + } + if (readingAhead && aheadRemaining_ != 0 && !retro_atomic_load_acquire_int(&aheadCancel_)) { + const u32 cacheStartPos = NextAheadBlock(ahead >= 0 ? (u32)ahead : 0); + // A failed read stops reading ahead rather than retrying it in a loop. + if (cacheStartPos != 0xFFFFFFFF && SaveIntoCache((s64)cacheStartPos << BLOCK_SHIFT, BLOCK_SIZE * BLOCK_READAHEAD, Flags::NONE) != 0) { + continue; + } + } + // Done until the next read posts a position. + readingAhead = false; + wake_.Wait(seen); + } +} + +u32 RamCachingFileLoader::NextAheadBlock(u32 startFrom) { + for (u32 i = startFrom; i < blockCount_; ++i) { + if (!BlockReady(i)) { return i; } } - return 0xFFFFFFFF; } diff --git a/Core/FileLoaders/RamCachingFileLoader.h b/Core/FileLoaders/RamCachingFileLoader.h index 37d1ec7a85..d01de130e8 100644 --- a/Core/FileLoaders/RamCachingFileLoader.h +++ b/Core/FileLoaders/RamCachingFileLoader.h @@ -17,13 +17,17 @@ #pragma once -#include -#include -#include +#include + +#include #include "Common/CommonTypes.h" #include "Core/Loaders.h" +#include "Common/Thread/ParkingLot.h" +#include "Common/Thread/Thread.h" +// ReadAt from one thread at a time. While the cache exists, every backend read happens on +// the loader's own worker thread, so the backend is only ever read from that one thread. class RamCachingFileLoader : public ProxiedFileLoader { public: RamCachingFileLoader(FileLoader *backend); @@ -45,10 +49,17 @@ private: void InitCache(); void ShutdownCache(); size_t ReadFromCache(s64 pos, size_t bytes, void *data); - // Guaranteed to read at least one block into the cache. - void SaveIntoCache(s64 pos, size_t bytes, Flags flags); + size_t RunRequest(s64 pos, size_t bytes, void *data, Flags flags); void StartReadAhead(s64 pos); - u32 NextAheadBlock(); + // Worker only. + void WorkerLoop(); + // Returns how many blocks it loaded. + u32 SaveIntoCache(s64 pos, size_t bytes, Flags flags); + u32 NextAheadBlock(u32 startFrom); + + bool BlockReady(size_t block) { + return retro_atomic_load_acquire_int(&blocks_[block]) != 0; + } enum { BLOCK_SIZE = 65536, @@ -57,16 +68,33 @@ private: BLOCK_READAHEAD = 4, }; + // Fixed before the worker starts. s64 filesize_ = 0; u8 *cache_ = nullptr; + size_t blockCount_ = 0; + // Reader thread only. int exists_ = -1; int isDirectory_ = -1; - std::vector blocks_; - std::mutex blocksMutex_; + // Only the worker writes cache_ data and sets a block, with a release store after the data. + std::unique_ptr blocks_; + // Worker only. u32 aheadRemaining_ = 0; - s64 aheadPos_ = 0; - std::thread aheadThread_; - bool aheadThreadRunning_ = false; - bool aheadCancel_ = false; + retro_atomic_int_t allLoaded_{ 0 }; + + // One request in flight at a time: the reader fills req*, publishes reqSeq_, waits for doneSeq_. + s64 reqPos_ = 0; + size_t reqBytes_ = 0; + void *reqData_ = nullptr; + Flags reqFlags_ = Flags::NONE; + size_t reqResult_ = 0; + int reqSeqLocal_ = 0; + retro_atomic_int_t reqSeq_{ 0 }; + retro_atomic_int_t doneSeq_{ 0 }; + // Block to read ahead from next, or -1. Reading ahead starts with the first one posted. + retro_atomic_int_t aheadBlock_{ -1 }; + retro_atomic_int_t aheadCancel_{ 0 }; + retro_atomic_int_t quit_{ 0 }; + EventCounter wake_; + Thread worker_; }; diff --git a/Core/FileSystems/BlockDevices.cpp b/Core/FileSystems/BlockDevices.cpp index b872e1b14d..8010eebc46 100644 --- a/Core/FileSystems/BlockDevices.cpp +++ b/Core/FileSystems/BlockDevices.cpp @@ -896,8 +896,6 @@ NPDRMDemoBlockDevice::NPDRMDemoBlockDevice(FileLoader *fileLoader) return; } - std::lock_guard guard(mutex_); - // Local kirk instance to not clash with other block devices and other decryption things. kirk_init(&kirk_); @@ -984,7 +982,6 @@ NPDRMDemoBlockDevice::NPDRMDemoBlockDevice(FileLoader *fileLoader) } NPDRMDemoBlockDevice::~NPDRMDemoBlockDevice() { - std::lock_guard guard(mutex_); delete [] table_; delete [] tempBuf_; delete [] blockBuf_; @@ -994,7 +991,6 @@ int lzrc_decompress(void *out, int out_len, void *in, int in_len); bool NPDRMDemoBlockDevice::ReadBlock(int blockNumber, u8 *outPtr, bool uncached) { FileLoader::Flags flags = uncached ? FileLoader::Flags::HINT_UNCACHED : FileLoader::Flags::NONE; - std::lock_guard guard(mutex_); if (blockSize_ == 0) { // Wasn't opened successfully. diff --git a/Core/FileSystems/BlockDevices.h b/Core/FileSystems/BlockDevices.h index d3f45ae67d..4f31b01ab6 100644 --- a/Core/FileSystems/BlockDevices.h +++ b/Core/FileSystems/BlockDevices.h @@ -23,7 +23,6 @@ // The ISOFileSystemReader reads from a BlockDevice, so it automatically works // with CISO images. -#include #include #include @@ -178,9 +177,7 @@ public: bool IsDisc() const override { return false; } private: - // This is in case two threads hit this same block device, which shouldn't really happen. - std::mutex mutex_; - + // Used from one thread at a time (the emulation thread, or the loader thread before it). u32 lbaSize_ = 0; u32 psarOffset = 0; diff --git a/Core/FileSystems/MetaFileSystem.cpp b/Core/FileSystems/MetaFileSystem.cpp index d1065b17c3..ce5315c0c6 100644 --- a/Core/FileSystems/MetaFileSystem.cpp +++ b/Core/FileSystems/MetaFileSystem.cpp @@ -164,7 +164,6 @@ static bool RealPath(const std::string ¤tDirectory, const std::string &inP IFileSystem *MetaFileSystem::GetHandleOwner(u32 handle) const { - std::lock_guard guard(lock); for (size_t i = 0; i < fileSystems.size(); i++) { if (fileSystems[i].system->OwnsHandle(handle)) @@ -185,7 +184,6 @@ std::string MetaFileSystem::GetCurrentDirForThread(int threadID) const { int MetaFileSystem::MapFilePath(std::string_view _inpath, std::string *outpath, MountPoint **system) { int error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND; - std::lock_guard guard(lock); std::string realpath; std::string inpath(_inpath); @@ -282,7 +280,6 @@ std::string_view MetaFileSystem::NormalizePrefix(std::string_view prefix) const } void MetaFileSystem::Mount(std::string_view prefix, std::shared_ptr system, std::string_view subDir) { - std::lock_guard guard(lock); for (auto &it : fileSystems) { if (it.prefix == prefix) { // Overwrite the old mount. @@ -304,7 +301,6 @@ void MetaFileSystem::Mount(std::string_view prefix, std::shared_ptr fileSystems.push_back(x); } -// Assumes the lock is held void MetaFileSystem::UnmountAll() { fileSystems.clear(); currentDir.clear(); @@ -312,7 +308,6 @@ void MetaFileSystem::UnmountAll() { } void MetaFileSystem::Unmount(std::string_view prefix) { - std::lock_guard guard(lock); if (equalsNoCase(prefix, "host0:")) { host0Mapped_ = false; } @@ -332,7 +327,6 @@ IFileSystem *MetaFileSystem::GetSystemFromFilename(std::string_view filename) { } IFileSystem *MetaFileSystem::GetSystem(std::string_view prefix) { - std::lock_guard guard(lock); for (auto it = fileSystems.begin(); it != fileSystems.end(); ++it) { if (it->prefix == NormalizePrefix(prefix)) return it->system.get(); @@ -341,7 +335,6 @@ IFileSystem *MetaFileSystem::GetSystem(std::string_view prefix) { } void MetaFileSystem::Shutdown() { - std::lock_guard guard(lock); UnmountAll(); Reset(); @@ -349,7 +342,6 @@ void MetaFileSystem::Shutdown() { int MetaFileSystem::OpenFile(std::string filename, FileAccess access, const char *devicename) { - std::lock_guard guard(lock); std::string of; MountPoint *mount; int error = MapFilePath(filename, &of, &mount); @@ -361,7 +353,6 @@ int MetaFileSystem::OpenFile(std::string filename, FileAccess access, const char PSPFileInfo MetaFileSystem::GetFileInfo(std::string filename) { - std::lock_guard guard(lock); std::string of; IFileSystem *system; int error = MapFilePath(filename, &of, &system); @@ -377,7 +368,6 @@ PSPFileInfo MetaFileSystem::GetFileInfo(std::string filename) } PSPFileInfo MetaFileSystem::GetFileInfoByHandle(u32 handle) { - std::lock_guard guard(lock); IFileSystem *sys = GetHandleOwner(handle); if (sys) return sys->GetFileInfoByHandle(handle); @@ -385,7 +375,6 @@ PSPFileInfo MetaFileSystem::GetFileInfoByHandle(u32 handle) { } std::vector MetaFileSystem::GetDirListing(std::string_view path, bool *exists) { - std::lock_guard guard(lock); std::string of; IFileSystem *system; int error = MapFilePath(path, &of, &system); @@ -401,13 +390,11 @@ std::vector MetaFileSystem::GetDirListing(std::string_view path, bo void MetaFileSystem::ThreadEnded(int threadID) { - std::lock_guard guard(lock); currentDir.erase(threadID); } int MetaFileSystem::ChDir(const std::string &dir) { - std::lock_guard guard(lock); // Retain the old path and fail if the arg is 1023 bytes or longer. if (dir.size() >= 1023) return SCE_KERNEL_ERROR_NAMETOOLONG; @@ -443,7 +430,6 @@ int MetaFileSystem::ChDir(const std::string &dir) bool MetaFileSystem::MkDir(const std::string &dirname) { - std::lock_guard guard(lock); std::string of; IFileSystem *system; int error = MapFilePath(dirname, &of, &system); @@ -459,7 +445,6 @@ bool MetaFileSystem::MkDir(const std::string &dirname) bool MetaFileSystem::RmDir(const std::string &dirname) { - std::lock_guard guard(lock); std::string of; IFileSystem *system; int error = MapFilePath(dirname, &of, &system); @@ -475,7 +460,6 @@ bool MetaFileSystem::RmDir(const std::string &dirname) int MetaFileSystem::RenameFile(const std::string &from, const std::string &to) { - std::lock_guard guard(lock); std::string of; std::string rf; IFileSystem *osystem; @@ -509,7 +493,6 @@ int MetaFileSystem::RenameFile(const std::string &from, const std::string &to) bool MetaFileSystem::SetFileWritable(const std::string &filename, bool writable) { - std::lock_guard guard(lock); std::string of; IFileSystem *system; int error = MapFilePath(filename, &of, &system); @@ -522,7 +505,6 @@ bool MetaFileSystem::SetFileWritable(const std::string &filename, bool writable) bool MetaFileSystem::RemoveFile(const std::string &filename) { - std::lock_guard guard(lock); std::string of; IFileSystem *system; int error = MapFilePath(filename, &of, &system); @@ -535,7 +517,6 @@ bool MetaFileSystem::RemoveFile(const std::string &filename) int MetaFileSystem::Ioctl(u32 handle, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen, int &usec) { - std::lock_guard guard(lock); IFileSystem *sys = GetHandleOwner(handle); if (sys) return sys->Ioctl(handle, cmd, indataPtr, inlen, outdataPtr, outlen, usec); @@ -544,7 +525,6 @@ int MetaFileSystem::Ioctl(u32 handle, u32 cmd, u32 indataPtr, u32 inlen, u32 out PSPDevType MetaFileSystem::DevType(u32 handle) { - std::lock_guard guard(lock); IFileSystem *sys = GetHandleOwner(handle); if (sys) return sys->DevType(handle); @@ -553,7 +533,6 @@ PSPDevType MetaFileSystem::DevType(u32 handle) void MetaFileSystem::CloseFile(u32 handle) { - std::lock_guard guard(lock); IFileSystem *sys = GetHandleOwner(handle); if (sys) sys->CloseFile(handle); @@ -561,7 +540,6 @@ void MetaFileSystem::CloseFile(u32 handle) size_t MetaFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size) { - std::lock_guard guard(lock); IFileSystem *sys = GetHandleOwner(handle); if (sys) return sys->ReadFile(handle, pointer, size); @@ -571,7 +549,6 @@ size_t MetaFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size) size_t MetaFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size) { - std::lock_guard guard(lock); IFileSystem *sys = GetHandleOwner(handle); if (sys) return sys->WriteFile(handle, pointer, size); @@ -581,7 +558,6 @@ size_t MetaFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size) size_t MetaFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size, int &usec) { - std::lock_guard guard(lock); IFileSystem *sys = GetHandleOwner(handle); if (sys) return sys->ReadFile(handle, pointer, size, usec); @@ -591,7 +567,6 @@ size_t MetaFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size, int &usec) size_t MetaFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size, int &usec) { - std::lock_guard guard(lock); IFileSystem *sys = GetHandleOwner(handle); if (sys) return sys->WriteFile(handle, pointer, size, usec); @@ -601,7 +576,6 @@ size_t MetaFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size, int &u size_t MetaFileSystem::SeekFile(u32 handle, s32 position, FileMove type) { - std::lock_guard guard(lock); IFileSystem *sys = GetHandleOwner(handle); if (sys) return sys->SeekFile(handle, position, type); @@ -633,7 +607,6 @@ int MetaFileSystem::ReadEntireFile(const std::string &filename, std::vector } u64 MetaFileSystem::FreeDiskSpace(const std::string &path) { - std::lock_guard guard(lock); std::string of; IFileSystem *system; int error = MapFilePath(path, &of, &system); @@ -644,7 +617,6 @@ u64 MetaFileSystem::FreeDiskSpace(const std::string &path) { } void MetaFileSystem::DoState(PointerWrap &p) { - std::lock_guard guard(lock); auto s = p.Section("MetaFileSystem", 1); if (!s) @@ -716,7 +688,6 @@ int64_t MetaFileSystem::RecursiveSize(std::string_view dirPath) { } int64_t MetaFileSystem::ComputeRecursiveDirectorySize(std::string_view filename) { - std::lock_guard guard(lock); std::string of; IFileSystem *system; int error = MapFilePath(filename, &of, &system); diff --git a/Core/FileSystems/MetaFileSystem.h b/Core/FileSystems/MetaFileSystem.h index ffbde32dd1..292d77e82a 100644 --- a/Core/FileSystems/MetaFileSystem.h +++ b/Core/FileSystems/MetaFileSystem.h @@ -20,11 +20,12 @@ #include #include #include -#include #include #include "Core/FileSystems/FileSystem.h" +// The emulation thread's view of the PSP devices. Not thread safe: everything that touches it runs +// on the emulation thread (sceIo, the utility dialogs, savestates, boot). class MetaFileSystem : public IHandleAllocator, public IFileSystem { private: s32 current; @@ -47,13 +48,11 @@ private: std::map currentDir; std::string startingDirectory; - mutable std::recursive_mutex lock; // must be recursive. TODO: fix that // For the UMD host0 hack. bool host0Mapped_ = false; - // Assumes the lock is held - void Reset() { + void Reset() { // This used to be 6, probably an attempt to replicate PSP handles. // However, that's an artifact of using psplink anyway... current = 1; @@ -147,7 +146,6 @@ public: int ReadEntireFile(const std::string &filename, std::vector &data, bool quiet = false); void SetStartingDirectory(std::string_view dir) { - std::lock_guard guard(lock); startingDirectory = dir; } diff --git a/Core/HLE/NetAdhocCommon.cpp b/Core/HLE/NetAdhocCommon.cpp index 9b5ea7a63c..3251f9a78a 100644 --- a/Core/HLE/NetAdhocCommon.cpp +++ b/Core/HLE/NetAdhocCommon.cpp @@ -36,7 +36,6 @@ void netAdhocValidateLoopMemory() { int netAdhocEnterGameModeTimeout = 15000000; // 15 sec as default timeout, to wait for all players to join -std::recursive_mutex adhocEvtMtx; int IsAdhocctlInCB = 0; @@ -45,4 +44,4 @@ u32 matchingThreadHackAddr = 0; u32_le matchingThreadCode[3]; bool g_adhocServerConnected = false; -std::atomic g_adhocServerLoginFailed(false); +bool g_adhocServerLoginFailed = false; diff --git a/Core/HLE/NetAdhocCommon.h b/Core/HLE/NetAdhocCommon.h index 125f492764..b91550dd37 100644 --- a/Core/HLE/NetAdhocCommon.h +++ b/Core/HLE/NetAdhocCommon.h @@ -20,8 +20,6 @@ #include "CommonTypes.h" #include "Swap.h" -#include -#include extern u32 dummyThreadHackAddr; extern u32_le dummyThreadCode[3]; @@ -43,7 +41,6 @@ constexpr int adhocEventPollDelay = 100000; //100000; // Seems to be the same wi //1000000 constexpr int adhocEventDelay = 2000000; //2000000 on real PSP ? -extern std::recursive_mutex adhocEvtMtx; // TODO: this one is broken, perhaps delete it entirely? extern int IsAdhocctlInCB; @@ -55,6 +52,6 @@ extern u32_le matchingThreadCode[3]; extern bool g_adhocServerConnected; // Set by the friend finder when its attempt to connect and log in for the latest login request // failed, so a caller waiting on that login can stop waiting. -extern std::atomic g_adhocServerLoginFailed; +extern bool g_adhocServerLoginFailed; constexpr u32 defaultLastRecvDelta = 10000; //10000 usec worked well for games published by Falcom (ie. Ys vs Sora Kiseki, Vantage Master Portable) diff --git a/Core/HLE/Plugins.cpp b/Core/HLE/Plugins.cpp index 5e403fc787..672e54cdf9 100644 --- a/Core/HLE/Plugins.cpp +++ b/Core/HLE/Plugins.cpp @@ -16,7 +16,6 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include -#include #include "Common/Data/Format/IniFile.h" #include "Common/File/FileUtil.h" @@ -34,7 +33,6 @@ namespace HLEPlugins { -std::mutex g_inputMutex; float PluginDataAxis[JOYSTICK_AXIS_MAX]; std::map PluginDataKeys; @@ -214,7 +212,6 @@ bool Load(PSPModule *pluginWaitingModule, SceUID threadID) { INFO_LOG(Log::System, "Loaded plugin: '%s'", plugin.name.c_str()); } - std::lock_guard guard(g_inputMutex); PluginDataKeys.clear(); return started; } @@ -226,7 +223,6 @@ void Unload() { void Shutdown() { prxPlugins.clear(); anyEnabled = false; - std::lock_guard guard(g_inputMutex); PluginDataKeys.clear(); } @@ -243,15 +239,7 @@ bool HasEnabled() { return anyEnabled; } -void SetKey(int key, uint8_t value) { - if (anyEnabled) { - std::lock_guard guard(g_inputMutex); - PluginDataKeys[key] = value; - } -} - uint8_t GetKey(int key) { - std::lock_guard guard(g_inputMutex); return PluginDataKeys[key]; } diff --git a/Core/HLE/Plugins.h b/Core/HLE/Plugins.h index 7c15718465..367863e22e 100644 --- a/Core/HLE/Plugins.h +++ b/Core/HLE/Plugins.h @@ -51,7 +51,6 @@ struct PluginInfo { std::vector FindPlugins(const std::string &gameID, const std::string &lang); -void SetKey(int key, uint8_t value); uint8_t GetKey(int key); extern float PluginDataAxis[JOYSTICK_AXIS_MAX]; diff --git a/Core/HLE/SocketManager.cpp b/Core/HLE/SocketManager.cpp index 89df3e8e44..4218900bf7 100644 --- a/Core/HLE/SocketManager.cpp +++ b/Core/HLE/SocketManager.cpp @@ -3,10 +3,9 @@ #include "Core/HLE/SocketManager.h" #include "Common/Log.h" -#include +// sceNetInet only, on the emulation thread. SocketManager g_socketManager; -static std::mutex g_socketMutex; // TODO: Remove once the adhoc thread is gone InetSocket *SocketManager::CreateSocket(int *index, int *returned_errno, SocketState state, int domain, int type, int protocol) { _dbg_assert_(state != SocketState::Unused); @@ -21,8 +20,6 @@ InetSocket *SocketManager::CreateSocket(int *index, int *returned_errno, SocketS return nullptr; } - std::lock_guard guard(g_socketMutex); - for (int i = MIN_VALID_INET_SOCKET; i < ARRAY_SIZE(inetSockets_); i++) { if (inetSockets_[i].state == SocketState::Unused) { *index = i; @@ -48,8 +45,6 @@ InetSocket *SocketManager::CreateSocket(int *index, int *returned_errno, SocketS } InetSocket *SocketManager::AdoptSocket(int *index, SOCKET hostSocket, const InetSocket *derive) { - std::lock_guard guard(g_socketMutex); - for (int i = MIN_VALID_INET_SOCKET; i < ARRAY_SIZE(inetSockets_); i++) { if (inetSockets_[i].state == SocketState::Unused) { *index = i; @@ -82,7 +77,6 @@ bool SocketManager::Close(InetSocket *inetSocket) { } bool SocketManager::GetInetSocket(int sock, InetSocket **inetSocket) { - std::lock_guard guard(g_socketMutex); if (sock < MIN_VALID_INET_SOCKET || sock >= ARRAY_SIZE(inetSockets_) || inetSockets_[sock].state == SocketState::Unused) { *inetSocket = nullptr; return false; @@ -93,7 +87,6 @@ bool SocketManager::GetInetSocket(int sock, InetSocket **inetSocket) { // Simplified mappers, only really useful in select/poll SOCKET SocketManager::GetHostSocketFromInetSocket(int sock) { - std::lock_guard guard(g_socketMutex); if (sock < MIN_VALID_INET_SOCKET || sock >= ARRAY_SIZE(inetSockets_) || inetSockets_[sock].state == SocketState::Unused) { _dbg_assert_(false); return -1; diff --git a/Core/HLE/__sceAudio.cpp b/Core/HLE/__sceAudio.cpp index 2be45a25c2..caa8a95d9b 100644 --- a/Core/HLE/__sceAudio.cpp +++ b/Core/HLE/__sceAudio.cpp @@ -15,8 +15,6 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include -#include #include #include "Common/Common.h" @@ -45,10 +43,6 @@ #include "Core/HLE/sceKernelInterrupt.h" #include "Core/Util/AudioFormat.h" -// Should be used to lock anything related to the outAudioQueue. -// atomic locks are used on the lock. TODO: make this lock-free -std::atomic_flag atomicLock_; - int eventAudioUpdate = -1; // TODO: This is now useless and should be removed. Just scared of breaking states. diff --git a/Core/HLE/proAdhoc.cpp b/Core/HLE/proAdhoc.cpp index ea75127114..43a9174550 100644 --- a/Core/HLE/proAdhoc.cpp +++ b/Core/HLE/proAdhoc.cpp @@ -24,7 +24,6 @@ #include "ppsspp_config.h" #include -#include #include #include "Common/Net/SocketCompat.h" @@ -59,6 +58,8 @@ #include "Core/HLE/NetAdhocCommon.h" #include "ext/aemu_postoffice_client/postoffice_client.h" +#include "Common/Thread/Thread.h" +#include "Common/Thread/MpscQueue.h" #ifdef _WIN32 #undef errno @@ -78,7 +79,7 @@ SceNetAdhocMatchingContext * contexts = NULL; char* dummyPeekBuf64k = NULL; int dummyPeekBuf64kSize = 65536; int one = 1; -std::atomic friendFinderRunning(false); +retro_atomic_int_t friendFinderRunning{ 0 }; SceNetAdhocctlPeerInfo * friends = NULL; SceNetAdhocctlScanInfo * networks = NULL; SceNetAdhocctlScanInfo * newnetworks = NULL; @@ -106,11 +107,21 @@ int actionAfterMatchingMipsCall; uint8_t broadcastMAC[ETHER_ADDR_LEN] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; // NOTE: This does not need to be managed by the socket manager - not exposed to the game. -std::atomic metasocket((int)INVALID_SOCKET); +// The friend finder thread opens it; the emulation thread sends on it and closes it once the +// friend finder has been joined. +static retro_atomic_int_t metasocket{ (int)INVALID_SOCKET }; + +int MetaSocket() { + return retro_atomic_load_acquire_int(&metasocket); +} + +void SetMetaSocket(int sock) { + retro_atomic_store_release_int(&metasocket, sock); +} SceNetAdhocctlParameter parameter; SceNetAdhocctlAdhocId product_code; -std::thread friendFinderThread; +Thread friendFinderThread; // Wakes the friend finder's wait when the game side changes something it acts on. // Made once before the first start and kept, so a wake never races a free. static net::WakeSocket *friendFinderWake; @@ -126,6 +137,25 @@ void FriendFinderWake() { wake->Wake(); } +bool PostofficeConnectFinished(AdhocSocket *sock, int *result) { + PostofficeConnectState *st = sock->connectState; + if (!st) { + if (result) + *result = 0; + return true; + } + if (!retro_atomic_load_acquire_int(&st->done)) { + return false; + } + if (st->handle) { + sock->postofficeHandle = st->handle; + st->handle = nullptr; + } + if (result) + *result = st->result; + return true; +} + void JoinPostofficeConnect(AdhocSocket *sock, bool cancel) { if (sock->connectThread != NULL) { if (cancel && sock->connectCancel != NULL) @@ -134,10 +164,13 @@ void JoinPostofficeConnect(AdhocSocket *sock, bool cancel) { delete sock->connectThread; sock->connectThread = NULL; } + // A connect that finished just before the cancel still made a handle; adopt it so it's closed. + PostofficeConnectFinished(sock); + delete sock->connectState; + sock->connectState = NULL; delete sock->connectCancel; sock->connectCancel = NULL; } -std::recursive_mutex peerlock; AdhocSocket* adhocSockets[MAX_SOCKET]; bool isOriPort = false; bool isLocalServer = false; @@ -146,20 +179,6 @@ SockAddrIN4 g_localhostIP; sockaddr LocalIP; int defaultWlanChannel = PSP_SYSTEMPARAM_ADHOC_CHANNEL_11; // Don't put 0(Auto) here, it needed to be a valid/actual channel number -static std::mutex chatLogLock; -static std::vector chatLog; -// Enough to scroll back through a decent conversation without growing unbounded. -static const size_t MAX_CHAT_LOG_LINES = 250; - -// chatLogLock must be held. -static void AddChatLogEntry(std::string text) { - chatLog.push_back(ChatLogEntry{std::move(text), time(nullptr)}); - if (chatLog.size() > MAX_CHAT_LOG_LINES) { - chatLog.erase(chatLog.begin(), chatLog.begin() + (chatLog.size() - MAX_CHAT_LOG_LINES)); - } -} -static int chatMessageGeneration = 0; -static int chatMessageCount = 0; bool isMacMatch(const SceNetEtherAddr* addr1, const SceNetEtherAddr* addr2) { // Ignoring the 1st byte since there are games (ie. Gran Turismo) who tamper with the 1st byte of OUI to change the unicast/multicast bit @@ -245,10 +264,8 @@ SceNetAdhocMatchingMemberInternal* addMember(SceNetAdhocMatchingContext * contex memset(peer, 0, sizeof(SceNetAdhocMatchingMemberInternal)); peer->mac = *mac; peer->lastping = CoreTiming::GetGlobalTimeUsScaled(); - peerlock.lock(); peer->next = context->peerlist; context->peerlist = peer; - peerlock.unlock(); } } return peer; @@ -257,8 +274,6 @@ SceNetAdhocMatchingMemberInternal* addMember(SceNetAdhocMatchingContext * contex void addFriend(SceNetAdhocctlConnectPacketS2C * packet) { if (packet == NULL) return; - // Multithreading Lock - std::lock_guard guard(peerlock); SceNetAdhocctlPeerInfo * peer = findFriend(&packet->mac); // Already existed @@ -531,8 +546,6 @@ void deleteFriendByIP(uint32_t ip) { // Found Peer if (peer->ip_addr == ip) { - // Multithreading Lock - peerlock.lock(); // Unlink Left (Beginning) /*if (prev == NULL) friends = peer->next; @@ -550,8 +563,6 @@ void deleteFriendByIP(uint32_t ip) { // Instead of removing it from the list we'll make it timed out since most Matching games are moving group and may still need the peer data thus not recognizing it as Unknown peer peer->last_recv = 0; //CoreTiming::GetGlobalTimeUsScaled(); - // Multithreading Unlock - peerlock.unlock(); // Stop Search break; @@ -626,7 +637,6 @@ void postAcceptCleanPeerList(SceNetAdhocMatchingContext * context) int delcount = 0; int peercount = 0; // Acquire Peer Lock - peerlock.lock(); // Iterate Peer List SceNetAdhocMatchingMemberInternal * peer = context->peerlist; @@ -647,7 +657,6 @@ void postAcceptCleanPeerList(SceNetAdhocMatchingContext * context) } // Free Peer Lock - peerlock.unlock(); INFO_LOG(Log::sceNet, "Removing Unneeded Peers (%i/%i)", delcount, peercount); } @@ -665,7 +674,6 @@ void postAcceptAddSiblings(SceNetAdhocMatchingContext * context, int siblingcoun // As the buffer of "siblings" isn't properly aligned I don't want to risk a crash. uint8_t * siblings_u8 = (uint8_t *)siblings; - peerlock.lock(); // Iterate Siblings. Reversed so these siblings are added into peerlist in the same order with the peerlist on host/parent side for (int i = siblingcount - 1; i >= 0 ; i--) { @@ -710,7 +718,6 @@ void postAcceptAddSiblings(SceNetAdhocMatchingContext * context, int siblingcoun } } } - peerlock.unlock(); } /** @@ -814,7 +821,6 @@ void deletePeer(SceNetAdhocMatchingContext * context, SceNetAdhocMatchingMemberI // Valid Arguments if (context != NULL && peer != NULL) { - peerlock.lock(); // Previous Peer Reference SceNetAdhocMatchingMemberInternal * previous = NULL; @@ -844,7 +850,6 @@ void deletePeer(SceNetAdhocMatchingContext * context, SceNetAdhocMatchingMemberI free(peer); peer = NULL; - peerlock.unlock(); } } @@ -855,15 +860,11 @@ void deletePeer(SceNetAdhocMatchingContext * context, SceNetAdhocMatchingMemberI */ void linkEVMessage(SceNetAdhocMatchingContext * context, ThreadMessage * message) { - // Lock Access - context->eventlock->lock(); // Link Message message->next = context->event_stack; context->event_stack = message; - // Unlock Access - context->eventlock->unlock(); } /** @@ -873,15 +874,11 @@ void linkEVMessage(SceNetAdhocMatchingContext * context, ThreadMessage * message */ void linkIOMessage(SceNetAdhocMatchingContext * context, ThreadMessage * message) { - // Lock Access - context->inputlock->lock(); // Link Message message->next = context->input_stack; context->input_stack = message; - // Unlock Access - context->inputlock->unlock(); } /** @@ -932,11 +929,9 @@ void sendGenericMessage(SceNetAdhocMatchingContext * context, int stack, SceNetE return; } - peerlock.lock(); // Out of Memory Emergency Delete auto peer = findPeer(context, mac); deletePeer(context, peer); - peerlock.unlock(); } /** @@ -1093,7 +1088,6 @@ void spawnLocalEvent(SceNetAdhocMatchingContext * context, int event, SceNetEthe */ void handleTimeout(SceNetAdhocMatchingContext * context) { - peerlock.lock(); // Iterate Peer List SceNetAdhocMatchingMemberInternal * peer = context->peerlist; while (peer != NULL && contexts != NULL && coreState != CORE_POWERDOWN) @@ -1126,7 +1120,6 @@ void handleTimeout(SceNetAdhocMatchingContext * context) // Move Pointer peer = next; } - peerlock.unlock(); } /** @@ -1155,27 +1148,23 @@ void clearStack(SceNetAdhocMatchingContext * context, int stack) // Clear Event Stack if (stack == PSP_ADHOC_MATCHING_EVENT_STACK) { - context->eventlock->lock(); // Free Memory Recursively clearStackRecursive(context->event_stack); // Destroy Reference context->event_stack = NULL; - context->eventlock->unlock(); } // Clear IO Stack else { - context->inputlock->lock(); // Free Memory Recursively clearStackRecursive(context->input_stack); // Destroy Reference context->input_stack = NULL; - context->inputlock->unlock(); } } @@ -1186,7 +1175,6 @@ void clearStack(SceNetAdhocMatchingContext * context, int stack) void clearPeerList(SceNetAdhocMatchingContext * context) { // Acquire Peer Lock - peerlock.lock(); // Iterate Peer List SceNetAdhocMatchingMemberInternal * peer = context->peerlist; @@ -1205,7 +1193,6 @@ void clearPeerList(SceNetAdhocMatchingContext * context) } // Free Peer Lock - peerlock.unlock(); } void AfterMatchingMipsCall::DoState(PointerWrap & p) { @@ -1229,9 +1216,7 @@ void AfterMatchingMipsCall::DoState(PointerWrap & p) { // It seems After Actions being called in reverse order of Mipscall order (ie. MipsCall order of ACCEPT(6)->ESTABLISH(7) getting AfterAction order of ESTABLISH(7)->ACCEPT(6) void AfterMatchingMipsCall::run(MipsCall &call) { if (context == NULL) { - peerlock.lock(); context = findMatchingContext(contextID); - peerlock.unlock(); } u32 v0 = currentMIPS->r[MIPS_REG_V0]; if (__IsInInterrupt()) ERROR_LOG(Log::sceNet, "AfterMatchingMipsCall::run [ID=%i][Event=%d] is Returning Inside an Interrupt!", contextID, EventID); @@ -1245,25 +1230,19 @@ void AfterMatchingMipsCall::SetData(int ContextID, int eventId, u32_le BufAddr) contextID = ContextID; EventID = eventId; bufAddr = BufAddr; - peerlock.lock(); context = findMatchingContext(ContextID); - peerlock.unlock(); } bool SetMatchingInCallback(SceNetAdhocMatchingContext* context, bool IsInCB) { if (context == NULL) return false; - peerlock.lock(); context->IsMatchingInCB = IsInCB; - peerlock.unlock(); return IsInCB; } bool IsMatchingInCallback(SceNetAdhocMatchingContext* context) { bool inCB = false; if (context == NULL) return inCB; - peerlock.lock(); inCB = (context->IsMatchingInCB); - peerlock.unlock(); return inCB; } @@ -1298,13 +1277,11 @@ void AfterAdhocMipsCall::SetData(int handlerID, int eventId, u32_le ArgsAddr) { } int SetAdhocctlInCallback(bool IsInCB) { - std::lock_guard adhocGuard(adhocEvtMtx); IsAdhocctlInCB += (IsInCB?1:-1); return IsAdhocctlInCB; } int IsAdhocctlInCallback() { - std::lock_guard adhocGuard(adhocEvtMtx); int inCB = IsAdhocctlInCB; return inCB; } @@ -1339,50 +1316,172 @@ void timeoutFriendsRecursive(SceNetAdhocctlPeerInfo * node, int32_t* count) { if (count != NULL) (*count)++; } -void sendChat(std::string_view chatString) { - SceNetAdhocctlChatPacketC2S chat{}; - chat.base.opcode = OPCODE_CHAT; - //TODO check network inited, check send success or not, chatlog.pushback error on failed send, pushback error on not connected - if (friendFinderRunning) { - // Send Chat to Server - if (!chatString.empty()) { - //maximum char allowed is 64 character for compability with original server (pro.coldbird.net) - std::string message(chatString.substr(0, 60)); // 64 return chat variable corrupted is it out of memory? - strcpy(chat.message, message.c_str()); - //Send Chat Messages - if (IsSocketReady((int)metasocket, false, true) > 0) { - int chatResult = (int)send((int)metasocket, (const char*)&chat, sizeof(chat), MSG_NOSIGNAL); - NOTICE_LOG(Log::sceNet, "Send Chat %s to Adhoc Server", chat.message); - std::string name = g_Config.sNickName; +// The friend finder thread only talks to the adhoc server: it logs in when asked, pings, and cuts +// what the server sends into packets. Everything it learns goes to the emulation thread as events, +// which FriendFinderProcess() applies, so the game state is only ever touched there. +enum class FriendFinderEventType { + RESOLVED, + CONNECTED, + LOGIN_FAILED, + DISCONNECTED, + PACKET, +}; - std::lock_guard guard(chatLogLock); - AddChatLogEntry(name.substr(0, 8) + ": " + chat.message); - chatMessageGeneration++; - } - } - } else { - std::lock_guard guard(chatLogLock); - auto n = GetI18NCategory(I18NCat::NETWORKING); - AddChatLogEntry(std::string(n->T("You're in Offline Mode, go to lobby or online hall"))); - INFO_LOG(Log::sceNet, "Offline. Would have sent: %.*s", STR_VIEW(chatString)); - chatMessageGeneration++; +struct FriendFinderEvent { + FriendFinderEventType type; + int error = 0; + SockAddrIN4 addr{}; + sockaddr localIP{}; + std::vector packet; +}; + +struct FriendFinderLogin { + SceNetAdhocctlLoginPacketC2S packet; + SockAddrIN4 localhostIP; +}; + +static MpscQueue g_friendFinderLogins; +static MpscQueue g_friendFinderEvents; +// A login was handed to the friend finder and its answer hasn't been applied yet. +static bool g_friendFinderLoginPending = false; + +bool FriendFinderIsRunning() { + return retro_atomic_load_acquire_int(&friendFinderRunning) != 0; +} + +static void PostFriendFinderEvent(FriendFinderEvent &&ev) { + g_friendFinderEvents.Push(std::move(ev)); +} + +void CloseMetaSocket() { + const int sock = MetaSocket(); + if (sock != (int)INVALID_SOCKET) { + shutdown(sock, SD_BOTH); + closesocket(sock); + SetMetaSocket((int)INVALID_SOCKET); } } -std::vector getChatLog() { - std::lock_guard guard(chatLogLock); - return chatLog; +// Friend finder thread. Connects to the server and sends the login. Returns 0 or an error. +static int FriendFinderConnect(const FriendFinderLogin &login, const SockAddrIN4 &serverIP, sockaddr *localIP) { + auto n = GetI18NCategory(I18NCat::NETWORKING); + int iResult = 0; + const int sock = (int)socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (sock == (int)INVALID_SOCKET) { + ERROR_LOG(Log::sceNet, "Invalid socket"); + return SOCKET_ERROR; + } + SetMetaSocket(sock); + setSockKeepAlive(sock, true); + // Disable Nagle Algo to prevent delaying small packets + setSockNoDelay(sock, 1); + // Switch to Nonblocking Behaviour + changeBlockingMode(sock, 1); + // Ignore SIGPIPE when supported (ie. BSD/MacOS) + setSockNoSIGPIPE(sock, 1); + + // If Server is at localhost Try to Bind socket to specific adapter before connecting to prevent 2nd instance being recognized as already existing 127.0.0.1 by AdhocServer + // (may not works in WinXP/2003 for IPv4 due to "Weak End System" model) + if (isLoopbackIP(serverIP.in.sin_addr.s_addr)) { + int on = 1; + // Not sure what is this SO_DONTROUTE supposed to fix, but i do remembered there were issue related to multiple-instances without SO_DONTROUTE, but forgot how to reproduce it :( + setsockopt(sock, SOL_SOCKET, SO_DONTROUTE, (const char*)&on, sizeof(on)); + setSockReuseAddrPort(sock); + + SockAddrIN4 localhost = login.localhostIP; + localhost.in.sin_port = 0; + // Bind Local Address to Socket + iResult = bind(sock, &localhost.addr, sizeof(localhost.addr)); + if (iResult == SOCKET_ERROR) { + ERROR_LOG(Log::sceNet, "Bind to alternate localhost[%s] failed(%i).", ip2str(localhost.in.sin_addr).c_str(), iResult); + g_OSD.Show(OSDType::MESSAGE_ERROR, std::string(n->T("Failed to Bind Localhost IP")) + " " + ip2str(localhost.in.sin_addr).c_str()); + } + } + + // Don't need to connect if AdhocServer DNS was not resolved + if (serverIP.in.sin_addr.s_addr == INADDR_NONE) + return SOCKET_ERROR; + + // Connect to Adhoc Server + int errorcode = 0; + DEBUG_LOG(Log::sceNet, "InitNetwork: Connecting to AdhocServer"); + iResult = connect(sock, &serverIP.addr, sizeof(serverIP)); + errorcode = socket_errno; + + if (iResult == SOCKET_ERROR && errorcode != EISCONN) { + u64 startTime = (u64)(time_now_d() * 1000000.0); + bool done = false; + while (!done) { + if (coreState == CORE_POWERDOWN || !FriendFinderIsRunning()) + return iResult; + + // Writable, the timeout, or a wake to recheck the above. + const u64 waited = (u64)(time_now_d() * 1000000.0) - startTime; + uintptr_t waitSock = (uintptr_t)(intptr_t)sock; + const double left = waited < (u64)adhocDefaultTimeout ? (adhocDefaultTimeout - waited) / 1000000.0 : 0.0; + if (net::WaitSocketsOrWake(&waitSock, nullptr, 1, true, left, friendFinderWake) == net::WaitResult::CANCELLED) { + friendFinderWake->Drain(); + continue; + } + done = (IsSocketReady(sock, false, true) > 0); + if (done) { + // Writable can also mean the attempt failed (refused, say). Then there's no point + // waiting out the timeout. + int soError = 0; + socklen_t soErrorLen = sizeof(soError); + if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (char *)&soError, &soErrorLen) == 0 && soError != 0) { + errorcode = soError; + break; + } + } + struct sockaddr_in sin; + socklen_t sinlen = sizeof(sin); + memset(&sin, 0, sinlen); + // Ensure that the connection really established or not, since "select" alone can't accurately detects it + const bool writable = done; + done &= (getpeername(sock, (struct sockaddr*)&sin, &sinlen) != SOCKET_ERROR); + // Writable but not connected is a failed attempt, and would stay writable. + if (writable && !done) + break; + u64 now = (u64)(time_now_d() * 1000000.0); + if (static_cast(now - startTime) >= adhocDefaultTimeout) { + if (connectInProgress(errorcode)) + errorcode = ETIMEDOUT; + break; + } + } + if (!done) { + ERROR_LOG(Log::sceNet, "Socket error (%i) when connecting to AdhocServer [%s/%s:%u]", errorcode, g_Config.sProAdhocServer.c_str(), ip2str(serverIP.in.sin_addr).c_str(), ntohs(serverIP.in.sin_port)); + g_OSD.Show(OSDType::MESSAGE_ERROR, std::string(n->T("Failed to connect to Adhoc Server")) + " (" + std::string(n->T("Error")) + ": " + std::to_string(errorcode) + ")"); + return iResult; + } + } + + IsSocketReady(sock, false, true, nullptr, adhocDefaultTimeout); + DEBUG_LOG(Log::sceNet, "InitNetwork: Sending LOGIN OPCODE %d", login.packet.base.opcode); + int sent = (int)send(sock, (const char*)&login.packet, sizeof(login.packet), MSG_NOSIGNAL); + if (sent > 0) { + socklen_t addrLen = sizeof(*localIP); + memset(localIP, 0, addrLen); + getsockname(sock, localIP, &addrLen); + return 0; + } + return SOCKET_ERROR; } -int GetChatChangeID() { - return chatMessageGeneration; +// Size of a whole packet from the server starting with this opcode, or 0 if unknown. +static int ServerPacketSize(uint8_t opcode) { + switch (opcode) { + case OPCODE_CONNECT_BSSID: return (int)sizeof(SceNetAdhocctlConnectBSSIDPacketS2C); + case OPCODE_CHAT: return (int)sizeof(SceNetAdhocctlChatPacketS2C); + case OPCODE_CONNECT: return (int)sizeof(SceNetAdhocctlConnectPacketS2C); + case OPCODE_DISCONNECT: return (int)sizeof(SceNetAdhocctlDisconnectPacketS2C); + case OPCODE_SCAN: return (int)sizeof(SceNetAdhocctlScanPacketS2C); + case OPCODE_SCAN_COMPLETE: return 1; + default: return 0; + } } -int GetChatMessageCount() { - return chatMessageCount; -} - -// TODO: We should probably change this thread into PSPThread (or merging it into the existing AdhocThread PSPThread) as there are too many global vars being used here which also being used within some HLEs int friendFinder() { SetCurrentThreadName("FriendFinder"); auto n = GetI18NCategory(I18NCat::NETWORKING); @@ -1390,17 +1489,9 @@ int friendFinder() { int rxpos = 0; uint8_t rx[1024]; - // Chat Packet - SceNetAdhocctlChatPacketC2S chat; - chat.base.opcode = OPCODE_CHAT; - // Last Ping Time uint64_t lastping = 0; - - // Last Time Reception got updated - uint64_t lastreceptionupdate = 0; - - uint64_t now; + bool connected = false; // Log Startup INFO_LOG(Log::sceNet, "FriendFinder: Begin of Friend Finder Thread"); @@ -1408,7 +1499,9 @@ int friendFinder() { // Resolve and cache AdhocServer DNS addrinfo* resolved = nullptr; std::string err; - g_adhocServerIP.in.sin_addr.s_addr = INADDR_NONE; + SockAddrIN4 serverIP{}; + serverIP.in.sin_family = AF_INET; + serverIP.in.sin_addr.s_addr = INADDR_NONE; if (g_Config.bEnableWlan && !net::DNSResolve(g_Config.sProAdhocServer, "", &resolved, err)) { ERROR_LOG(Log::sceNet, "DNS Error Resolving %s\n", g_Config.sProAdhocServer.c_str()); g_OSD.Show(OSDType::MESSAGE_ERROR, std::string(n->T("DNS Error Resolving")) + g_Config.sProAdhocServer); @@ -1417,400 +1510,116 @@ int friendFinder() { for (auto ptr = resolved; ptr != NULL; ptr = ptr->ai_next) { switch (ptr->ai_family) { case AF_INET: - g_adhocServerIP.in = *(sockaddr_in*)ptr->ai_addr; + serverIP.in = *(sockaddr_in*)ptr->ai_addr; break; } } net::DNSResolveFree(resolved); } - g_adhocServerIP.in.sin_port = htons(SERVER_PORT); + serverIP.in.sin_port = htons(SERVER_PORT); + { + FriendFinderEvent ev{ FriendFinderEventType::RESOLVED }; + ev.addr = serverIP; + PostFriendFinderEvent(std::move(ev)); + } // Finder Loop. The flag was set by whoever started us, and cleared to stop us. - while (friendFinderRunning) { - // Bytes buffered before this pass handled a packet. - int rxQueued = 0; - - // Acquire Network Lock - //_acquireNetworkLock(); - - // Reconnect when disconnected while Adhocctl is still inited - if (metasocket == (int)INVALID_SOCKET && netAdhocctlInited && isAdhocctlNeedLogin) { - if (g_Config.bEnableWlan) { - // Not really initNetwork. - if (initNetwork(&product_code) == 0) { - g_adhocServerConnected = true; - INFO_LOG(Log::sceNet, "FriendFinder: Network [RE]Initialized"); - // At this point we are most-likely not in a Group within the Adhoc Server, so we should probably reset AdhocctlState - adhocctlState = ADHOCCTL_STATE_DISCONNECTED; - netAdhocGameModeEntered = false; - isAdhocctlBusy = false; - } - else { - g_adhocServerConnected = false; - g_adhocServerLoginFailed = true; - shutdown((int)metasocket, SD_BOTH); - closesocket((int)metasocket); - metasocket = (int)INVALID_SOCKET; - } + while (FriendFinderIsRunning()) { + // Log in when the game side asks (see FriendFinderProcess). + g_friendFinderLogins.Drain([&](FriendFinderLogin &&login) { + if (connected || MetaSocket() != (int)INVALID_SOCKET) { + // Already connected. Answer anyway, so the request is settled. + FriendFinderEvent ev{ connected ? FriendFinderEventType::CONNECTED : FriendFinderEventType::LOGIN_FAILED }; + PostFriendFinderEvent(std::move(ev)); + return; } - } + FriendFinderEvent ev{ FriendFinderEventType::CONNECTED }; + if (FriendFinderConnect(login, serverIP, &ev.localIP) == 0) { + connected = true; + rxpos = 0; + lastping = 0; + INFO_LOG(Log::sceNet, "FriendFinder: Network [RE]Initialized"); + } else { + ev.type = FriendFinderEventType::LOGIN_FAILED; + CloseMetaSocket(); + } + PostFriendFinderEvent(std::move(ev)); + }); - // Prevent retrying to Login again unless it was on demand - isAdhocctlNeedLogin = false; - - if (g_adhocServerConnected) { + if (connected) { // Ping Server - now = time_now_d() * 1000000.0; // Use time_now_d()*1000000.0 instead of CoreTiming::GetGlobalTimeUsScaled() if the game gets disconnected from AdhocServer too soon when FPS wasn't stable + const uint64_t now = time_now_d() * 1000000.0; // Use time_now_d()*1000000.0 instead of CoreTiming::GetGlobalTimeUsScaled() if the game gets disconnected from AdhocServer too soon when FPS wasn't stable // original code : ((sceKernelGetSystemTimeWide() - lastping) >= ADHOCCTL_PING_TIMEOUT) if (static_cast(now - lastping) >= PSP_ADHOCCTL_PING_TIMEOUT) { // We may need to use lower interval to prevent getting timeout at Pro Adhoc Server through internet // Prepare Packet uint8_t opcode = OPCODE_PING; // Send Ping to Server, may failed with socket error 10054/10053 if someone else with the same IP already connected to AdHoc Server (the server might need to be modified to differentiate MAC instead of IP) - if (IsSocketReady((int)metasocket, false, true) > 0) { - int iResult = (int)send((int)metasocket, (const char*)&opcode, 1, MSG_NOSIGNAL); + if (IsSocketReady(MetaSocket(), false, true) > 0) { + int iResult = (int)send(MetaSocket(), (const char*)&opcode, 1, MSG_NOSIGNAL); int error = socket_errno; // KHBBS seems to be getting error 10053 often if (iResult == SOCKET_ERROR) { ERROR_LOG(Log::sceNet, "FriendFinder: Socket Error (%i) when sending OPCODE_PING", error); if (error != EAGAIN && error != EWOULDBLOCK) { - g_adhocServerConnected = false; - shutdown((int)metasocket, SD_BOTH); - closesocket((int)metasocket); - metasocket = (int)INVALID_SOCKET; - g_OSD.Show(OSDType::MESSAGE_ERROR, std::string(n->T("Disconnected from AdhocServer")) + " (" + std::string(n->T("Error")) + ": " + std::to_string(error) + ")"); - // Mark all friends as timedout since we won't be able to detects disconnected friends anymore without being connected to Adhoc Server - peerlock.lock(); - timeoutFriendsRecursive(friends); - peerlock.unlock(); + connected = false; + CloseMetaSocket(); + FriendFinderEvent ev{ FriendFinderEventType::DISCONNECTED }; + ev.error = error; + PostFriendFinderEvent(std::move(ev)); } - } - else { + } else { // Update Ping Time lastping = now; VERBOSE_LOG(Log::sceNet, "FriendFinder: Sending OPCODE_PING (%llu)", static_cast(now)); } } } + } + if (connected) { // Check for Incoming Data - if (IsSocketReady((int)metasocket, true, false) > 0) { - int received = (int)recv((int)metasocket, (char*)(rx + rxpos), sizeof(rx) - rxpos, MSG_NOSIGNAL); - - // Free Network Lock - //_freeNetworkLock(); - - // Received Data + if (IsSocketReady(MetaSocket(), true, false) > 0) { + int received = (int)recv(MetaSocket(), (char*)(rx + rxpos), sizeof(rx) - rxpos, MSG_NOSIGNAL); if (received > 0) { - // Fix Position rxpos += received; - - // Log Incoming Traffic - //printf("Received %d Bytes of Data from Server\n", received); INFO_LOG(Log::sceNet, "Received %d Bytes of Data from Adhoc Server", received); } } - // Calculate EnterGameMode Timeout to prevent waiting forever for disconnected players - if (isAdhocctlBusy && adhocctlState == ADHOCCTL_STATE_DISCONNECTED && adhocctlCurrentMode == ADHOCCTL_MODE_GAMEMODE && netAdhocGameModeEntered && static_cast(now - adhocctlStartTime) > netAdhocEnterGameModeTimeout) { - netAdhocGameModeEntered = false; - notifyAdhocctlHandlers(ADHOCCTL_EVENT_ERROR, SCE_NET_ADHOC_ERROR_TIMEOUT); + // Hand over every whole packet. + int pos = 0; + while (pos < rxpos) { + const int size = ServerPacketSize(rx[pos]); + if (size == 0) { + ERROR_LOG(Log::sceNet, "FriendFinder: Unknown opcode %d from the server, dropping %d bytes", rx[pos], rxpos - pos); + pos = rxpos; + break; + } + if (rxpos - pos < size) { + break; + } + FriendFinderEvent ev{ FriendFinderEventType::PACKET }; + ev.packet.assign(rx + pos, rx + pos + size); + PostFriendFinderEvent(std::move(ev)); + pos += size; } - - // Handle Packets - rxQueued = rxpos; - if (rxpos > 0) { - // BSSID Packet - if (rx[0] == OPCODE_CONNECT_BSSID) { - // Enough Data available - if (rxpos >= (int)sizeof(SceNetAdhocctlConnectBSSIDPacketS2C)) { - // Cast Packet - SceNetAdhocctlConnectBSSIDPacketS2C* packet = (SceNetAdhocctlConnectBSSIDPacketS2C*)rx; - - INFO_LOG(Log::sceNet, "FriendFinder: Incoming OPCODE_CONNECT_BSSID [%s]", mac2str(&packet->mac).c_str()); - // Update Group BSSID - parameter.bssid.mac_addr = packet->mac; // This packet seems to contains Adhoc Group Creator's BSSID (similar to AP's BSSID) so it shouldn't get mixed up with local MAC address. Note: On JPCSP + prx files params.bssid is hardcoded to "Jpcsp\0" and doesn't match to any of player's mac - - // From JPCSP: Some games have problems when the PSP_ADHOCCTL_EVENT_CONNECTED is sent too quickly after connecting to a network. The connection will be set CONNECTED with a small delay (200ms or 200us?) - // Notify Event Handlers - if (adhocctlCurrentMode == ADHOCCTL_MODE_GAMEMODE) { - SceNetEtherAddr localMac; - getLocalMac(&localMac); - if (std::find_if(gameModeMacs.begin(), gameModeMacs.end(), - [localMac](SceNetEtherAddr const& e) { - return isMacMatch(&e, &localMac); - }) == gameModeMacs.end()) { - // Arrange the order to be consistent on all players (Host on top), Starting from our self the rest of new players will be added to the back - gameModeMacs.push_back(localMac); - - // FIXME: OPCODE_CONNECT_BSSID only triggered once, but the timing of ADHOCCTL_EVENT_GAME notification could be too soon, since there could be more players that need to join before the event should be notified - if (netAdhocGameModeEntered && gameModeMacs.size() >= requiredGameModeMacs.size()) { - notifyAdhocctlHandlers(ADHOCCTL_EVENT_GAME, 0); - } - } - else - WARN_LOG(Log::sceNet, "GameMode SelfMember [%s] Already Existed!", mac2str(&localMac).c_str()); - } - else { - //adhocctlState = ADHOCCTL_STATE_CONNECTED; - notifyAdhocctlHandlers(ADHOCCTL_EVENT_CONNECT, 0); - } - - // Give time a little time - //sceKernelDelayThread(adhocEventDelayMS * 1000); - //sleep_ms(adhocEventDelayMS); - - // Move RX Buffer - memmove(rx, rx + sizeof(SceNetAdhocctlConnectBSSIDPacketS2C), sizeof(rx) - sizeof(SceNetAdhocctlConnectBSSIDPacketS2C)); - - // Fix RX Buffer Length - rxpos -= sizeof(SceNetAdhocctlConnectBSSIDPacketS2C); - } - } - - // Chat Packet - else if (rx[0] == OPCODE_CHAT) { - // Enough Data available - if (rxpos >= (int)sizeof(SceNetAdhocctlChatPacketS2C)) { - // Cast Packet - SceNetAdhocctlChatPacketS2C* packet = (SceNetAdhocctlChatPacketS2C*)rx; - INFO_LOG(Log::sceNet, "FriendFinder: Incoming OPCODE_CHAT"); - - // Fix strings with null-terminated - packet->name.data[ADHOCCTL_NICKNAME_LEN - 1] = 0; - packet->base.message[ADHOCCTL_MESSAGE_LEN - 1] = 0; - - // Add Incoming Chat to HUD - NOTICE_LOG(Log::sceNet, "Received chat message %s", packet->base.message); - std::string incoming = ""; - std::string name = (char*)packet->name.data; - incoming.append(name.substr(0, 8)); - incoming.append(": "); - incoming.append((char*)packet->base.message); - - std::lock_guard guard(chatLogLock); - AddChatLogEntry(incoming); - chatMessageGeneration++; - chatMessageCount++; - - // Move RX Buffer - memmove(rx, rx + sizeof(SceNetAdhocctlChatPacketS2C), sizeof(rx) - sizeof(SceNetAdhocctlChatPacketS2C)); - - // Fix RX Buffer Length - rxpos -= sizeof(SceNetAdhocctlChatPacketS2C); - } - } - - // Connect Packet - else if (rx[0] == OPCODE_CONNECT) { - // Enough Data available - if (rxpos >= (int)sizeof(SceNetAdhocctlConnectPacketS2C)) { - // Cast Packet - SceNetAdhocctlConnectPacketS2C* packet = (SceNetAdhocctlConnectPacketS2C*)rx; - - // Fix strings with null-terminated - packet->name.data[ADHOCCTL_NICKNAME_LEN - 1] = 0; - - // Log Incoming Peer - u32_le ipaddr = packet->ip; - INFO_LOG(Log::sceNet, "FriendFinder: Incoming OPCODE_CONNECT [%s][%s][%s]", mac2str(&packet->mac).c_str(), ip2str(*(in_addr*)&ipaddr).c_str(), packet->name.data); - - // Add User - addFriend(packet); - - // Make sure GameMode participants are all joined (including self MAC) - if (adhocctlCurrentMode == ADHOCCTL_MODE_GAMEMODE) { - if (std::find_if(gameModeMacs.begin(), gameModeMacs.end(), - [packet](SceNetEtherAddr const& e) { - return isMacMatch(&e, &packet->mac); - }) == gameModeMacs.end()) { - // Arrange the order to be consistent on all players (Host on top), Existing players are sent in reverse by AdhocServer - SceNetEtherAddr localMac; - getLocalMac(&localMac); - auto it = std::find_if(gameModeMacs.begin(), gameModeMacs.end(), - [localMac](SceNetEtherAddr const& e) { - return isMacMatch(&e, &localMac); - }); - // Starting from our self the rest of new players will be added to the back - if (it != gameModeMacs.end()) { - gameModeMacs.push_back(packet->mac); - } - else { - it = gameModeMacs.begin() + 1; - gameModeMacs.insert(it, packet->mac); - } - - // From JPCSP: Join complete when all the required MACs have joined - if (netAdhocGameModeEntered && requiredGameModeMacs.size() > 0 && gameModeMacs.size() == requiredGameModeMacs.size()) { - // TODO: Should we replace gameModeMacs contents with requiredGameModeMacs contents to make sure they are in the same order with macs from sceNetAdhocctlCreateEnterGameMode? But may not be consistent with the list on client side! - //gameModeMacs = requiredGameModeMacs; - notifyAdhocctlHandlers(ADHOCCTL_EVENT_GAME, 0); - } - } - else - WARN_LOG(Log::sceNet, "GameMode Member [%s] Already Existed!", mac2str(&packet->mac).c_str()); - } - - // Update HUD User Count - std::string name = (char*)packet->name.data; - std::string incoming = ""; - incoming.append(name.substr(0, 8)); - incoming.append(" Joined "); - //do we need ip? - //joined.append((char *)packet->ip); - - std::lock_guard guard(chatLogLock); - AddChatLogEntry(incoming); - chatMessageGeneration++; - -#ifdef LOCALHOST_AS_PEER - setUserCount(getActivePeerCount()); -#else - // setUserCount(getActivePeerCount()+1); -#endif - - // Move RX Buffer - memmove(rx, rx + sizeof(SceNetAdhocctlConnectPacketS2C), sizeof(rx) - sizeof(SceNetAdhocctlConnectPacketS2C)); - - // Fix RX Buffer Length - rxpos -= sizeof(SceNetAdhocctlConnectPacketS2C); - } - } - - // Disconnect Packet - else if (rx[0] == OPCODE_DISCONNECT) { - // Enough Data available - if (rxpos >= (int)sizeof(SceNetAdhocctlDisconnectPacketS2C)) { - // Cast Packet - SceNetAdhocctlDisconnectPacketS2C* packet = (SceNetAdhocctlDisconnectPacketS2C*)rx; - - DEBUG_LOG(Log::sceNet, "FriendFinder: OPCODE_DISCONNECT"); - - // Log Incoming Peer Delete Request - INFO_LOG(Log::sceNet, "FriendFinder: Incoming Peer Data Delete Request..."); - - if (adhocctlCurrentMode == ADHOCCTL_MODE_GAMEMODE) { - auto peer = findFriendByIP(packet->ip); - for (auto& gma : replicaGameModeAreas) - if (isMacMatch(&gma.mac, &peer->mac_addr)) { - gma.updateTimestamp = 0; - break; - } - } - - // Delete User by IP, should delete by MAC since IP can be shared (behind NAT) isn't? - deleteFriendByIP(packet->ip); - - // Update HUD User Count -#ifdef LOCALHOST_AS_PEER - setUserCount(_getActivePeerCount()); -#else - //setUserCount(_getActivePeerCount()+1); -#endif - - // Move RX Buffer - memmove(rx, rx + sizeof(SceNetAdhocctlDisconnectPacketS2C), sizeof(rx) - sizeof(SceNetAdhocctlDisconnectPacketS2C)); - - // Fix RX Buffer Length - rxpos -= sizeof(SceNetAdhocctlDisconnectPacketS2C); - } - } - - // Scan Packet - else if (rx[0] == OPCODE_SCAN) { - // Enough Data available - if (rxpos >= (int)sizeof(SceNetAdhocctlScanPacketS2C)) { - // Cast Packet - SceNetAdhocctlScanPacketS2C* packet = (SceNetAdhocctlScanPacketS2C*)rx; - - DEBUG_LOG(Log::sceNet, "FriendFinder: OPCODE_SCAN"); - - // Log Incoming Network Information - INFO_LOG(Log::sceNet, "Incoming Group Information..."); - - // Multithreading Lock - peerlock.lock(); - - // Allocate Structure Data - SceNetAdhocctlScanInfo* group = (SceNetAdhocctlScanInfo*)malloc(sizeof(SceNetAdhocctlScanInfo)); - - // Allocated Structure Data - if (group != NULL) { - // Clear Memory, should this be done only when allocating new group? - memset(group, 0, sizeof(SceNetAdhocctlScanInfo)); - - // Link to existing Groups - group->next = newnetworks; - - // Copy Group Name - group->group_name = packet->group; - - // Set Group Host - group->bssid.mac_addr = packet->mac; - - // Set group parameters - // Since 0 is not a valid active channel we fake the channel for Automatic Channel (JPCSP use 11 as default). Ridge Racer 2 will ignore any groups with channel 0 or that doesn't matched with channel value returned from sceUtilityGetSystemParamInt (which mean sceUtilityGetSystemParamInt must not return channel 0 when connected to a network?) - group->channel = parameter.channel; //(parameter.channel == PSP_SYSTEMPARAM_ADHOC_CHANNEL_AUTOMATIC) ? defaultWlanChannel : parameter.channel; - // This Mode should be a valid mode (>=0), probably should be sent by AdhocServer since there are 2 possibilities (Normal and GameMode). Air Conflicts - Aces Of World War 2 (which use GameMode) seems to relies on this Mode value. - group->mode = std::max(ADHOCCTL_MODE_NORMAL, adhocctlCurrentMode); // default to ADHOCCTL_MODE_NORMAL - - // Link into Group List - newnetworks = group; - } - - // Multithreading Unlock - peerlock.unlock(); - - // Move RX Buffer - memmove(rx, rx + sizeof(SceNetAdhocctlScanPacketS2C), sizeof(rx) - sizeof(SceNetAdhocctlScanPacketS2C)); - - // Fix RX Buffer Length - rxpos -= sizeof(SceNetAdhocctlScanPacketS2C); - } - } - - // Scan Complete Packet - else if (rx[0] == OPCODE_SCAN_COMPLETE) { - DEBUG_LOG(Log::sceNet, "FriendFinder: OPCODE_SCAN_COMPLETE"); - // Log Scan Completion - INFO_LOG(Log::sceNet, "FriendFinder: Incoming Scan complete response..."); - - // Reset current networks to prevent disbanded host to be listed again - peerlock.lock(); - if (networks != newnetworks) { - freeGroupsRecursive(networks); - networks = newnetworks; - } - newnetworks = NULL; - peerlock.unlock(); - - // Notify Event Handlers - notifyAdhocctlHandlers(ADHOCCTL_EVENT_SCAN, 0); - - // Move RX Buffer - memmove(rx, rx + 1, sizeof(rx) - 1); - - // Fix RX Buffer Length - rxpos -= 1; - } + if (pos > 0) { + memmove(rx, rx + pos, rxpos - pos); + rxpos -= pos; } } - // A pass handles one packet; another whole one may already be buffered. - if (rxpos > 0 && rxpos < rxQueued) - continue; - // Wait for the server, the game side (FriendFinderWake), or the next ping or game mode deadline. + // Wait for the server, the game side (FriendFinderWake), or the next ping. uintptr_t sock = (uintptr_t)(intptr_t)-1; bool forWrite = false; double timeout = -1.0; - if (g_adhocServerConnected && metasocket != (int)INVALID_SOCKET) { - sock = (uintptr_t)(intptr_t)metasocket; + if (connected && MetaSocket() != (int)INVALID_SOCKET) { + sock = (uintptr_t)(intptr_t)MetaSocket(); const u64 t = (u64)(time_now_d() * 1000000.0); s64 left = PSP_ADHOCCTL_PING_TIMEOUT - static_cast(t - lastping); // A ping that is due but couldn't go out waits for the socket to take it. forWrite = left <= 0; - if (isAdhocctlBusy && adhocctlState == ADHOCCTL_STATE_DISCONNECTED && adhocctlCurrentMode == ADHOCCTL_MODE_GAMEMODE && netAdhocGameModeEntered) - left = std::min(left, (s64)netAdhocEnterGameModeTimeout - static_cast(t - adhocctlStartTime) + 1); timeout = forWrite ? -1.0 : std::max(left, (s64)0) / 1000000.0; } const net::WaitResult wait = net::WaitSocketsOrWake(&sock, nullptr, 1, forWrite, timeout, friendFinderWake); @@ -1822,12 +1631,6 @@ int friendFinder() { friendFinderWake->Drain(); } - // Groups/Networks should be deallocated isn't? - - // Prevent the games from having trouble to reInitiate Adhoc (the next NetInit -> PdpCreate after NetTerm) - adhocctlState = ADHOCCTL_STATE_DISCONNECTED; - friendFinderRunning = false; - // Log Shutdown INFO_LOG(Log::sceNet, "FriendFinder: End of Friend Finder Thread"); @@ -1835,6 +1638,273 @@ int friendFinder() { return 0; } +// Emulation thread. What used to run on the friend finder thread for a packet. +static void ApplyServerPacket(uint8_t *rx) { + auto n = GetI18NCategory(I18NCat::NETWORKING); + // BSSID Packet + if (rx[0] == OPCODE_CONNECT_BSSID) { + SceNetAdhocctlConnectBSSIDPacketS2C* packet = (SceNetAdhocctlConnectBSSIDPacketS2C*)rx; + + INFO_LOG(Log::sceNet, "FriendFinder: Incoming OPCODE_CONNECT_BSSID [%s]", mac2str(&packet->mac).c_str()); + // Update Group BSSID + parameter.bssid.mac_addr = packet->mac; // This packet seems to contains Adhoc Group Creator's BSSID (similar to AP's BSSID) so it shouldn't get mixed up with local MAC address. Note: On JPCSP + prx files params.bssid is hardcoded to "Jpcsp\0" and doesn't match to any of player's mac + + // From JPCSP: Some games have problems when the PSP_ADHOCCTL_EVENT_CONNECTED is sent too quickly after connecting to a network. The connection will be set CONNECTED with a small delay (200ms or 200us?) + // Notify Event Handlers + if (adhocctlCurrentMode == ADHOCCTL_MODE_GAMEMODE) { + SceNetEtherAddr localMac; + getLocalMac(&localMac); + if (std::find_if(gameModeMacs.begin(), gameModeMacs.end(), + [localMac](SceNetEtherAddr const& e) { + return isMacMatch(&e, &localMac); + }) == gameModeMacs.end()) { + // Arrange the order to be consistent on all players (Host on top), Starting from our self the rest of new players will be added to the back + gameModeMacs.push_back(localMac); + + // FIXME: OPCODE_CONNECT_BSSID only triggered once, but the timing of ADHOCCTL_EVENT_GAME notification could be too soon, since there could be more players that need to join before the event should be notified + if (netAdhocGameModeEntered && gameModeMacs.size() >= requiredGameModeMacs.size()) { + notifyAdhocctlHandlers(ADHOCCTL_EVENT_GAME, 0); + } + } else { + WARN_LOG(Log::sceNet, "GameMode SelfMember [%s] Already Existed!", mac2str(&localMac).c_str()); + } + } else { + //adhocctlState = ADHOCCTL_STATE_CONNECTED; + notifyAdhocctlHandlers(ADHOCCTL_EVENT_CONNECT, 0); + } + } + + // Chat Packet + else if (rx[0] == OPCODE_CHAT) { + SceNetAdhocctlChatPacketS2C* packet = (SceNetAdhocctlChatPacketS2C*)rx; + INFO_LOG(Log::sceNet, "FriendFinder: Incoming OPCODE_CHAT"); + + // Fix strings with null-terminated + packet->name.data[ADHOCCTL_NICKNAME_LEN - 1] = 0; + packet->base.message[ADHOCCTL_MESSAGE_LEN - 1] = 0; + + std::string name = (char*)packet->name.data; + NOTICE_LOG(Log::sceNet, "Received chat message from %s: %s", name.substr(0, 8).c_str(), packet->base.message); + } + + // Connect Packet + else if (rx[0] == OPCODE_CONNECT) { + SceNetAdhocctlConnectPacketS2C* packet = (SceNetAdhocctlConnectPacketS2C*)rx; + + // Fix strings with null-terminated + packet->name.data[ADHOCCTL_NICKNAME_LEN - 1] = 0; + + // Log Incoming Peer + u32_le ipaddr = packet->ip; + INFO_LOG(Log::sceNet, "FriendFinder: Incoming OPCODE_CONNECT [%s][%s][%s]", mac2str(&packet->mac).c_str(), ip2str(*(in_addr*)&ipaddr).c_str(), packet->name.data); + + // Add User + addFriend(packet); + + // Make sure GameMode participants are all joined (including self MAC) + if (adhocctlCurrentMode == ADHOCCTL_MODE_GAMEMODE) { + if (std::find_if(gameModeMacs.begin(), gameModeMacs.end(), + [packet](SceNetEtherAddr const& e) { + return isMacMatch(&e, &packet->mac); + }) == gameModeMacs.end()) { + // Arrange the order to be consistent on all players (Host on top), Existing players are sent in reverse by AdhocServer + SceNetEtherAddr localMac; + getLocalMac(&localMac); + auto it = std::find_if(gameModeMacs.begin(), gameModeMacs.end(), + [localMac](SceNetEtherAddr const& e) { + return isMacMatch(&e, &localMac); + }); + // Starting from our self the rest of new players will be added to the back + if (it != gameModeMacs.end()) { + gameModeMacs.push_back(packet->mac); + } else { + it = gameModeMacs.begin() + 1; + gameModeMacs.insert(it, packet->mac); + } + + // From JPCSP: Join complete when all the required MACs have joined + if (netAdhocGameModeEntered && requiredGameModeMacs.size() > 0 && gameModeMacs.size() == requiredGameModeMacs.size()) { + // TODO: Should we replace gameModeMacs contents with requiredGameModeMacs contents to make sure they are in the same order with macs from sceNetAdhocctlCreateEnterGameMode? But may not be consistent with the list on client side! + //gameModeMacs = requiredGameModeMacs; + notifyAdhocctlHandlers(ADHOCCTL_EVENT_GAME, 0); + } + } else { + WARN_LOG(Log::sceNet, "GameMode Member [%s] Already Existed!", mac2str(&packet->mac).c_str()); + } + } + + std::string name = (char*)packet->name.data; + NOTICE_LOG(Log::sceNet, "%s Joined", name.substr(0, 8).c_str()); + } + + // Disconnect Packet + else if (rx[0] == OPCODE_DISCONNECT) { + SceNetAdhocctlDisconnectPacketS2C* packet = (SceNetAdhocctlDisconnectPacketS2C*)rx; + + DEBUG_LOG(Log::sceNet, "FriendFinder: OPCODE_DISCONNECT"); + + // Log Incoming Peer Delete Request + INFO_LOG(Log::sceNet, "FriendFinder: Incoming Peer Data Delete Request..."); + + if (adhocctlCurrentMode == ADHOCCTL_MODE_GAMEMODE) { + auto peer = findFriendByIP(packet->ip); + if (peer) { + for (auto& gma : replicaGameModeAreas) { + if (isMacMatch(&gma.mac, &peer->mac_addr)) { + gma.updateTimestamp = 0; + break; + } + } + } + } + + // Delete User by IP, should delete by MAC since IP can be shared (behind NAT) isn't? + deleteFriendByIP(packet->ip); + } + + // Scan Packet + else if (rx[0] == OPCODE_SCAN) { + SceNetAdhocctlScanPacketS2C* packet = (SceNetAdhocctlScanPacketS2C*)rx; + + DEBUG_LOG(Log::sceNet, "FriendFinder: OPCODE_SCAN"); + + // Log Incoming Network Information + INFO_LOG(Log::sceNet, "Incoming Group Information..."); + + // Allocate Structure Data + SceNetAdhocctlScanInfo* group = (SceNetAdhocctlScanInfo*)malloc(sizeof(SceNetAdhocctlScanInfo)); + + // Allocated Structure Data + if (group != NULL) { + // Clear Memory, should this be done only when allocating new group? + memset(group, 0, sizeof(SceNetAdhocctlScanInfo)); + + // Link to existing Groups + group->next = newnetworks; + + // Copy Group Name + group->group_name = packet->group; + + // Set Group Host + group->bssid.mac_addr = packet->mac; + + // Set group parameters + // Since 0 is not a valid active channel we fake the channel for Automatic Channel (JPCSP use 11 as default). Ridge Racer 2 will ignore any groups with channel 0 or that doesn't matched with channel value returned from sceUtilityGetSystemParamInt (which mean sceUtilityGetSystemParamInt must not return channel 0 when connected to a network?) + group->channel = parameter.channel; //(parameter.channel == PSP_SYSTEMPARAM_ADHOC_CHANNEL_AUTOMATIC) ? defaultWlanChannel : parameter.channel; + // This Mode should be a valid mode (>=0), probably should be sent by AdhocServer since there are 2 possibilities (Normal and GameMode). Air Conflicts - Aces Of World War 2 (which use GameMode) seems to relies on this Mode value. + group->mode = std::max(ADHOCCTL_MODE_NORMAL, adhocctlCurrentMode); // default to ADHOCCTL_MODE_NORMAL + + // Link into Group List + newnetworks = group; + } + } + + // Scan Complete Packet + else if (rx[0] == OPCODE_SCAN_COMPLETE) { + DEBUG_LOG(Log::sceNet, "FriendFinder: OPCODE_SCAN_COMPLETE"); + // Log Scan Completion + INFO_LOG(Log::sceNet, "FriendFinder: Incoming Scan complete response..."); + + // Reset current networks to prevent disbanded host to be listed again + if (networks != newnetworks) { + freeGroupsRecursive(networks); + networks = newnetworks; + } + newnetworks = NULL; + + // Notify Event Handlers + notifyAdhocctlHandlers(ADHOCCTL_EVENT_SCAN, 0); + } +} + +void FriendFinderProcess() { + auto n = GetI18NCategory(I18NCat::NETWORKING); + g_friendFinderEvents.Drain([&](FriendFinderEvent &&ev) { + switch (ev.type) { + case FriendFinderEventType::RESOLVED: + g_adhocServerIP = ev.addr; + break; + case FriendFinderEventType::CONNECTED: + g_friendFinderLoginPending = false; + isAdhocctlNeedLogin = false; + if (!g_adhocServerConnected) { + g_adhocServerConnected = true; + LocalIP = ev.localIP; + // At this point we are most-likely not in a Group within the Adhoc Server, so we should probably reset AdhocctlState + adhocctlState = ADHOCCTL_STATE_DISCONNECTED; + netAdhocGameModeEntered = false; + isAdhocctlBusy = false; + } + break; + case FriendFinderEventType::LOGIN_FAILED: + g_friendFinderLoginPending = false; + isAdhocctlNeedLogin = false; + g_adhocServerConnected = false; + g_adhocServerLoginFailed = true; + break; + case FriendFinderEventType::DISCONNECTED: + g_adhocServerConnected = false; + g_OSD.Show(OSDType::MESSAGE_ERROR, std::string(n->T("Disconnected from AdhocServer")) + " (" + std::string(n->T("Error")) + ": " + std::to_string(ev.error) + ")"); + // Mark all friends as timedout since we won't be able to detects disconnected friends anymore without being connected to Adhoc Server + timeoutFriendsRecursive(friends); + break; + case FriendFinderEventType::PACKET: + if (g_adhocServerConnected) { + ApplyServerPacket(ev.packet.data()); + } + break; + } + }); + + if (!FriendFinderIsRunning()) { + return; + } + + // Reconnect when disconnected while Adhocctl is still inited. Retries only on demand. + if (isAdhocctlNeedLogin && !g_friendFinderLoginPending) { + if (MetaSocket() == (int)INVALID_SOCKET && netAdhocctlInited && g_Config.bEnableWlan) { + // Default/Initial Network Parameters + memset(¶meter, 0, sizeof(parameter)); + strncpy((char *)¶meter.nickname.data, g_Config.sNickName.c_str(), ADHOCCTL_NICKNAME_LEN); + parameter.nickname.data[ADHOCCTL_NICKNAME_LEN - 1] = 0; + parameter.channel = g_Config.iWlanAdhocChannel; + // Assign a Valid Channel when connected to AP/Adhoc if it's Auto. JPCSP use 11 as default for Auto (Commonly for Auto: 1, 6, 11) + if (parameter.channel == PSP_SYSTEMPARAM_ADHOC_CHANNEL_AUTOMATIC) parameter.channel = defaultWlanChannel; // Faked Active channel to default channel + + FriendFinderLogin login{}; + login.packet.base.opcode = OPCODE_LOGIN; + SceNetEtherAddr addres; + getLocalMac(&addres); + login.packet.mac = addres; + strncpy((char *)&login.packet.name.data, g_Config.sNickName.c_str(), ADHOCCTL_NICKNAME_LEN); + login.packet.name.data[ADHOCCTL_NICKNAME_LEN - 1] = 0; + memcpy(login.packet.game.data, product_code.data, ADHOCCTL_ADHOCID_LEN); + login.localhostIP = g_localhostIP; + g_friendFinderLoginPending = true; + g_friendFinderLogins.Push(login); + FriendFinderWake(); + } else { + isAdhocctlNeedLogin = false; + } + } + + // Calculate EnterGameMode Timeout to prevent waiting forever for disconnected players + const u64 now = (u64)(time_now_d() * 1000000.0); + if (g_adhocServerConnected && isAdhocctlBusy && adhocctlState == ADHOCCTL_STATE_DISCONNECTED && adhocctlCurrentMode == ADHOCCTL_MODE_GAMEMODE && netAdhocGameModeEntered && static_cast(now - adhocctlStartTime) > netAdhocEnterGameModeTimeout) { + netAdhocGameModeEntered = false; + notifyAdhocctlHandlers(ADHOCCTL_EVENT_ERROR, SCE_NET_ADHOC_ERROR_TIMEOUT); + } +} + +void FriendFinderStopped() { + // Whatever it posted is moot now. + g_friendFinderEvents.Drain([](FriendFinderEvent &&) {}); + g_friendFinderLogins.Drain([](FriendFinderLogin &&) {}); + g_friendFinderLoginPending = false; + // Prevent the games from having trouble to reInitiate Adhoc (the next NetInit -> PdpCreate after NetTerm) + adhocctlState = ADHOCCTL_STATE_DISCONNECTED; +} + int getActivePeerCount(const bool excludeTimedout) { // Counter int count = 0; @@ -1865,11 +1935,11 @@ int getLocalIp(sockaddr_in* SocketAddress) { } #if !PPSSPP_PLATFORM(SWITCH) - if (metasocket != (int)INVALID_SOCKET) { + if (MetaSocket() != (int)INVALID_SOCKET) { struct sockaddr_in localAddr {}; localAddr.sin_addr.s_addr = INADDR_ANY; socklen_t addrLen = sizeof(localAddr); - int ret = getsockname((int)metasocket, (struct sockaddr*)&localAddr, &addrLen); + int ret = getsockname(MetaSocket(), (struct sockaddr*)&localAddr, &addrLen); // Note: Sometimes metasocket still contains a valid socket fd right after failed to connect to AdhocServer on a different thread, thus ended with 0.0.0.0 here if (SOCKET_ERROR != ret && localAddr.sin_addr.s_addr != 0) { SocketAddress->sin_addr = localAddr.sin_addr; @@ -2233,136 +2303,6 @@ int getPTPSocketCount() { return counter; } -int initNetwork(SceNetAdhocctlAdhocId *adhoc_id){ - auto n = GetI18NCategory(I18NCat::NETWORKING); - int iResult = 0; - metasocket = (int)INVALID_SOCKET; - metasocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (metasocket == INVALID_SOCKET){ - ERROR_LOG(Log::sceNet, "Invalid socket"); - return SOCKET_ERROR; - } - setSockKeepAlive((int)metasocket, true); - // Disable Nagle Algo to prevent delaying small packets - setSockNoDelay((int)metasocket, 1); - // Switch to Nonblocking Behaviour - changeBlockingMode((int)metasocket, 1); - // Ignore SIGPIPE when supported (ie. BSD/MacOS) - setSockNoSIGPIPE((int)metasocket, 1); - - // If Server is at localhost Try to Bind socket to specific adapter before connecting to prevent 2nd instance being recognized as already existing 127.0.0.1 by AdhocServer - // (may not works in WinXP/2003 for IPv4 due to "Weak End System" model) - if (isLoopbackIP(g_adhocServerIP.in.sin_addr.s_addr)) { - int on = 1; - // Not sure what is this SO_DONTROUTE supposed to fix, but i do remembered there were issue related to multiple-instances without SO_DONTROUTE, but forgot how to reproduce it :( - setsockopt((int)metasocket, SOL_SOCKET, SO_DONTROUTE, (const char*)&on, sizeof(on)); - setSockReuseAddrPort((int)metasocket); - - g_localhostIP.in.sin_port = 0; - // Bind Local Address to Socket - iResult = bind((int)metasocket, &g_localhostIP.addr, sizeof(g_localhostIP.addr)); - if (iResult == SOCKET_ERROR) { - ERROR_LOG(Log::sceNet, "Bind to alternate localhost[%s] failed(%i).", ip2str(g_localhostIP.in.sin_addr).c_str(), iResult); - g_OSD.Show(OSDType::MESSAGE_ERROR, std::string(n->T("Failed to Bind Localhost IP")) + " " + ip2str(g_localhostIP.in.sin_addr).c_str()); - } - } - - // Default/Initial Network Parameters - memset(¶meter, 0, sizeof(parameter)); - strncpy((char *)¶meter.nickname.data, g_Config.sNickName.c_str(), ADHOCCTL_NICKNAME_LEN); - parameter.nickname.data[ADHOCCTL_NICKNAME_LEN - 1] = 0; - parameter.channel = g_Config.iWlanAdhocChannel; - // Assign a Valid Channel when connected to AP/Adhoc if it's Auto. JPCSP use 11 as default for Auto (Commonly for Auto: 1, 6, 11) - if (parameter.channel == PSP_SYSTEMPARAM_ADHOC_CHANNEL_AUTOMATIC) parameter.channel = defaultWlanChannel; // Faked Active channel to default channel - //getLocalMac(¶meter.bssid.mac_addr); - - // Default ProductId - product_code.type = adhoc_id->type; - memcpy(product_code.data, adhoc_id->data, ADHOCCTL_ADHOCID_LEN); - - // Don't need to connect if AdhocServer DNS was not resolved - if (g_adhocServerIP.in.sin_addr.s_addr == INADDR_NONE) - return SOCKET_ERROR; - - // Connect to Adhoc Server - int errorcode = 0; - int cnt = 0; - DEBUG_LOG(Log::sceNet, "InitNetwork: Connecting to AdhocServer"); - iResult = connect((int)metasocket, &g_adhocServerIP.addr, sizeof(g_adhocServerIP)); - errorcode = socket_errno; - - if (iResult == SOCKET_ERROR && errorcode != EISCONN) { - u64 startTime = (u64)(time_now_d() * 1000000.0); - bool done = false; - while (!done) { - if (coreState == CORE_POWERDOWN || !friendFinderRunning) - return iResult; - - // Writable, the timeout, or a wake to recheck the above. - const u64 waited = (u64)(time_now_d() * 1000000.0) - startTime; - uintptr_t sock = (uintptr_t)(intptr_t)metasocket; - const double left = waited < (u64)adhocDefaultTimeout ? (adhocDefaultTimeout - waited) / 1000000.0 : 0.0; - if (net::WaitSocketsOrWake(&sock, nullptr, 1, true, left, friendFinderWake) == net::WaitResult::CANCELLED) { - friendFinderWake->Drain(); - continue; - } - done = (IsSocketReady((int)metasocket, false, true) > 0); - if (done) { - // Writable can also mean the attempt failed (refused, say). Then there's no point - // waiting out the timeout. - int soError = 0; - socklen_t soErrorLen = sizeof(soError); - if (getsockopt((int)metasocket, SOL_SOCKET, SO_ERROR, (char *)&soError, &soErrorLen) == 0 && soError != 0) { - errorcode = soError; - break; - } - } - struct sockaddr_in sin; - socklen_t sinlen = sizeof(sin); - memset(&sin, 0, sinlen); - // Ensure that the connection really established or not, since "select" alone can't accurately detects it - const bool writable = done; - done &= (getpeername((int)metasocket, (struct sockaddr*)&sin, &sinlen) != SOCKET_ERROR); - // Writable but not connected is a failed attempt, and would stay writable. - if (writable && !done) - break; - u64 now = (u64)(time_now_d() * 1000000.0); - if (static_cast(now - startTime) >= adhocDefaultTimeout) { - if (connectInProgress(errorcode)) - errorcode = ETIMEDOUT; - break; - } - } - if (!done) { - ERROR_LOG(Log::sceNet, "Socket error (%i) when connecting to AdhocServer [%s/%s:%u]", errorcode, g_Config.sProAdhocServer.c_str(), ip2str(g_adhocServerIP.in.sin_addr).c_str(), ntohs(g_adhocServerIP.in.sin_port)); - g_OSD.Show(OSDType::MESSAGE_ERROR, std::string(n->T("Failed to connect to Adhoc Server")) + " (" + std::string(n->T("Error")) + ": " + std::to_string(errorcode) + ")"); - return iResult; - } - } - - // Prepare Login Packet - SceNetAdhocctlLoginPacketC2S packet; - packet.base.opcode = OPCODE_LOGIN; - SceNetEtherAddr addres; - getLocalMac(&addres); - packet.mac = addres; - strncpy((char *)&packet.name.data, g_Config.sNickName.c_str(), ADHOCCTL_NICKNAME_LEN); - packet.name.data[ADHOCCTL_NICKNAME_LEN - 1] = 0; - memcpy(packet.game.data, adhoc_id->data, ADHOCCTL_ADHOCID_LEN); - - IsSocketReady((int)metasocket, false, true, nullptr, adhocDefaultTimeout); - DEBUG_LOG(Log::sceNet, "InitNetwork: Sending LOGIN OPCODE %d", packet.base.opcode); - int sent = (int)send((int)metasocket, (char*)&packet, sizeof(packet), MSG_NOSIGNAL); - if (sent > 0) { - socklen_t addrLen = sizeof(LocalIP); - memset(&LocalIP, 0, addrLen); - getsockname((int)metasocket, &LocalIP, &addrLen); - return 0; - } else { - return SOCKET_ERROR; - } -} - bool isZeroMAC(const SceNetEtherAddr* addr) { return (memcmp(addr->data, "\x00\x00\x00\x00\x00\x00", ETHER_ADDR_LEN) == 0); } @@ -2381,8 +2321,6 @@ bool resolveIP(uint32_t ip, SceNetEtherAddr * mac) { return true; } - // Multithreading Lock - std::lock_guard peer_guard(peerlock); // Peer Reference SceNetAdhocctlPeerInfo * peer = friends; @@ -2411,7 +2349,6 @@ void fixGameMac(SceNetEtherAddr *mac) { return; } - std::lock_guard peer_guard(peerlock); SceNetAdhocctlPeerInfo * peer = friends; for (; peer != NULL; peer = peer->next) { if (isMacMatch(&peer->mac_addr, mac)) { @@ -2436,8 +2373,6 @@ bool resolveMAC(SceNetEtherAddr* mac, uint32_t* ip, u16* port_offset) { return true; // return succes } - // Multithreading Lock - std::lock_guard peer_guard(peerlock); // Peer Reference SceNetAdhocctlPeerInfo * peer = friends; diff --git a/Core/HLE/proAdhoc.h b/Core/HLE/proAdhoc.h index c9bfc7130b..a7c3010272 100644 --- a/Core/HLE/proAdhoc.h +++ b/Core/HLE/proAdhoc.h @@ -25,9 +25,7 @@ #define PACK __attribute__((packed)) #endif -#include -#include -#include +#include #include #include @@ -38,6 +36,7 @@ #include "Core/HLE/sceKernel.h" #include "Core/HLE/sceKernelMutex.h" #include "Core/HLE/sceUtility.h" +#include "Common/Thread/Thread.h" namespace net { class CancelToken; } @@ -332,13 +331,23 @@ typedef struct AdhocSocket { SceNetAdhocPtpStat ptp; } data; void *postofficeHandle; // aemu_postoffice mode handle - std::thread *connectThread; + Thread *connectThread; // Cancelled before a join, so closing doesn't wait out a connect. net::CancelToken *connectCancel; - bool connectThreadDone; - int connectThreadResult; + // What the connect thread hands back, or null when no attempt was made yet. + struct PostofficeConnectState *connectState; } PACK AdhocSocket; +// Filled by a postoffice connect thread, which then sets done (release). +struct PostofficeConnectState { + retro_atomic_int_t done{ 0 }; + int result = 0; + void *handle = nullptr; +}; + +// Emulation thread. Whether the last connect attempt (if any) is over, adopting its handle as +// postofficeHandle if it made one. result gets the attempt's result. +bool PostofficeConnectFinished(AdhocSocket *sock, int *result = nullptr); // Joins a postoffice socket's connect thread (cancelling it first if asked) and frees it. void JoinPostofficeConnect(AdhocSocket *sock, bool cancel); @@ -456,8 +465,6 @@ typedef struct SceNetAdhocMatchingContext { // Local PDP Socket s32_le socket; - // Socket Lock - std::recursive_mutex *socketlock; // Receive Buffer Length s32_le rxbuflen; @@ -511,11 +518,9 @@ typedef struct SceNetAdhocMatchingContext { u64_le inputLastHello = 0; // Event Caller Thread Message Stack - std::recursive_mutex *eventlock; // s32_le event_stack_lock; ThreadMessage *event_stack; // IO Handler Thread Message Stack - std::recursive_mutex *inputlock; // s32_le input_stack_lock; ThreadMessage *input_stack; // Socket Connectivity @@ -771,14 +776,22 @@ extern int actionAfterMatchingMipsCall; #define SOCK_PDP 1 #define SOCK_PTP 2 // Aux vars -extern std::atomic metasocket; +// The connection to the adhoc server. See proAdhoc.cpp for who touches it when. +int MetaSocket(); +void SetMetaSocket(int sock); +void CloseMetaSocket(); extern SceNetAdhocctlParameter parameter; extern SceNetAdhocctlAdhocId product_code; -extern std::thread friendFinderThread; +extern Thread friendFinderThread; // Make the wake before starting the thread; wake it after changing what it acts on. void FriendFinderPrepare(); void FriendFinderWake(); -extern std::recursive_mutex peerlock; +bool FriendFinderIsRunning(); +// Emulation thread: applies what the friend finder thread received, and asks it to log in when +// needed. Called often (the adhoc HLE loop, pending adhocctl requests). +void FriendFinderProcess(); +// Emulation thread, after joining the friend finder. +void FriendFinderStopped(); extern AdhocSocket* adhocSockets[MAX_SOCKET]; union SockAddrIN4 { @@ -800,7 +813,7 @@ extern SceNetAdhocMatchingContext * contexts; extern char* dummyPeekBuf64k; extern int dummyPeekBuf64kSize; extern int one; -extern std::atomic friendFinderRunning; +extern retro_atomic_int_t friendFinderRunning; extern SceNetAdhocctlPeerInfo * friends; extern SceNetAdhocctlScanInfo * networks; extern u64 adhocctlStartTime; @@ -886,19 +899,6 @@ SceNetAdhocMatchingMemberInternal* addMember(SceNetAdhocMatchingContext * contex */ void addFriend(SceNetAdhocctlConnectPacketS2C * packet); -/** -* Send chat or get that -* @param std::string ChatString -*/ -void sendChat(std::string_view chatString); - -struct ChatLogEntry { - std::string text; // "name: message", or an info line with no colon. - time_t timestamp; -}; -std::vector getChatLog(); -int GetChatChangeID(); -int GetChatMessageCount(); /* * Find a Peer/Friend by MAC address @@ -1295,13 +1295,6 @@ int getPDPSocketCount(); */ int getPTPSocketCount(); -/** - * Initialize Networking Components for Adhocctl Emulator - * @param adhoc_id Game Product Code - * @param server_ip Server IP - * @return 0 on success or... -1 - */ -int initNetwork(SceNetAdhocctlAdhocId *adhocid); /** * Zero MAC Check diff --git a/Core/HLE/proAdhocServer.cpp b/Core/HLE/proAdhocServer.cpp index e02422d310..1fda96ac67 100644 --- a/Core/HLE/proAdhocServer.cpp +++ b/Core/HLE/proAdhocServer.cpp @@ -44,6 +44,7 @@ #include "Core/Core.h" #include "Core/Config.h" #include "Core/HLE/proAdhocServer.h" +#include "Common/Thread/Thread.h" #ifdef _WIN32 #undef errno @@ -60,8 +61,8 @@ SceNetAdhocctlUserNode * _db_user = NULL; SceNetAdhocctlGameNode * _db_game = NULL; // Server Status -std::atomic adhocServerRunning(false); -std::thread adhocServerThread; +retro_atomic_int_t adhocServerRunning{ 0 }; +Thread adhocServerThread; // Made once before the first start and kept, so a wake never races a free. static net::WakeSocket *adhocServerWake; @@ -1717,7 +1718,7 @@ int proAdhocServerThread(int port) // (int argc, char * argv[]) } //_status = 0; - adhocServerRunning = false; + retro_atomic_store_release_int(&adhocServerRunning, 0); INFO_LOG(Log::sceNet, "AdhocServer: End of AdhocServer Thread"); @@ -1736,7 +1737,7 @@ void interrupt(int sig) // Trigger Shutdown //_status = 0; - adhocServerRunning = false; + retro_atomic_store_release_int(&adhocServerRunning, 0); } /** @@ -1886,7 +1887,7 @@ int server_loop(int server) update_status(); // Handling Loop - while (adhocServerRunning) //(_status == 1) + while (retro_atomic_load_acquire_int(&adhocServerRunning)) //(_status == 1) { // Login Block { diff --git a/Core/HLE/proAdhocServer.h b/Core/HLE/proAdhocServer.h index 7224c7fa24..6659665737 100644 --- a/Core/HLE/proAdhocServer.h +++ b/Core/HLE/proAdhocServer.h @@ -26,6 +26,7 @@ #include #include #include "proAdhoc.h" +#include "Common/Thread/Thread.h" // Server Listening Port //#define SERVER_PORT 27312 @@ -371,8 +372,8 @@ void update_status(); int proAdhocServerThread(int port); // (int argc, char * argv[]) //extern int _status; -extern std::atomic adhocServerRunning; -extern std::thread adhocServerThread; +extern retro_atomic_int_t adhocServerRunning; +extern Thread adhocServerThread; // Make the wake before starting the thread; wake it after clearing adhocServerRunning. void AdhocServerPrepare(); void AdhocServerWake(); diff --git a/Core/HLE/sceChnnlsv.cpp b/Core/HLE/sceChnnlsv.cpp index 918ca97696..2ba249b45b 100644 --- a/Core/HLE/sceChnnlsv.cpp +++ b/Core/HLE/sceChnnlsv.cpp @@ -15,7 +15,6 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include #include "Core/MemMapHelpers.h" #include "Core/HLE/HLE.h" @@ -30,9 +29,6 @@ KirkState *__ChnnlsvKirkState() { return &g_kirk; } -// The savedata IO thread uses these through the sceSd functions below, while the game can call them -// (and the kirk ones) on the emulator thread. -static std::mutex g_lock; static u8 dataBuf[2048+20]; static u8 *dataBuf2 = dataBuf + 20; @@ -231,7 +227,6 @@ static int sceSdGetLastIndex(u32 addressCtx, u32 addressHash, u32 addressKey) { int sceSdMacFinal(pspChnnlsvContext1& ctx, u8* in_hash, const u8* in_key) { - std::lock_guard guard(g_lock); if(ctx.keyLength >= 17) return -1026; @@ -358,7 +353,6 @@ static int sceSdRemoveValue(u32 addressCtx, u32 addressData, int length) { int sceSdMacUpdate(pspChnnlsvContext1& ctx, const u8* data, int length) { - std::lock_guard guard(g_lock); if(ctx.keyLength >= 17) return -1026; @@ -411,7 +405,6 @@ static int sceSdCreateList(u32 ctx2Addr, int mode, int unkwn, u32 dataAddr, u32 int sceSdCipherInit(pspChnnlsvContext2& ctx2, int mode, int uknw, u8* data, const u8* cryptkey) { - std::lock_guard guard(g_lock); ctx2.mode = mode; ctx2.unkn = 1; if (uknw == 2) @@ -475,7 +468,6 @@ static int sceSdSetMember(u32 ctxAddr, u32 dataAddr, int alignedLen) { int sceSdCipherUpdate(pspChnnlsvContext2& ctx, u8* data, int alignedLen) { - std::lock_guard guard(g_lock); if (alignedLen == 0) { return 0; @@ -547,7 +539,6 @@ void Register_sceChnnlsv() static u32 sceUtilsBufferCopyWithRange(u32 outAddr, int outSize, u32 inAddr, int inSize, int cmd) { u8 *outAddress = Memory::IsValidRange(outAddr, outSize) ? Memory::GetPointerWriteUnchecked(outAddr) : nullptr; u8 *inAddress = Memory::IsValidRange(inAddr, inSize) ? Memory::GetPointerWriteUnchecked(inAddr) : nullptr; - std::lock_guard guard(g_lock); int temp = kirk_sceUtilsBufferCopyWithRange(&g_kirk, outAddress, outSize, inAddress, inSize, cmd); if (temp != 0) { ERROR_LOG(Log::sceKernel, "hleUtilsBufferCopyWithRange: Failed with %d", temp); @@ -559,7 +550,6 @@ static u32 sceUtilsBufferCopyWithRange(u32 outAddr, int outSize, u32 inAddr, int static int sceUtilsBufferCopyByPollingWithRange(u32 outAddr, int outSize, u32 inAddr, int inSize, int cmd) { u8 *outAddress = Memory::IsValidRange(outAddr, outSize) ? Memory::GetPointerWriteUnchecked(outAddr) : nullptr; u8 *inAddress = Memory::IsValidRange(inAddr, inSize) ? Memory::GetPointerWriteUnchecked(inAddr) : nullptr; - std::lock_guard guard(g_lock); return hleNoLog(kirk_sceUtilsBufferCopyWithRange(&g_kirk, outAddress, outSize, inAddress, inSize, cmd)); } diff --git a/Core/HLE/sceCtrl.cpp b/Core/HLE/sceCtrl.cpp index fd098ccb52..f05131c0ea 100644 --- a/Core/HLE/sceCtrl.cpp +++ b/Core/HLE/sceCtrl.cpp @@ -17,13 +17,14 @@ #include #include -#include +#include #include #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" #include "Common/Math/math_util.h" #include "Core/CoreTiming.h" +#include "Common/Thread/MpscQueue.h" #include "Core/HLE/HLE.h" #include "Core/HLE/ErrorCodes.h" #include "Core/HLE/FunctionWrappers.h" @@ -89,7 +90,6 @@ static int ctrlIdleBack = -1; static int ctrlCycle = 0; static std::vector waitingThreads; -static std::mutex ctrlMutex; static int ctrlTimer = -1; @@ -109,12 +109,54 @@ static u32 emuRapidFireFrames = 0; static bool emuRapidFireToggle = true; static u32 emuRapidFireInterval = 5; +// The host's input, set from any thread (the frontend, the debugger) and sampled by the emulation +// thread into ctrlCurrent. Analog is packed: byte (stick * 2 + axis). +static retro_atomic_int_t inputButtons{ 0 }; +static retro_atomic_int_t inputAnalog{ (int)0x80808080u }; + +static void UpdateInputButtons(u32 bitsToSet, u32 bitsToClear) { + int cur = retro_atomic_load_relaxed_int(&inputButtons); + while (!retro_atomic_cas_int(&inputButtons, cur, (int)(((u32)cur & ~bitsToClear) | bitsToSet))) { + cur = retro_atomic_load_relaxed_int(&inputButtons); + } +} + +static void SetInputAnalog(int stick, int axisMask, u8 x, u8 y) { + const int shift = stick * 16; + u32 mask = 0; + u32 bits = 0; + if (axisMask & 1) { + mask |= 0xFFu << shift; + bits |= (u32)x << shift; + } + if (axisMask & 2) { + mask |= 0xFFu << (shift + 8); + bits |= (u32)y << (shift + 8); + } + int cur = retro_atomic_load_relaxed_int(&inputAnalog); + while (!retro_atomic_cas_int(&inputAnalog, cur, (int)(((u32)cur & ~mask) | bits))) { + cur = retro_atomic_load_relaxed_int(&inputAnalog); + } +} + +static u8 InputAnalogAxis(u32 packed, int stick, int axis) { + return (u8)(packed >> (stick * 16 + axis * 8)); +} + // These buttons are not affected by rapid fire (neither is analog.) const u32 CTRL_EMU_RAPIDFIRE_MASK = CTRL_UP | CTRL_DOWN | CTRL_LEFT | CTRL_RIGHT; +static void __CtrlTakeTimedPresses(); + static void __CtrlUpdateLatch() { - std::lock_guard guard(ctrlMutex); + __CtrlTakeTimedPresses(); + ctrlCurrent.buttons = (u32)retro_atomic_load_acquire_int(&inputButtons); + const u32 analog = (u32)retro_atomic_load_acquire_int(&inputAnalog); + for (int stick = 0; stick < 2; stick++) { + ctrlCurrent.analog[stick][CTRL_ANALOG_X] = InputAnalogAxis(analog, stick, CTRL_ANALOG_X); + ctrlCurrent.analog[stick][CTRL_ANALOG_Y] = InputAnalogAxis(analog, stick, CTRL_ANALOG_Y); + } u64 t = CoreTiming::GetGlobalTimeUs(); u32 buttons = ctrlCurrent.buttons; @@ -173,18 +215,12 @@ static int __CtrlResetLatch() u32 __CtrlPeekButtons() { - std::lock_guard guard(ctrlMutex); - - return ctrlCurrent.buttons; + return (u32)retro_atomic_load_acquire_int(&inputButtons); } u32 __CtrlPeekButtonsVisual() { - u32 buttons; - { - std::lock_guard guard(ctrlMutex); - buttons = ctrlCurrent.buttons; - } + u32 buttons = __CtrlPeekButtons(); if (emuRapidFire && emuRapidFireToggle) buttons &= CTRL_EMU_RAPIDFIRE_MASK; @@ -193,10 +229,9 @@ u32 __CtrlPeekButtonsVisual() void __CtrlPeekAnalog(int stick, float *x, float *y) { - std::lock_guard guard(ctrlMutex); - - *x = (ctrlCurrent.analog[stick][CTRL_ANALOG_X] - 127.5f) / 127.5f; - *y = -(ctrlCurrent.analog[stick][CTRL_ANALOG_Y] - 127.5f) / 127.5f; + const u32 analog = (u32)retro_atomic_load_acquire_int(&inputAnalog); + *x = (InputAnalogAxis(analog, stick, CTRL_ANALOG_X) - 127.5f) / 127.5f; + *y = -(InputAnalogAxis(analog, stick, CTRL_ANALOG_Y) - 127.5f) / 127.5f; } @@ -209,26 +244,33 @@ u32 __CtrlReadLatch() // Presses held for a number of vblank samples (the debugger's input.buttons.press). Counted down on // the emulator thread at each vblank, so how long a press lasts doesn't depend on how fast the -// emulator runs. Guarded by ctrlMutex; not saved. +// emulator runs. Requests arrive in an inbox; the list belongs to the emulation thread. Not saved. struct TimedPress { int id; u32 buttons; int samplesLeft; }; static std::vector timedPresses; -static int nextTimedPressId = 1; +static MpscQueue newTimedPresses; +static retro_atomic_int_t nextTimedPressId{ 1 }; int __CtrlPressFor(u32 buttons, int vblanks) { - std::lock_guard guard(ctrlMutex); buttons &= CTRL_MASK_USER; - ctrlCurrent.buttons |= buttons; - const int id = nextTimedPressId++; - timedPresses.push_back(TimedPress{ id, buttons, std::max(vblanks, 1) }); + const int id = retro_atomic_fetch_add_int(&nextTimedPressId, 1); + newTimedPresses.Push(TimedPress{ id, buttons, std::max(vblanks, 1) }); return id; } +// Emulation thread. Takes new presses, which hold their buttons from the next sample. +static void __CtrlTakeTimedPresses() { + newTimedPresses.Drain([](TimedPress &&press) { + UpdateInputButtons(press.buttons, 0); + timedPresses.push_back(press); + }); +} + bool __CtrlPressActive(int id) { - std::lock_guard guard(ctrlMutex); + __CtrlTakeTimedPresses(); for (const TimedPress &press : timedPresses) { if (press.id == id) { return true; @@ -239,7 +281,6 @@ bool __CtrlPressActive(int id) { // After the vblank's sample, so a press of N vblanks is seen by N samples. static void __CtrlUpdateTimedPresses() { - std::lock_guard guard(ctrlMutex); if (timedPresses.empty()) { return; } @@ -256,7 +297,7 @@ static void __CtrlUpdateTimedPresses() { for (const TimedPress &press : timedPresses) { released &= ~press.buttons; } - ctrlCurrent.buttons &= ~released; + UpdateInputButtons(0, released); } void __CtrlUpdateButtons(u32 bitsToSet, u32 bitsToClear) @@ -264,9 +305,7 @@ void __CtrlUpdateButtons(u32 bitsToSet, u32 bitsToClear) bitsToClear &= CTRL_MASK_USER; bitsToSet &= CTRL_MASK_USER; - std::lock_guard guard(ctrlMutex); - // There's no atomic operation for this, so mutex it is. - ctrlCurrent.buttons = (ctrlCurrent.buttons & ~bitsToClear) | bitsToSet; + UpdateInputButtons(bitsToSet, bitsToClear); } void __CtrlSetAnalogXY(int stick, float x, float y) @@ -275,22 +314,17 @@ void __CtrlSetAnalogXY(int stick, float x, float y) // TODO: We might have too many negations of Y... u8 scaledY = clamp_u8((int)ceilf(-y * 127.5f + 127.5f)); - std::lock_guard guard(ctrlMutex); - ctrlCurrent.analog[stick][CTRL_ANALOG_X] = scaledX; - ctrlCurrent.analog[stick][CTRL_ANALOG_Y] = scaledY; + SetInputAnalog(stick, 3, scaledX, scaledY); } -// not making XY to use these due to mutex guard usage void __CtrlSetAnalogX(int stick, float x) { u8 scaledX = clamp_u8((int)ceilf(x * 127.5f + 127.5f)); - std::lock_guard guard(ctrlMutex); - ctrlCurrent.analog[stick][CTRL_ANALOG_X] = scaledX; + SetInputAnalog(stick, 1, scaledX, 0); } void __CtrlSetAnalogY(int stick, float y) { u8 scaledY = clamp_u8((int)ceilf(-y * 127.5f + 127.5f)); - std::lock_guard guard(ctrlMutex); - ctrlCurrent.analog[stick][CTRL_ANALOG_Y] = scaledY; + SetInputAnalog(stick, 2, 0, scaledY); } void __CtrlSetRapidFire(bool state, int interval) { @@ -417,8 +451,6 @@ void __CtrlInit() { ctrlIdleBack = -1; ctrlCycle = 0; - std::lock_guard guard(ctrlMutex); - ctrlBuf = 1; ctrlBufRead = 0; ctrlOldButtons = 0; @@ -431,6 +463,8 @@ void __CtrlInit() { memset(&ctrlCurrent, 0, sizeof(ctrlCurrent)); memset(ctrlCurrent.analog, CTRL_ANALOG_CENTER, sizeof(ctrlCurrent.analog)); + retro_atomic_store_release_int(&inputButtons, 0); + retro_atomic_store_release_int(&inputAnalog, (int)0x80808080u); analogEnabled = false; for (u32 i = 0; i < NUM_CTRL_BUFFERS; i++) @@ -439,8 +473,6 @@ void __CtrlInit() { void __CtrlDoState(PointerWrap &p) { - std::lock_guard guard(ctrlMutex); - auto s = p.Section("sceCtrl", 1, 3); if (!s) return; @@ -478,7 +510,7 @@ void __CtrlDoState(PointerWrap &p) void __CtrlShutdown() { waitingThreads.clear(); - std::lock_guard guard(ctrlMutex); + newTimedPresses.Drain([](TimedPress &&) {}); timedPresses.clear(); } diff --git a/Core/HLE/sceDisplay.cpp b/Core/HLE/sceDisplay.cpp index 8a062c3abd..d6e9065add 100644 --- a/Core/HLE/sceDisplay.cpp +++ b/Core/HLE/sceDisplay.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include // TODO: Move this somewhere else, cleanup. diff --git a/Core/HLE/sceGe.cpp b/Core/HLE/sceGe.cpp index 67d8dca6f7..18d73728ea 100644 --- a/Core/HLE/sceGe.cpp +++ b/Core/HLE/sceGe.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" diff --git a/Core/HLE/sceHttp.cpp b/Core/HLE/sceHttp.cpp index 9d15fa91da..0ad2cd3d85 100644 --- a/Core/HLE/sceHttp.cpp +++ b/Core/HLE/sceHttp.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include // for std::tolower @@ -36,7 +35,7 @@ static std::vector> httpObjects; -static std::mutex httpLock; +// HLE calls only, on the emulation thread. bool httpInited = false; bool httpsInited = false; @@ -295,7 +294,6 @@ void __HttpInit() { } void __HttpShutdown() { - std::lock_guard guard(httpLock); httpInited = false; httpsInited = false; httpCacheInited = false; @@ -325,7 +323,6 @@ static int sceHttpInit(int poolSize) { if (httpInited) return hleLogError(Log::sceNet, SCE_HTTP_ERROR_ALREADY_INITED, "http already inited"); - std::lock_guard guard(httpLock); httpObjects.clear(); // Reserve at least 1 element to prevent ::begin() from returning null when no element has been added yet httpObjects.reserve(1); @@ -335,7 +332,6 @@ static int sceHttpInit(int poolSize) { static int sceHttpEnd() { WARN_LOG(Log::sceNet, "UNTESTED sceHttpEnd()"); - std::lock_guard guard(httpLock); httpObjects.clear(); httpInited = false; return hleLogDebug(Log::sceNet, 0); @@ -432,7 +428,6 @@ static int sceHttpSendRequest(int requestID, u32 dataPtr, u32 dataSize) { } static int sceHttpDeleteRequest(int requestID) { - std::lock_guard guard(httpLock); if (requestID <= 0 || requestID > (int)httpObjects.size()) return hleLogError(Log::sceNet, SCE_HTTP_ERROR_INVALID_ID, "invalid id"); @@ -453,7 +448,6 @@ static int sceHttpDeleteHeader(int id, const char *name) { } static int sceHttpDeleteConnection(int connectionID) { - std::lock_guard guard(httpLock); if (connectionID <= 0 || connectionID > (int)httpObjects.size()) return hleLogError(Log::sceNet, SCE_HTTP_ERROR_INVALID_ID, "invalid id"); @@ -531,7 +525,6 @@ static int sceHttpsDisableOption(int id) { // Parameter "method" should be one of PSPHttpMethod's listed entries static int sceHttpCreateRequest(int connectionID, int method, const char *path, u64 contentLength) { WARN_LOG(Log::sceNet, "UNTESTED sceHttpCreateRequest(%d, %d, %s, %llx)", connectionID, method, safe_string(path), contentLength); - std::lock_guard guard(httpLock); if (connectionID <= 0 || connectionID > (int)httpObjects.size()) return hleLogError(Log::sceNet, SCE_HTTP_ERROR_INVALID_ID, "invalid id"); @@ -549,7 +542,6 @@ static int sceHttpCreateRequest(int connectionID, int method, const char *path, // FIXME: port type is probably u16 (but passed in a single register anyway, so type doesn't matter) static int sceHttpCreateConnection(int templateID, const char *hostString, const char *scheme, u32 port, int enableKeepalive) { WARN_LOG(Log::sceNet, "UNTESTED sceHttpCreateConnection(%d, %s, %s, %d, %d)", templateID, safe_string(hostString), safe_string(scheme), port, enableKeepalive); - std::lock_guard guard(httpLock); if (templateID <= 0 || templateID > (int)httpObjects.size()) return hleLogError(Log::sceNet, SCE_HTTP_ERROR_INVALID_ID, "invalid id"); @@ -594,7 +586,6 @@ static int sceHttpAbortRequest(int requestID) { static int sceHttpDeleteTemplate(int templateID) { WARN_LOG(Log::sceNet, "UNTESTED sceHttpDeleteTemplate(%d)", templateID); - std::lock_guard guard(httpLock); if (templateID <= 0 || templateID > (int)httpObjects.size()) return hleLogError(Log::sceNet, SCE_HTTP_ERROR_INVALID_ID, "invalid id"); @@ -678,7 +669,6 @@ static int sceHttpCreateTemplate(const char *userAgent, int httpVer, int autoPro WARN_LOG(Log::sceNet, "UNTESTED sceHttpCreateTemplate(%s, %d, %d) at %08x", safe_string(userAgent), httpVer, autoProxyConf, currentMIPS->pc); // Reporting to find more games to be tested WARN_LOG_REPORT_ONCE(sceHttpCreateTemplate, Log::sceNet, "UNTESTED sceHttpCreateTemplate(%s, %d, %d)", safe_string(userAgent), httpVer, autoProxyConf); - std::lock_guard guard(httpLock); httpObjects.push_back(std::make_shared(userAgent? userAgent:"", httpVer, autoProxyConf)); int retid = (int)httpObjects.size(); return hleLogDebug(Log::sceNet, retid); @@ -687,7 +677,6 @@ static int sceHttpCreateTemplate(const char *userAgent, int httpVer, int autoPro // Parameter "method" should be one of PSPHttpMethod's listed entries static int sceHttpCreateRequestWithURL(int connectionID, int method, const char *url, u64 contentLength) { WARN_LOG(Log::sceNet, "UNTESTED sceHttpCreateRequestWithURL(%d, %d, %s, %llx)", connectionID, method, safe_string(url), contentLength); - std::lock_guard guard(httpLock); if (connectionID <= 0 || connectionID > (int)httpObjects.size()) return hleLogError(Log::sceNet, SCE_HTTP_ERROR_INVALID_ID, "invalid id"); @@ -708,7 +697,6 @@ static int sceHttpCreateRequestWithURL(int connectionID, int method, const char static int sceHttpCreateConnectionWithURL(int templateID, const char *url, int enableKeepalive) { WARN_LOG(Log::sceNet, "UNTESTED sceHttpCreateConnectionWithURL(%d, %s, %d)", templateID, safe_string(url), enableKeepalive); - std::lock_guard guard(httpLock); if (templateID <= 0 || templateID > (int)httpObjects.size()) return hleLogError(Log::sceNet, SCE_HTTP_ERROR_INVALID_ID, "invalid id"); diff --git a/Core/HLE/sceIo.cpp b/Core/HLE/sceIo.cpp index 4c473dff78..49b87e4aa4 100644 --- a/Core/HLE/sceIo.cpp +++ b/Core/HLE/sceIo.cpp @@ -18,10 +18,8 @@ #include // std::remove #include #include -#include #include -#include "Common/Thread/ThreadUtil.h" #include "Common/Profiler/Profiler.h" #include "Common/TimeUtil.h" @@ -138,8 +136,8 @@ static MemStickState lastMemStickState; static MemStickFatState lastMemStickFatState; static AsyncIOManager ioManager; -static bool ioManagerThreadEnabled = false; -static std::thread ioManagerThread; +// Between __IoInit and __IoShutdown, large reads and writes complete through ioManager. +static bool ioManagerEnabled = false; // TODO: Is it better to just put all on the thread? // Let's try. (was 256) @@ -584,23 +582,6 @@ static void __IoAsyncEndCallback(SceUID threadID, SceUID prevCallbackId) { } } -static void __IoManagerThread() { - SetCurrentThreadName("IO"); - INFO_LOG(Log::sceIo, "Entering __IoManagerThread"); - while (ioManagerThreadEnabled) { - ioManager.RunEventsUntil(CoreTiming::GetTicks(currentMIPS) + msToCycles(1000)); - } - INFO_LOG(Log::sceIo, "Leaving __IoManagerThread"); -} - -static void __IoWakeManager(CoreLifecycle stage) { - // Ping the thread so that it knows to check coreState. - if (stage == CoreLifecycle::STOPPING) { - ioManagerThreadEnabled = false; - ioManager.FinishEventLoop(); - } -} - void __IoVblank() { // We update memstick status here just to avoid possible thread safety issues. // It doesn't actually need to be on a vblank. @@ -656,10 +637,7 @@ void __IoInit() { memset(fds, 0, sizeof(fds)); - ioManagerThreadEnabled = true; - ioManager.SetThreadEnabled(true); - Core_ListenLifecycle(&__IoWakeManager); - ioManagerThread = std::thread(&__IoManagerThread); + ioManagerEnabled = true; __KernelRegisterWaitTypeFuncs(WAITTYPE_ASYNCIO, __IoAsyncBeginCallback, __IoAsyncEndCallback); @@ -668,18 +646,9 @@ void __IoInit() { lastMemStickFatState = MemoryStick_FatState(); } -void __IoWaitForAsync() { - ioManager.SyncThread(); -} - void __IoShutdown() { - ioManagerThreadEnabled = false; - ioManager.SyncThread(); - ioManager.FinishEventLoop(); - if (ioManagerThread.joinable()) { - ioManagerThread.join(); - ioManager.Shutdown(); - } + ioManagerEnabled = false; + ioManager.Shutdown(); for (int i = 0; i < PSP_COUNT_FDS; ++i) { asyncParams[i].op = IoAsyncOp::NONE; @@ -1116,13 +1085,10 @@ static bool __IoRead(int &result, int id, u32 data_addr, int size, int &us) { return true; } - bool useThread = __KernelIsDispatchEnabled() && ioManagerThreadEnabled && size > IO_THREAD_MIN_DATA_SIZE; + bool useThread = __KernelIsDispatchEnabled() && ioManagerEnabled && size > IO_THREAD_MIN_DATA_SIZE; if (useThread) { // If there's a pending operation on this file, wait for it to finish and don't overwrite it. useThread = !ioManager.HasOperation(f->handle); - if (!useThread) { - ioManager.SyncThread(); - } } if (useThread) { AsyncIOEvent ev = IO_EVENT_READ; @@ -1275,13 +1241,10 @@ static bool __IoWrite(int &result, int id, u32 data_addr, int size, int &us) { return true; } - bool useThread = __KernelIsDispatchEnabled() && ioManagerThreadEnabled && size > IO_THREAD_MIN_DATA_SIZE; + bool useThread = __KernelIsDispatchEnabled() && ioManagerEnabled && size > IO_THREAD_MIN_DATA_SIZE; if (useThread) { // If there's a pending operation on this file, wait for it to finish and don't overwrite it. useThread = !ioManager.HasOperation(f->handle); - if (!useThread) { - ioManager.SyncThread(); - } } if (useThread) { AsyncIOEvent ev = IO_EVENT_WRITE; @@ -1434,11 +1397,6 @@ static s64 __IoLseekDest(FileNode *f, s64 offset, int whence, FileMove &seek) { PROFILE_THIS_SCOPE("io_rw"); seek = FILEMOVE_BEGIN; - // Let's make sure this isn't incorrect mid-operation. - if (ioManager.HasOperation(f->handle)) { - ioManager.SyncThread(); - } - s64 newPos = 0; switch (whence) { case 0: diff --git a/Core/HLE/sceIo.h b/Core/HLE/sceIo.h index 422a0ea4fd..e24b37d85a 100644 --- a/Core/HLE/sceIo.h +++ b/Core/HLE/sceIo.h @@ -24,7 +24,6 @@ void __IoInit(); void __IoDoState(PointerWrap &p); void __IoShutdown(); // Waits for async reads and writes still accessing PSP memory on the IO thread. -void __IoWaitForAsync(); void __IoVblank(); struct ScePspDateTime; diff --git a/Core/HLE/sceKernelThread.cpp b/Core/HLE/sceKernelThread.cpp index f9e37c7a69..20223c3228 100644 --- a/Core/HLE/sceKernelThread.cpp +++ b/Core/HLE/sceKernelThread.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include "Common/CommonTypes.h" @@ -531,8 +530,6 @@ static std::vector pendingDeleteThreads; // Lists all thread ids that aren't deleted/etc. static std::vector threadqueue; -// Only for debugger, so not needed to read, just write. -static std::mutex threadqueueLock; // Lists only ready thread ids. static ThreadQueueList threadReadyQueue; @@ -1098,8 +1095,6 @@ void __KernelIdle() } void __KernelThreadingShutdown() { - std::lock_guard guard(threadqueueLock); - kernelMemory.Free(threadReturnHackAddr); threadqueue.clear(); threadReadyQueue.clear(); @@ -1608,8 +1603,6 @@ void __KernelCancelThreadEndTimeout(SceUID threadID) } static void __KernelRemoveFromThreadQueue(SceUID threadID) { - std::lock_guard guard(threadqueueLock); - int prio = __KernelGetThreadPrio(threadID); if (prio != 0) threadReadyQueue.remove(prio, threadID); @@ -1993,8 +1986,6 @@ void __KernelResetThread(PSPThread *t, int lowestPriority) { } PSPThread *__KernelCreateThreadObject(SceUID &id, SceUID moduleId, const char *name, u32 entryPoint, u32 priority, int stacksize, u32 attr, BlockAllocator *stackAllocator) { - std::lock_guard guard(threadqueueLock); - PSPThread *t = new PSPThread(); id = kernelObjects.Create(t); @@ -3886,7 +3877,6 @@ void __KernelRegisterWaitTypeFuncs(WaitType type, WaitBeginCallbackFunc beginFun } std::vector GetThreadsInfo() { - std::lock_guard guard(threadqueueLock); std::vector threadList; u32 error; diff --git a/Core/HLE/sceNet.cpp b/Core/HLE/sceNet.cpp index ae18d981a4..9c27823eb9 100644 --- a/Core/HLE/sceNet.cpp +++ b/Core/HLE/sceNet.cpp @@ -15,7 +15,6 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include #include #include @@ -87,7 +86,6 @@ static u32_le apctlThreadCode[3]; static SceUID apctlThreadID = 0; static int apctlStateEvent = -1; static int actionAfterApctlMipsCall; -static std::recursive_mutex apctlEvtMtx; static std::deque apctlEvents; // Currently loaded auto-config @@ -694,7 +692,6 @@ void __NetShutdown() { } static void __UpdateApctlHandlers(u32 oldState, u32 newState, u32 flag, u32 error) { - std::lock_guard apctlGuard(apctlEvtMtx); apctlEvents.push_back({ oldState, newState, flag, error }); } @@ -768,12 +765,10 @@ void __NetDoState(PointerWrap &p) { if (s >= 7) { // The state only moves on when an event is processed, and each queues the next, so a // connect in progress would never finish without them. - std::lock_guard apctlGuard(apctlEvtMtx); Do(p, apctlEvents); // Allocated from user memory, which the load just replaced. Do(p, apctlProdCodeAddr); } else if (p.mode == p.MODE_READ) { - std::lock_guard apctlGuard(apctlEvtMtx); apctlEvents.clear(); apctlProdCodeAddr = 0; } @@ -793,9 +788,6 @@ void __NetDoState(PointerWrap &p) { void __NetApctlCallbacks() { - std::lock_guard apctlGuard(apctlEvtMtx); - std::lock_guard npAuthGuard(npAuthEvtMtx); - std::lock_guard npMatching2Guard(npMatching2EvtMtx); hleSkipDeadbeef(); int delayus = 10000; diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 93cc3c73e7..ae6c64a796 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -20,7 +20,7 @@ // All credit goes to him! -#include +#include #include #include "Common/Net/SocketCompat.h" @@ -61,6 +61,8 @@ #include "Common/Net/Cancel.h" #include "ext/aemu_postoffice_client/postoffice_client.h" +#include "Common/Thread/Thread.h" +#include "Common/Thread/MpscQueue.h" #ifdef _WIN32 #undef errno @@ -96,7 +98,12 @@ std::chrono::time_point relayLastFailure; bool trackingRelayFailure = false; bool relayDisabled = false; bool relayFirstConnect = true; -bool g_serverListLoaded = false; +// Set by whichever thread loaded the list (the download callback can run elsewhere). +static retro_atomic_int_t g_serverListLoadedFlag{ 0 }; + +static void MarkServerListLoaded() { + retro_atomic_store_release_int(&g_serverListLoadedFlag, 1); +} int gameModeNotifyEvent = -1; @@ -119,8 +126,23 @@ static const char *AdhocDataModeToString(AdhocDataMode mode) { // We download the list and cache it on disk. // The URL can also be a local file path, in which case the download doesn't happen. This is only // for power users / debugging. -std::mutex g_proAdhocServerListMutex; -std::vector g_proAdhocServerList; +// The current list, replaced whole by whoever parses a new one. A replaced list is kept until exit +// instead of freed, since readers copy it without any grace period and it changes a few times +// per session at most. +typedef std::vector AdhocServerList; +static retro_atomic_ptr_t g_proAdhocServerList{}; +struct RetiredServerLists { + ~RetiredServerLists() { + lists.Drain([](AdhocServerList *&&list) { delete list; }); + delete (AdhocServerList *)retro_atomic_exchange_ptr(&g_proAdhocServerList, nullptr); + } + MpscQueue lists; +}; +static RetiredServerLists g_retiredServerLists; + +static const AdhocServerList *CurrentServerList() { + return (const AdhocServerList *)retro_atomic_load_acquire_ptr(&g_proAdhocServerList); +} // TODO: Should convert this to use rapidjson static bool ParseServerListEntriesJSON(std::string_view json) { @@ -171,9 +193,9 @@ static bool ParseServerListEntriesJSON(std::string_view json) { newList.push_back(entry); } - { - std::lock_guard guard(g_proAdhocServerListMutex); - g_proAdhocServerList = newList; + AdhocServerList *old = (AdhocServerList *)retro_atomic_exchange_ptr(&g_proAdhocServerList, (void *)new AdhocServerList(std::move(newList))); + if (old) { + g_retiredServerLists.lists.Push(old); } System_PostUIMessage(UIMessage::ADHOC_SERVER_LIST_CHANGED); return true; @@ -190,13 +212,13 @@ static void LoadFallbackServerList() { return; } ParseServerListEntriesJSON(std::string_view((char*)jsonStr.get(), jsonSize)); - g_serverListLoaded = true; + MarkServerListLoaded(); } void AdhocLoadServerList(AdhocLoadListMode loadMode) { if (loadMode == AdhocLoadListMode::CacheOnlySync) { - std::lock_guard guard(g_proAdhocServerListMutex); - if (!g_proAdhocServerList.empty()) { + const AdhocServerList *list = CurrentServerList(); + if (list && !list->empty()) { return; } } @@ -209,7 +231,7 @@ void AdhocLoadServerList(AdhocLoadListMode loadMode) { std::string json; if (g_DownloadManager.ReadFileFromCache(g_Config.sAdhocServerListUrl, &json)) { if (ParseServerListEntriesJSON(std::string_view(json.data(), json.size()))) { - g_serverListLoaded = true; + MarkServerListLoaded(); return; } } @@ -235,7 +257,7 @@ void AdhocLoadServerList(AdhocLoadListMode loadMode) { return; } }); - g_serverListLoaded = true; + MarkServerListLoaded(); } else if (!g_Config.sAdhocServerListUrl.empty()) { // Try to read local file. std::string json; @@ -254,14 +276,14 @@ void AdhocLoadServerList(AdhocLoadListMode loadMode) { } std::vector AdhocGetServerList(AdhocLoadListMode loadMode) { - if (!g_serverListLoaded) { + if (!retro_atomic_load_acquire_int(&g_serverListLoadedFlag)) { // We're probably in-game - so we don't want to do a download and risk blocking. // Instead we read out of cache, it should be good enough. AdhocLoadServerList(loadMode); } - std::lock_guard guard(g_proAdhocServerListMutex); - return g_proAdhocServerList; + const AdhocServerList *list = CurrentServerList(); + return list ? *list : AdhocServerList(); } bool AdhocGetServerByHost(std::string_view host, AdhocServerListEntry *dest) { @@ -326,7 +348,7 @@ bool __NetAdhocConnected() { void __NetAdhocShutdown() { // Kill AdhocServer Thread - adhocServerRunning = false; + retro_atomic_store_release_int(&adhocServerRunning, 0); AdhocServerWake(); if (adhocServerThread.joinable()) { adhocServerThread.join(); @@ -465,13 +487,11 @@ static void __GameModeNotify(u64 userdata, int cyclesLate) { if (senderport != ADHOC_GAMEMODE_PORT && senderport != gameModePeerPorts[sendermac]) { char name[9] = {}; auto n = GetI18NCategory(I18NCat::NETWORKING); - peerlock.lock(); SceNetAdhocctlPeerInfo* peer = findFriend(&sendermac); if (peer != NULL) truncate_cpy(name, sizeof(name), (const char*)peer->nickname.data); WARN_LOG(Log::sceNet, "GameMode: Unknown Source Port from [%s][%s:%u -> %u] (Result=%i, Size=%i)", name, mac2str(&sendermac).c_str(), senderport, ADHOC_GAMEMODE_PORT, ret, bufsz); g_OSD.Show(OSDType::MESSAGE_WARNING, std::string(n->T("GM: Data from Unknown Port")) + std::string(" [") + std::string(name) + std::string("]:") + std::to_string(senderport) + std::string(" -> ") + std::to_string(ADHOC_GAMEMODE_PORT) + std::string(" (") + std::to_string(portOffset) + std::string(")"), 0.0f, "unknownport"); - peerlock.unlock(); } // Keeping track of the source port for further communication, in case it was re-mapped by router or ISP for some reason. gameModePeerPorts[sendermac] = senderport; @@ -502,6 +522,7 @@ static void __GameModeNotify(u64 userdata, int cyclesLate) { } static void __AdhocctlNotify(u64 userdata, int cyclesLate) { + FriendFinderProcess(); SceUID threadID = userdata >> 32; int uid = (int)(userdata & 0xFFFFFFFF); @@ -549,8 +570,8 @@ static void __AdhocctlNotify(u64 userdata, int cyclesLate) { ret = SOCKET_ERROR; sockerr = EAGAIN; // Don't send anything yet if connection to Adhoc Server is still in progress - if (!isAdhocctlNeedLogin && IsSocketReady((int)metasocket, false, true) > 0) { - ret = send((int)metasocket, (const char*)&packet, len, MSG_NOSIGNAL); + if (!isAdhocctlNeedLogin && IsSocketReady(MetaSocket(), false, true) > 0) { + ret = send(MetaSocket(), (const char*)&packet, len, MSG_NOSIGNAL); sockerr = socket_errno; // Successfully Sent or Connection has been closed or Connection failure occurred if (ret >= 0 || (ret == SOCKET_ERROR && sockerr != EAGAIN && sockerr != EWOULDBLOCK)) { @@ -617,7 +638,7 @@ static void __AdhocctlState(u64 userdata, int cyclesLate) { // Used to simulate blocking on metasocket when send OP code to AdhocServer int WaitBlockingAdhocctlSocket(AdhocctlRequest request, int usec, const char* reason) { - int uid = (metasocket <= 0) ? 1 : (int)metasocket; + int uid = (MetaSocket() <= 0) ? 1 : MetaSocket(); if (adhocctlRequests.find(uid) != adhocctlRequests.end()) { WARN_LOG(Log::sceNet, "sceNetAdhocctl - WaitID[%d] already existed, Socket is busy!", uid); @@ -876,10 +897,8 @@ int DoBlockingPdpRecv(AdhocSocketRequest& req, s64& result) { *req.length = ret; // Update last recv timestamp - peerlock.lock(); auto peer = findFriend(&mac); if (peer != NULL) peer->last_recv = CoreTiming::GetGlobalTimeUsScaled(); - peerlock.unlock(); } // Unknown Peer else { @@ -913,10 +932,8 @@ int DoBlockingPdpRecv(AdhocSocketRequest& req, s64& result) { *req.remotePort = ntohs(sin.sin_port) - portOffset; // FIXME: Do we need to update last recv timestamp? eventhough data hasn't been retrieved yet (ie. peeked) - peerlock.lock(); auto peer = findFriend(&mac); if (peer != NULL) peer->last_recv = CoreTiming::GetGlobalTimeUsScaled(); - peerlock.unlock(); } result = SCE_NET_ADHOC_ERROR_NOT_ENOUGH_SPACE; } @@ -1211,10 +1228,8 @@ int DoBlockingPtpRecv(AdhocSocketRequest& req, s64& result) { *req.length = ret; // Update last recv timestamp - peerlock.lock(); auto peer = findFriend(&ptpsocket.paddr); if (peer != NULL) peer->last_recv = CoreTiming::GetGlobalTimeUsScaled(); - peerlock.unlock(); // Set to Established on successful Recv when an attempt to Connect was initiated if (ptpsocket.state == ADHOC_PTP_STATE_SYN_SENT) @@ -1323,6 +1338,7 @@ static int ptp_accept_postoffice(int idx, SceNetEtherAddr *saddr, uint16_t *spor internal->flags = 0; internal->connectThread = NULL; internal->connectCancel = NULL; + internal->connectState = NULL; AdhocSocket **slot = NULL; int i; @@ -1425,38 +1441,39 @@ static int ptp_connect_postoffice(int idx, const char *caller) { addr.addr = g_adhocServerIP.in.sin_addr.s_addr; addr.port = htons(AEMU_POSTOFFICE_PORT); + const bool finished = PostofficeConnectFinished(internal); if (internal->postofficeHandle != NULL) { return 0; } - if (internal->connectThreadDone) { + if (finished) { // The last attempt is over, so this join returns at once. JoinPostofficeConnect(internal, false); - internal->connectThreadDone = false; - internal->connectThreadResult = 0; - internal->connectCancel = new net::CancelToken(); const int cancelFd = (int)internal->connectCancel->WakeFd(); - internal->connectThread = new std::thread([internal, addr, idx, cancelFd] { + PostofficeConnectState *st = new PostofficeConnectState(); + internal->connectState = st; + // The thread only reads the socket's addresses, which don't change while it connects. + internal->connectThread = new Thread([internal, st, addr, idx, cancelFd] { int state; SceNetEtherAddr fixed_daddr = internal->data.ptp.paddr; fixGameMac(&fixed_daddr); void *ptp_socket = ptp_connect_v4(&addr, (const char *)&internal->data.ptp.laddr, offset_port_simple(internal->data.ptp.lport), (const char *)&fixed_daddr, offset_port_simple(internal->data.ptp.pport), &state, cancelFd); if (ptp_socket == NULL) { - internal->connectThreadResult = SCE_NET_ADHOC_ERROR_CONNECTION_REFUSED; + st->result = SCE_NET_ADHOC_ERROR_CONNECTION_REFUSED; ERROR_LOG(Log::sceNet, "%s: failed connecting to ptp socket, %d", __func__, state); // do not count ptp connect failure as relay failure, since it could be the other client not accepting the connection //handle_relay_connect_failure(); - internal->connectThreadDone = true; + retro_atomic_store_release_int(&st->done, 1); return; } // see above, ptp connect result is not used for checking if relay is working //handle_relay_connect_success(); - internal->postofficeHandle = ptp_socket; - internal->connectThreadResult = 0; - INFO_LOG(Log::sceNet, "%s: connected ptp socket with id %d %p", __func__, idx + 1, internal->postofficeHandle); - internal->connectThreadDone = true; + st->handle = ptp_socket; + st->result = 0; + INFO_LOG(Log::sceNet, "%s: connected ptp socket with id %d %p", __func__, idx + 1, ptp_socket); + retro_atomic_store_release_int(&st->done, 1); return; });; @@ -1508,9 +1525,11 @@ int DoBlockingPtpConnect(AdhocSocketRequest& req, s64& result, AdhocSendTargets& // Note: On Linux "select" can return > 0 (with SO_ERROR = 0) even when the connection is not accepted yet, thus need "getpeername" to ensure else { if (serverHasRelay) { + int connectResult = 0; + const bool connectFinished = PostofficeConnectFinished(sock, &connectResult); if (sock->postofficeHandle == NULL) { ret = SOCKET_ERROR; - if (sock->connectThreadDone && sock->connectThreadResult == SCE_NET_ADHOC_ERROR_CONNECTION_REFUSED){ + if (connectFinished && connectResult == SCE_NET_ADHOC_ERROR_CONNECTION_REFUSED){ sockerr = ECONNREFUSED; } else { sockerr = EAGAIN; @@ -1885,7 +1904,6 @@ void __NetAdhocDoState(PointerWrap &p) { if (p.mode == p.MODE_READ) { // Discard leftover events { - std::lock_guard adhocGuard(adhocEvtMtx); adhocctlEvents.clear(); } adhocSocketRequests.clear(); @@ -1904,7 +1922,6 @@ void __NetAdhocDoState(PointerWrap &p) { } void __UpdateAdhocctlHandlers(u32 flag, u32 error) { - std::lock_guard adhocGuard(adhocEvtMtx); adhocctlEvents.push_back({ flag, error }); } @@ -1920,7 +1937,7 @@ void __AdhocNotifInit() { } void __NetAdhocInit() { - friendFinderRunning = false; + retro_atomic_store_release_int(&friendFinderRunning, 0); netAdhocInited = false; netAdhocctlInited = false; adhocctlHandlers.clear(); @@ -1929,15 +1946,15 @@ void __NetAdhocInit() { // Create built-in AdhocServer Thread. The flag is set here rather than by the thread, so that a // shutdown that clears it before the thread gets going can't be undone (see friendFinder). - adhocServerRunning = false; + retro_atomic_store_release_int(&adhocServerRunning, 0); AdhocServerWake(); if (adhocServerThread.joinable()) { adhocServerThread.join(); } if (g_Config.bEnableWlan && g_Config.bEnableAdhocServer) { AdhocServerPrepare(); - adhocServerRunning = true; - adhocServerThread = std::thread(proAdhocServerThread, SERVER_PORT); + retro_atomic_store_release_int(&adhocServerRunning, 1); + adhocServerThread = Thread(proAdhocServerThread, SERVER_PORT); } } @@ -2022,15 +2039,16 @@ int sceNetAdhocctlInit(int stackSize, int prio, u32 productAddr) { } // TODO: Merging friendFinder (real) thread to AdhocThread (fake) thread on PSP side - if (!friendFinderRunning) { + if (!FriendFinderIsRunning()) { // Set before the thread starts, not by it: shutting down before it got going used to clear // the flag first, and then the thread set it again and never stopped, hanging the join. if (friendFinderThread.joinable()) { friendFinderThread.join(); + FriendFinderStopped(); } FriendFinderPrepare(); - friendFinderRunning = true; - friendFinderThread = std::thread(friendFinder); + retro_atomic_store_release_int(&friendFinderRunning, 1); + friendFinderThread = Thread(friendFinder); } // Need to make sure to be connected to Adhoc Server (indicated by networkInited) before returning to prevent GTA VCS failed to create/join a group and unable to see any game room @@ -2039,7 +2057,7 @@ int sceNetAdhocctlInit(int stackSize, int prio, u32 productAddr) { AdhocctlRequest dummyreq = { OPCODE_LOGIN, {0} }; return hleLogDebugOrWarn(Log::sceNet, WaitBlockingAdhocctlSocket(dummyreq, us, "adhocctl init")); } - // Give a little time for friendFinder thread to be ready before the game use the next sceNet functions, should've checked for friendFinderRunning status instead of guessing the time? + // Give a little time for friendFinder thread to be ready before the game use the next sceNet functions, should've checked for FriendFinderIsRunning() status instead of guessing the time? hleEatMicro(us); return hleLogDebug(Log::sceNet, 0); @@ -2458,7 +2476,6 @@ int sceNetAdhocPdpSend(int id, const char *mac, u32 port, void *data, int len, i #endif // Acquire Peer Lock - peerlock.lock(); AdhocSendTargets dest = { len, {}, true }; // Iterate Peers SceNetAdhocctlPeerInfo* peer = friends; @@ -2470,7 +2487,6 @@ int sceNetAdhocPdpSend(int id, const char *mac, u32 port, void *data, int len, i dest.peers.push_back({ peer->ip_addr, dport, peer->port_offset, peer->mac_addr }); } // Free Peer Lock - peerlock.unlock(); // Send Data // Simulate blocking behaviour with non-blocking socket @@ -2709,10 +2725,8 @@ int sceNetAdhocPdpRecv(int id, void *addr, void * port, void *buf, void *dataLen *sport = ntohs(sin.sin_port) - portOffset; // Update last recv timestamp, may cause disconnection not detected properly tho - peerlock.lock(); auto peer = findFriend(&mac); if (peer != NULL) peer->last_recv = CoreTiming::GetGlobalTimeUsScaled(); - peerlock.unlock(); return hleLogVerbose(Log::sceNet, SCE_NET_ADHOC_ERROR_NOT_ENOUGH_SPACE, "not enough space"); } @@ -2752,10 +2766,8 @@ int sceNetAdhocPdpRecv(int id, void *addr, void * port, void *buf, void *dataLen *len = received; // Kurok homebrew seems to use the new value of len than returned value as data length // Update last recv timestamp, may cause disconnection not detected properly tho - peerlock.lock(); auto peer = findFriend(&mac); if (peer != NULL) peer->last_recv = CoreTiming::GetGlobalTimeUsScaled(); - peerlock.unlock(); // Free Network Lock //_freeNetworkLock(); @@ -3209,12 +3221,10 @@ int sceNetAdhocctlScan() { adhocctlCurrentMode = ADHOCCTL_MODE_NORMAL; // Reset Networks/Group list to prevent other threads from using these soon to be replaced networks - peerlock.lock(); freeGroupsRecursive(networks); networks = NULL; - peerlock.unlock(); - if (friendFinderRunning) { + if (FriendFinderIsRunning()) { AdhocctlRequest req = { OPCODE_SCAN, {0} }; return hleLogDebugOrError(Log::sceNet, WaitBlockingAdhocctlSocket(req, us, "adhocctl scan")); } @@ -3262,8 +3272,6 @@ int sceNetAdhocctlGetScanInfo(u32 sizeAddr, u32 bufAddr) { // FIXME: Do we need to exclude Groups created by this device it's self? bool excludeSelf = false; - // Multithreading Lock - peerlock.lock(); // FIXME: When already connected to a group GetScanInfo will return size = 0 ? or may be only hides the group created by it's self? if (adhocctlState == ADHOCCTL_STATE_CONNECTED || adhocctlState == ADHOCCTL_STATE_GAMEMODE) { @@ -3325,8 +3333,6 @@ int sceNetAdhocctlGetScanInfo(u32 sizeAddr, u32 bufAddr) { DEBUG_LOG(Log::sceNet, "NetworkList [Requested: %i][Discovered: %i]", requestcount, discovered); } - // Multithreading Unlock - peerlock.unlock(); hleEatMicro(200); // Return Success @@ -3411,7 +3417,7 @@ u32 NetAdhocctl_Disconnect() { //_acquireNetworkLock(); // Send Disconnect Request Packet - iResult = send((int)metasocket, (const char*)&opcode, 1, MSG_NOSIGNAL); + iResult = send(MetaSocket(), (const char*)&opcode, 1, MSG_NOSIGNAL); error = socket_errno; // Sending may get socket error 10053 if the AdhocServer is already shutted down @@ -3421,7 +3427,7 @@ u32 NetAdhocctl_Disconnect() { // Set Disconnected State adhocctlState = ADHOCCTL_STATE_DISCONNECTED; } - else if (friendFinderRunning) { + else if (FriendFinderIsRunning()) { AdhocctlRequest req = { OPCODE_DISCONNECT, {0} }; WaitBlockingAdhocctlSocket(req, 0, "adhocctl disconnect"); } @@ -3435,9 +3441,7 @@ u32 NetAdhocctl_Disconnect() { //_freeNetworkLock(); } - // Multithreading Lock - //peerlock.lock(); - + // // Clear Peer List, since games are moving to a different a group when the mission started may be we shouldn't free all peers yet int32_t peercount = 0; timeoutFriendsRecursive(friends, &peercount); @@ -3451,9 +3455,7 @@ u32 NetAdhocctl_Disconnect() { // Delete Group Reference //networks = NULL; - // Multithreading Unlock - //peerlock.unlock(); - + // adhocctlCurrentMode = ADHOCCTL_MODE_NONE; // Notify Event Handlers (even if we weren't connected, not doing this will freeze games like God Eater, which expect this behaviour) // FIXME: When there are no handler the state will immediately became ADHOCCTL_STATE_DISCONNECTED ? @@ -3499,11 +3501,12 @@ int NetAdhocctl_Term() { } // Terminate Adhoc Threads - friendFinderRunning = false; + retro_atomic_store_release_int(&friendFinderRunning, 0); FriendFinderWake(); if (friendFinderThread.joinable()) { friendFinderThread.join(); } + FriendFinderStopped(); // TODO: May need to block current thread to make sure all Adhocctl callbacks have been fully executed before terminating Adhoc PSPThread (ie. threadAdhocID). @@ -3521,9 +3524,7 @@ int NetAdhocctl_Term() { adhocctlHandlers.clear(); // Free stuff here g_adhocServerConnected = false; - shutdown((int)metasocket, SD_BOTH); - closesocket((int)metasocket); - metasocket = (int)INVALID_SOCKET; + CloseMetaSocket(); // Delete fake PSP Thread. // kernelObjects may already been cleared early during a Shutdown, thus trying to access it may generates Warning/Error in the log if (threadAdhocID > 0 && strcmp(__KernelGetThreadName(threadAdhocID), "ERROR") != 0) { @@ -3575,8 +3576,6 @@ static int sceNetAdhocctlGetNameByAddr(const char *mac, u32 nameAddr) { return 0; } - // Multithreading Lock - peerlock.lock(); // Peer Reference SceNetAdhocctlPeerInfo * peer = friends; @@ -3590,8 +3589,6 @@ static int sceNetAdhocctlGetNameByAddr(const char *mac, u32 nameAddr) { // Write Data *nickname = peer->nickname; - // Multithreading Unlock - peerlock.unlock(); DEBUG_LOG(Log::sceNet, "sceNetAdhocctlGetNameByAddr - [PeerName:%s]", (char*)nickname); @@ -3600,8 +3597,6 @@ static int sceNetAdhocctlGetNameByAddr(const char *mac, u32 nameAddr) { } } - // Multithreading Unlock - peerlock.unlock(); // Player not found return hleLogDebug(Log::sceNet, SCE_NET_ADHOC_ERROR_NO_ENTRY, "PlayerName not found"); @@ -3651,8 +3646,6 @@ int sceNetAdhocctlGetPeerInfo(const char *mac, int size, u32 peerInfoAddr) { // Find Peer by MAC else { - // Multithreading Lock - peerlock.lock(); SceNetAdhocctlPeerInfo * peer = findFriend(maddr); if (peer != NULL && peer->last_recv != 0) { @@ -3671,8 +3664,6 @@ int sceNetAdhocctlGetPeerInfo(const char *mac, int size, u32 peerInfoAddr) { retval = 0; } - // Multithreading Unlock - peerlock.unlock(); } hleEatMicro(50); return hleNoLog(retval); @@ -3714,7 +3705,7 @@ int NetAdhocctl_Create(const char *groupName) { // Wait for Status to be connected to prevent Ford Street Racing from Failed to create game session int us = adhocDefaultDelay; - if (friendFinderRunning) { + if (FriendFinderIsRunning()) { AdhocctlRequest req = { OPCODE_CONNECT, parameter.group_name }; return WaitBlockingAdhocctlSocket(req, us, "adhocctl connect"); } @@ -4336,7 +4327,7 @@ static int ptp_open_postoffice(const SceNetEtherAddr *saddr, uint16_t sport, con internal->flags = 0; internal->connectThread = NULL; internal->connectCancel = NULL; - internal->connectThreadDone = true; + internal->connectState = NULL; internal->lastAttempt = 0; internal->internalLastAttempt = 0; @@ -5029,6 +5020,7 @@ static int ptp_listen_postoffice(const SceNetEtherAddr *saddr, uint16_t sport, u internal->flags = 0; internal->connectThread = NULL; internal->connectCancel = NULL; + internal->connectState = NULL; internal->lastAttempt = 0; internal->internalLastAttempt = 0; @@ -5453,10 +5445,8 @@ static int sceNetAdhocPtpRecv(int id, u32 dataAddr, u32 dataSizeAddr, int timeou *len = received; // Update last recv timestamp, may cause disconnection not detected properly tho - peerlock.lock(); auto peer = findFriend(&ptpsocket.paddr); if (peer != NULL) peer->last_recv = CoreTiming::GetGlobalTimeUsScaled(); - peerlock.unlock(); DEBUG_LOG(Log::sceNet, "sceNetAdhocPtpRecv[%i:%u]: Received %u bytes from %s:%u\n", id, ptpsocket.lport, received, mac2str(&ptpsocket.paddr).c_str(), ptpsocket.pport); @@ -5839,7 +5829,7 @@ int sceNetAdhocGetSocketAlert(int id, u32 flagPtr) { void __NetTriggerCallbacks() { - std::lock_guard adhocGuard(adhocEvtMtx); + FriendFinderProcess(); hleSkipDeadbeef(); int delayus = adhocDefaultDelay; @@ -6023,8 +6013,6 @@ static int sceNetAdhocctlGetPeerList(u32 sizeAddr, u32 bufAddr) { if (buflen != NULL) { // FIXME: Sometimes returing 0x80410682 when Adhocctl is still BUSY or before AdhocctlGetState became ADHOCCTL_STATE_CONNECTED or related to Auth/Library ? - // Multithreading Lock - peerlock.lock(); bool excludeTimedout = true; // Length Calculation Mode @@ -6085,8 +6073,6 @@ static int sceNetAdhocctlGetPeerList(u32 sizeAddr, u32 bufAddr) { DEBUG_LOG(Log::sceNet, "PeerList [Requested: %i][Discovered: %i]", requestcount, discovered); } - // Multithreading Unlock - peerlock.unlock(); // Return Success return hleDelayResult(0, "delay 100 ~ 1000us", 100); // seems to have different thread running within the delay duration @@ -6121,8 +6107,6 @@ static int sceNetAdhocctlGetAddrByName(const char *nickName, u32 sizeAddr, u32 b SceNetAdhocctlPeerInfoEmu *buf = NULL; if (Memory::IsValidAddress(bufAddr)) buf = (SceNetAdhocctlPeerInfoEmu *)Memory::GetPointerOrException(bufAddr); - // Multithreading Lock - peerlock.lock(); // Length Calculation Mode if (buf == NULL) { @@ -6204,8 +6188,6 @@ static int sceNetAdhocctlGetAddrByName(const char *nickName, u32 sizeAddr, u32 b DEBUG_LOG(Log::sceNet, "PeerNameList [%s][Requested: %i][Discovered: %i]", nickName, requestcount, discovered); } - // Multithreading Unlock - peerlock.unlock(); // Return Success return hleDelayResult(hleLogDebug(Log::sceNet, 0, "success"), "delay 100 ~ 1000us", 100); // FIXME: Might have similar delay with GetPeerList? need to know which games using this tho diff --git a/Core/HLE/sceNetAdhocMatching.cpp b/Core/HLE/sceNetAdhocMatching.cpp index 422d81c895..1cf67f1410 100644 --- a/Core/HLE/sceNetAdhocMatching.cpp +++ b/Core/HLE/sceNetAdhocMatching.cpp @@ -74,13 +74,11 @@ int netAdhocMatchingStarted = 0; void __UpdateMatchingHandler(const MatchingArgs &ArgsPtr) { - std::lock_guard adhocGuard(adhocEvtMtx); matchingEvents.push_back(ArgsPtr); } // Matching callback is void function: typedef void(*SceNetAdhocMatchingHandler)(int id, int event, SceNetEtherAddr * peer, int optlen, void * opt); // Important! The MIPS call need to be fully executed before the next MIPS call invoked, as the game (ie. DBZ Tag Team) may need to prepare something for the next callback event to use -// Note: Must not lock peerlock within this function to prevent race-condition with other thread whos owning peerlock and trying to lock context->eventlock owned by this thread void notifyMatchingHandler(SceNetAdhocMatchingContext * context, ThreadMessage * msg, void * opt, u32_le &bufAddr, u32_le &bufLen, u32_le * args) { // Don't share buffer address space with other mipscall in the queue since mipscalls aren't immediately executed MatchingArgs argsNew = { 0 }; @@ -123,7 +121,6 @@ void deleteMatchingEvents(const int matchingId = -1) { // For savestate loads: the events' buffers were allocated from the memory the load just replaced. void discardMatchingEvents() { - std::lock_guard adhocGuard(adhocEvtMtx); matchingEvents.clear(); } @@ -135,8 +132,6 @@ void broadcastPingMessage(SceNetAdhocMatchingContext * context) { // Ping Opcode uint8_t ping = PSP_ADHOC_MATCHING_PACKET_PING; - // Lock the peer - std::lock_guard peer_guard(peerlock); // Send Broadcast // FIXME: Not sure whether this PING supposed to be sent only to AdhocMatching members or to everyone in Adhocctl Group, since we already pinging the AdhocServer to avoid getting kicked out of Adhocctl Group @@ -151,9 +146,7 @@ void broadcastPingMessage(SceNetAdhocMatchingContext * context) { if (it != (*context->peerPort).end()) port = it->second; - context->socketlock->lock(); hleCall(sceNetAdhoc, int, sceNetAdhocPdpSend, context->socket, (const char*)&peer->mac_addr, port, &ping, (u32)sizeof(ping), 0, ADHOC_F_NONBLOCK); - context->socketlock->unlock(); } } @@ -197,7 +190,6 @@ void broadcastHelloMessage(SceNetAdhocMatchingContext * context) { DEBUG_LOG(Log::sceNet, "HELLO Dump (%d bytes):\n%s", context->hellolen, hellohex.c_str()); // Send Broadcast, so everyone know we have a room here - peerlock.lock(); SceNetAdhocctlPeerInfo* peer = friends; for (; peer != NULL; peer = peer->next) { // Skipping soon to be removed peer @@ -209,11 +201,8 @@ void broadcastHelloMessage(SceNetAdhocMatchingContext * context) { if (it != (*context->peerPort).end()) port = it->second; - context->socketlock->lock(); hleCall(sceNetAdhoc, int, sceNetAdhocPdpSend, context->socket, (const char*)&peer->mac_addr, port, hello, 5 + context->hellolen, 0, ADHOC_F_NONBLOCK); - context->socketlock->unlock(); } - peerlock.unlock(); } /** @@ -224,8 +213,6 @@ void broadcastHelloMessage(SceNetAdhocMatchingContext * context) { * @param opt Optional Data */ void sendAcceptPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * mac, int optlen, void * opt) { - // Lock the peer - std::lock_guard peer_guard(peerlock); // Find Peer SceNetAdhocMatchingMemberInternal * peer = findPeer(context, mac); @@ -287,9 +274,7 @@ void sendAcceptPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * ma } // Send Data - context->socketlock->lock(); hleCall(sceNetAdhoc, int, sceNetAdhocPdpSend, context->socket, (const char*)mac, (*context->peerPort)[*mac], accept, 9 + optlen + siblingbuflen, 0, ADHOC_F_NONBLOCK); - context->socketlock->unlock(); // Free Memory free(accept); @@ -306,8 +291,6 @@ void sendAcceptPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * ma * @param opt Optional Data */ void sendJoinPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * mac, int optlen, void * opt) { - // Lock the peer - std::lock_guard peer_guard(peerlock); // Find Peer SceNetAdhocMatchingMemberInternal * peer = findPeer(context, mac); @@ -335,9 +318,7 @@ void sendJoinPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * mac, if (optlen > 0) memcpy(join + 5, opt, optlen); // Send Data - context->socketlock->lock(); hleCall(sceNetAdhoc, int, sceNetAdhocPdpSend, context->socket, (const char*)mac, (*context->peerPort)[*mac], join, 5 + optlen, 0, ADHOC_F_NONBLOCK); - context->socketlock->unlock(); // Free Memory free(join); @@ -351,8 +332,6 @@ void sendJoinPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * mac, * @param opt Optional Data */ void sendCancelPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * mac, int optlen, void * opt) { - // Lock the peer - std::lock_guard peer_guard(peerlock); // Allocate Cancel Message Buffer uint8_t * cancel = (uint8_t *)malloc(5LL + optlen); @@ -369,9 +348,7 @@ void sendCancelPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * ma if (optlen > 0) memcpy(cancel + 5, opt, optlen); // Send Data - context->socketlock->lock(); hleCall(sceNetAdhoc, int, sceNetAdhocPdpSend, context->socket, (const char*)mac, (*context->peerPort)[*mac], cancel, 5 + optlen, 0, ADHOC_F_NONBLOCK); - context->socketlock->unlock(); // Free Memory free(cancel); @@ -406,8 +383,6 @@ void sendCancelPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * ma * @param data Data */ void sendBulkDataPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * mac, int datalen, void * data) { - // Lock the peer - std::lock_guard peer_guard(peerlock); // Find Peer SceNetAdhocMatchingMemberInternal * peer = findPeer(context, mac); @@ -438,9 +413,7 @@ void sendBulkDataPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * memcpy(send + 5, data, datalen); // Send Data - context->socketlock->lock(); hleCall(sceNetAdhoc, int, sceNetAdhocPdpSend, context->socket, (const char*)mac, (*context->peerPort)[*mac], send, 5 + datalen, 0, ADHOC_F_NONBLOCK); - context->socketlock->unlock(); // Free Memory free(send); @@ -458,8 +431,6 @@ void sendBulkDataPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * * @param mac New Child's MAC */ void sendBirthPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * mac) { - // Lock the peer - std::lock_guard peer_guard(peerlock); // Find Newborn Child SceNetAdhocMatchingMemberInternal * newborn = findPeer(context, mac); @@ -490,9 +461,7 @@ void sendBirthPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * mac } // Send Packet - context->socketlock->lock(); int sent = hleCall(sceNetAdhoc, int, sceNetAdhocPdpSend, context->socket, (const char*)&peer->mac, (*context->peerPort)[peer->mac], packet, (u32)sizeof(packet), 0, ADHOC_F_NONBLOCK); - context->socketlock->unlock(); // Log Send Success if (sent >= 0) @@ -508,8 +477,6 @@ void sendBirthPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * mac * @param mac Dead Child's MAC */ void sendDeathPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * mac) { - // Lock the peer - std::lock_guard peer_guard(peerlock); // Find abandoned Child SceNetAdhocMatchingMemberInternal * deadkid = findPeer(context, mac); @@ -534,9 +501,7 @@ void sendDeathPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * mac packet[0] = PSP_ADHOC_MATCHING_PACKET_BYE; // Send Bye Packet - context->socketlock->lock(); hleCall(sceNetAdhoc, int, sceNetAdhocPdpSend, context->socket, (const char*)&peer->mac, (*context->peerPort)[peer->mac], packet, (u32)sizeof(packet[0]), 0, ADHOC_F_NONBLOCK); - context->socketlock->unlock(); } else { // Send to other children @@ -545,9 +510,7 @@ void sendDeathPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * mac packet[0] = PSP_ADHOC_MATCHING_PACKET_DEATH; // Send Death Packet - context->socketlock->lock(); hleCall(sceNetAdhoc, int, sceNetAdhocPdpSend, context->socket, (const char*)&peer->mac, (*context->peerPort)[peer->mac], packet, (u32)sizeof(packet), 0, ADHOC_F_NONBLOCK); - context->socketlock->unlock(); } } } @@ -561,8 +524,6 @@ void sendDeathPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * mac * @param context Matching Context Pointer */ void sendByePacket(SceNetAdhocMatchingContext * context) { - // Lock the peer - std::lock_guard peer_guard(peerlock); // Iterate Peers SceNetAdhocMatchingMemberInternal * peer = context->peerlist; @@ -573,9 +534,7 @@ void sendByePacket(SceNetAdhocMatchingContext * context) { uint8_t opcode = PSP_ADHOC_MATCHING_PACKET_BYE; // Send Bye Packet - context->socketlock->lock(); hleCall(sceNetAdhoc, int, sceNetAdhocPdpSend, context->socket, (const char*)&peer->mac, (*context->peerPort)[peer->mac], &opcode, (u32)sizeof(opcode), 0, ADHOC_F_NONBLOCK); - context->socketlock->unlock(); } } } @@ -646,11 +605,9 @@ void actOnHelloPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * se // Initialize Ping Timer peer->lastping = CoreTiming::GetGlobalTimeUsScaled(); //time_now_d()*1000000.0; - peerlock.lock(); // Link Peer into List peer->next = context->peerlist; context->peerlist = peer; - peerlock.unlock(); } } @@ -723,11 +680,9 @@ void actOnJoinPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * sen // Initialize Ping Timer peer->lastping = CoreTiming::GetGlobalTimeUsScaled(); //time_now_d()*1000000.0; - peerlock.lock(); // Link Peer into List peer->next = context->peerlist; context->peerlist = peer; - peerlock.unlock(); // Spawn Request Event spawnLocalEvent(context, PSP_ADHOC_MATCHING_EVENT_REQUEST, sendermac, optlen, opt); @@ -1051,13 +1006,11 @@ void actOnBirthPacket(SceNetAdhocMatchingContext * context, SceNetEtherAddr * se // Initialize Ping Timer sibling->lastping = CoreTiming::GetGlobalTimeUsScaled(); //time_now_d()*1000000.0; - peerlock.lock(); // Link Peer sibling->next = context->peerlist; context->peerlist = sibling; - peerlock.unlock(); // Spawn Established Event. FIXME: ESTABLISHED event should only be triggered for Parent/P2P peer? //spawnLocalEvent(context, PSP_ADHOC_MATCHING_EVENT_ESTABLISHED, &sibling->mac, 0, NULL); @@ -1165,7 +1118,6 @@ static void MatchingEventPass(SceNetAdhocMatchingContext *context, int matchingI // Messages on Stack ready for processing while (context != NULL && context->event_stack != NULL) { // Claim Stack - context->eventlock->lock(); // Iterate Message List ThreadMessage * msg = context->event_stack; @@ -1180,8 +1132,7 @@ static void MatchingEventPass(SceNetAdhocMatchingContext *context, int matchingI INFO_LOG(Log::sceNet, "EventLoop[%d]: Matching Event [%d=%s][%s] OptSize=%d", matchingId, msg->opcode, getMatchingEventStr(msg->opcode), mac2str(&msg->mac).c_str(), msg->optlen); // Unlock to prevent race-condition with other threads due to recursive lock - //context->eventlock->unlock(); - // Call Event Handler + // // Call Event Handler //context->handler(context->id, msg->opcode, &msg->mac, msg->optlen, opt); // Notify Event Handlers notifyMatchingHandler(context, msg, opt, bufAddr, bufLen, args); // If we're using shared Buffer & Args for All Events We should wait for the Mipscall to be fully executed before processing the next event. GTA VCS need this delay/sleep. @@ -1191,8 +1142,7 @@ static void MatchingEventPass(SceNetAdhocMatchingContext *context, int matchingI //sleep_ms(10); //sceKernelDelayThread(10000); // Lock again - //context->eventlock->lock(); - + // // Pop event stack from front (this should be queue instead of stack?) context->event_stack = msg->next; free(msg); @@ -1200,7 +1150,6 @@ static void MatchingEventPass(SceNetAdhocMatchingContext *context, int matchingI } // Unlock Stack - context->eventlock->unlock(); } } @@ -1213,7 +1162,6 @@ static void MatchingEventFinish(SceNetAdhocMatchingContext *context, int matchin // Process Last Messages if (contexts != NULL && context->event_stack != NULL) { // Claim Stack - context->eventlock->lock(); // Iterate Message List int msg_count = 0; @@ -1227,20 +1175,17 @@ static void MatchingEventFinish(SceNetAdhocMatchingContext *context, int matchin INFO_LOG(Log::sceNet, "EventLoop[%d]: Matching Event [EVENT=%d]\n", matchingId, msg->opcode); - //context->eventlock->unlock(); - // Original Call Event Handler + // // Original Call Event Handler //context->handler(context->id, msg->opcode, &msg->mac, msg->optlen, opt); // Notify Event Handlers notifyMatchingHandler(context, msg, opt, bufAddr, bufLen, args); - //context->eventlock->lock(); - msg_count++; + // msg_count++; } // Clear Event Message Stack clearStack(context, PSP_ADHOC_MATCHING_EVENT_STACK); // Free Stack - context->eventlock->unlock(); INFO_LOG(Log::sceNet, "EventLoop[%d]: Finished (%d msg)", matchingId, msg_count); } } @@ -1292,7 +1237,6 @@ static void MatchingInputPass(SceNetAdhocMatchingContext *context, int matchingI // Messages on Stack ready for processing if (context->input_stack != NULL) { // Claim Stack - context->inputlock->lock(); // Iterate Message List ThreadMessage* msg = context->input_stack; @@ -1303,8 +1247,7 @@ static void MatchingInputPass(SceNetAdhocMatchingContext *context, int matchingI // Grab Optional Data if (msg->optlen > 0) opt = ((u8*)msg) + sizeof(ThreadMessage); - //context->inputlock->unlock(); // Unlock to prevent race condition when locking peerlock - + // // Send Accept Packet if (msg->opcode == PSP_ADHOC_MATCHING_PACKET_ACCEPT) sendAcceptPacket(context, &msg->mac, msg->optlen, opt); @@ -1326,8 +1269,7 @@ static void MatchingInputPass(SceNetAdhocMatchingContext *context, int matchingI // Cancel Bulk Data Transfer (does nothing as of now as we fire and forget anyway) // Do we need to check DeathPacket and ByePacket here? //else if(msg->opcode == PSP_ADHOC_MATCHING_PACKET_BULK_ABORT) sendAbortBulkDataPacket(context, &msg->mac, msg->optlen, opt); - //context->inputlock->lock(); // Lock again - + // // Pop input stack from front (this should be queue instead of stack?) context->input_stack = msg->next; free(msg); @@ -1335,7 +1277,6 @@ static void MatchingInputPass(SceNetAdhocMatchingContext *context, int matchingI } // Free Stack - context->inputlock->unlock(); } // Receive PDP Datagram @@ -1343,11 +1284,7 @@ static void MatchingInputPass(SceNetAdhocMatchingContext *context, int matchingI rxbuflen = context->rxbuflen; senderport = 0; // Lock the peer first before locking the socket to avoid race condiion - peerlock.lock(); - context->socketlock->lock(); int recvresult = sceNetAdhocPdpRecv(context->socket, &sendermac, &senderport, context->rxbuf, &rxbuflen, 0, ADHOC_F_NONBLOCK); - context->socketlock->unlock(); - peerlock.unlock(); // Received Data from a Sender that interests us // Note: There are cases where the sender port might be re-mapped by router or ISP, so we shouldn't check the source port. @@ -1358,7 +1295,6 @@ static void MatchingInputPass(SceNetAdhocMatchingContext *context, int matchingI } // Update Peer Timestamp - peerlock.lock(); SceNetAdhocctlPeerInfo* peer = findFriend(&sendermac); if (peer != NULL) { now = CoreTiming::GetGlobalTimeUsScaled(); @@ -1382,7 +1318,6 @@ static void MatchingInputPass(SceNetAdhocMatchingContext *context, int matchingI // Keep tracks of re-mapped peer's ports for further communication. // Note: This will only works if this player were able to receives data on normal port from other players (ie. this player's port wasn't remapped) (*context->peerPort)[sendermac] = senderport; - peerlock.unlock(); // Ping Packet if (context->rxbuf[0] == PSP_ADHOC_MATCHING_PACKET_PING) actOnPingPacket(context, &sendermac); @@ -1427,7 +1362,6 @@ static void MatchingInputFinish(SceNetAdhocMatchingContext *context, int matchin // Process Last Messages if (context->input_stack != NULL) { // Claim Stack - context->inputlock->lock(); // Iterate Message List int msg_count = 0; @@ -1468,7 +1402,6 @@ static void MatchingInputFinish(SceNetAdhocMatchingContext *context, int matchin } // Free Stack - context->inputlock->unlock(); INFO_LOG(Log::sceNet, "InputLoop[%d]: Finished (%d msg)", matchingId, msg_count); } @@ -1488,9 +1421,7 @@ static void MatchingInputFinish(SceNetAdhocMatchingContext *context, int matchin static void __AdhocMatchingTick(u64 userdata, int cyclesLate) { const int matchingId = (int)userdata; - peerlock.lock(); SceNetAdhocMatchingContext *context = findMatchingContext(matchingId); - peerlock.unlock(); if (context == NULL || (!context->inputRunning && !context->eventRunning)) return; if (context->inputRunning) @@ -1546,13 +1477,9 @@ int NetAdhocMatching_Stop(int matchingId) { matchingThreads[item->matching_thid] = 0; // Make sure nobody locking/using the socket - item->socketlock->lock(); // Delete the socket NetAdhocPdp_Delete(item->socket, 0); // item->connected = (sceNetAdhocPdpDelete(item->socket, 0) < 0); - item->socketlock->unlock(); - // Multithreading Lock - peerlock.lock(); // Remove your own MAC, or All members, or don't remove at all or we should do this on MatchingDelete ? clearPeerList(item); //deleteAllMembers(item); @@ -1560,8 +1487,6 @@ int NetAdhocMatching_Stop(int matchingId) { item->running = 0; netAdhocMatchingStarted--; - // Multithreading Unlock - peerlock.unlock(); return 0; } @@ -1573,8 +1498,6 @@ int sceNetAdhocMatchingStop(int matchingId) { } int NetAdhocMatching_Delete(int matchingId) { - // Multithreading Lock - std::lock_guard peer_guard(peerlock); // Previous Context Reference SceNetAdhocMatchingContext* prev = NULL; @@ -1607,15 +1530,6 @@ int NetAdhocMatching_Delete(int matchingId) { (*context->peerPort).clear(); delete context->peerPort; // Destroy locks - context->eventlock->lock(); // Make sure it's not locked when being deleted - context->eventlock->unlock(); - delete context->eventlock; - context->inputlock->lock(); // Make sure it's not locked when being deleted - context->inputlock->unlock(); - delete context->inputlock; - context->socketlock->lock(); // Make sure it's not locked when being deleted - context->socketlock->unlock(); - delete context->socketlock; // Free item context memory delete context; context = nullptr; @@ -1770,12 +1684,7 @@ static int sceNetAdhocMatchingCreate(int mode, int maxnum, int port, int rxbufle context->mac = localmac; // Create locks - context->socketlock = new std::recursive_mutex; - context->eventlock = new std::recursive_mutex; - context->inputlock = new std::recursive_mutex; - // Multithreading Lock - peerlock.lock(); //contextlock.lock(); // Add Callback Handler context->handler.entryPoint = callbackAddr; @@ -1788,7 +1697,6 @@ static int sceNetAdhocMatchingCreate(int mode, int maxnum, int port, int rxbufle contexts = context; // Multithreading UnLock - peerlock.unlock(); //contextlock.unlock(); // Just to make sure Adhoc is already connected //hleDelayResult(context->id, "give time to init/cleanup", adhocEventDelayMS * 1000); @@ -1806,8 +1714,6 @@ static int sceNetAdhocMatchingCreate(int mode, int maxnum, int port, int rxbufle } int NetAdhocMatching_Start(int matchingId, int evthPri, int evthPartitionId, int evthStack, int inthPri, int inthPartitionId, int inthStack, int optLen, u32 optDataAddr) { - // Multithreading Lock - std::lock_guard peer_guard(peerlock); SceNetAdhocMatchingContext* item = findMatchingContext(matchingId); @@ -2123,8 +2029,6 @@ int sceNetAdhocMatchingGetHelloOpt(int matchingId, u32 optLenAddr, u32 optDataAd s32_le *optlen = PSPPointer::Create(optLenAddr); - // Multithreading Lock - peerlock.lock(); SceNetAdhocMatchingContext * item = findMatchingContext(matchingId); @@ -2139,8 +2043,6 @@ int sceNetAdhocMatchingGetHelloOpt(int matchingId, u32 optLenAddr, u32 optDataAd } //else return SCE_NET_ADHOC_MATCHING_ERROR_INVALID_ID; - // Multithreading Unlock - peerlock.unlock(); return hleLogDebug(Log::sceNet, 0); } @@ -2153,13 +2055,9 @@ int sceNetAdhocMatchingSetHelloOpt(int matchingId, int optLenAddr, u32 optDataAd if (!netAdhocMatchingInited) return hleLogDebug(Log::sceNet, SCE_NET_ADHOC_MATCHING_ERROR_NOT_INITIALIZED, "adhocmatching not initialized"); - // Multithreading Lock - peerlock.lock(); SceNetAdhocMatchingContext* context = findMatchingContext(matchingId); - // Multithreading Unlock - peerlock.unlock(); // Context not found if (context == NULL) @@ -2228,12 +2126,8 @@ static int sceNetAdhocMatchingGetMembers(int matchingId, u32 sizeAddr, u32 buf) if (!Memory::IsValidAddress(sizeAddr)) return hleLogError(Log::sceNet, SCE_NET_ADHOC_MATCHING_ERROR_INVALID_ARG, "adhocmatching invalid arg"); - // Multithreading Lock - peerlock.lock(); // Find Matching Context SceNetAdhocMatchingContext* context = findMatchingContext(matchingId); - // Multithreading Unlock - peerlock.unlock(); // Context not found if (context == NULL) @@ -2448,8 +2342,6 @@ int sceNetAdhocMatchingSendData(int matchingId, const char *mac, int dataLen, u3 void* data = NULL; if (Memory::IsValidAddress(dataAddr)) data = Memory::GetPointerWriteUnchecked(dataAddr); - // Lock the peer - std::lock_guard peer_guard(peerlock); // Find Target Peer SceNetAdhocMatchingMemberInternal* peer = findPeer(context, (SceNetEtherAddr*)mac); @@ -2566,7 +2458,6 @@ int sceNetAdhocMatchingGetPoolStat(u32 poolstatPtr) { } void __NetMatchingCallbacks() { //(int matchingId) - std::lock_guard adhocGuard(adhocEvtMtx); hleSkipDeadbeef(); // Note: Super Pocket Tennis / Thrillville Off the Rails seems to have a very short timeout (ie. ~5ms) while waiting for the event to arrived on the callback handler, but Lord of Arcana may not work well with 5ms (~3m or ~10ms seems to be good) // Games with 4-players or more (ie. Gundam: Senjou No Kizuna Portable) will also need lower delay/latency (ie. ~3ms seems to be good, 2ms or lower doesn't work well) so MatchingEvents can be processed faster, thus won't be piling up in the queue. diff --git a/Core/HLE/sceNetInet.cpp b/Core/HLE/sceNetInet.cpp index 2f16bd145f..b2869451df 100644 --- a/Core/HLE/sceNetInet.cpp +++ b/Core/HLE/sceNetInet.cpp @@ -1040,8 +1040,6 @@ static int sceNetInetSendto(int socket, u32 bufferPtr, int len, int flags, u32 t bool isBcast = isBroadcastIP(saddr.in.sin_addr.s_addr); // Broadcast/Multicast, use real broadcast/multicast if there is no one in peerlist if (isBcast && getActivePeerCount() > 0) { - // Acquire Peer Lock - peerlock.lock(); SceNetAdhocctlPeerInfo* peer = friends; for (; peer != NULL; peer = peer->next) { // Does Skipping sending to timed out friends could cause desync when players moving group at the time MP game started? @@ -1056,8 +1054,6 @@ static int sceNetInetSendto(int socket, u32 bufferPtr, int len, int flags, u32 t DEBUG_LOG(Log::sceNet, "SendTo(BC): Address = %s, Port = %d", ip2str(saddr.in.sin_addr).c_str(), ntohs(saddr.in.sin_port)); } } - // Free Peer Lock - peerlock.unlock(); retval = len; } // Unicast or real broadcast/multicast @@ -1207,8 +1203,6 @@ static int sceNetInetSendmsg(int socket, u32 msghdrPtr, int flags) { bool isBcast = isBroadcastIP(saddr.in.sin_addr.s_addr); // Broadcast/Multicast, use real broadcast/multicast if there is no one in peerlist if (isBcast && getActivePeerCount() > 0) { - // Acquire Peer Lock - peerlock.lock(); SceNetAdhocctlPeerInfo* peer = friends; for (; peer != NULL; peer = peer->next) { // Does Skipping sending to timed out friends could cause desync when players moving group at the time MP game started? @@ -1231,8 +1225,6 @@ static int sceNetInetSendmsg(int socket, u32 msghdrPtr, int flags) { DEBUG_LOG(Log::sceNet, "SendMsg(BC): Socket error %d", socket_errno); } } - // Free Peer Lock - peerlock.unlock(); // TODO: Calculate number of bytes supposed to be sent retval = std::max(retval, 0); // Broadcast always success? } diff --git a/Core/HLE/sceNetResolver.cpp b/Core/HLE/sceNetResolver.cpp index 7eaf796df5..fe299c7eb3 100644 --- a/Core/HLE/sceNetResolver.cpp +++ b/Core/HLE/sceNetResolver.cpp @@ -46,7 +46,6 @@ #include "Core/HLE/sceNetResolver.h" #include -#include #include "Core/HLE/sceNet.h" #include "Core/HLE/sceNp.h" diff --git a/Core/HLE/sceNp.cpp b/Core/HLE/sceNp.cpp index fc174bc697..41771ddaba 100644 --- a/Core/HLE/sceNp.cpp +++ b/Core/HLE/sceNp.cpp @@ -18,7 +18,6 @@ // This is pretty much a stub implementation. Doesn't actually do anything, just tries to return values // to keep games happy anyway. -#include #include #include "Common/System/OSD.h" @@ -52,7 +51,6 @@ std::string npAvatarUrl = "http://DummyAvatarUrl"; // SceNpAvatarUrl struct? // Game-specific ID, I guess we can use this to auto-choose DNS? SceNpCommunicationId npTitleId; -std::recursive_mutex npAuthEvtMtx; std::deque npAuthEvents; std::map npAuthHandlers; @@ -116,7 +114,6 @@ static int writeTicketU64Param(u8* buffer, const u16_be type, const u64_be data) } static void notifyNpAuthHandlers(u32 id, u32 result, u32 argAddr) { - std::lock_guard npAuthGuard(npAuthEvtMtx); npAuthEvents.push_back({ { id, result, argAddr } }); } @@ -483,7 +480,6 @@ int sceNpAuthGetEntitlementById(u32 ticketBufferAddr, u32 ticketLength, u32 enti int sceNpAuthAbortRequest(int requestId) { // TODO: Disconnect HTTPS connection & cancel the callback event - std::lock_guard npAuthGuard(npAuthEvtMtx); for (auto it = npAuthEvents.begin(); it != npAuthEvents.end(); ) { (it->data[0] == requestId) ? it = npAuthEvents.erase(it) : ++it; } diff --git a/Core/HLE/sceNp.h b/Core/HLE/sceNp.h index 30ffc67ca4..e357845dff 100644 --- a/Core/HLE/sceNp.h +++ b/Core/HLE/sceNp.h @@ -17,7 +17,6 @@ #pragma once -#include #include #include #include "Core/HLE/sceRtc.h" @@ -429,7 +428,6 @@ extern PSPTimeval npSigninTimestamp; // Used by sceNet.cpp since we're borrowing Apctl's PSPThread to process NP events & callbacks. // TODO: NP events should be processed on it's own PSPThread -extern std::recursive_mutex npAuthEvtMtx; // Used by sceNp2.cpp extern SceNpCommunicationId npTitleId; diff --git a/Core/HLE/sceNp2.cpp b/Core/HLE/sceNp2.cpp index d8881e859d..544b2a983a 100644 --- a/Core/HLE/sceNp2.cpp +++ b/Core/HLE/sceNp2.cpp @@ -15,7 +15,6 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include #include #include #include "Core/MemMapHelpers.h" @@ -29,7 +28,6 @@ bool npMatching2Inited = false; SceNpAuthMemoryStat npMatching2MemStat = {}; -std::recursive_mutex npMatching2EvtMtx; std::deque npMatching2Events; std::map npMatching2Handlers; //std::map npMatching2Contexts; @@ -44,7 +42,6 @@ std::map npMatching2Handlers; // 0x09 on 0x0101, 0x0A on 0x0102, 0x0C on 0x0103, 0x0D on 0x0104, 0x0E on 0x0105, 0x0F on 0x0106, 0x10 on 0x0107, 0x11 on 0x0108, // 0x12 on 0x0109, 0x13 on 0x010a, 0x14 on 0x010b, 0x15 on 0x010c, 0x16 on 0x010d, 0x17 on 0x010e, 0x18 on 0xa102 void notifyNpMatching2Handlers(NpMatching2Args &args, u32 ctxId, u32 serverId, u32 cbFuncAddr, u32 cbArgAddr, u32 unk3, u32 unk4, u32 unk5, u8 unk6) { - std::lock_guard npMatching2Guard(npMatching2EvtMtx); // TODO: separate/map each list per ctxId npMatching2Events.push_back(args); } diff --git a/Core/HLE/sceNp2.h b/Core/HLE/sceNp2.h index 8cc5c1d12b..1236144cbe 100644 --- a/Core/HLE/sceNp2.h +++ b/Core/HLE/sceNp2.h @@ -250,7 +250,6 @@ struct NpMatching2Args { #pragma pack(pop) -extern std::recursive_mutex npMatching2EvtMtx; bool NpMatching2ProcessEvents(); diff --git a/Core/HLE/sceSas.cpp b/Core/HLE/sceSas.cpp index d617444143..942652bcea 100644 --- a/Core/HLE/sceSas.cpp +++ b/Core/HLE/sceSas.cpp @@ -28,9 +28,6 @@ #include #include -#include -#include -#include #include "Common/Profiler/Profiler.h" #include "Common/Thread/ThreadUtil.h" @@ -51,6 +48,8 @@ #include "Core/HLE/sceKernel.h" #include "Core/HLE/sceKernelThread.h" #include "Core/HLE/sceKernelInterrupt.h" +#include "Common/Thread/Thread.h" +#include "Common/Thread/ParkingLot.h" // TODO - allow more than one, associating each with one Core pointer (passed in to all the functions) // No known games use more than one instance of Sas though. @@ -72,12 +71,10 @@ struct SasThreadParams { int rightVol; }; -static std::thread g_sasThread; -static std::mutex sasWakeMutex; -static std::mutex sasDoneMutex; -static std::condition_variable sasWake; -static std::condition_variable sasDone; -static volatile int sasThreadState = SasThreadState::DISABLED; +static Thread g_sasThread; +// SasThreadState. The emulation thread queues a mix and the SAS thread moves it back to READY. +static retro_atomic_int_t sasThreadState{ SasThreadState::DISABLED }; +static EventCounter sasWork; static SasThreadParams sasThreadParams; static int sasMixEvent = -1; @@ -87,62 +84,60 @@ bool *__SasGetGlobalMuteFlag() { return &g_sasMuteFlag; } +static int SasState() { + return retro_atomic_load_acquire_int(&sasThreadState); +} + int __SasThread() { SetCurrentThreadName("SAS"); - std::unique_lock guard(sasWakeMutex); - while (sasThreadState != SasThreadState::DISABLED) { - sasWake.wait(guard); - if (sasThreadState == SasThreadState::QUEUED) { + for (;;) { + const int seen = sasWork.Seen(); + const int state = SasState(); + if (state == SasThreadState::DISABLED) { + break; + } + if (state == SasThreadState::QUEUED) { const bool mute = g_sasMuteFlag; sas->Mix(sasThreadParams.outAddr, sasThreadParams.inAddr, sasThreadParams.leftVol, sasThreadParams.rightVol, mute); - std::lock_guard doneGuard(sasDoneMutex); - sasThreadState = SasThreadState::READY; - sasDone.notify_one(); + // Fails only if the thread was disabled meanwhile. + retro_atomic_cas_int(&sasThreadState, SasThreadState::QUEUED, SasThreadState::READY); + ParkingLotNotify(&sasThreadState); + continue; } + sasWork.Wait(seen); } return 0; } static void __SasDrain() { - std::unique_lock guard(sasDoneMutex); - while (sasThreadState == SasThreadState::QUEUED) - sasDone.wait(guard); + ParkingLotWait(&sasThreadState, [] { return SasState() != SasThreadState::QUEUED; }); } static void __SasEnqueueMix(u32 outAddr, u32 inAddr = 0, int leftVol = 0, int rightVol = 0) { - if (sasThreadState == SasThreadState::DISABLED) { + if (SasState() == SasThreadState::DISABLED) { // No thread, call it immediately. const bool mute = g_sasMuteFlag; sas->Mix(outAddr, inAddr, leftVol, rightVol, mute); return; } - if (sasThreadState == SasThreadState::QUEUED) { - // Wait for the queue to drain. - __SasDrain(); - } - - // We're safe to write, since it can't be processing now anymore. - // No other thread enqueues. + // Wait for the last mix, so the params are free to write. No other thread enqueues. + __SasDrain(); sasThreadParams.outAddr = outAddr; sasThreadParams.inAddr = inAddr; sasThreadParams.leftVol = leftVol; sasThreadParams.rightVol = rightVol; - // And now, notify. - sasWakeMutex.lock(); - sasThreadState = SasThreadState::QUEUED; - sasWake.notify_one(); - sasWakeMutex.unlock(); + retro_atomic_store_release_int(&sasThreadState, SasThreadState::QUEUED); + sasWork.Notify(); } static void __SasDisableThread() { - if (sasThreadState != SasThreadState::DISABLED) { - sasWakeMutex.lock(); - sasThreadState = SasThreadState::DISABLED; - sasWake.notify_one(); - sasWakeMutex.unlock(); + if (SasState() != SasThreadState::DISABLED) { + retro_atomic_store_release_int(&sasThreadState, SasThreadState::DISABLED); + sasWork.Notify(); + ParkingLotNotify(&sasThreadState); if (g_sasThread.joinable()) { g_sasThread.join(); } @@ -176,10 +171,10 @@ void __SasInit() { sasMixEvent = CoreTiming::RegisterEvent("SasMix", sasMixFinish); if (g_Config.bSeparateSASThread) { - sasThreadState = SasThreadState::READY; - g_sasThread = std::thread(__SasThread); + retro_atomic_store_release_int(&sasThreadState, SasThreadState::READY); + g_sasThread = Thread(__SasThread); } else { - sasThreadState = SasThreadState::DISABLED; + retro_atomic_store_release_int(&sasThreadState, SasThreadState::DISABLED); } } @@ -188,10 +183,8 @@ void __SasDoState(PointerWrap &p) { if (!s) return; - if (sasThreadState == SasThreadState::QUEUED) { - // Wait for the queue to drain. Don't want to save the wrong stuff. - __SasDrain(); - } + // Wait for the queue to drain. Don't want to save the wrong stuff. + __SasDrain(); DoClass(p, sas); @@ -206,9 +199,7 @@ void __SasDoState(PointerWrap &p) { } void __SasWaitForMix() { - if (sasThreadState == SasThreadState::QUEUED) { - __SasDrain(); - } + __SasDrain(); } void __SasShutdown() { diff --git a/Core/HLE/sceUsbCam.cpp b/Core/HLE/sceUsbCam.cpp index 2cea6816c7..5b7d1e23f1 100644 --- a/Core/HLE/sceUsbCam.cpp +++ b/Core/HLE/sceUsbCam.cpp @@ -16,10 +16,10 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include -#include -#include #include +#include + #include "ppsspp_config.h" #include "Common/System/System.h" @@ -43,12 +43,31 @@ unsigned int nextVideoFrame = 0; // When video capture started, which the frame clock counts from. Not saved in states, it only sets the phase. static u64 videoStartUs = 0; uint8_t *videoBuffer; -std::mutex videoBufferMutex; enum { VIDEO_BUFFER_SIZE = 40 * 1000, }; +// A frame from Camera::pushCameraImage, which can come from any thread. The newest one waits here +// until the emulation thread takes it into videoBuffer. +struct PushedFrame { + std::vector data; +}; +static retro_atomic_ptr_t g_pushedFrame{}; + +static void TakePushedFrame() { + PushedFrame *frame = (PushedFrame *)retro_atomic_exchange_ptr(&g_pushedFrame, nullptr); + if (!frame) { + return; + } + if (videoBuffer) { + memset(videoBuffer, 0, VIDEO_BUFFER_SIZE); + videoBufferLength = (unsigned int)frame->data.size(); + memcpy(videoBuffer, frame->data.data(), frame->data.size()); + } + delete frame; +} + void __UsbCamInit() { config = new Camera::Config(); config->mode = Camera::Mode::Unused; @@ -90,6 +109,7 @@ void __UsbCamShutdown() { videoBuffer = nullptr; delete config; config = nullptr; + delete (PushedFrame *)retro_atomic_exchange_ptr(&g_pushedFrame, nullptr); } // TODO: Technically, we should store the videoBuffer into the savestate, if this @@ -189,7 +209,8 @@ static int sceUsbCamSetupVideoEx(u32 paramAddr, u32 workareaAddr, int wasize) { } static int sceUsbCamStartVideo() { - std::lock_guard lock(videoBufferMutex); + // Whatever was pushed before is stale. + delete (PushedFrame *)retro_atomic_exchange_ptr(&g_pushedFrame, nullptr); int width, height; getCameraResolution(config->type, &width, &height); @@ -227,7 +248,7 @@ static int getFrameIntervalUs() { } static int sceUsbCamReadVideoFrameBlocking(u32 bufAddr, u32 size) { - std::lock_guard lock(videoBufferMutex); + TakePushedFrame(); u32 transferSize = std::min(videoBufferLength, size); if (Memory::IsValidRange(bufAddr, size)) { Memory::Memcpy(bufAddr, videoBuffer, transferSize); @@ -241,7 +262,7 @@ static int sceUsbCamReadVideoFrameBlocking(u32 bufAddr, u32 size) { } static int sceUsbCamReadVideoFrame(u32 bufAddr, u32 size) { - std::lock_guard lock(videoBufferMutex); + TakePushedFrame(); u32 transferSize = std::min(videoBufferLength, size); if (Memory::IsValidRange(bufAddr, size)) { Memory::Memcpy(bufAddr, videoBuffer, transferSize); @@ -410,10 +431,10 @@ int Camera::getMaxFrameSize() { // The JPEG quality that fit the last frame. Frames of one scene are similar in size, so it's usually // right first time. Only the capture thread uses it, but atomic in case a platform has several. -static std::atomic g_jpegQuality{ 80 }; +static retro_atomic_int_t g_jpegQuality{ 80 }; int Camera::encodeToFit(int maxSize, const std::function &encode) { - int quality = g_jpegQuality; + int quality = retro_atomic_load_relaxed_int(&g_jpegQuality); int size = encode(quality); while ((size < 0 || size > maxSize) && quality > 10) { quality = std::max(10, quality - 10); @@ -423,7 +444,7 @@ int Camera::encodeToFit(int maxSize, const std::function &enco if (size >= 0 && size < maxSize / 2 && quality < 90) { quality += 10; } - g_jpegQuality = quality; + retro_atomic_store_relaxed_int(&g_jpegQuality, quality); return size; } @@ -458,16 +479,12 @@ void Camera::pushCameraImage(long long length, unsigned char* image) { length = (long long)recompressed.size(); } - std::lock_guard lock(videoBufferMutex); - if (!videoBuffer) { - return; - } - memset(videoBuffer, 0, VIDEO_BUFFER_SIZE); - if (length > VIDEO_BUFFER_SIZE) { - videoBufferLength = 0; + PushedFrame *frame = new PushedFrame(); + if (length > VIDEO_BUFFER_SIZE || length < 0) { ERROR_LOG(Log::HLE, "pushCameraImage: length error: %lld > %d", length, VIDEO_BUFFER_SIZE); } else { - videoBufferLength = length; - memcpy(videoBuffer, image, length); + frame->data.assign(image, image + length); } + // Replaces a frame the game hasn't read yet. Nothing else holds it once it's swapped out. + delete (PushedFrame *)retro_atomic_exchange_ptr(&g_pushedFrame, (void *)frame); } diff --git a/Core/HLE/sceUsbMic.cpp b/Core/HLE/sceUsbMic.cpp index 7e64f12f89..87ee1df17a 100644 --- a/Core/HLE/sceUsbMic.cpp +++ b/Core/HLE/sceUsbMic.cpp @@ -17,7 +17,6 @@ #include "ppsspp_config.h" #include -#include #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" @@ -184,7 +183,6 @@ QueueBuf::~QueueBuf() { int QueueBuf::push(const u8 *buf, int size) { int addedSize = 0; // This will overwrite the old data if the size prepare to add more than remaining size. - std::unique_lock lock(mutex); if (size > capacity) resize(size); while (end + size > capacity) { @@ -197,7 +195,6 @@ int QueueBuf::push(const u8 *buf, int size) { addedSize += size; end = (end + size) % capacity; available = std::min(capacity, available + addedSize); - lock.unlock(); return addedSize; } @@ -206,7 +203,6 @@ int QueueBuf::pop(u8 *buf, int size) { return 0; } int ret = 0; - std::unique_lock lock(mutex); if (getAvailableSize() < size) size = getAvailableSize(); ret = size; @@ -219,7 +215,6 @@ int QueueBuf::pop(u8 *buf, int size) { memcpy(buf + capacity - startPos, buf_, size - (capacity - startPos)); } available -= size; - lock.unlock(); return ret; } @@ -239,10 +234,8 @@ void QueueBuf::resize(int newSize) { } void QueueBuf::flush() { - std::unique_lock lock(mutex); available = 0; end = 0; - lock.unlock(); } int QueueBuf::getRemainingSize() const { diff --git a/Core/HLE/sceUsbMic.h b/Core/HLE/sceUsbMic.h index 240666d0ca..bb49646415 100644 --- a/Core/HLE/sceUsbMic.h +++ b/Core/HLE/sceUsbMic.h @@ -18,7 +18,6 @@ #pragma once #include "sceKernel.h" -#include void Register_sceUsbMic(); @@ -65,8 +64,6 @@ private: int end = 0; int capacity; // set in constructor u8 *buf_; - // TODO: Turn back to a regular mutex, will take some refactoring. - std::recursive_mutex mutex; }; namespace Microphone { diff --git a/Core/HLE/sceUtility.cpp b/Core/HLE/sceUtility.cpp index 538c811a43..189bbea8e6 100644 --- a/Core/HLE/sceUtility.cpp +++ b/Core/HLE/sceUtility.cpp @@ -634,12 +634,6 @@ void __UtilityDoState(PointerWrap &p) { } } -void __UtilityWaitForIO() { - if (saveDialog) { - saveDialog->WaitForIO(); - } -} - void __UtilityShutdown() { saveDialog->Shutdown(true); msgDialog->Shutdown(true); diff --git a/Core/HLE/sceUtility.h b/Core/HLE/sceUtility.h index 93b20b9a2a..a1588051c0 100644 --- a/Core/HLE/sceUtility.h +++ b/Core/HLE/sceUtility.h @@ -124,7 +124,6 @@ void __UtilityInit(); void __UtilityDoState(PointerWrap &p); void __UtilityShutdown(); // Before a savestate saves or loads memory: the savedata IO thread reads and writes it directly. -void __UtilityWaitForIO(); void UtilityDialogInitialize(UtilityDialogType type, int delayUs, int accessPriority, int graphicsPriority); void UtilityDialogShutdown(UtilityDialogType type, int delayUs, int accessPriority, int graphicsPriority); diff --git a/Core/HW/AsyncIOManager.cpp b/Core/HW/AsyncIOManager.cpp index 35d26f58e9..724434ce74 100644 --- a/Core/HW/AsyncIOManager.cpp +++ b/Core/HW/AsyncIOManager.cpp @@ -15,8 +15,6 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include -#include #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" @@ -28,96 +26,13 @@ #include "Core/FileSystems/MetaFileSystem.h" bool AsyncIOManager::HasOperation(u32 handle) { - std::lock_guard guard(resultsLock_); - if (resultsPending_.find(handle) != resultsPending_.end()) { - return true; - } - if (results_.find(handle) != results_.end()) { - return true; - } - return false; + return resultsPending_.find(handle) != resultsPending_.end() || results_.find(handle) != results_.end(); } void AsyncIOManager::ScheduleOperation(const AsyncIOEvent &ev) { - { - std::lock_guard guard(resultsLock_); - if (!resultsPending_.insert(ev.handle).second) { - ERROR_LOG_REPORT(Log::sceIo, "Scheduling operation for file %d while one is pending (type %d)", ev.handle, ev.type); - } + if (!resultsPending_.insert(ev.handle).second) { + ERROR_LOG_REPORT(Log::sceIo, "Scheduling operation for file %d while one is pending (type %d)", ev.handle, ev.type); } - ScheduleEvent(ev); -} - -void AsyncIOManager::Shutdown() { - std::lock_guard guard(resultsLock_); - resultsPending_.clear(); - results_.clear(); -} - -bool AsyncIOManager::HasResult(u32 handle) { - std::lock_guard guard(resultsLock_); - return results_.find(handle) != results_.end(); -} - -bool AsyncIOManager::PopResult(u32 handle, AsyncIOResult &result) { - // This is called under lock from WaitResult, no need to lock again. - if (results_.find(handle) != results_.end()) { - result = results_[handle]; - results_.erase(handle); - resultsPending_.erase(handle); - - if (result.invalidateAddr && result.result > 0) { - currentMIPS->InvalidateICacheRangeImmediate(result.invalidateAddr, (int)result.result); - } - return true; - } else { - return false; - } -} - -bool AsyncIOManager::ReadResult(u32 handle, AsyncIOResult &result) { - // This is called under lock from WaitResult, no need to lock again. - if (results_.find(handle) != results_.end()) { - result = results_[handle]; - return true; - } else { - return false; - } -} - -bool AsyncIOManager::WaitResult(u32 handle, AsyncIOResult &result) { - ScheduleEvent(IO_EVENT_SYNC); - for (;;) { - const int seen = retro_atomic_load_acquire_int(&progress_); - { - std::lock_guard guard(resultsLock_); - if (PopResult(handle, result)) - return true; - if (!HasEvents() || !ThreadEnabled() || resultsPending_.find(handle) == resultsPending_.end()) - return false; - } - ParkingLotWait(&progress_, [&] { return retro_atomic_load_acquire_int(&progress_) != seen; }); - } -} - -u64 AsyncIOManager::ResultFinishTicks(u32 handle) { - AsyncIOResult result; - - ScheduleEvent(IO_EVENT_SYNC); - for (;;) { - const int seen = retro_atomic_load_acquire_int(&progress_); - { - std::lock_guard guard(resultsLock_); - if (ReadResult(handle, result)) - return result.finishTicks; - if (!HasEvents() || !ThreadEnabled() || resultsPending_.find(handle) == resultsPending_.end()) - return 0; - } - ParkingLotWait(&progress_, [&] { return retro_atomic_load_acquire_int(&progress_) != seen; }); - } -} - -void AsyncIOManager::ProcessEvent(AsyncIOEvent ev) { switch (ev.type) { case IO_EVENT_READ: Read(ev.handle, ev.buf, ev.bytes, ev.invalidateAddr); @@ -129,9 +44,45 @@ void AsyncIOManager::ProcessEvent(AsyncIOEvent ev) { default: ERROR_LOG_REPORT(Log::sceIo, "Unsupported IO event type"); + resultsPending_.erase(ev.handle); + break; } } +void AsyncIOManager::Shutdown() { + resultsPending_.clear(); + results_.clear(); +} + +bool AsyncIOManager::HasResult(u32 handle) { + return results_.find(handle) != results_.end(); +} + +bool AsyncIOManager::PopResult(u32 handle, AsyncIOResult &result) { + auto iter = results_.find(handle); + if (iter == results_.end()) { + return false; + } + result = iter->second; + results_.erase(iter); + resultsPending_.erase(handle); + + if (result.invalidateAddr && result.result > 0) { + currentMIPS->InvalidateICacheRangeImmediate(result.invalidateAddr, (int)result.result); + } + return true; +} + +bool AsyncIOManager::WaitResult(u32 handle, AsyncIOResult &result) { + // Operations finish as they're scheduled, so there's nothing to wait for. + return PopResult(handle, result); +} + +u64 AsyncIOManager::ResultFinishTicks(u32 handle) { + auto iter = results_.find(handle); + return iter != results_.end() ? iter->second.finishTicks : 0; +} + void AsyncIOManager::Read(u32 handle, u8 *buf, size_t bytes, u32 invalidateAddr) { int usec = 0; s64 result = pspFileSystem.ReadFile(handle, buf, bytes, usec); @@ -145,14 +96,10 @@ void AsyncIOManager::Write(u32 handle, const u8 *buf, size_t bytes) { } void AsyncIOManager::EventResult(u32 handle, const AsyncIOResult &result) { - { - std::lock_guard guard(resultsLock_); - if (results_.find(handle) != results_.end()) { - ERROR_LOG_REPORT(Log::sceIo, "Overwriting previous result for file action on handle %d", handle); - } - results_[handle] = result; + if (results_.find(handle) != results_.end()) { + ERROR_LOG_REPORT(Log::sceIo, "Overwriting previous result for file action on handle %d", handle); } - Progress(); + results_[handle] = result; } void AsyncIOManager::DoState(PointerWrap &p) { @@ -160,8 +107,6 @@ void AsyncIOManager::DoState(PointerWrap &p) { if (!s) return; - SyncThread(); - std::lock_guard guard(resultsLock_); Do(p, resultsPending_); if (s >= 2) { Do(p, results_); diff --git a/Core/HW/AsyncIOManager.h b/Core/HW/AsyncIOManager.h index fe7891d09a..941c13403e 100644 --- a/Core/HW/AsyncIOManager.h +++ b/Core/HW/AsyncIOManager.h @@ -17,15 +17,9 @@ #pragma once -#include -#include -#include #include #include -#include - -#include "Common/Thread/ParkingLot.h" #include "Core/Core.h" #include "Core/System.h" @@ -80,12 +74,11 @@ struct AsyncIOResult { u32 invalidateAddr; }; +// Reads and writes for sceIo that complete later in emulated time. The host IO runs right away on +// the emulation thread, like every other filesystem access, and the result waits here until the +// game is told about it. Emulation thread only. class AsyncIOManager { public: - AsyncIOManager() { - retro_atomic_int_init(&progress_, 0); - } - void DoState(PointerWrap &p); bool HasOperation(u32 handle); @@ -96,199 +89,13 @@ public: bool WaitResult(u32 handle, AsyncIOResult &result); u64 ResultFinishTicks(u32 handle); - void SetThreadEnabled(bool threadEnabled) { - threadEnabled_ = threadEnabled; - Progress(); - } - - bool ThreadEnabled() { - return threadEnabled_; - } - - void ScheduleEvent(AsyncIOEvent ev) { - if (threadEnabled_) { - std::lock_guard guard(eventsLock_); - events_.push_back(ev); - eventsWait_.notify_one(); - } else { - events_.push_back(ev); - } - - if (!threadEnabled_) { - RunEventsUntil(0); - } - } - - bool HasEvents() { - if (threadEnabled_) { - std::lock_guard guard(eventsLock_); - return !events_.empty(); - } else { - return !events_.empty(); - } - } - - void NotifyDrain() { - if (threadEnabled_) { - std::lock_guard guard(eventsLock_); - eventsDrain_.notify_one(); - } - } - - AsyncIOEvent GetNextEvent() { - if (threadEnabled_) { - std::lock_guard guard(eventsLock_); - if (events_.empty()) { - NotifyDrain(); - return IO_EVENT_INVALID; - } - - AsyncIOEvent ev = events_.front(); - events_.pop_front(); - return ev; - } else { - if (events_.empty()) { - return IO_EVENT_INVALID; - } - AsyncIOEvent ev = events_.front(); - events_.pop_front(); - return ev; - } - } - - // This is the threadfunc, really. Although it can also run on the main thread if threadEnabled_ is set. - // TODO: Remove threadEnabled_, always be on a thread. - void RunEventsUntil(u64 globalticks) { - if (!threadEnabled_) { - do { - for (AsyncIOEvent ev = GetNextEvent(); AsyncIOEventType(ev) != IO_EVENT_INVALID; ev = GetNextEvent()) { - ProcessEventIfApplicable(ev, globalticks); - } - } while (CoreTiming::GetTicks(currentMIPS) < globalticks); - return; - } - - std::unique_lock guard(eventsLock_); - eventsRunning_ = true; - eventsHaveRun_ = true; - do { - while (events_.empty()) { - eventsWait_.wait(guard); - } - // Quit the loop if the queue is drained and coreState has tripped, or threading is disabled. - if (events_.empty()) { - break; - } - - for (AsyncIOEvent ev = GetNextEvent(); AsyncIOEventType(ev) != IO_EVENT_INVALID; ev = GetNextEvent()) { - guard.unlock(); - ProcessEventIfApplicable(ev, globalticks); - Progress(); - guard.lock(); - } - } while (CoreTiming::GetTicks(currentMIPS) < globalticks); - - // This will force the waiter to check coreState, even if we didn't actually drain. - NotifyDrain(); - eventsRunning_ = false; - } - - void SyncBeginFrame() { - if (threadEnabled_) { - std::lock_guard guard(eventsLock_); - eventsHaveRun_ = false; - } else { - eventsHaveRun_ = false; - } - } - - inline bool ShouldSyncThread(bool force) { - if (!HasEvents()) - return false; - if (coreState != CORE_RUNNING_CPU && !force) - return false; - - // Don't run if it's not running, but wait for startup. - if (!eventsRunning_) { - if (eventsHaveRun_ || coreState == CORE_RUNTIME_ERROR || coreState == CORE_POWERDOWN) { - return false; - } - } - - return true; - } - - // Force ignores coreState. - void SyncThread(bool force = false) { - if (!threadEnabled_) { - return; - } - - std::unique_lock guard(eventsLock_); - // While processing the last event, HasEvents() will be false even while not done. - // So we schedule a nothing event and wait for that to finish. - ScheduleEvent(IO_EVENT_SYNC); - while (ShouldSyncThread(force)) { - eventsDrain_.wait(guard); - } - } - - void FinishEventLoop() { - if (!threadEnabled_) { - return; - } - - std::lock_guard guard(eventsLock_); - // Don't schedule a finish if it's not even running. - if (eventsRunning_) { - ScheduleEvent(IO_EVENT_FINISH); - } - } - -protected: - void ProcessEvent(AsyncIOEvent ref); - - inline void ProcessEventIfApplicable(AsyncIOEvent &ev, u64 &globalticks) { - switch (AsyncIOEventType(ev)) { - case IO_EVENT_FINISH: - // Stop waiting. - globalticks = 0; - break; - - case IO_EVENT_SYNC: - // Nothing special to do, this event it just to wait on, see SyncThread. - break; - - default: - ProcessEvent(ev); - } - } - private: bool PopResult(u32 handle, AsyncIOResult &result); - bool ReadResult(u32 handle, AsyncIOResult &result); void Read(u32 handle, u8 *buf, size_t bytes, u32 invalidateAddr); void Write(u32 handle, const u8 *buf, size_t bytes); void EventResult(u32 handle, const AsyncIOResult &result); - // Bumped after anything a result waiter's predicate reads changes, so waiters - // park on it with no timeout and miss no wakeup. - void Progress() { - retro_atomic_fetch_add_int(&progress_, 1); - ParkingLotNotify(&progress_); - } - retro_atomic_int_t progress_; - - bool threadEnabled_ = false; - bool eventsRunning_ = false; - bool eventsHaveRun_ = false; - std::deque events_; - std::recursive_mutex eventsLock_; // TODO: Should really make this non-recursive - condition_variable_any is dangerous - std::condition_variable_any eventsWait_; - std::condition_variable_any eventsDrain_; - - std::mutex resultsLock_; std::set resultsPending_; std::map results_; }; diff --git a/Core/HW/Display.cpp b/Core/HW/Display.cpp index f21d442d33..ec6d70e9a0 100644 --- a/Core/HW/Display.cpp +++ b/Core/HW/Display.cpp @@ -15,10 +15,9 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include #include #include -#include +#include #include #include "Common/CommonTypes.h" #include "Common/Serialize/SerializeFuncs.h" @@ -37,14 +36,13 @@ #include "GPU/GPUCommon.h" // Called when vblank happens (like an internal interrupt.) Not part of state, should be static. -static std::mutex listenersLock; +// Emulation thread only. typedef std::pair FlipListener; static std::vector flipListeners; static uint64_t frameStartTicks; -// Atomic because the WebSocket debugger reads it from its own thread (input.buttons.press uses -// it to count down frames) while the CPU thread bumps it. -static std::atomic numVBlanks; +// Read by the libretro frontend thread for its speed estimate; only the emulation thread writes it. +static retro_atomic_int_t numVBlanks{ 0 }; // hCount is computed now. static int vCount; // The "AccumulatedHcount" can be adjusted, this is the base. @@ -80,13 +78,13 @@ static void CalculateFPS() { double now = time_now_d(); if (now >= lastFpsTime + 1.0) { - double frames = (numVBlanks - lastFpsFrame); + double frames = (retro_atomic_load_relaxed_int(&numVBlanks) - lastFpsFrame); actualFps = (float)(actualFlips - lastActualFlips); fps = frames / (now - lastFpsTime); flips = (float)(g_Config.iDisplayRefreshRate * (double)(gpuStats.totals.numFlips - lastNumFlips) / frames); - lastFpsFrame = numVBlanks; + lastFpsFrame = retro_atomic_load_relaxed_int(&numVBlanks); lastNumFlips = gpuStats.totals.numFlips; lastActualFlips = actualFlips; lastFpsTime = now; @@ -143,7 +141,7 @@ int __DisplayGetFlipCount() { } int __DisplayGetNumVblanks() { - return numVBlanks; + return retro_atomic_load_relaxed_int(&numVBlanks); } int __DisplayGetVCount() { @@ -232,7 +230,7 @@ bool DisplayIsRunningSlow() { void DisplayFireVblankStart() { frameStartTicks = CoreTiming::GetTicks(currentMIPS); - numVBlanks++; + retro_atomic_fetch_add_int(&numVBlanks, 1); isVblank = 1; vCount++; // vCount increases at each VBLANK. @@ -250,10 +248,8 @@ void DisplayFireVblankEnd() { } void DisplayFireFlip() { - std::vector toCall = [] { - std::lock_guard guard(listenersLock); - return flipListeners; - }(); + // A copy, since a listener may unregister itself. + std::vector toCall = flipListeners; // This is also the right time to calculate FPS. CalculateFPS(); @@ -268,12 +264,10 @@ void DisplayFireActualFlip() { } void __DisplayListenFlip(FlipCallback callback, void *userdata) { - std::lock_guard guard(listenersLock); flipListeners.emplace_back(callback, userdata); } void __DisplayForgetFlip(FlipCallback callback, void *userdata) { - std::lock_guard guard(listenersLock); flipListeners.erase(std::remove_if(flipListeners.begin(), flipListeners.end(), [&](FlipListener item) { return item.first == callback && item.second == userdata; }), flipListeners.end()); @@ -283,7 +277,7 @@ void __DisplayForgetFlip(FlipCallback callback, void *userdata) { void DisplayHWInit() { flipListeners.clear(); frameStartTicks = 0; - numVBlanks = 0; + retro_atomic_store_relaxed_int(&numVBlanks, 0); isVblank = 0; vCount = 0; hCountBase = 0; @@ -305,7 +299,6 @@ void DisplayHWInit() { } void DisplayHWShutdown() { - std::lock_guard guard(listenersLock); flipListeners.clear(); } diff --git a/Core/HW/MemoryStick.cpp b/Core/HW/MemoryStick.cpp index 0fb41717c3..c8e1a8d2f5 100644 --- a/Core/HW/MemoryStick.cpp +++ b/Core/HW/MemoryStick.cpp @@ -16,8 +16,6 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include -#include -#include #include #include #include @@ -44,12 +42,10 @@ static MemStickFatState memStickFatState; static bool memStickNeedsAssign = false; static uint64_t memStickInsertedAt = 0; static uint64_t memstickInitialFree = 0; -// The savedata IO thread asks for the free space too, so the cached use is guarded, and a write -// during the calculation leaves it stale rather than marked current. -static std::mutex memstickCurrentUseLock; +// The savedata use is cached until the next write. static uint64_t memstickCurrentUse = 0; static uint32_t memstickCurrentUseGeneration = 0; -static std::atomic memstickWriteGeneration{ 1 }; +static uint32_t memstickWriteGeneration = 1; enum FreeCalcStatus { NONE, @@ -132,17 +128,12 @@ u64 MemoryStick_FreeSpace(std::string gameID) { const u64 memStickSize = flags.ReportSmallMemstick ? smallMemstickSize : (u64)g_Config.iMemStickSizeGB * 1024 * 1024 * 1024; // Assume the memory stick is only used to store savedata, for the current game only. - u64 currentUse; - { - std::lock_guard guard(memstickCurrentUseLock); - const uint32_t generation = memstickWriteGeneration; - if (memstickCurrentUseGeneration != generation) { - Path saveFolder = GetSysDirectory(DIRECTORY_SAVEDATA); - memstickCurrentUse = ComputeSizeOfSavedataForGame(saveFolder, gameID); - memstickCurrentUseGeneration = generation; - } - currentUse = memstickCurrentUse; + if (memstickCurrentUseGeneration != memstickWriteGeneration) { + Path saveFolder = GetSysDirectory(DIRECTORY_SAVEDATA); + memstickCurrentUse = ComputeSizeOfSavedataForGame(saveFolder, gameID); + memstickCurrentUseGeneration = memstickWriteGeneration; } + const u64 currentUse = memstickCurrentUse; u64 simulatedFreeSpace = 0; if (currentUse < memStickSize) { diff --git a/Core/Instance.cpp b/Core/Instance.cpp index 09cba0c3f5..604793afea 100644 --- a/Core/Instance.cpp +++ b/Core/Instance.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #endif @@ -36,25 +35,41 @@ #include +#include + uint8_t PPSSPP_ID = 0; #if PPSSPP_PLATFORM(WINDOWS) static HANDLE hIDMapFile = nullptr; -static HANDLE mapLock = nullptr; #else static int hIDMapFile = -1; static long BUF_SIZE = 4096; #endif +// Shared between processes. One little-endian word: two pad bytes, then next, then total. Updated with +// CAS, which works across processes on the shared mapping. struct InstanceInfo { - uint8_t pad[2]; - uint8_t next; - uint8_t total; + retro_atomic_int_t word; }; +static uint8_t InstanceNext(uint32_t word) { return (uint8_t)(word >> 16); } +static uint8_t InstanceTotal(uint32_t word) { return (uint8_t)(word >> 24); } +static uint32_t MakeInstanceWord(uint32_t word, uint8_t next, uint8_t total) { + return (word & 0xFFFF) | ((uint32_t)next << 16) | ((uint32_t)total << 24); +} + +// update returns the new word for the old one. It may run more than once; the last run is the one +// that took effect. +static void ApplyInstanceUpdate(InstanceInfo *buf, uint32_t (*update)(uint32_t)) { + uint32_t cur = (uint32_t)retro_atomic_load_acquire_int(&buf->word); + while (!retro_atomic_cas_int(&buf->word, (int)cur, (int)update(cur))) { + cur = (uint32_t)retro_atomic_load_acquire_int(&buf->word); + } +} + #define ID_SHM_NAME "/PPSSPP_ID" -static bool UpdateInstanceCounter(void (*callback)(volatile InstanceInfo *)) { +static bool UpdateInstanceCounter(uint32_t (*update)(uint32_t)) { #if PPSSPP_PLATFORM(WINDOWS) if (!hIDMapFile) { return false; @@ -71,17 +86,9 @@ static bool UpdateInstanceCounter(void (*callback)(volatile InstanceInfo *)) { return false; } - bool result = false; - if (!mapLock || WaitForSingleObject(mapLock, INFINITE) == 0) { - callback(buf); - if (mapLock) { - ReleaseMutex(mapLock); - } - result = true; - } + ApplyInstanceUpdate(buf, update); UnmapViewOfFile(buf); - - return result; + return true; #elif PPSSPP_PLATFORM(ANDROID) || defined(__LIBRETRO__) || PPSSPP_PLATFORM(SWITCH) // TODO: replace shm_open & shm_unlink with ashmem or android-shmem return false; @@ -96,28 +103,19 @@ static bool UpdateInstanceCounter(void (*callback)(volatile InstanceInfo *)) { return false; } - bool result = false; - // An actual advisory lock on the shm object. This used to call mlock(), which only pins - // pages in RAM and provides no mutual exclusion whatsoever - two instances starting at the - // same moment could both read the counter and come away with the same PPSSPP_ID, then both - // believe they were the first instance and write the config over each other. - if (flock(hIDMapFile, LOCK_EX) == 0) { - callback(buf); - flock(hIDMapFile, LOCK_UN); - result = true; - } else { - ERROR_LOG(Log::sceNet, "flock(%s) failure: %s", ID_SHM_NAME, GetLastErrorMsg().c_str()); - } - + // Two instances starting at the same moment must not get the same PPSSPP_ID, so the whole + // update is one CAS. + ApplyInstanceUpdate(buf, update); munmap(buf, BUF_SIZE); - return result; + return true; #endif } int GetInstancePeerCount() { static int c = 0; - UpdateInstanceCounter([](volatile InstanceInfo *buf) { - c = buf->total; + UpdateInstanceCounter([](uint32_t word) { + c = InstanceTotal(word); + return word; }); return c; } @@ -133,8 +131,6 @@ void InitInstanceCounter() { int gran = sysInfo.dwAllocationGranularity ? sysInfo.dwAllocationGranularity : 0x10000; BUF_SIZE = (BUF_SIZE + gran - 1) & ~(gran - 1); - mapLock = CreateMutex(nullptr, FALSE, L"PPSSPP_ID_mutex"); - hIDMapFile = CreateFileMapping( INVALID_HANDLE_VALUE, // use paging file NULL, // default security @@ -163,16 +159,17 @@ void InitInstanceCounter() { } #endif - bool success = UpdateInstanceCounter([](volatile InstanceInfo *buf) { + bool success = UpdateInstanceCounter([](uint32_t word) { // The shared segment outlives the processes that used it (see the shm_unlink comment), // so next keeps climbing across runs and eventually wraps this uint8_t. ID 0 is not a // valid instance - it fails the IsFirstInstance() check, which quietly disables config // saving - so skip past it. - if (++buf->next == 0) { - buf->next = 1; + uint8_t next = InstanceNext(word) + 1; + if (next == 0) { + next = 1; } - PPSSPP_ID = buf->next; - buf->total++; + PPSSPP_ID = next; + return MakeInstanceWord(word, next, InstanceTotal(word) + 1); }); if (!success) { PPSSPP_ID = 1; @@ -180,8 +177,8 @@ void InitInstanceCounter() { } void ShutdownInstanceCounter() { - UpdateInstanceCounter([](volatile InstanceInfo *buf) { - buf->total--; + UpdateInstanceCounter([](uint32_t word) { + return MakeInstanceWord(word, InstanceNext(word), InstanceTotal(word) - 1); }); #if PPSSPP_PLATFORM(WINDOWS) @@ -189,10 +186,6 @@ void ShutdownInstanceCounter() { CloseHandle(hIDMapFile); // If program exited(or crashed?) or the last handle reference closed the shared memory object will be deleted. hIDMapFile = nullptr; } - if (mapLock) { - CloseHandle(mapLock); - mapLock = nullptr; - } #elif PPSSPP_PLATFORM(ANDROID) || defined(__LIBRETRO__) || PPSSPP_PLATFORM(SWITCH) // Do nothing #else diff --git a/Core/KeyMap.cpp b/Core/KeyMap.cpp index 4522f807ac..a4587a9c54 100644 --- a/Core/KeyMap.cpp +++ b/Core/KeyMap.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include "ppsspp_config.h" @@ -40,9 +39,7 @@ namespace KeyMap { -// We actually need to lock g_controllerMap since it can be modified! Crashes will probably be rare though, -// but I've seen one. Let's just protect it with a mutex. -std::recursive_mutex g_controllerMapLock; +// Belongs to the thread that loads and saves the config. KeyMapping g_controllerMap; // Incremented on modification, so we know when to update menus. @@ -601,7 +598,6 @@ const KeyMap::KeyMap_IntStrPair *GetMappableKeys(size_t *count) { bool InputMappingToPspButton(const InputMapping &mapping, std::vector *pspButtons) { bool found = false; - std::lock_guard guard(g_controllerMapLock); for (auto iter = g_controllerMap.begin(); iter != g_controllerMap.end(); ++iter) { for (auto iter2 = iter->second.begin(); iter2 != iter->second.end(); ++iter2) { if (iter2->EqualsSingleMapping(mapping)) { @@ -614,19 +610,7 @@ bool InputMappingToPspButton(const InputMapping &mapping, std::vector *pspB return found; } -void GetAllComboMappingsNoLock(std::vector *combos) { - combos->clear(); - for (const auto &iter : g_controllerMap) { - for (const auto &mapping : iter.second) { - if (mapping.mappings.size() > 1) { - combos->push_back(mapping); - } - } - } -} - -// This is the main workhorse of the ControlMapper. -bool InputMappingsFromPspButtonNoLock(int btn, std::vector *mappings, bool ignoreMouse) { +bool InputMappingsFromPspButton(int btn, std::vector *mappings, bool ignoreMouse) { auto iter = g_controllerMap.find(btn); if (iter == g_controllerMap.end()) { return false; @@ -647,21 +631,7 @@ bool InputMappingsFromPspButtonNoLock(int btn, std::vector *m return mapped; } -bool InputMappingsFromPspButton(int btn, std::vector *mappings, bool ignoreMouse) { - std::lock_guard guard(g_controllerMapLock); - return InputMappingsFromPspButtonNoLock(btn, mappings, ignoreMouse); -} - -void LockMappings() { - g_controllerMapLock.lock(); -} - -void UnlockMappings() { - g_controllerMapLock.unlock(); -} - bool PspButtonHasMappings(int btn) { - std::lock_guard guard(g_controllerMapLock); auto iter = g_controllerMap.find(btn); if (iter == g_controllerMap.end()) { return false; @@ -696,7 +666,6 @@ MappedAnalogAxes MappedAxesForDevice(InputDeviceID deviceId) { }; MappedAnalogAxes result; - std::lock_guard guard(g_controllerMapLock); result.leftX = findAxisIdPair(VIRTKEY_AXIS_X_MIN, VIRTKEY_AXIS_X_MAX); result.leftY = findAxisIdPair(VIRTKEY_AXIS_Y_MIN, VIRTKEY_AXIS_Y_MAX); result.rightX = findAxisIdPair(VIRTKEY_AXIS_RIGHT_X_MIN, VIRTKEY_AXIS_RIGHT_X_MAX); @@ -705,7 +674,6 @@ MappedAnalogAxes MappedAxesForDevice(InputDeviceID deviceId) { } void RemoveButtonMapping(int btn) { - std::lock_guard guard(g_controllerMapLock); for (auto iter = g_controllerMap.begin(); iter != g_controllerMap.end(); ++iter) { if (iter->first == btn) { g_controllerMap.erase(iter); @@ -715,7 +683,6 @@ void RemoveButtonMapping(int btn) { } bool IsKeyMapped(InputDeviceID device, int key) { - std::lock_guard guard(g_controllerMapLock); for (auto &iter : g_controllerMap) { for (auto &mappedKey : iter.second) { if (mappedKey.mappings.contains(InputMapping(device, key))) { @@ -727,7 +694,6 @@ bool IsKeyMapped(InputDeviceID device, int key) { } bool ReplaceSingleKeyMapping(int btn, int index, const MultiInputMapping &key) { - std::lock_guard guard(g_controllerMapLock); // Check for duplicate for (int i = 0; i < (int)g_controllerMap[btn].size(); ++i) { if (i != index && g_controllerMap[btn][i] == key) { @@ -754,7 +720,6 @@ bool ReplaceSingleKeyMapping(int btn, int index, const MultiInputMapping &key) { } void DeleteNthMapping(int key, int number) { - std::lock_guard guard(g_controllerMapLock); auto iter = g_controllerMap.find(key); if (iter != g_controllerMap.end()) { if (number < iter->second.size()) { @@ -765,7 +730,6 @@ void DeleteNthMapping(int key, int number) { } void SetInputMapping(int btn, const MultiInputMapping &key, bool replace) { - std::lock_guard guard(g_controllerMapLock); if (key.empty()) { g_controllerMap.erase(btn); return; @@ -789,7 +753,6 @@ void SetInputMapping(int btn, const MultiInputMapping &key, bool replace) { } void RestoreDefault() { - std::lock_guard guard(g_controllerMapLock); g_controllerMap.clear(); g_controllerMapGeneration++; @@ -833,7 +796,6 @@ void LoadFromIni(IniFile &file) { return; } - std::lock_guard guard(g_controllerMapLock); Section *controls = file.GetOrCreateSection("ControlMapping"); for (size_t i = 0; i < ARRAY_SIZE(psp_button_names); i++) { @@ -866,7 +828,6 @@ void LoadFromIni(IniFile &file) { void SaveToIni(IniFile &file) { Section *controls = file.GetOrCreateSection("ControlMapping"); - std::lock_guard guard(g_controllerMapLock); for (size_t i = 0; i < ARRAY_SIZE(psp_button_names); i++) { std::vector keys; @@ -884,7 +845,6 @@ void SaveToIni(IniFile &file) { } void ClearAllMappings() { - std::lock_guard guard(g_controllerMapLock); g_controllerMap.clear(); g_controllerMapGeneration++; } @@ -917,7 +877,6 @@ bool HasBuiltinController(std::string_view name) { void NotifyPadConnected(InputDeviceID deviceId, std::string_view name) { { - std::lock_guard guard(g_controllerMapLock); g_seenPads.insert(std::string(name)); g_padNames[deviceId] = name; @@ -934,7 +893,6 @@ void NotifyPadConnected(InputDeviceID deviceId, std::string_view name) { void NotifyPadDisconnected(InputDeviceID deviceId) { { - std::lock_guard guard(g_controllerMapLock); auto iter = g_padNames.find(deviceId); if (iter != g_padNames.end()) { auto co = GetI18NCategory(I18NCat::CONTROLS); @@ -948,7 +906,6 @@ void NotifyPadDisconnected(InputDeviceID deviceId) { void ClearControlsWithDeviceId(InputDeviceID deviceId) { bool modified = false; - std::lock_guard guard(g_controllerMapLock); for (auto iter = g_controllerMap.begin(); iter != g_controllerMap.end(); ++iter) { auto &mappings = iter->second; for (auto mapIter = mappings.begin(); mapIter != mappings.end(); ) { @@ -974,7 +931,6 @@ void ClearControlsWithDeviceId(InputDeviceID deviceId) { } void AutoConfForPad(std::string_view name) { - std::lock_guard guard(g_controllerMapLock); InputDeviceID deviceId = DEVICE_ID_PAD_0; for (auto [padDeviceId, padName] : g_padNames) { @@ -1015,12 +971,10 @@ void AutoConfForPad(std::string_view name) { } const std::set &GetSeenPads() { - std::lock_guard guard(g_controllerMapLock); return g_seenPads; } std::string PadName(InputDeviceID deviceId) { - std::lock_guard guard(g_controllerMapLock); auto it = g_padNames.find(deviceId); if (it != g_padNames.end()) return it->second; diff --git a/Core/KeyMap.h b/Core/KeyMap.h index eac575f24a..6a811f3b52 100644 --- a/Core/KeyMap.h +++ b/Core/KeyMap.h @@ -184,13 +184,6 @@ namespace KeyMap { bool InputMappingToPspButton(const InputMapping &mapping, std::vector *pspButtons); bool InputMappingsFromPspButton(int btn, std::vector *keys, bool ignoreMouse); - // Careful with these. - bool InputMappingsFromPspButtonNoLock(int btn, std::vector *keys, bool ignoreMouse); - // Collects every mapping that consists of more than one input, ie. a combo. - void GetAllComboMappingsNoLock(std::vector *combos); - void LockMappings(); - void UnlockMappings(); - // Simplified check. bool PspButtonHasMappings(int btn); diff --git a/Core/MIPS/ARM/ArmAsm.cpp b/Core/MIPS/ARM/ArmAsm.cpp index e5a8f8adf1..aa400fcd90 100644 --- a/Core/MIPS/ARM/ArmAsm.cpp +++ b/Core/MIPS/ARM/ArmAsm.cpp @@ -53,7 +53,6 @@ static const bool disasm = false; // R10 : MIPS state // R11 : Memory base pointer. // R7 : Down counter -extern volatile CoreState coreState; void ShowPC(u32 sp) { ERROR_LOG(Log::JIT, "ShowPC : %08x ArmSP : %08x", currentMIPS->pc, sp); diff --git a/Core/MIPS/ARM64/Arm64Asm.cpp b/Core/MIPS/ARM64/Arm64Asm.cpp index 1fa073b6ce..c2f73f7bf8 100644 --- a/Core/MIPS/ARM64/Arm64Asm.cpp +++ b/Core/MIPS/ARM64/Arm64Asm.cpp @@ -72,7 +72,6 @@ static const bool enableDisasm = false; // x27 : MIPS state (Could eliminate by placing the MIPS state right at the memory base) // x28 : Memory base pointer. -extern volatile CoreState coreState; void ShowPC(u32 downcount, void *membase, void *jitbase) { static int count = 0; diff --git a/Core/MIPS/IR/IRNativeCommon.cpp b/Core/MIPS/IR/IRNativeCommon.cpp index 399089b8dc..898827a456 100644 --- a/Core/MIPS/IR/IRNativeCommon.cpp +++ b/Core/MIPS/IR/IRNativeCommon.cpp @@ -15,9 +15,7 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include #include -#include #include "Common/Profiler/Profiler.h" #include "Common/StringUtils.h" #include "Common/TimeUtil.h" @@ -33,36 +31,16 @@ namespace MIPSComp { // Compile time flag to enable debug stats for not compiled ops. static constexpr bool enableDebugStats = false; -// Compile time flag for enabling the simple IR jit profiler. +// Compile time flag for the jit's profiler hooks (the current PC and status, kept in memory for a +// sampling profiler to read). static constexpr bool enableDebugProfiler = false; // Used only for debugging when enableDebug is true above. static std::map debugSeenNotCompiledIR; static std::map debugSeenNotCompiled; -static std::map, int> debugSeenPCUsage; static double lastDebugStatsLog = 0.0; static constexpr double debugStatsFrequency = 5.0; -static std::thread debugProfilerThread; -std::atomic debugProfilerThreadStatus = false; - -template -class IRProfilerTopValues { -public: - void Add(const std::pair &v, int c) { - for (int i = 0; i < N; ++i) { - if (c > counts[i]) { - counts[i] = c; - values[i] = v; - return; - } - } - } - - int counts[N]{}; - std::pair values[N]{}; -}; - const char *IRProfilerStatusToString(IRProfilerStatus s) { switch (s) { case IRProfilerStatus::NOT_RUNNING: return "NOT_RUNNING"; @@ -79,7 +57,7 @@ const char *IRProfilerStatusToString(IRProfilerStatus s) { } static void LogDebugStats() { - if (!enableDebugStats && !enableDebugProfiler) + if (!enableDebugStats) return; double now = time_now_d(); @@ -107,26 +85,10 @@ static void LogDebugStats() { } debugSeenNotCompiled.clear(); - IRProfilerTopValues<4> slowestPCs; - int64_t totalCount = 0; - for (auto it : debugSeenPCUsage) { - slowestPCs.Add(it.first, it.second); - totalCount += it.second; - } - debugSeenPCUsage.clear(); - if (worstIROp != -1) WARN_LOG(Log::JIT, "Most not compiled IR op: %s (%d)", GetIRMeta((IROp)worstIROp)->name, worstIRVal); if (worstName != nullptr) WARN_LOG(Log::JIT, "Most not compiled op: %s (%d)", worstName, worstVal); - if (slowestPCs.counts[0] != 0) { - for (int i = 0; i < 4; ++i) { - uint32_t pc = slowestPCs.values[i].first; - const char *status = IRProfilerStatusToString(slowestPCs.values[i].second); - const std::string label = g_symbolMap ? g_symbolMap->GetDescription(pc) : ""; - WARN_LOG(Log::JIT, "Slowest sampled PC #%d: %08x (%s)/%s (%f%%)", i, pc, label.c_str(), status, 100.0 * (double)slowestPCs.counts[i] / (double)totalCount); - } - } } bool IRNativeBackend::DebugStatsEnabled() const { @@ -184,10 +146,6 @@ int IRNativeBackend::ReportBadAddress(uint32_t addr, uint32_t alignment, uint32_ IRNativeBackend::IRNativeBackend(IRBlockCache &blocks) : blocks_(blocks) {} IRNativeBackend::~IRNativeBackend() { - if (debugProfilerThreadStatus) { - debugProfilerThreadStatus = false; - debugProfilerThread.join(); - } } void IRNativeBackend::CompileIRInst(IRInst inst) { @@ -492,20 +450,6 @@ void IRNativeJit::Init(IRNativeBackend &backend) { // Wanted this to be a reference, but vtbls get in the way. Shouldn't change. hooks_ = backend.GetNativeHooks(); - - if (enableDebugProfiler && hooks_.profilerPC) { - debugProfilerThreadStatus = true; - debugProfilerThread = std::thread([&] { - // Spin, spin spin... maybe could at least hook into sleeps. - while (debugProfilerThreadStatus) { - IRProfilerStatus stat = *hooks_.profilerStatus; - uint32_t pc = *hooks_.profilerPC; - if (stat != IRProfilerStatus::NOT_RUNNING && stat != IRProfilerStatus::SYSCALL) { - debugSeenPCUsage[std::make_pair(pc, stat)]++; - } - } - }); - } } bool IRNativeJit::CompileNativeBlock(IRBlockCache *irblockCache, int block_num) { @@ -517,7 +461,7 @@ void IRNativeJit::FinalizeNativeBlock(IRBlockCache *irblockCache, int block_num) } void IRNativeJit::RunLoopUntil(u64 globalticks) { - if constexpr (enableDebugStats || enableDebugProfiler) { + if constexpr (enableDebugStats) { LogDebugStats(); } diff --git a/Core/MIPS/JitCommon/JitCommon.cpp b/Core/MIPS/JitCommon/JitCommon.cpp index e9f3811c96..4095284b49 100644 --- a/Core/MIPS/JitCommon/JitCommon.cpp +++ b/Core/MIPS/JitCommon/JitCommon.cpp @@ -18,7 +18,6 @@ #include "ppsspp_config.h" #include -#include #include "ext/disarm.h" #include "ext/riscv-disas.h" diff --git a/Core/MIPS/MIPS.cpp b/Core/MIPS/MIPS.cpp index 828d52170d..a230807b6f 100644 --- a/Core/MIPS/MIPS.cpp +++ b/Core/MIPS/MIPS.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include "ppsspp_config.h" diff --git a/Core/MIPS/MIPSAnalyst.cpp b/Core/MIPS/MIPSAnalyst.cpp index c3d55859f9..21f3e26e3e 100644 --- a/Core/MIPS/MIPSAnalyst.cpp +++ b/Core/MIPS/MIPSAnalyst.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include "ext/cityhash/city.h" #include "Common/File/FileUtil.h" @@ -42,8 +41,9 @@ using namespace MIPSCodeUtils; // Not in a namespace because MSVC's debugger doesn't like it typedef std::vector FunctionsVector; +// functions, hashToFunction and hashMap are the CPU thread's (module loading, and the debugger +// through Core_RunOnCPUThread()). static FunctionsVector functions; -std::recursive_mutex functions_lock; // One function can appear in multiple copies in memory, and they will all have // the same hash and should all be replaced if possible. @@ -753,13 +753,11 @@ namespace MIPSAnalyst { } void Reset() { - std::lock_guard guard(functions_lock); functions.clear(); hashToFunction.clear(); } void UpdateHashToFunctionMap() { - std::lock_guard guard(functions_lock); hashToFunction.clear(); // Really need to detect C++11 features with better defines. #if !PPSSPP_PLATFORM(IOS) @@ -881,7 +879,6 @@ namespace MIPSAnalyst { } void HashFunctions() { - std::lock_guard guard(functions_lock); std::vector buffer; for (auto iter = functions.begin(), end = functions.end(); iter != end; iter++) { @@ -1010,7 +1007,6 @@ skip: _assert_((startAddr & 3) == 0); _assert_((endAddr & 3) == 0); - std::lock_guard guard(functions_lock); FunctionsVector new_functions; @@ -1194,7 +1190,6 @@ skip: _assert_((startAddr & 3) == 0); _assert_((size & 3) == 0); - std::lock_guard guard(functions_lock); // Check if we have this already for (auto iter = functions.begin(); iter != functions.end(); iter++) { @@ -1229,7 +1224,6 @@ skip: // endAddr is exclusive. void ForgetFunctions(u32 startAddr, u32 endAddr) { - std::lock_guard guard(functions_lock); _assert_((startAddr & 3) == 0); _assert_((endAddr & 3) == 0); @@ -1269,7 +1263,6 @@ skip: } bool GetAnalyzedFunctionAt(u32 addr, AnalyzedFunction *out) { - std::lock_guard guard(functions_lock); for (auto iter = functions.begin(), end = functions.end(); iter != end; ++iter) { if (iter->start <= addr && iter->end >= addr) { *out = *iter; @@ -1280,13 +1273,11 @@ skip: } void RehashFunctions() { - std::lock_guard guard(functions_lock); HashFunctions(); UpdateHashToFunctionMap(); } void ReplaceFunctions() { - std::lock_guard guard(functions_lock); for (size_t i = 0; i < functions.size(); i++) { WriteReplaceInstructions(functions[i].start, functions[i].hash, functions[i].size); @@ -1294,7 +1285,6 @@ skip: } void UpdateHashMap() { - std::lock_guard guard(functions_lock); for (auto it = functions.begin(), end = functions.end(); it != end; ++it) { const AnalyzedFunction &f = *it; diff --git a/Core/MIPS/x86/Asm.cpp b/Core/MIPS/x86/Asm.cpp index fc450ec226..58d6886230 100644 --- a/Core/MIPS/x86/Asm.cpp +++ b/Core/MIPS/x86/Asm.cpp @@ -36,7 +36,6 @@ using namespace Gen; using namespace X64JitConstants; -extern volatile CoreState coreState; namespace MIPSComp { diff --git a/Core/MemMap.cpp b/Core/MemMap.cpp index 4fd9f969c0..ef20342709 100644 --- a/Core/MemMap.cpp +++ b/Core/MemMap.cpp @@ -22,7 +22,6 @@ #endif #include -#include #include "Common/CommonTypes.h" #include "Common/MemArena.h" diff --git a/Core/PSPLoaders.cpp b/Core/PSPLoaders.cpp index 091bdcc95f..599b0e245f 100644 --- a/Core/PSPLoaders.cpp +++ b/Core/PSPLoaders.cpp @@ -15,7 +15,6 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include #include "Core/Core.h" #include "Common/System/Request.h" @@ -49,6 +48,7 @@ #include "Core/System.h" #include "Core/PSPLoaders.h" #include "Core/HLE/sceKernelModule.h" +#include "Common/Thread/Thread.h" static void UseLargeMem(int memsize) { if (memsize != 1) { @@ -68,7 +68,7 @@ void DumpBlockDeviceAsync(std::shared_ptr bd, Path destPath, std::s NPDRMDemoBlockDevice *npdrmDemoBD = dynamic_cast(bd.get()); if (npdrmDemoBD) { INFO_LOG(Log::System, "Dumping NPDRM demo ISO... (%s)", destPath.c_str()); - std::thread dumpThread([bd, title, destPath]() { + Thread dumpThread([bd, title, destPath]() { File::CreateFullPath(destPath.NavigateUp()); if (File::Exists(destPath)) { INFO_LOG(Log::System, "Dump file already exists, skipping: %s", destPath.c_str()); diff --git a/Core/Reporting.cpp b/Core/Reporting.cpp index 2121462ad7..05380baa39 100644 --- a/Core/Reporting.cpp +++ b/Core/Reporting.cpp @@ -18,10 +18,6 @@ #include "ppsspp_config.h" #include -#include -#include -#include -#include #include #include #include @@ -31,6 +27,8 @@ extern "C" { #include } +#include + #include "Core/Reporting.h" #include "Common/File/VFS/VFS.h" #include "Common/CPUDetect.h" @@ -60,6 +58,8 @@ extern "C" { #include "Core/HW/Display.h" #include "GPU/GPUCommon.h" #include "GPU/GPUState.h" +#include "Common/Thread/ParkingLot.h" +#include "Common/Thread/Thread.h" namespace Reporting { @@ -67,14 +67,14 @@ namespace Reporting const u32 SPAM_LIMIT = 100; const int PAYLOAD_BUFFER_SIZE = 200; - // Internal limiter on number of requests per instance. - static u32 spamProtectionCount = 0; + // Internal limiter on number of requests per instance. Bumped by whichever thread logs. + static retro_atomic_int_t spamProtectionCount{ 0 }; // Keeps track of whether a harmful setting was ever used. static bool everUnsupported = false; // Support is cached here to avoid checking it on every single request. static bool currentSupported = false; // Whether the most recent server request seemed successful. - static bool serverWorking = true; + static retro_atomic_int_t serverWorking{ 1 }; // The latest compatibility result from the server. static std::vector lastCompatResult; @@ -95,17 +95,20 @@ namespace Reporting int int1; int int2; int int3; + std::vector icon; }; - static std::mutex crcLock; - static std::condition_variable crcCond; + // The CRC thread computes one CRC at a time. crcFilename and crcResults belong to the thread + // that calls the functions below (the emulation thread); the CRC thread reads crcFilename, + // writes crcValue, then clears crcPending. static Path crcFilename; static std::map crcResults; - static std::atomic crcPending{}; - static std::atomic crcCancel{}; - static std::thread crcThread; + static u32 crcValue; + static retro_atomic_int_t crcPending{ 0 }; + static retro_atomic_int_t crcCancel{ 0 }; + static Thread crcThread; - static u32 CalculateCRC(BlockDevice *blockDevice, std::atomic *cancel) { + static u32 CalculateCRC(BlockDevice *blockDevice, retro_atomic_int_t *cancel) { auto ga = GetI18NCategory(I18NCat::GAME); u32 crc = 0; @@ -113,7 +116,7 @@ namespace Reporting u8 block[2048]; u32 numBlocks = blockDevice->GetNumBlocks(); for (u32 i = 0; i < numBlocks; ++i) { - if (cancel && *cancel) { + if (cancel && retro_atomic_load_acquire_int(cancel)) { g_OSD.RemoveProgressBar("crc", false, 0.0f); return 0; } @@ -130,20 +133,22 @@ namespace Reporting return crc; } + static void FinishCRCThread(u32 crc) { + crcValue = crc; + retro_atomic_store_release_int(&crcPending, 0); + ParkingLotNotify(&crcPending); + } + static int CalculateCRCThread() { SetCurrentThreadName("ReportCRC"); - IdentifiedFileType type; std::string errorString; FileLoader *fileLoader = ResolveFileLoaderTarget(ConstructFileLoader(crcFilename), &type, &errorString); if (!fileLoader) { ERROR_LOG(Log::Loader, "Failed to construct file loader for CRC: %s", errorString.c_str()); - std::lock_guard guard(crcLock); - crcResults[crcFilename] = 0; - crcPending = false; - crcCond.notify_one(); + FinishCRCThread(0); return 0; } @@ -159,15 +164,21 @@ namespace Reporting blockDevice.reset(); delete fileLoader; - std::lock_guard guard(crcLock); - crcResults[crcFilename] = crc; - crcPending = false; - crcCond.notify_one(); + FinishCRCThread(crc); return 0; } + // Picks up the result of a finished CRC thread, if there is one. + static void CollectCRC() { + if (crcThread.joinable() && !retro_atomic_load_acquire_int(&crcPending)) { + crcThread.join(); + crcResults[crcFilename] = crcValue; + INFO_LOG(Log::System, "Finished CRC calculation"); + } + } + void QueueCRC(const Path &gamePath) { - std::lock_guard guard(crcLock); + CollectCRC(); auto it = crcResults.find(gamePath); if (it != crcResults.end()) { @@ -176,38 +187,33 @@ namespace Reporting return; } - if (crcPending) { + if (crcThread.joinable()) { // Already in process. This is OK - on the crash screen we call this in a polling fashion. return; } INFO_LOG(Log::System, "Starting CRC calculation"); crcFilename = gamePath; - crcPending = true; - crcCancel = false; - crcThread = std::thread(CalculateCRCThread); + retro_atomic_store_release_int(&crcCancel, 0); + retro_atomic_store_release_int(&crcPending, 1); + crcThread = Thread(CalculateCRCThread); } bool HasCRC(const Path &gamePath) { - std::lock_guard guard(crcLock); + CollectCRC(); return crcResults.find(gamePath) != crcResults.end(); } uint32_t RetrieveCRC(const Path &gamePath) { - QueueCRC(gamePath); - - std::unique_lock guard(crcLock); - auto it = crcResults.find(gamePath); - while (it == crcResults.end()) { - crcCond.wait(guard); - it = crcResults.find(gamePath); + for (;;) { + QueueCRC(gamePath); + auto it = crcResults.find(gamePath); + if (it != crcResults.end()) { + return it->second; + } + // Running, for this path or (then QueueCRC starts ours next lap) another one. + ParkingLotWait(&crcPending, [] { return retro_atomic_load_acquire_int(&crcPending) == 0; }); } - - if (crcThread.joinable()) { - INFO_LOG(Log::System, "Finished CRC calculation"); - crcThread.join(); - } - return it->second; } static uint32_t RetrieveCRCUnlessPowerSaving(const Path &gamePath) { @@ -220,19 +226,16 @@ namespace Reporting } static void PurgeCRC() { - std::unique_lock guard(crcLock); - if (crcPending) { - INFO_LOG(Log::System, "Cancelling CRC calculation"); - crcCancel = true; - while (crcPending) { - crcCond.wait(guard); + if (crcThread.joinable()) { + if (retro_atomic_load_acquire_int(&crcPending)) { + INFO_LOG(Log::System, "Cancelling CRC calculation"); + retro_atomic_store_release_int(&crcCancel, 1); + ParkingLotWait(&crcPending, [] { return retro_atomic_load_acquire_int(&crcPending) == 0; }); } + CollectCRC(); } else { DEBUG_LOG(Log::System, "No CRC pending"); } - - if (crcThread.joinable()) - crcThread.join(); } void CancelCRC() { @@ -300,7 +303,7 @@ namespace Reporting // Should only be called once per request. bool CheckSpamLimited() { - return ++spamProtectionCount >= SPAM_LIMIT; + return (u32)retro_atomic_fetch_add_int(&spamProtectionCount, 1) + 1 >= SPAM_LIMIT; } static void SendReportRequest(const char *uri, const std::string &data, const std::string &mimeType, std::function callback) { @@ -361,7 +364,7 @@ namespace Reporting void Init() { // New game, clean slate. - spamProtectionCount = 0; + retro_atomic_store_release_int(&spamProtectionCount, 0); ResetCounts(); everUnsupported = false; currentSupported = IsSupported(); @@ -465,17 +468,15 @@ namespace Reporting postdata.Add("savestate_used", SaveState::HasLoadedState()); } - void AddScreenshotData(MultipartFormDataEncoder &postdata, const Path &filename) + void AddScreenshotData(MultipartFormDataEncoder &postdata, const Path &filename, const std::vector &icon) { std::string data; if (!filename.empty() && File::ReadBinaryFileToString(filename, &data)) { postdata.Add("screenshot", data, "screenshot.jpg", "image/jpeg"); } - const std::string iconFilename = "disc0:/PSP_GAME/ICON0.PNG"; - std::vector iconData; - if (pspFileSystem.ReadEntireFile(iconFilename, iconData) >= 0) { - postdata.Add("icon", iconData, "icon.png", "image/png"); + if (!icon.empty()) { + postdata.Add("icon", icon, "icon.png", "image/png"); } } @@ -498,7 +499,7 @@ namespace Reporting postdata.Finish(); SendReportRequest("/report/message", postdata.ToString(), postdata.GetMimeType(), [=](http::Request &req) { - serverWorking = !req.Failed(); + retro_atomic_store_release_int(&serverWorking, req.Failed() ? 0 : 1); }); break; @@ -511,22 +512,22 @@ namespace Reporting postdata.Add("gameplay", StringFromFormat("%d", payload.int3)); postdata.Add("crc", StringFromFormat("%08x", RetrieveCRCUnlessPowerSaving(PSP_CoreParameter().fileToStart))); postdata.Add("suggestions", payload.string1 != "perfect" && payload.string1 != "playable" ? "1" : "0"); - AddScreenshotData(postdata, Path(payload.string2)); + AddScreenshotData(postdata, Path(payload.string2), payload.icon); postdata.Finish(); - serverWorking = true; + retro_atomic_store_release_int(&serverWorking, 1); SendReportRequest("/report/compat", postdata.ToString(), postdata.GetMimeType(), [=](http::Request &req) { if (req.Failed()) { - serverWorking = false; + retro_atomic_store_release_int(&serverWorking, 0); return; } - serverWorking = true; + retro_atomic_store_release_int(&serverWorking, 1); std::string result; req.buffer().TakeAll(&result); lastCompatResult.clear(); if (result.empty() || result[0] == '0') - serverWorking = false; + retro_atomic_store_release_int(&serverWorking, 0); else if (result[0] != '1') SplitString(result, '\n', lastCompatResult); }); @@ -586,7 +587,7 @@ namespace Reporting } ReportStatus GetStatus() { - if (!serverWorking) + if (!retro_atomic_load_acquire_int(&serverWorking)) return ReportStatus::FAILING; return ReportStatus::WORKING; } @@ -608,10 +609,14 @@ namespace Reporting Process(payload); } + // Emulation thread: pspFileSystem is read here. void ReportCompatibility(const char *compat, int graphics, int speed, int gameplay, const std::string &screenshotFilename) { if (!IsEnabled()) return; Payload payload{}; + if (pspFileSystem.ReadEntireFile("disc0:/PSP_GAME/ICON0.PNG", payload.icon) < 0) { + payload.icon.clear(); + } payload.type = RequestType::COMPAT; payload.string1 = compat; payload.string2 = screenshotFilename; diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index ed96079923..66aae716fe 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -17,12 +17,13 @@ #include #include -#include -#include #include #include #include "Common/Data/Text/I18n.h" +#include + +#include "Common/Thread/MpscQueue.h" #include "Common/Thread/ThreadUtil.h" #include "Common/Data/Text/Parsers.h" #include "Common/System/System.h" @@ -81,9 +82,9 @@ namespace SaveState { // Used for "confirm exit if you haven't saved in a while" double g_lastSaveTime = -1.0; -static bool needsProcess = false; +// Set by Enqueue on any thread, taken by Process on the emulation thread. +static retro_atomic_int_t needsProcess{ 0 }; static bool needsRestart = false; -static std::mutex mutex; static bool hasLoadedState = false; static const int STALE_STATE_USES = 2; // 4 hours of total gameplay since the virtual PSP started the game. @@ -121,9 +122,10 @@ struct Operation { int slot; }; -static std::vector g_pendingOperations; +static MpscQueue g_pendingOperations; -int g_screenshotFailures; +// Bumped from the screenshot callback, which can run on the render thread. +static retro_atomic_int_t g_screenshotFailures{ 0 }; CChunkFileReader::Error SaveToRam(std::vector &data) { SaveStart state; @@ -142,9 +144,7 @@ int g_screenshotFailures; void SaveStart::DoState(PointerWrap &p) { // Nothing may still be writing PSP memory while it's saved, or be left to write into what's loaded. - __UtilityWaitForIO(); __SasWaitForMix(); - __IoWaitForAsync(); if (gpu) gpu->FlushPendingDrawing(); @@ -225,12 +225,11 @@ int g_screenshotFailures; return; } - std::lock_guard guard(mutex); - g_pendingOperations.push_back(op); + g_pendingOperations.Push(op); // Don't actually run it until next frame. // It's possible there might be a duplicate but it won't hurt us. - needsProcess = true; + retro_atomic_store_release_int(&needsProcess, 1); } void Load(const Path &filename, int slot, Callback callback) { @@ -391,8 +390,7 @@ int g_screenshotFailures; } static void ScheduleSaveScreenshot(const Path &path) { - std::lock_guard guard(mutex); - g_screenshotFailures = 0; + retro_atomic_store_relaxed_int(&g_screenshotFailures, 0); // Savestate thumbnails don't need to be bigger. constexpr int maxResMultiplier = 2; @@ -401,14 +399,14 @@ int g_screenshotFailures; case ScreenshotResult::ScreenshotNotPossible: // Try again soon, for a short while. WARN_LOG(Log::SaveState, "Failed to take a screenshot for the savestate! (%s) The savestate will lack an icon.", path.c_str()); - if (coreState != CORE_STEPPING_CPU && g_screenshotFailures++ < SCREENSHOT_FAILURE_RETRIES) { + if (coreState != CORE_STEPPING_CPU && retro_atomic_fetch_add_int(&g_screenshotFailures, 1) < SCREENSHOT_FAILURE_RETRIES) { // Requeue for next frame (if we were stepping, no point, will just spam errors quickly). ScheduleSaveScreenshot(path); } break; case ScreenshotResult::FailedToWriteFile: case ScreenshotResult::Success: - g_screenshotFailures = 0; + retro_atomic_store_relaxed_int(&g_screenshotFailures, 0); break; } }); @@ -764,10 +762,10 @@ int g_screenshotFailures; } std::vector Flush() { - std::lock_guard guard(mutex); - std::vector copy = g_pendingOperations; - g_pendingOperations.clear(); - + std::vector copy; + g_pendingOperations.Drain([&](Operation &&op) { + copy.push_back(std::move(op)); + }); return copy; } @@ -854,14 +852,14 @@ int g_screenshotFailures; void Process() { rewindStates.Process(); - if (!needsProcess) + if (!retro_atomic_load_acquire_int(&needsProcess)) return; if (coreState == CORE_STEPPING_GE || coreState == CORE_RUNNING_GE) { // A display list stopped in the GE debugger still belongs to the sceGe call that started // it, which finishes when the list does. Wait for that. return; } - needsProcess = false; + retro_atomic_store_relaxed_int(&needsProcess, 0); if (!__KernelIsRunning()) { ERROR_LOG(Log::SaveState, "Savestate failure: Unable to load without kernel, this should never happen."); @@ -1044,7 +1042,6 @@ int g_screenshotFailures; // Make sure there's a directory for save slots File::CreateFullPath(GetSysDirectory(DIRECTORY_SAVESTATE)); - std::lock_guard guard(mutex); rewindStates.Clear(); hasLoadedState = false; @@ -1057,7 +1054,6 @@ int g_screenshotFailures; } void Shutdown() { - std::lock_guard guard(mutex); rewindStates.Clear(); } diff --git a/Core/SaveStateRewind.cpp b/Core/SaveStateRewind.cpp index d2b059d22e..619229cf8d 100644 --- a/Core/SaveStateRewind.cpp +++ b/Core/SaveStateRewind.cpp @@ -5,6 +5,7 @@ #include "Core/SaveStateRewind.h" #include "Core/Core.h" #include "Core/Config.h" +#include "Common/Thread/Thread.h" namespace SaveState { @@ -16,8 +17,6 @@ CChunkFileReader::Error StateRingbuffer::Save() { if (compressThread_.joinable()) compressThread_.join(); - std::lock_guard guard(lock_); - int n = next_++ % size_; if ((next_ % size_) == first_) ++first_; @@ -48,7 +47,9 @@ CChunkFileReader::Error StateRingbuffer::Save() { } CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString, std::string *metadata) { - std::lock_guard guard(lock_); + // The state being compressed may be the one restored. + if (compressThread_.joinable()) + compressThread_.join(); // No valid states left. if (Empty()) @@ -70,7 +71,7 @@ CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString, std:: } static std::vector buffer; - LockedDecompress(buffer, states_[n].stateBuffer, bases_[baseSlot]); + Decompress(buffer, states_[n].stateBuffer, bases_[baseSlot]); CChunkFileReader::Error error = LoadFromRam(buffer, errorString); *metadata = pa->T("Rewind"); @@ -88,7 +89,7 @@ CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString, std:: void StateRingbuffer::ScheduleCompress(std::vector *result, const std::vector *state, const std::vector *base) { if (compressThread_.joinable()) compressThread_.join(); - compressThread_ = std::thread([=] { + compressThread_ = Thread([=] { SetCurrentThreadName("SaveStateCompress"); // Should do no I/O, so no JNI thread context needed. @@ -97,11 +98,6 @@ void StateRingbuffer::ScheduleCompress(std::vector *result, const std::vecto } void StateRingbuffer::Compress(std::vector &result, const std::vector &state, const std::vector &base) { - std::lock_guard guard(lock_); - // Bail if we were cleared before locking. - if (first_ == 0 && next_ == 0) - return; - double start_time = time_now_d(); result.clear(); result.reserve(512 * 1024); @@ -120,7 +116,7 @@ void StateRingbuffer::Compress(std::vector &result, const std::vector &s DEBUG_LOG(Log::SaveState, "Rewind: Compressed save from %d bytes to %d in %0.2f ms.", (int)state.size(), (int)result.size(), taken_s * 1000.0); } -void StateRingbuffer::LockedDecompress(std::vector &result, const std::vector &compressed, const std::vector &base) { +void StateRingbuffer::Decompress(std::vector &result, const std::vector &compressed, const std::vector &base) { result.clear(); result.reserve(base.size()); auto basePos = base.begin(); @@ -153,8 +149,6 @@ void StateRingbuffer::Clear() { if (compressThread_.joinable()) compressThread_.join(); - // This lock is mainly for shutdown. - std::lock_guard guard(lock_); first_ = 0; next_ = 0; for (auto &b : bases_) { diff --git a/Core/SaveStateRewind.h b/Core/SaveStateRewind.h index da6a2c1afa..7c7bf2048f 100644 --- a/Core/SaveStateRewind.h +++ b/Core/SaveStateRewind.h @@ -1,10 +1,9 @@ #pragma once -#include -#include #include "Common/Serialize/Serializer.h" #include "Common/CommonTypes.h" #include "Common/TimeUtil.h" +#include "Common/Thread/Thread.h" namespace SaveState { @@ -12,7 +11,7 @@ namespace SaveState { // Save states are compressed against one of two reference saves (bases_), and the reference // is switched to a fresh save every N saves, where N is BASE_USAGE_INTERVAL. // The compression is a simple block based scheme where 0 means to copy a block from the base, -// and 1 means that the following bytes are the next block. See Compress/LockedDecompress. +// and 1 means that the following bytes are the next block. See Compress/Decompress. class StateRingbuffer { public: StateRingbuffer() { @@ -31,7 +30,7 @@ public: CChunkFileReader::Error Restore(std::string *errorString, std::string *metadata); void ScheduleCompress(std::vector *result, const std::vector *state, const std::vector *base); void Compress(std::vector &result, const std::vector &state, const std::vector &base); - void LockedDecompress(std::vector &result, const std::vector &compressed, const std::vector &base); + void Decompress(std::vector &result, const std::vector &compressed, const std::vector &base); void Clear(); bool Empty() const { @@ -70,8 +69,9 @@ private: int baseGeneration_[2] = {-1, -1}; int nextBaseGeneration_ = 0; std::vector baseMapping_; - std::mutex lock_; - std::thread compressThread_; + // Emulation thread only. The compress thread only touches the buffers handed to it, and + // everything that reads or replaces those joins it first. + Thread compressThread_; std::vector buffer_; int base_ = -1; diff --git a/Core/System.cpp b/Core/System.cpp index ce033480a8..8e1bbfffe4 100644 --- a/Core/System.cpp +++ b/Core/System.cpp @@ -15,7 +15,6 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include #include "ppsspp_config.h" #ifdef _WIN32 @@ -25,7 +24,7 @@ #include #endif -#include +#include #include "ext/lua/lapi.h" @@ -76,6 +75,7 @@ #include "GPU/GPUCommon.h" #include "GPU/Debugger/Playback.h" #include "GPU/Debugger/RecordFormat.h" +#include "Common/Thread/Thread.h" enum CPUThreadState { CPU_THREAD_NOT_RUNNING, @@ -89,24 +89,29 @@ static GlobalUIState globalUIState; CoreParameter g_CoreParameter; static FileLoader *g_loadedFile; // For background loading thread. -static std::mutex loadingLock; -static std::thread g_loadingThread; +static Thread g_loadingThread; bool g_coreCollectDebugStats = false; static int g_coreCollectDebugStatsCounter = 0; -static volatile CPUThreadState cpuThreadState = CPU_THREAD_NOT_RUNNING; - static GPUBackend gpuBackend; static std::string gpuBackendDevice; static bool g_fileLoggingWasEnabled; // Atomic because it's read as a fast-fail from the WebSocket debugger's own thread while the // CPU and loader threads move it along. -static std::atomic g_bootState = BootState::Off; +static retro_atomic_int_t g_bootState{ (int)BootState::Off }; + +static BootState GetBootState() { + return (BootState)retro_atomic_load_acquire_int(&g_bootState); +} + +static void SetBootState(BootState state) { + retro_atomic_store_release_int(&g_bootState, (int)state); +} BootState PSP_GetBootState() { - return g_bootState; + return GetBootState(); } FileLoader *PSP_LoadedFile() { @@ -694,7 +699,7 @@ static bool InitGPU(std::string *error_string) { if (!success) { *error_string = "Unable to initialize rendering engine."; CPU_Shutdown(false); - g_bootState = BootState::Failed; + SetBootState(BootState::Failed); return false; } } @@ -702,14 +707,14 @@ static bool InitGPU(std::string *error_string) { } bool PSP_InitStart(const CoreParameter &coreParam) { - if (g_bootState != BootState::Off) { + if (GetBootState() != BootState::Off) { ERROR_LOG(Log::Loader, "Can't start loader thread - already on."); return false; } IncrementDebugCounter(DebugCounter::GAME_BOOT); - g_bootState = BootState::Booting; + SetBootState(BootState::Booting); GraphicsContext *temp = g_CoreParameter.graphicsContext; g_CoreParameter = coreParam; @@ -727,7 +732,7 @@ bool PSP_InitStart(const CoreParameter &coreParam) { Core_NotifyLifecycle(CoreLifecycle::STARTING); - g_loadingThread = std::thread([errorString]() { + g_loadingThread = Thread([errorString]() { SetCurrentThreadName("ExecLoader"); @@ -763,21 +768,13 @@ bool PSP_InitStart(const CoreParameter &coreParam) { if (errorString->empty()) { *errorString = "Failed initializing CPU/Memory"; } - g_bootState = BootState::Failed; + SetBootState(BootState::Failed); return; } - // Initialize the GPU as far as we can here (do things like load cache files). + // The GPU is created in PSP_InitFinish(), on the thread that goes on to render with it. _dbg_assert_(!gpu); -#ifndef __LIBRETRO__ - // Must not stamp Complete over the Failed that InitGPU sets - it has already run - // CPU_Shutdown(), so PSP_InitUpdate would take the success path on a core that no longer - // exists, right down to a null Memory::base. - if (!InitGPU(errorString)) { - return; - } -#endif - g_bootState = BootState::Complete; + SetBootState(BootState::Complete); }); return true; @@ -785,26 +782,24 @@ bool PSP_InitStart(const CoreParameter &coreParam) { // The loader thread has been joined; finishes on the calling thread. static BootState PSP_InitFinish(std::string *error_string) { - const BootState bootState = g_bootState; + const BootState bootState = GetBootState(); _dbg_assert_(bootState == BootState::Complete || bootState == BootState::Failed); if (bootState == BootState::Failed) { // Failed! (Note: PSP_Shutdown was already called on the loader thread). Core_NotifyLifecycle(CoreLifecycle::START_COMPLETE); *error_string = g_CoreParameter.errorString; - g_bootState = BootState::Off; + SetBootState(BootState::Off); return BootState::Failed; } -#ifdef __LIBRETRO__ if (!InitGPU(error_string)) { // Same as the Failed branch above - the core is already gone. Core_NotifyLifecycle(CoreLifecycle::START_COMPLETE); *error_string = g_CoreParameter.errorString; - g_bootState = BootState::Off; + SetBootState(BootState::Off); return BootState::Failed; } -#endif // Ok, async part of the boot completed, let's finish up things on the main thread. if (gpu) { @@ -820,7 +815,7 @@ static BootState PSP_InitFinish(std::string *error_string) { } BootState PSP_InitUpdate(std::string *error_string) { - const BootState bootState = g_bootState; + const BootState bootState = GetBootState(); if (bootState == BootState::Booting || bootState == BootState::Off) { // Nothing to do right now. @@ -839,7 +834,7 @@ BootState PSP_InitUpdate(std::string *error_string) { BootState PSP_Init(const CoreParameter &coreParam, std::string *error_string) { // InitStart doesn't really fail anymore. if (!PSP_InitStart(coreParam)) { - g_bootState = BootState::Off; + SetBootState(BootState::Off); return BootState::Failed; } @@ -847,26 +842,26 @@ BootState PSP_Init(const CoreParameter &coreParam, std::string *error_string) { } BootState PSP_InitWait(std::string *error_string) { - if (g_bootState == BootState::Off) + if (GetBootState() == BootState::Off) return BootState::Off; // Every path out of the loader thread sets Complete or Failed. - _assert_msg_(g_loadingThread.joinable(), "bootstate: %d", (int)g_bootState.load()); + _assert_msg_(g_loadingThread.joinable(), "bootstate: %d", (int)GetBootState()); g_loadingThread.join(); return PSP_InitFinish(error_string); } BootState PollBootState() { - return g_bootState; + return GetBootState(); } void PSP_Shutdown(bool success) { // Do nothing if we never inited. - if (g_bootState == BootState::Off) { + if (GetBootState() == BootState::Off) { ERROR_LOG(Log::Loader, "Unexpected PSP_Shutdown"); return; } - _assert_(g_bootState != BootState::Failed); + _assert_(GetBootState() != BootState::Failed); Core_Stop(); @@ -874,7 +869,7 @@ void PSP_Shutdown(bool success) { MIPSAnalyst::StoreHashMap(); } - if (g_bootState == BootState::Booting) { + if (GetBootState() == BootState::Booting) { // This should only happen during failures. Core_NotifyLifecycle(CoreLifecycle::START_COMPLETE); } @@ -896,7 +891,7 @@ void PSP_Shutdown(bool success) { Core_NotifyLifecycle(CoreLifecycle::STOPPED); if (success) { - g_bootState = BootState::Off; + SetBootState(BootState::Off); } IncrementDebugCounter(DebugCounter::GAME_SHUTDOWN); diff --git a/Core/Util/GameDB.cpp b/Core/Util/GameDB.cpp index 733ae906ae..6e4b5586d2 100644 --- a/Core/Util/GameDB.cpp +++ b/Core/Util/GameDB.cpp @@ -59,7 +59,6 @@ static void SplitSV(std::string_view strv, char delim, bool removeWhiteSpace, st } } -// Call this with the mutex locked. void GameDB::LoadIfNeeded() { if (loaded_) { // Already loaded @@ -148,8 +147,6 @@ bool GameDB::GetGameInfos(std::string_view id, std::vector *infos) { return false; } - std::lock_guard guard(loadMutex_); - LoadIfNeeded(); for (const auto &line : lines_) { diff --git a/Core/Util/GameDB.h b/Core/Util/GameDB.h index 853b808363..a98622e4e8 100644 --- a/Core/Util/GameDB.h +++ b/Core/Util/GameDB.h @@ -4,7 +4,6 @@ #include #include #include -#include class VFSInterface; @@ -42,8 +41,6 @@ private: std::vector lines_; std::vector columns_; bool loaded_ = false; - - std::mutex loadMutex_; }; extern GameDB g_gameDB; diff --git a/Core/Util/PortManager.cpp b/Core/Util/PortManager.cpp index 0e495c56eb..437d3baf38 100644 --- a/Core/Util/PortManager.cpp +++ b/Core/Util/PortManager.cpp @@ -20,18 +20,18 @@ // All credit goes to him and the official miniupnp project! http://miniupnp.free.fr/ // Threading model: everything in PortManager runs on the UPnP service thread, which exists only -// while the UPnP setting is on. Other threads only ever push requests onto g_upnpReqs -// (UPnP_Add/UPnP_Remove) or poke the condition variable (UPnP_Notify). Talking to a router means -// blocking socket I/O with multi-second timeouts, so none of it may happen on a thread anyone -// waits for - and the thread must never end up in a state where it can't be told to stop. +// while the UPnP setting is on. Other threads only push requests into g_upnpInbox +// (UPnP_Add/UPnP_Remove) or wake the thread (UPnP_Notify). Talking to a router means blocking +// socket I/O with multi-second timeouts, so none of it may happen on a thread anyone waits for - +// and the thread must never end up in a state where it can't be told to stop. The Thread object +// belongs to the thread that called __UPnPInit (the emulation thread): only it starts and joins. #include // find_if #include #include #include -#include -#include -#include + +#include #include "Common/TimeUtil.h" #include "Common/Data/Text/I18n.h" @@ -44,32 +44,36 @@ #include "Core/System.h" #include "Core/ELF/ParamSFO.h" #include "Core/Util/PortManager.h" +#include "Common/Thread/Thread.h" +#include "Common/Thread/MpscQueue.h" +#include "Common/Thread/ParkingLot.h" PortManager g_PortManager; -// Guards g_upnpServiceThread itself, so that a start racing a start (or a shutdown) can't end up -// with two service threads or a std::thread being reassigned while it's still running. -static std::mutex g_upnpThreadLock; -static std::thread g_upnpServiceThread; -static bool g_upnpInitialized; +enum UPnPThreadState { + UPNP_THREAD_NONE, + UPNP_THREAD_RUNNING, + // Exited by itself (the setting went off). Joined by the owner on the next start or shutdown. + UPNP_THREAD_EXITED, +}; -static std::mutex g_upnpLock; -static std::condition_variable g_upnpCond; - -// All of the following are guarded by g_upnpLock. -static std::deque g_upnpReqs; +static Thread g_upnpServiceThread; +static uintptr_t g_upnpOwnerThread; +static retro_atomic_int_t g_upnpThreadState{ UPNP_THREAD_NONE }; +static retro_atomic_int_t g_upnpInitialized{ 0 }; +static retro_atomic_int_t g_upnpExit{ 0 }; static unsigned int g_upnpTimeout = 2000; -static bool g_upnpExit; -// Whether a service thread is up. The thread only exists while the setting is on, so most users -// never pay for one at all - it used to be started unconditionally and parked for the session. -static bool g_upnpThreadRunning; -// Bumped whenever there's a reason for the service thread to wake up and look around. The thread -// snapshots it before doing (slow) work, so a request that arrives while it's busy can't be missed -// and leave it asleep with something queued. -static uint32_t g_upnpWakeSeq; + +// Requests from any thread. The service thread moves them into g_upnpReqs, which only it touches. +static MpscQueue g_upnpInbox; +static retro_atomic_int_t g_upnpInboxCount{ 0 }; +static std::deque g_upnpReqs; +// Wakes the service thread. It reads Seen() before doing (slow) work, so a request that arrives +// while it's busy can't be missed and leave it asleep with something queued. +static EventCounter g_upnpWork; // Set by UPnP_Notify() - the user flipped a setting, so retry immediately instead of continuing to // back off from earlier failures. -static bool g_upnpResetBackoff; +static retro_atomic_int_t g_upnpResetBackoff{ 0 }; // A game hammering bind() shouldn't be able to grow the queue without bound, especially while // UPnP is unreachable and nothing is being drained. @@ -442,8 +446,28 @@ bool PortManager::RefreshPortList() { // --- Service thread --- +static bool UPnPExiting() { + return retro_atomic_load_acquire_int(&g_upnpExit) != 0; +} + +// Last request wins for a given port: this collapses the repeated adds that games produce when +// they rebind in a loop, and lets a remove cancel an add that hasn't been sent yet. +static void TakeInbox() { + g_upnpInbox.Drain([](UPnPArgs &&args) { + retro_atomic_fetch_sub_int(&g_upnpInboxCount, 1); + for (auto it = g_upnpReqs.begin(); it != g_upnpReqs.end(); ) { + (it->port == args.port && it->protocol == args.protocol) ? it = g_upnpReqs.erase(it) : ++it; + } + if (g_upnpReqs.size() >= MAX_QUEUED_REQUESTS) { + WARN_LOG(Log::Net, "UPnP request queue is full, dropping request for %s port %d", args.protocol.c_str(), args.port); + return; + } + g_upnpReqs.push_back(std::move(args)); + }); +} + static void DiscardQueuedRequests() { - std::lock_guard lock(g_upnpLock); + TakeInbox(); if (!g_upnpReqs.empty()) { DEBUG_LOG(Log::Net, "UPnPService: discarding %d queued request(s)", (int)g_upnpReqs.size()); g_upnpReqs.clear(); @@ -454,18 +478,11 @@ static void DiscardQueuedRequests() { // Returns false if we lost the router, in which case the unfinished request stays queued. static bool ProcessQueuedRequests() { while (true) { - UPnPArgs arg; - { - std::lock_guard lock(g_upnpLock); - if (g_upnpExit || g_upnpReqs.empty()) - return true; - // Take it out of the queue for the duration. Talking to the router happens without the - // lock held, and QueueRequest() supersedes pending requests for the same port - if the - // in-flight one were still in the deque it could be erased out from under us, and we'd - // then drop whatever replaced it without ever running it. - arg = std::move(g_upnpReqs.front()); - g_upnpReqs.pop_front(); - } + TakeInbox(); + if (UPnPExiting() || g_upnpReqs.empty()) + return true; + UPnPArgs arg = std::move(g_upnpReqs.front()); + g_upnpReqs.pop_front(); bool ok; switch (arg.cmd) { @@ -484,9 +501,9 @@ static bool ProcessQueuedRequests() { // The router stopped answering, and Add()/Remove() have already reset us to disconnected. // Put the request back for after we reconnect and stop draining - everything behind it - // would just fail the same way. + // would just fail the same way. A newer request for the same port, taken meanwhile, wins. + TakeInbox(); if (++arg.attempts < MAX_REQUEST_ATTEMPTS) { - std::lock_guard lock(g_upnpLock); const bool superseded = std::any_of(g_upnpReqs.begin(), g_upnpReqs.end(), [&arg](const UPnPArgs &req) { return req.port == arg.port && req.protocol == arg.protocol; }); @@ -511,24 +528,18 @@ static int upnpService(unsigned int timeout) { double nextInitTime = 0.0; while (true) { - uint32_t seq; - { - std::lock_guard lock(g_upnpLock); - if (g_upnpExit) - break; - seq = g_upnpWakeSeq; - if (g_upnpResetBackoff) { - // The user just flipped the setting - don't make them wait out an old backoff. - g_upnpResetBackoff = false; - failCount = 0; - nextInitTime = 0.0; - } + const int seen = g_upnpWork.Seen(); + if (UPnPExiting()) + break; + if (retro_atomic_exchange_int(&g_upnpResetBackoff, 0)) { + // The user just flipped the setting - don't make them wait out an old backoff. + failCount = 0; + nextInitTime = 0.0; } if (!g_Config.bEnableUPnP) { // Callers queue requests without checking the setting (see bind() in sceNetInet), so throw - // them away here. Otherwise the queue grows without bound and, worse, the wait below never - // blocks - which is what pegged a core whenever UPnP was off but a game was using sockets. + // them away here, or the queue grows without bound. DiscardQueuedRequests(); if (wasEnabled) { // Turned off at runtime - take our mappings back down right away. @@ -536,15 +547,9 @@ static int upnpService(unsigned int timeout) { g_PortManager.Shutdown(); wasEnabled = false; } - failCount = 0; - nextInitTime = 0.0; - - std::lock_guard lock(g_upnpLock); if (!g_Config.bEnableUPnP) { - // Nothing left to do until the setting comes back on, and StartUPnPService() will - // spin up a fresh thread for that. Deciding this under the lock is what makes that - // safe: a start that observed us still running can't be left without a thread. - g_upnpThreadRunning = false; + // Nothing left to do until the setting comes back on, and the next start spins up + // a fresh thread for that. break; } // Turned back on while we were cleaning up (which involves network round trips, so @@ -576,104 +581,84 @@ static int upnpService(unsigned int timeout) { // Sleep until someone wakes us, with no timer: a router that couldn't be reached is tried // again on the next request (or setting change) once the backoff has passed. - std::unique_lock lock(g_upnpLock); - g_upnpCond.wait(lock, [seq] { return g_upnpExit || g_upnpWakeSeq != seq; }); + g_upnpWork.Wait(seen); } - // Clean up regardless of g_Config.bEnableUPnP, to avoid leaving open ports on the router. - g_PortManager.Shutdown(); - - { - std::lock_guard lock(g_upnpLock); - g_upnpReqs.clear(); + if (UPnPExiting()) { + // Clean up regardless of g_Config.bEnableUPnP, to avoid leaving open ports on the router. + g_PortManager.Shutdown(); + DiscardQueuedRequests(); } INFO_LOG(Log::Net, "UPnPService: End of UPnPService Thread"); + retro_atomic_store_release_int(&g_upnpThreadState, UPNP_THREAD_EXITED); return 0; } -// Starts the service thread if the setting is on and it isn't up already. +// Owner thread only. Starts the service thread if the setting is on and it isn't up already. static void StartUPnPService() { - std::lock_guard threadLock(g_upnpThreadLock); - { - std::lock_guard lock(g_upnpLock); - if (!g_upnpInitialized || g_upnpExit || !g_Config.bEnableUPnP) - return; - if (g_upnpThreadRunning) { - // Already up - it'll notice whatever changed on its own. - return; - } - g_upnpThreadRunning = true; - g_upnpResetBackoff = true; + if (!retro_atomic_load_acquire_int(&g_upnpInitialized) || UPnPExiting() || !g_Config.bEnableUPnP) + return; + if (retro_atomic_load_acquire_int(&g_upnpThreadState) == UPNP_THREAD_RUNNING) { + // Already up - it'll notice whatever changed on its own. + return; } - // A previous thread may have exited when the setting was turned off. By the time it clears - // g_upnpThreadRunning it has already cleaned up after itself, so this doesn't block on a router. + // A previous thread may have exited when the setting was turned off. It has already cleaned + // up after itself, so this doesn't block on a router. if (g_upnpServiceThread.joinable()) g_upnpServiceThread.join(); - g_upnpServiceThread = std::thread(upnpService, g_upnpTimeout); + retro_atomic_store_release_int(&g_upnpResetBackoff, 1); + retro_atomic_store_release_int(&g_upnpThreadState, UPNP_THREAD_RUNNING); + g_upnpServiceThread = Thread(upnpService, g_upnpTimeout); } void __UPnPInit(unsigned int timeout) { - { - std::lock_guard lock(g_upnpLock); - g_upnpExit = false; - g_upnpReqs.clear(); - g_upnpInitialized = true; - g_upnpTimeout = timeout; - } + g_upnpOwnerThread = CurrentThreadId(); + retro_atomic_store_release_int(&g_upnpExit, 0); + g_upnpTimeout = timeout; + retro_atomic_store_release_int(&g_upnpInitialized, 1); // Only actually spawns a thread if UPnP is enabled; otherwise UPnP_Notify() starts one when // the user turns it on. StartUPnPService(); } void __UPnPShutdown() { - { - std::lock_guard lock(g_upnpLock); - g_upnpInitialized = false; - g_upnpExit = true; - // Anything still queued is moot, and dropping it here means the thread won't try to talk to - // a router we may no longer be able to reach on its way out. - g_upnpReqs.clear(); - g_upnpWakeSeq++; - } - g_upnpCond.notify_all(); + retro_atomic_store_release_int(&g_upnpInitialized, 0); + retro_atomic_store_release_int(&g_upnpExit, 1); + g_upnpWork.Notify(); - std::lock_guard threadLock(g_upnpThreadLock); if (g_upnpServiceThread.joinable()) { INFO_LOG(Log::Net, "Waiting for upnp thread to shut down..."); g_upnpServiceThread.join(); INFO_LOG(Log::Net, "upnp thread shut down."); } - std::lock_guard lock(g_upnpLock); - g_upnpThreadRunning = false; + retro_atomic_store_release_int(&g_upnpThreadState, UPNP_THREAD_NONE); + // Anything still queued is moot. + g_upnpInbox.Drain([](UPnPArgs &&) { + retro_atomic_fetch_sub_int(&g_upnpInboxCount, 1); + }); + g_upnpReqs.clear(); } static void QueueRequest(UPnPArgs args) { // The enable setting can change after startup without the settings UI being involved - a // per-game config, or a libretro core option. Reconcile here rather than requiring every place // that can flip it to remember to call UPnP_Notify(): this starts a thread if the setting is on - // and there isn't one, and is a cheap no-op otherwise. The other direction takes care of itself, - // since queuing below wakes a thread that then notices the setting is off and cleans up. - StartUPnPService(); - - std::lock_guard lock(g_upnpLock); - // With UPnP off there's no service thread to drain the queue, so don't let one build up. - if (!g_upnpThreadRunning || g_upnpExit) - return; - - // Last request wins for a given port: this collapses the repeated adds that games produce when - // they rebind in a loop, and lets a remove cancel an add that hasn't been sent yet. - for (auto it = g_upnpReqs.begin(); it != g_upnpReqs.end(); ) { - (it->port == args.port && it->protocol == args.protocol) ? it = g_upnpReqs.erase(it) : ++it; + // and there isn't one. Only the owner thread can start one; a request from elsewhere waits + // for the next start. + if (CurrentThreadId() == g_upnpOwnerThread) { + StartUPnPService(); } - if (g_upnpReqs.size() >= MAX_QUEUED_REQUESTS) { + if (!retro_atomic_load_acquire_int(&g_upnpInitialized) || UPnPExiting() || !g_Config.bEnableUPnP) + return; + // A game hammering bind() mustn't grow the inbox without bound before the thread gets to it. + if (retro_atomic_fetch_add_int(&g_upnpInboxCount, 1) >= (int)MAX_QUEUED_REQUESTS) { + retro_atomic_fetch_sub_int(&g_upnpInboxCount, 1); WARN_LOG(Log::Net, "UPnP request queue is full, dropping request for %s port %d", args.protocol.c_str(), args.port); return; } - - g_upnpReqs.push_back(std::move(args)); - g_upnpWakeSeq++; - g_upnpCond.notify_one(); + g_upnpInbox.Push(std::move(args)); + g_upnpWork.Notify(); } // Built here rather than on the service thread, which can't safely read the game state. @@ -694,14 +679,12 @@ void UPnP_Remove(const char *protocol, unsigned short port) { } void UPnP_Notify() { - { - std::lock_guard lock(g_upnpLock); - g_upnpWakeSeq++; - g_upnpResetBackoff = true; - } - g_upnpCond.notify_one(); + retro_atomic_store_release_int(&g_upnpResetBackoff, 1); + g_upnpWork.Notify(); // Turned on: start a thread. Turned off: the running one cleans up and exits by itself, which - // we deliberately don't wait for here - this is called from the UI thread, and the cleanup - // means talking to a router that may be slow to answer. - StartUPnPService(); + // we deliberately don't wait for here, since the cleanup means talking to a router that may be + // slow to answer. + if (CurrentThreadId() == g_upnpOwnerThread) { + StartUPnPService(); + } } diff --git a/Core/WebServer.cpp b/Core/WebServer.cpp index c9bbb4d3ec..cdae01fc5c 100644 --- a/Core/WebServer.cpp +++ b/Core/WebServer.cpp @@ -15,21 +15,23 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include -#include #include -#include #include -#include +#include + +#include "Common/Net/Cancel.h" #include "Common/Net/HTTPServer.h" #include "Common/Net/Sinks.h" +#include "Common/Thread/ParkingLot.h" +#include "Common/Thread/Thread.h" #include "Common/Thread/ThreadUtil.h" #include "Common/Log.h" #include "Common/File/Path.h" #include "Common/File/VFS/VFS.h" #include "Common/StringUtils.h" #include "Core/Config.h" +#include "Core/Core.h" #include "Core/WebServer.h" #include "Core/Debugger/WebSocket.h" @@ -41,25 +43,24 @@ enum class ServerStatus { FINISHED, }; -static std::thread serverThread; -static ServerStatus serverStatus; -static std::mutex serverStatusLock; -static std::condition_variable serverStatusCond; -// See WebServerSetRequireExactPort(). Atomic because it's set from whichever thread parsed the -// command line, and read from the server thread. -static std::atomic serverRequireExactPort; +// serverThread and serverStop belong to the thread calling Start/ShutdownWebServer. +static Thread serverThread; +// Set by ShutdownWebServer(); wakes the accept loop and every connection. One per server run, +// deleted only after the server thread (and with it every connection thread) is joined. +static net::CancelToken *serverStop; +static retro_atomic_int_t serverStatus{ (int)ServerStatus::STOPPED }; +// See WebServerSetRequireExactPort(). +static retro_atomic_int_t serverRequireExactPort{ 0 }; static void UpdateStatus(ServerStatus s) { - { - std::lock_guard guard(serverStatusLock); - serverStatus = s; - } - serverStatusCond.notify_all(); + retro_atomic_store_release_int(&serverStatus, (int)s); + ParkingLotNotify(&serverStatus); + // ShutdownWebServer() may be waiting on the CPU thread's queue for this. + Core_WakeCPUThread(); } static ServerStatus RetrieveStatus() { - std::lock_guard guard(serverStatusLock); - return serverStatus; + return (ServerStatus)retro_atomic_load_acquire_int(&serverStatus); } static bool ServeAssetFile(const http::ServerRequest &request) { @@ -143,7 +144,7 @@ static void ForwardDebuggerRequest(const http::ServerRequest &request) { // Yes - proceed with the socket. if (strcasecmp(upgrade.c_str(), "websocket") == 0) { - HandleDebuggerRequest(request); + HandleDebuggerRequest(request, serverStop); } else { RedirectToDebugger(request); } @@ -157,7 +158,7 @@ static void WebServerThread() { http->RegisterHandler("/debugger", &ForwardDebuggerRequest); if (!http->Listen(g_Config.iRemoteISOPort, "debugger-webserver")) { - if (serverRequireExactPort) { + if (retro_atomic_load_acquire_int(&serverRequireExactPort)) { // Someone asked for this specific port (--debugger=PORT) - see // WebServerSetRequireExactPort(). Coming up on a different one would just look like // success while nothing can connect, so give up loudly instead. @@ -182,14 +183,13 @@ static void WebServerThread() { // line is the only way a client can find out which one it got. NOTICE_LOG(Log::HTTP, "Entering web server loop. Listening on port %d", g_Config.iRemoteISOPort); - while (RetrieveStatus() == ServerStatus::RUNNING) { - constexpr double webServerSliceSeconds = 0.2f; - http->RunSlice(webServerSliceSeconds); + while (!serverStop->IsCancelled()) { + http->RunSlice(serverStop); } INFO_LOG(Log::HTTP, "Leaving web server loop."); http->Stop(); - StopAllDebuggers(); + // Joins the connection threads, which see serverStop too. delete http; UpdateStatus(ServerStatus::FINISHED); @@ -197,15 +197,17 @@ static void WebServerThread() { } bool StartWebServer() { - std::lock_guard guard(serverStatusLock); - switch (serverStatus) { + switch (RetrieveStatus()) { case ServerStatus::FINISHED: serverThread.join(); + delete serverStop; + serverStop = nullptr; [[fallthrough]]; case ServerStatus::STOPPED: - serverStatus = ServerStatus::STARTING; - serverThread = std::thread(&WebServerThread); + serverStop = new net::CancelToken(); + UpdateStatus(ServerStatus::STARTING); + serverThread = Thread(&WebServerThread); return true; default: @@ -214,14 +216,28 @@ bool StartWebServer() { } void ShutdownWebServer() { - { - std::lock_guard guard(serverStatusLock); - if (serverStatus == ServerStatus::RUNNING) - serverStatus = ServerStatus::STOPPING; + if (retro_atomic_cas_int(&serverStatus, (int)ServerStatus::RUNNING, (int)ServerStatus::STOPPING)) { + ParkingLotNotify(&serverStatus); } - if (serverThread.joinable()) + if (serverStop) { + serverStop->Cancel(); + } + if (serverThread.joinable()) { + // Connections finishing up may still need the CPU thread (Core_RunOnCPUThread), and this + // is it, so keep serving them until the server thread is done. + for (;;) { + const int seen = Core_CPUWorkSeen(); + Core_ProcessCPUQueue(); + if (RetrieveStatus() == ServerStatus::FINISHED || RetrieveStatus() == ServerStatus::STOPPED) { + break; + } + Core_WaitForCPUWork(seen); + } serverThread.join(); - serverStatus = ServerStatus::STOPPED; + } + delete serverStop; + serverStop = nullptr; + UpdateStatus(ServerStatus::STOPPED); } bool WebServerRunning() { @@ -233,11 +249,10 @@ int WebServerPort() { } void WebServerSetRequireExactPort(bool require) { - serverRequireExactPort = require; + retro_atomic_store_release_int(&serverRequireExactPort, require ? 1 : 0); } bool WebServerWaitForStartup() { - std::unique_lock guard(serverStatusLock); - serverStatusCond.wait(guard, [] { return serverStatus != ServerStatus::STARTING; }); - return serverStatus == ServerStatus::RUNNING; + ParkingLotWait(&serverStatus, [] { return RetrieveStatus() != ServerStatus::STARTING; }); + return RetrieveStatus() == ServerStatus::RUNNING; } diff --git a/GPU/Common/ReplacedTexture.cpp b/GPU/Common/ReplacedTexture.cpp index aa07bedbfe..fcf847cabe 100644 --- a/GPU/Common/ReplacedTexture.cpp +++ b/GPU/Common/ReplacedTexture.cpp @@ -16,6 +16,7 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include +#include #include "ppsspp_config.h" @@ -142,7 +143,7 @@ void ReplacedTexture::PurgeIfNotUsedSinceTime(double t) { // If there's some leftover threadWaitable, get rid of it. if (threadWaitable_) { - if (threadWaitable_->WaitFor(0.0)) { + if (threadWaitable_->Ready()) { delete threadWaitable_; threadWaitable_ = nullptr; // Continue with purging. @@ -168,7 +169,7 @@ void ReplacedTexture::PurgeIfNotUsedSinceTime(double t) { } // This can only return true if ACTIVE or NOT_FOUND. -bool ReplacedTexture::Poll(double budget) { +bool ReplacedTexture::Poll(bool wait) { _assert_(vfs_ != nullptr); double now = time_now_d(); @@ -177,7 +178,9 @@ bool ReplacedTexture::Poll(double budget) { case ReplacementState::ACTIVE: case ReplacementState::NOT_FOUND: if (threadWaitable_) { - if (!threadWaitable_->WaitFor(budget)) { + if (wait) + threadWaitable_->Wait(); + if (!threadWaitable_->Ready()) { lastUsed_ = now; return false; } @@ -200,16 +203,13 @@ bool ReplacedTexture::Poll(double budget) { lastUsed_ = now; - // Let's not even start a new texture if we're already behind. - // Note that 0.0 is used as a signalling value that we don't want to wait (just handling already finished textures). - if (budget < 0.0) - return false; - _assert_(!threadWaitable_); threadWaitable_ = new LimitedWaitable(); SetState(ReplacementState::PENDING); g_threadManager.EnqueueTask(new ReplacedTextureTask(vfs_, *this, threadWaitable_)); - if (threadWaitable_->WaitFor(budget)) { + if (wait) + threadWaitable_->Wait(); + if (threadWaitable_->Ready()) { // If we successfully wait here, we're done. The thread will set state accordingly. _assert_(State() == ReplacementState::ACTIVE || State() == ReplacementState::NOT_FOUND || State() == ReplacementState::CANCEL_INIT); delete threadWaitable_; @@ -231,9 +231,8 @@ void ReplacedTexture::Prepare(VFSBackend *vfs) { return; } - this->vfs_ = vfs; - - std::unique_lock lock(lock_); + // vfs_ is already vfs: it only changes in Unload(), which waits for this task first. + _dbg_assert_(vfs_ == vfs); fmt = Draw::DataFormat::UNDEFINED; @@ -777,9 +776,6 @@ bool ReplacedTexture::CopyLevelTo(int level, uint8_t *out, size_t outDataSize, i int outW = levels_[level].fullW; int outH = levels_[level].fullH; - // We probably could avoid this lock, but better to play it safe. - std::lock_guard guard(lock_); - const ReplacedTextureLevel &info = levels_[level]; const std::vector &data = data_[level]; diff --git a/GPU/Common/ReplacedTexture.h b/GPU/Common/ReplacedTexture.h index 7be90e24ff..dfbf3c4f6d 100644 --- a/GPU/Common/ReplacedTexture.h +++ b/GPU/Common/ReplacedTexture.h @@ -17,7 +17,7 @@ #pragma once -#include +#include #include #include "Common/File/VFS/VFS.h" @@ -145,13 +145,15 @@ public: ReplacedTexture(VFSBackend *vfs, const ReplacementDesc &desc); ~ReplacedTexture(); + // The loader task publishes the levels with its final state (release), and the + // emu thread only reads them once it sees that state (acquire). inline ReplacementState State() const { - return state_; + return (ReplacementState)retro_atomic_load_acquire_int(const_cast(&state_)); } void SetState(ReplacementState state) { - _dbg_assert_(state != state_); - state_ = state; + _dbg_assert_(state != State()); + retro_atomic_store_release_int(&state_, (int)state); } void GetSize(int level, int *w, int *h) const { @@ -204,7 +206,8 @@ public: return alphaStatus_; } - bool Poll(double budget); + // wait: block until a pending load is done, rather than checking on it. + bool Poll(bool wait); bool CopyLevelTo(int level, uint8_t *out, size_t outDataSize, int rowPitch); std::string logId_; @@ -232,12 +235,11 @@ private: double lastUsed_ = 0.0; LimitedWaitable *threadWaitable_ = nullptr; - std::mutex lock_; Draw::DataFormat fmt = Draw::DataFormat::UNDEFINED; TextureAlpha alphaStatus_ = TextureAlpha::Any; double lastUsed = 0.0; - std::atomic state_ = ReplacementState::UNLOADED; + retro_atomic_int_t state_{ (int)ReplacementState::UNLOADED }; VFSBackend *vfs_ = nullptr; ReplacementDesc desc_; diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index 43f34a3680..edf5a2c8c4 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -1801,19 +1801,10 @@ ReplacedTexture *TextureCacheCommon::FindReplacement(TexCacheEntry *entry, int * } void TextureCacheCommon::PollReplacement(TexCacheEntry *entry, int *w, int *h, int *d) { - double waitBudget = replacementFrameBudgetSeconds_ - replacementTimeThisFrame_; - // Note: Don't avoid the Poll call if budget is 0, we do meaningful things there. - // Poll also handles negative budgets. - double replaceStart = time_now_d(); - // Unless the mode is set to Instant (where the user explicitly wants to wait for each texture), - // it's just a waste of time to wait here really. OK, we might get a texture one frame early but - // we wasted a lot of time waiting, likely slowing down our framerate. - if (g_Config.iReplacementTextureLoadSpeed != ReplacementTextureLoadSpeed::INSTANT) { - waitBudget = 0.0; - } - if (entry->replacedTexture->Poll(waitBudget)) { + // Only Instant waits for each texture to load; the others pick it up on a later frame. + if (entry->replacedTexture->Poll(g_Config.iReplacementTextureLoadSpeed == ReplacementTextureLoadSpeed::INSTANT)) { if (entry->replacedTexture->State() == ReplacementState::ACTIVE) { entry->replacedTexture->GetSize(0, w, h); // Consider it already "scaled.". diff --git a/GPU/Common/TextureReplacer.cpp b/GPU/Common/TextureReplacer.cpp index 32efa907c0..d86d0d42cd 100644 --- a/GPU/Common/TextureReplacer.cpp +++ b/GPU/Common/TextureReplacer.cpp @@ -927,12 +927,10 @@ void TextureReplacer::Decimate(ReplacerDecimateMode mode) { const double threshold = time_now_d() - age; size_t totalSize = 0; for (auto &item : levelCache_) { - // During decimation, it's fine to try-lock here to avoid blocking the main thread while - // the level is being loaded - in that case we don't want to decimate anyway. - if (item.second->lock_.try_lock()) { - item.second->PurgeIfNotUsedSinceTime(threshold); + // Skips levels still being loaded: only ACTIVE ones are done with their data. + item.second->PurgeIfNotUsedSinceTime(threshold); + if (item.second->State() == ReplacementState::ACTIVE) { totalSize += item.second->GetTotalDataSize(); // TODO: Make something better. - item.second->lock_.unlock(); } // don't actually delete the items here, just clean out the data. } diff --git a/GPU/Common/TextureReplacer.h b/GPU/Common/TextureReplacer.h index 210221562e..18466814de 100644 --- a/GPU/Common/TextureReplacer.h +++ b/GPU/Common/TextureReplacer.h @@ -19,7 +19,6 @@ #include "ppsspp_config.h" -#include #include #include #include diff --git a/GPU/Debugger/Breakpoints.cpp b/GPU/Debugger/Breakpoints.cpp index 576d1ead92..9924a4d68b 100644 --- a/GPU/Debugger/Breakpoints.cpp +++ b/GPU/Debugger/Breakpoints.cpp @@ -16,7 +16,6 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include -#include #include #include #include @@ -164,7 +163,6 @@ bool GPUBreakpoints::HitAddressBreakpoint(u32 pc, u32 op) { if (breakPCsCount == 0) return false; - std::lock_guard guard(breaksLock); auto entry = breakPCs.find(pc); if (entry == breakPCs.end()) return false; @@ -181,7 +179,6 @@ bool GPUBreakpoints::HitOpBreakpoint(u32 op) { return false; if (breakCmdsInfo[cmd].isConditional) { - std::lock_guard guard(breaksLock); return HitBreakpointCond(breakCmdsInfo[cmd], op); } @@ -210,7 +207,6 @@ bool GPUBreakpoints::IsAddressBreakpoint(u32 addr, bool &temp) { return false; } - std::lock_guard guard(breaksLock); temp = breakPCsTemp.find(addr) != breakPCsTemp.end(); return breakPCs.find(addr) != breakPCs.end(); } @@ -220,7 +216,6 @@ bool GPUBreakpoints::IsAddressBreakpoint(u32 addr) { return false; } - std::lock_guard guard(breaksLock); return breakPCs.find(addr) != breakPCs.end(); } @@ -230,7 +225,6 @@ bool GPUBreakpoints::IsTextureBreakpoint(u32 addr, bool &temp) { return false; } - std::lock_guard guard(breaksLock); temp = breakTexturesTemp.find(addr) != breakTexturesTemp.end(); return breakTextures.find(addr) != breakTextures.end(); } @@ -240,7 +234,6 @@ bool GPUBreakpoints::IsTextureBreakpoint(u32 addr) { return false; } - std::lock_guard guard(breaksLock); return breakTextures.find(addr) != breakTextures.end(); } @@ -252,7 +245,6 @@ bool GPUBreakpoints::IsRenderTargetBreakpoint(u32 addr, bool &temp) { addr &= 0x001FFFF0; - std::lock_guard guard(breaksLock); temp = breakRenderTargetsTemp.find(addr) != breakRenderTargetsTemp.end(); return breakRenderTargets.find(addr) != breakRenderTargets.end(); } @@ -264,7 +256,6 @@ bool GPUBreakpoints::IsRenderTargetBreakpoint(u32 addr) { addr &= 0x001FFFF0; - std::lock_guard guard(breaksLock); return breakRenderTargets.find(addr) != breakRenderTargets.end(); } @@ -300,8 +291,6 @@ bool GPUBreakpoints::HasAnyBreakpoints() const { } void GPUBreakpoints::AddAddressBreakpoint(u32 addr, bool temp) { - std::lock_guard guard(breaksLock); - if (temp) { if (breakPCs.find(addr) == breakPCs.end()) { breakPCsTemp.insert(addr); @@ -319,8 +308,6 @@ void GPUBreakpoints::AddAddressBreakpoint(u32 addr, bool temp) { } void GPUBreakpoints::AddCmdBreakpoint(u8 cmd, bool temp) { - // Debuggers call this from their own threads, racing ClearTempBreakpoints on the emu thread. - std::lock_guard guard(breaksLock); if (temp) { if (!breakCmds[cmd]) { breakCmdsTemp[cmd] = true; @@ -340,8 +327,6 @@ void GPUBreakpoints::AddCmdBreakpoint(u8 cmd, bool temp) { } void GPUBreakpoints::AddTextureBreakpoint(u32 addr, bool temp) { - std::lock_guard guard(breaksLock); - if (temp) { if (breakTextures.find(addr) == breakTextures.end()) { breakTexturesTemp.insert(addr); @@ -357,8 +342,6 @@ void GPUBreakpoints::AddTextureBreakpoint(u32 addr, bool temp) { } void GPUBreakpoints::AddRenderTargetBreakpoint(u32 addr, bool temp) { - std::lock_guard guard(breaksLock); - addr &= 0x001FFFF0; if (temp) { @@ -376,7 +359,6 @@ void GPUBreakpoints::AddRenderTargetBreakpoint(u32 addr, bool temp) { } void GPUBreakpoints::AddTextureChangeTempBreakpoint() { - std::lock_guard guard(breaksLock); textureChangeTemp = true; hasBreakpoints_ = true; } @@ -389,8 +371,6 @@ void GPUBreakpoints::AddAnyTempBreakpoint() { } void GPUBreakpoints::RemoveAddressBreakpoint(u32 addr) { - std::lock_guard guard(breaksLock); - breakPCsTemp.erase(addr); breakPCs.erase(addr); @@ -399,16 +379,12 @@ void GPUBreakpoints::RemoveAddressBreakpoint(u32 addr) { } void GPUBreakpoints::RemoveCmdBreakpoint(u8 cmd) { - std::lock_guard guard(breaksLock); - breakCmdsTemp[cmd] = false; breakCmds[cmd] = false; hasBreakpoints_ = HasAnyBreakpoints(); } void GPUBreakpoints::RemoveTextureBreakpoint(u32 addr) { - std::lock_guard guard(breaksLock); - breakTexturesTemp.erase(addr); breakTextures.erase(addr); @@ -417,8 +393,6 @@ void GPUBreakpoints::RemoveTextureBreakpoint(u32 addr) { } void GPUBreakpoints::RemoveRenderTargetBreakpoint(u32 addr) { - std::lock_guard guard(breaksLock); - addr &= 0x001FFFF0; breakRenderTargetsTemp.erase(addr); @@ -429,8 +403,6 @@ void GPUBreakpoints::RemoveRenderTargetBreakpoint(u32 addr) { } void GPUBreakpoints::RemoveTextureChangeTempBreakpoint() { - std::lock_guard guard(breaksLock); - textureChangeTemp = false; hasBreakpoints_ = HasAnyBreakpoints(); } @@ -457,13 +429,11 @@ bool GPUBreakpoints::SetAddressBreakpointCond(u32 addr, const std::string &expre // Must have one in the first place, make sure it's not temporary. AddAddressBreakpoint(addr); - std::lock_guard guard(breaksLock); auto &bp = breakPCs[addr]; return SetupCond(breakPCs[addr], expression, error); } bool GPUBreakpoints::GetAddressBreakpointCond(u32 addr, std::string *expression) { - std::lock_guard guard(breaksLock); auto entry = breakPCs.find(addr); if (entry != breakPCs.end() && entry->second.isConditional) { if (expression) @@ -477,14 +447,12 @@ bool GPUBreakpoints::SetCmdBreakpointCond(u8 cmd, const std::string &expression, // Must have one in the first place, make sure it's not temporary. AddCmdBreakpoint(cmd); - std::lock_guard guard(breaksLock); return SetupCond(breakCmdsInfo[cmd], expression, error); } bool GPUBreakpoints::GetCmdBreakpointCond(u8 cmd, std::string *expression) { if (breakCmds[cmd] && breakCmdsInfo[cmd].isConditional) { if (expression) { - std::lock_guard guard(breaksLock); *expression = breakCmdsInfo[cmd].expressionString; } return true; @@ -497,8 +465,6 @@ void GPUBreakpoints::UpdateLastTexture(u32 addr) { } void GPUBreakpoints::ClearAllBreakpoints() { - std::lock_guard guard(breaksLock); - for (int i = 0; i < 256; ++i) { breakCmds[i] = false; breakCmdsTemp[i] = false; @@ -520,8 +486,6 @@ void GPUBreakpoints::ClearAllBreakpoints() { } void GPUBreakpoints::ClearTempBreakpoints() { - std::lock_guard guard(breaksLock); - // Reset ones that were temporary back to non-breakpoints in the primary arrays. for (int i = 0; i < 256; ++i) { if (breakCmdsTemp[i]) { diff --git a/GPU/Debugger/Breakpoints.h b/GPU/Debugger/Breakpoints.h index f2ddb24be8..2e8434e420 100644 --- a/GPU/Debugger/Breakpoints.h +++ b/GPU/Debugger/Breakpoints.h @@ -19,7 +19,6 @@ #include #include -#include #include #include "Common/CommonTypes.h" #include "Common/Math/expression_parser.h" @@ -89,14 +88,14 @@ private: bool HitAddressBreakpoint(u32 pc, u32 op); bool HitOpBreakpoint(u32 op); - std::mutex breaksLock; - + // All of this is the emu thread's: the GE checks it per command, and the only writers are the GE + // debugger hooks in GPUCommon. bool breakCmds[256]{}; BreakpointInfo breakCmdsInfo[256]{}; std::unordered_map breakPCs; std::set breakTextures; std::set breakRenderTargets; - // Small optimization to avoid a lock/lookup for the common case. + // Small optimization to avoid a lookup for the common case. size_t breakPCsCount = 0; size_t breakTexturesCount = 0; size_t breakRenderTargetsCount = 0; diff --git a/GPU/Debugger/Playback.cpp b/GPU/Debugger/Playback.cpp index 7a01871fa7..c0f432db87 100644 --- a/GPU/Debugger/Playback.cpp +++ b/GPU/Debugger/Playback.cpp @@ -19,17 +19,16 @@ #include #include #include -#include #include -#include #include -#include #include #include +#include #include "Common/Profiler/Profiler.h" #include "Common/CommonTypes.h" #include "Common/Log.h" +#include "Common/Thread/ParkingLot.h" #include "Common/Thread/ThreadUtil.h" #include "Common/System/Request.h" #include "Core/Config.h" @@ -54,6 +53,7 @@ #include "GPU/Debugger/Playback.h" #include "GPU/Debugger/Record.h" #include "GPU/Debugger/RecordFormat.h" +#include "Common/Thread/Thread.h" namespace GPURecord { @@ -92,18 +92,23 @@ static std::vector lastExecPushbuf; // This thread is restarted every frame (dump execution) for simplicity. TODO: Make persistent? // Alternatively, get rid of it, but the code is written in a way that makes it difficult (you'll see if you try). -static std::thread replayThread; - -static std::mutex opStartLock; -static std::condition_variable g_condOpStartWait; - -static std::mutex opFinishLock; -static std::condition_variable opFinishWait; +static Thread replayThread; +// The replay thread posts an operation (g_opToExec, g_opPosted) and waits for g_opDone, which the emu +// thread sets once it has run it, on its next call. g_opToExec and g_retVal pass between them through these. static Operation g_opToExec; static u32 g_retVal; -static bool g_opDone = true; -static bool g_cancelled = false; +static retro_atomic_int_t g_opPosted{ 0 }; +static retro_atomic_int_t g_opDone{ 1 }; +static retro_atomic_int_t g_cancelled{ 0 }; + +// Emu thread: hands the current operation back to the replay thread. +static void FinishOp() { + g_opToExec = Operation{ OpType::None }; + retro_atomic_store_relaxed_int(&g_opPosted, 0); + retro_atomic_store_release_int(&g_opDone, 1); + ParkingLotNotify(&g_opDone); +} static int g_drawLimit = 0; void SetReplayDrawLimit(int lastPrim) { @@ -112,20 +117,17 @@ void SetReplayDrawLimit(int lastPrim) { // Runs on operation thread u32 ExecuteOnMain(Operation opToExec) { - { - std::unique_lock startLock(opStartLock); - g_opToExec = opToExec; - g_retVal = 0; - g_opDone = false; - g_condOpStartWait.notify_one(); - } + g_opToExec = opToExec; + g_retVal = 0; + retro_atomic_store_relaxed_int(&g_opDone, 0); + retro_atomic_store_release_int(&g_opPosted, 1); + ParkingLotNotify(&g_opPosted); // now wait for completion. At that point, noone cares about g_opToExec anymore, and we can safely // overwrite it next time. - { - std::unique_lock lock(opFinishLock); - opFinishWait.wait(lock, []() { return g_opDone || g_cancelled; }); - } + ParkingLotWait(&g_opDone, [] { + return retro_atomic_load_acquire_int(&g_opDone) || retro_atomic_load_acquire_int(&g_cancelled); + }); return g_retVal; } @@ -582,7 +584,7 @@ void DumpExecute::Registers(u32 ptr, u32 sz) { } void DumpExecute::SubmitListEnd() { - if (execListPos == 0 || g_cancelled) { + if (execListPos == 0 || retro_atomic_load_acquire_int(&g_cancelled)) { return; } @@ -981,7 +983,7 @@ ReplayResult DumpExecute::Run() { int start = resumeIndex_ >= 0 ? resumeIndex_ : 0; for (size_t i = start; i < commands_.size(); i++) { - if (g_cancelled) { + if (retro_atomic_load_acquire_int(&g_cancelled)) { break; } @@ -1094,7 +1096,7 @@ static u32 LoadReplay(const std::string &filename) { NOTICE_LOG(Log::GeDebugger, "LoadReplay %s", filename.c_str()); - g_cancelled = false; + retro_atomic_store_release_int(&g_cancelled, 0); u32 fp = pspFileSystem.OpenFile(filename, FILEACCESS_READ); Header header; @@ -1155,13 +1157,9 @@ static u32 LoadReplay(const std::string &filename) { void Replay_Unload() { // We might be paused inside a replay - in this case, the thread is still running and we need to tell it to stop. if (replayThread.joinable()) { - { - // We just finish processing the commands until done. - g_cancelled = true; - - std::unique_lock lock(opFinishLock); - opFinishWait.notify_one(); - } + // We just finish processing the commands until done. + retro_atomic_store_release_int(&g_cancelled, 1); + ParkingLotNotify(&g_opDone); replayThread.join(); } @@ -1172,7 +1170,9 @@ void Replay_Unload() { lastExecCommands.clear(); lastExecPushbuf.clear(); - g_opDone = true; + g_opToExec = Operation{ OpType::None }; + retro_atomic_store_relaxed_int(&g_opPosted, 0); + retro_atomic_store_relaxed_int(&g_opDone, 1); g_retVal = 0; } @@ -1225,16 +1225,13 @@ ReplayResult RunMountedReplay(const std::string &filename) { } if (g_opToExec.type != OpType::None) { - std::unique_lock waitLock(opFinishLock); - g_opDone = true; - g_opToExec = Operation{ OpType::None }; - opFinishWait.notify_one(); + FinishOp(); } if (!replayThread.joinable()) { _dbg_assert_(g_opToExec.type == OpType::None); g_opToExec = Operation{ OpType::None }; - replayThread = std::thread([version]() { + replayThread = Thread([version]() { SetCurrentThreadName("Replay"); DumpExecute executor(lastExecPushbuf, lastExecCommands, version); GPURecord::ReplayResult retval = executor.Run(); @@ -1244,10 +1241,7 @@ ReplayResult RunMountedReplay(const std::string &filename) { } // OK, now wait for and perform the desired action. - { - std::unique_lock lock(opStartLock); - g_condOpStartWait.wait(lock, []() { return g_opToExec.type != OpType::None; }); - } + ParkingLotWait(&g_opPosted, [] { return retro_atomic_load_acquire_int(&g_opPosted) != 0; }); switch (g_opToExec.type) { case OpType::UpdateStallAddr: @@ -1295,13 +1289,8 @@ ReplayResult RunMountedReplay(const std::string &filename) { case OpType::Done: { _dbg_assert_(replayThread.joinable()); - { - std::unique_lock lock(opFinishLock); - g_opDone = true; - opFinishWait.notify_one(); - } + FinishOp(); replayThread.join(); - g_opToExec = { OpType::None }; break; } case OpType::None: diff --git a/GPU/Debugger/Record.cpp b/GPU/Debugger/Record.cpp index a0e64730c6..6793a437de 100644 --- a/GPU/Debugger/Record.cpp +++ b/GPU/Debugger/Record.cpp @@ -16,13 +16,12 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include -#include #include #include #include #include -#include #include +#include #include "Common/CommonTypes.h" #include "Common/File/FileUtil.h" @@ -134,7 +133,6 @@ void Recorder::DirtyDrawnVRAM() { } bool Recorder::BeginRecording() { - std::unique_lock guard(callbackLock_); nextFrame = false; if (PSP_CoreParameter().fileType == IdentifiedFileType::PPSSPP_GE_DUMP) { // Can't record a GE dump. RecordNextFrame refuses this too. @@ -143,7 +141,6 @@ bool Recorder::BeginRecording() { } active = true; - guard.unlock(); lastTextures.clear(); lastRenderTargets.clear(); flipLastAction = gpuStats.totals.numFlips; @@ -245,8 +242,8 @@ static const u8 *mymemmem(const u8 *haystack, size_t off, size_t hlen, const u8 const u8 *first_possible = haystack + off; int first = *needle; - const u8 *result = nullptr; - std::mutex resultLock; + retro_atomic_ptr_t result; + retro_atomic_ptr_init(&result, nullptr); int range = (int)(last_possible - first_possible); ParallelRangeLoop(&g_threadManager, [&](int l, int h) { @@ -270,10 +267,14 @@ static const u8 *mymemmem(const u8 *haystack, size_t off, size_t hlen, const u8 return; } if (poffset() == 0 && !memcmp(p, needle, nlen)) { - std::lock_guard guard(resultLock); // Take the lowest result so we get the same file for any # of threads. - if (!result || p < result) - result = p; + void *cur = retro_atomic_load_acquire_ptr(&result); + while (!cur || p < (const u8 *)cur) { + if (retro_atomic_cas_ptr(&result, cur, (void *)p)) { + break; + } + cur = retro_atomic_load_acquire_ptr(&result); + } return; } @@ -282,7 +283,7 @@ static const u8 *mymemmem(const u8 *haystack, size_t off, size_t hlen, const u8 } }, 0, range, 128 * 1024, TaskPriority::LOW); - return result; + return (const u8 *)retro_atomic_load_acquire_ptr(&result); } Command Recorder::EmitCommandWithRAM(CommandType t, const void *p, u32 sz, u32 align) { @@ -589,7 +590,6 @@ bool Recorder::RecordNextFrame(const std::function callback) if (PSP_CoreParameter().fileType == IdentifiedFileType::PPSSPP_GE_DUMP) { return false; } - std::lock_guard guard(callbackLock_); // Don't take over a recording in progress, it would get the wrong callback and end point. if (nextFrame || active) { return false; @@ -618,12 +618,9 @@ void Recorder::FinishRecording() { lastEdramTrans = 0x400; std::function callback; - { - std::lock_guard guard(callbackLock_); - callback = std::move(writeCallback); - writeCallback = nullptr; - active = false; - } + callback = std::move(writeCallback); + writeCallback = nullptr; + active = false; if (callback && !filename.empty()) { callback(filename); diff --git a/GPU/Debugger/Record.h b/GPU/Debugger/Record.h index 3c291057af..f4468e76d7 100644 --- a/GPU/Debugger/Record.h +++ b/GPU/Debugger/Record.h @@ -18,8 +18,6 @@ #pragma once #include -#include -#include #include #include @@ -51,7 +49,6 @@ public: } bool RecordNextFrame(const std::function callback); void ClearCallback() { - std::lock_guard guard(callbackLock_); writeCallback = nullptr; } @@ -91,13 +88,12 @@ private: void EmitBezierSpline(u32 op); bool active = false; - std::atomic nextFrame = false; + bool nextFrame = false; int flipLastAction = -1; int flipFinishAt = -1; uint32_t lastEdramTrans = 0x400; std::function writeCallback; - // RecordNextFrame is called from other threads. Guards writeCallback, nextFrame and the writes to active. - std::mutex callbackLock_; + // All of the recorder is the emu thread's: debugger requests reach it through Core_RunOnCPUThread(). std::vector pushbuf; std::vector commands; diff --git a/GPU/Debugger/Stepping.cpp b/GPU/Debugger/Stepping.cpp index 6c7b1b296e..28762c8459 100644 --- a/GPU/Debugger/Stepping.cpp +++ b/GPU/Debugger/Stepping.cpp @@ -15,11 +15,11 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include -#include -#include +#include #include "Common/Log.h" +#include "Common/Thread/MpscQueue.h" +#include "Common/Thread/ParkingLot.h" #include "Common/Thread/ThreadUtil.h" #include "Core/Core.h" #include "Core/HW/Display.h" @@ -42,35 +42,48 @@ enum PauseAction { PAUSE_FLUSHDRAW, }; -static bool isStepping; -// Number of times we've entered stepping, to detect a resume asynchronously. -static int stepCounter = 0; +enum RequestState { + REQUEST_PENDING, + REQUEST_RUNNING, + REQUEST_DONE, + REQUEST_WITHDRAWN, +}; -static std::mutex pauseLock; -static PauseAction pauseAction = PAUSE_CONTINUE; -static std::mutex actionLock; -static std::condition_variable actionWait; -// Protected by actionLock. Set when the emu thread has run the requested action. -static bool actionComplete; -// Held by a requesting thread for the whole request, so two debuggers can't overwrite each other's action. -static std::mutex requestLock; -// Threads inside RequestPauseAction(); WakeStaleRequests() only looks when this is set. -static std::atomic requestsWaiting{0}; +// One debugger request for the emu thread. Shared by the requester and the queue, freed by the last. +struct PauseRequest { + PauseAction action; + GPUDebugFramebufferType bufferType = GPU_DBG_FRAMEBUF_RENDER; + int bufferLevel = 0; + u32 setCmdValue = 0; + int epoch = 0; + // Written by the emu thread before REQUEST_DONE. + bool result = false; + bool wasFramebuffer = false; + retro_atomic_int_t state{ REQUEST_PENDING }; + retro_atomic_int_t refs{ 1 }; +}; + +static retro_atomic_int_t isStepping{ 0 }; +// Number of times we've entered stepping, to detect a resume asynchronously. +static retro_atomic_int_t stepCounter{ 0 }; +// Set to resume running the GE (PAUSE_CONTINUE), cleared on entering stepping (PAUSE_BREAK). +static retro_atomic_int_t continueRequested{ 1 }; +// Bumped on resume and reset: requests from before are withdrawn rather than run at some later break. +static retro_atomic_int_t requestEpoch{ 0 }; +// Requests posted by debugger threads, drained on the emu thread. +static MpscQueue requests; +// Threads waiting in RequestPauseAction(); also the address they park on. +static retro_atomic_int_t requestsWaiting{ 0 }; // Many things need to run on the GPU thread. For example, reading the framebuffer. // A message system is used to achieve this (temporarily "unpausing" the thread.) -// Below are values used to perform actions that return results. +// Below are the results of actions, written on the emu thread. -static bool bufferResult; -static GPUDebugFramebufferType bufferType = GPU_DBG_FRAMEBUF_RENDER; static GPUDebugBuffer bufferFrame; static GPUDebugBuffer bufferDepth; static GPUDebugBuffer bufferStencil; static GPUDebugBuffer bufferTex; static GPUDebugBuffer bufferClut; -static int bufferLevel; -static bool lastWasFramebuffer; -static u32 pauseSetCmdValue; // This is used only to highlight differences. Should really be owned by the debugger. static GEState lastGState; @@ -91,11 +104,10 @@ const char *PauseActionToString(PauseAction action) { } } -static void SetPauseAction(PauseAction act) { - std::lock_guard pauseGuard(pauseLock); - std::lock_guard guard(actionLock); - pauseAction = act; - actionComplete = false; +static void ReleaseRequest(PauseRequest *req) { + if (retro_atomic_fetch_sub_int(&req->refs, 1) == 1) { + delete req; + } } static bool CanRunActions() { @@ -103,46 +115,37 @@ static bool CanRunActions() { return coreState == CORE_STEPPING_GE || coreState == CORE_STEPPING_CPU; } -// Called with pauseLock held. -static void RunPauseAction() { - std::lock_guard guard(actionLock); - if (pauseAction == PAUSE_BREAK || pauseAction == PAUSE_CONTINUE) { - // Nothing requested. - return; - } - - DEBUG_LOG(Log::GeDebugger, "RunPauseAction: %s", PauseActionToString(pauseAction)); - - switch (pauseAction) { - case PAUSE_BREAK: - break; +// Emu thread. +static void RunPauseAction(PauseRequest &req) { + DEBUG_LOG(Log::GeDebugger, "RunPauseAction: %s", PauseActionToString(req.action)); + switch (req.action) { case PAUSE_GETOUTPUTBUF: - bufferResult = gpu->GetOutputFramebuffer(bufferFrame); + req.result = gpu->GetOutputFramebuffer(bufferFrame); break; case PAUSE_GETFRAMEBUF: - bufferResult = gpu->GetCurrentFramebuffer(bufferFrame, bufferType); + req.result = gpu->GetCurrentFramebuffer(bufferFrame, req.bufferType); break; case PAUSE_GETDEPTHBUF: - bufferResult = gpu->GetCurrentDepthbuffer(bufferDepth); + req.result = gpu->GetCurrentDepthbuffer(bufferDepth); break; case PAUSE_GETSTENCILBUF: - bufferResult = gpu->GetCurrentStencilbuffer(bufferStencil); + req.result = gpu->GetCurrentStencilbuffer(bufferStencil); break; case PAUSE_GETTEX: - bufferResult = gpu->GetCurrentTexture(bufferTex, bufferLevel, &lastWasFramebuffer); + req.result = gpu->GetCurrentTexture(bufferTex, req.bufferLevel, &req.wasFramebuffer); break; case PAUSE_GETCLUT: - bufferResult = gpu->GetCurrentClut(bufferClut); + req.result = gpu->GetCurrentClut(bufferClut); break; case PAUSE_SETCMDVALUE: - gpu->SetCmdValue(pauseSetCmdValue); + gpu->SetCmdValue(req.setCmdValue); break; case PAUSE_FLUSHDRAW: @@ -153,53 +156,59 @@ static void RunPauseAction() { ERROR_LOG(Log::GeDebugger, "Unsupported pause action, forgot to add it to the switch."); break; } +} - actionComplete = true; - actionWait.notify_all(); - - pauseAction = PAUSE_BREAK; +// Emu thread. Runs the queued requests, or withdraws them all (or the stale ones). +static void RunPauseRequests(bool withdrawAll) { + const int epoch = retro_atomic_load_acquire_int(&requestEpoch); + const bool any = requests.Drain([&](PauseRequest *&&req) { + if (!withdrawAll && req->epoch == epoch && retro_atomic_cas_int(&req->state, REQUEST_PENDING, REQUEST_RUNNING)) { + RunPauseAction(*req); + retro_atomic_store_release_int(&req->state, REQUEST_DONE); + } else { + retro_atomic_cas_int(&req->state, REQUEST_PENDING, REQUEST_WITHDRAWN); + } + ReleaseRequest(req); + }); + if (any) { + ParkingLotNotify(&requestsWaiting); + } } // Requests an action from the emu thread and waits for it to run. Returns false if stepping ended first // (resume, game shutdown), in which case the action is withdrawn. -static bool RequestPauseAction(PauseAction act) { +static bool RequestPauseAction(PauseRequest *req) { _dbg_assert_(strcmp(GetCurrentThreadName(), "EmuThread") != 0); - SetPauseAction(act); + req->epoch = retro_atomic_load_acquire_int(&requestEpoch); + retro_atomic_fetch_add_int(&requestsWaiting, 1); + retro_atomic_fetch_add_int(&req->refs, 1); + requests.Push(req); + // The emu thread may be stopped, waiting for work. + Core_WakeCPUThread(); - struct WaitingScope { - WaitingScope() { requestsWaiting.fetch_add(1); } - ~WaitingScope() { requestsWaiting.fetch_sub(1); } - } waiting; - - std::unique_lock guard(actionLock); - while (!actionComplete) { - if (!CanRunActions()) { - // Nobody will run it. Don't leave it for a later break to run at some unrelated point. - // (Lock order is pauseLock before actionLock.) - guard.unlock(); - std::lock_guard pauseGuard(pauseLock); - guard.lock(); - if (actionComplete) { - break; - } - if (pauseAction == act) { - pauseAction = PAUSE_BREAK; - } - return false; - } - // Leaving stepping wakes us through WakeStaleRequests(). - actionWait.wait(guard); + // Leaving stepping wakes us through WakeStaleRequests(). + auto finished = [req] { + const int state = retro_atomic_load_acquire_int(&req->state); + return state == REQUEST_DONE || state == REQUEST_WITHDRAWN; + }; + ParkingLotWait(&requestsWaiting, [&] { + return finished() || !CanRunActions(); + }); + // Nobody will run it. Don't leave it for a later break to run at some unrelated point. + if (!retro_atomic_cas_int(&req->state, REQUEST_PENDING, REQUEST_WITHDRAWN)) { + // Already running: it finishes without us. + ParkingLotWait(&requestsWaiting, finished); } - return true; + retro_atomic_fetch_sub_int(&requestsWaiting, 1); + return retro_atomic_load_acquire_int(&req->state) == REQUEST_DONE; } bool ProcessStepping() { _dbg_assert_(gpu); - std::unique_lock guard(pauseLock); if (coreState == CORE_STEPPING_CPU) { - RunPauseAction(); + RunPauseRequests(false); return true; } if (coreState != CORE_STEPPING_GE) { @@ -207,21 +216,21 @@ bool ProcessStepping() { return false; } - if (pauseAction == PAUSE_CONTINUE) { + if (retro_atomic_load_acquire_int(&continueRequested)) { // This is fine, can just mean to run to the next breakpoint/event. DEBUG_LOG(Log::GeDebugger, "Continuing..."); coreState = CORE_RUNNING_GE; return false; } - RunPauseAction(); + RunPauseRequests(false); return true; } +// Emu thread. bool EnterStepping(CoreState coreState) { _dbg_assert_(gpu); - std::unique_lock guard(pauseLock); if (coreState == CORE_STEPPING_GE) { // Already there. Should avoid this happening, I think. return true; @@ -238,13 +247,11 @@ bool EnterStepping(CoreState coreState) { lastGState.cmdmem[1] |= 0x01000000; } - isStepping = true; - stepCounter++; + retro_atomic_store_release_int(&isStepping, 1); + retro_atomic_fetch_add_int(&stepCounter, 1); // Just to be sure. - if (pauseAction == PAUSE_CONTINUE) { - pauseAction = PAUSE_BREAK; - } + retro_atomic_store_release_int(&continueRequested, 0); ::coreState = CORE_STEPPING_GE; return true; @@ -252,102 +259,101 @@ bool EnterStepping(CoreState coreState) { void ResumeFromStepping() { lastGState = gstate; - isStepping = false; - SetPauseAction(PAUSE_CONTINUE); + retro_atomic_store_release_int(&isStepping, 0); + retro_atomic_fetch_add_int(&requestEpoch, 1); + retro_atomic_store_release_int(&continueRequested, 1); } +// Emu thread. void Reset() { - { - std::lock_guard pauseGuard(pauseLock); - std::lock_guard guard(actionLock); - isStepping = false; - pauseAction = PAUSE_CONTINUE; - lastGState = {}; - } + retro_atomic_store_release_int(&isStepping, 0); + retro_atomic_store_release_int(&continueRequested, 1); + retro_atomic_fetch_add_int(&requestEpoch, 1); + lastGState = {}; + RunPauseRequests(true); WakeStaleRequests(); } void WakeStaleRequests() { - if (requestsWaiting.load(std::memory_order_relaxed) == 0 || CanRunActions()) + if (retro_atomic_load_relaxed_int(&requestsWaiting) == 0 || CanRunActions()) return; - // Taking the lock orders this after a waiter's check, so the wakeup can't be lost. - std::lock_guard guard(actionLock); - actionWait.notify_all(); + ParkingLotNotify(&requestsWaiting); } bool IsStepping() { - return isStepping; + return retro_atomic_load_acquire_int(&isStepping) != 0; } int GetSteppingCounter() { - return stepCounter; + return retro_atomic_load_acquire_int(&stepCounter); +} + +static bool CanRequest() { + return IsStepping() || coreState == CORE_STEPPING_CPU; +} + +static PauseRequest *NewRequest(PauseAction action) { + PauseRequest *req = new PauseRequest(); + req->action = action; + return req; } // NOTE: This can't be called on the EmuThread! -// Called with requestLock held. -static bool GetBuffer(const GPUDebugBuffer *&buffer, PauseAction type, const GPUDebugBuffer &resultBuffer) { - if (!isStepping && coreState != CORE_STEPPING_CPU) { - return false; +static bool GetBuffer(const GPUDebugBuffer *&buffer, PauseRequest *req, const GPUDebugBuffer &resultBuffer, bool *wasFramebuffer = nullptr) { + bool result = false; + if (CanRequest() && RequestPauseAction(req)) { + buffer = &resultBuffer; + result = req->result; } - - if (!RequestPauseAction(type)) { - return false; + if (wasFramebuffer) { + *wasFramebuffer = req->wasFramebuffer; } - buffer = &resultBuffer; - return bufferResult; -} - -bool GPU_GetOutputFramebuffer(const GPUDebugBuffer *&buffer) { - std::lock_guard guard(requestLock); - return GetBuffer(buffer, PAUSE_GETOUTPUTBUF, bufferFrame); -} - -bool GPU_GetCurrentFramebuffer(const GPUDebugBuffer *&buffer, GPUDebugFramebufferType type) { - std::lock_guard guard(requestLock); - bufferType = type; - return GetBuffer(buffer, PAUSE_GETFRAMEBUF, bufferFrame); -} - -bool GPU_GetCurrentDepthbuffer(const GPUDebugBuffer *&buffer) { - std::lock_guard guard(requestLock); - return GetBuffer(buffer, PAUSE_GETDEPTHBUF, bufferDepth); -} - -bool GPU_GetCurrentStencilbuffer(const GPUDebugBuffer *&buffer) { - std::lock_guard guard(requestLock); - return GetBuffer(buffer, PAUSE_GETSTENCILBUF, bufferStencil); -} - -bool GPU_GetCurrentTexture(const GPUDebugBuffer *&buffer, int level, bool *isFramebuffer) { - std::lock_guard guard(requestLock); - bufferLevel = level; - bool result = GetBuffer(buffer, PAUSE_GETTEX, bufferTex); - *isFramebuffer = lastWasFramebuffer; + ReleaseRequest(req); return result; } +bool GPU_GetOutputFramebuffer(const GPUDebugBuffer *&buffer) { + return GetBuffer(buffer, NewRequest(PAUSE_GETOUTPUTBUF), bufferFrame); +} + +bool GPU_GetCurrentFramebuffer(const GPUDebugBuffer *&buffer, GPUDebugFramebufferType type) { + PauseRequest *req = NewRequest(PAUSE_GETFRAMEBUF); + req->bufferType = type; + return GetBuffer(buffer, req, bufferFrame); +} + +bool GPU_GetCurrentDepthbuffer(const GPUDebugBuffer *&buffer) { + return GetBuffer(buffer, NewRequest(PAUSE_GETDEPTHBUF), bufferDepth); +} + +bool GPU_GetCurrentStencilbuffer(const GPUDebugBuffer *&buffer) { + return GetBuffer(buffer, NewRequest(PAUSE_GETSTENCILBUF), bufferStencil); +} + +bool GPU_GetCurrentTexture(const GPUDebugBuffer *&buffer, int level, bool *isFramebuffer) { + PauseRequest *req = NewRequest(PAUSE_GETTEX); + req->bufferLevel = level; + return GetBuffer(buffer, req, bufferTex, isFramebuffer); +} + bool GPU_GetCurrentClut(const GPUDebugBuffer *&buffer) { - std::lock_guard guard(requestLock); - return GetBuffer(buffer, PAUSE_GETCLUT, bufferClut); + return GetBuffer(buffer, NewRequest(PAUSE_GETCLUT), bufferClut); +} + +static bool RunRequest(PauseRequest *req) { + bool result = CanRequest() && RequestPauseAction(req); + ReleaseRequest(req); + return result; } bool GPU_SetCmdValue(u32 op) { - std::lock_guard guard(requestLock); - if (!isStepping && coreState != CORE_STEPPING_CPU) { - return false; - } - - pauseSetCmdValue = op; - return RequestPauseAction(PAUSE_SETCMDVALUE); + PauseRequest *req = NewRequest(PAUSE_SETCMDVALUE); + req->setCmdValue = op; + return RunRequest(req); } bool GPU_FlushDrawing() { - std::lock_guard guard(requestLock); - if (!isStepping && coreState != CORE_STEPPING_CPU) { - return false; - } - - return RequestPauseAction(PAUSE_FLUSHDRAW); + return RunRequest(NewRequest(PAUSE_FLUSHDRAW)); } const GEState &LastState() { diff --git a/GPU/GPUCommon.h b/GPU/GPUCommon.h index a0bcbf9403..23b9c10a9c 100644 --- a/GPU/GPUCommon.h +++ b/GPU/GPUCommon.h @@ -16,10 +16,6 @@ #include "GPU/Debugger/Debugger.h" #include "GPU/ge_constants.h" -#if defined(__ANDROID__) -#include -#endif - // X11, sigh. #ifdef None #undef None diff --git a/GPU/Software/BinManager.cpp b/GPU/Software/BinManager.cpp index a029ce7131..e4da6c1047 100644 --- a/GPU/Software/BinManager.cpp +++ b/GPU/Software/BinManager.cpp @@ -15,10 +15,7 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include -#include -#include -#include +#include #include "Common/BitSet.h" #include "Common/Profiler/Profiler.h" @@ -48,36 +45,28 @@ using namespace Rasterizer; struct BinWaitable : public Waitable { public: BinWaitable() { - count_ = 0; + retro_atomic_int_init(&count_, 0); } void Fill() { - count_++; + retro_atomic_fetch_add_int(&count_, 1); } bool Empty() { - return count_ == 0; + return retro_atomic_load_acquire_int(&count_) == 0; } void Drain() { - int result = --count_; - if (result == 0) { - // We were the last one to increment. - std::unique_lock lock(mutex_); - cond_.notify_all(); + if (retro_atomic_fetch_sub_int(&count_, 1) == 1) { + ParkingLotNotify(this); } } void Wait() override { - std::unique_lock lock(mutex_); - while (count_ != 0) { - cond_.wait(lock); - } + ParkingLotWait(this, [this] { return retro_atomic_load_acquire_int(&count_) == 0; }); } - std::atomic count_; - std::mutex mutex_; - std::condition_variable cond_; + retro_atomic_int_t count_; }; static inline void DrawBinItem(const BinItem &item, const BinCoords &range, const RasterizerState &state) { @@ -114,7 +103,7 @@ static inline void DrawBinItem(const BinItem &item, const RasterizerState &state class DrawBinItemsTask : public Task { public: - DrawBinItemsTask(BinWaitable *notify, BinManager *binner, int index, std::atomic &status) + DrawBinItemsTask(BinWaitable *notify, BinManager *binner, int index, retro_atomic_int_t &status) : notify_(notify), binner_(binner), index_(index), status_(status) { } @@ -130,8 +119,10 @@ public: void Run() override { binner_->WakeChained(); binner_->ProcessTiles(index_); - status_ = false; + retro_atomic_store_release_int(&status_, 0); // Work queued after the last look, but before status_ said we were done, would otherwise wait. + // Paired with the fence in WakeTasks(). + retro_atomic_thread_fence_seq_cst(); binner_->ProcessTiles(index_); notify_->Drain(); } @@ -144,7 +135,7 @@ private: BinWaitable *notify_; BinManager *binner_; int index_; - std::atomic &status_; + retro_atomic_int_t &status_; }; constexpr int BinManager::MAX_POSSIBLE_TASKS; @@ -152,7 +143,9 @@ constexpr int BinManager::MAX_POSSIBLE_TASKS; BinManager::BinManager() { waitable_ = new BinWaitable(); for (auto &s : taskStatus_) - s = false; + retro_atomic_int_init(&s, 0); + for (auto &c : chainWake_) + retro_atomic_int_init(&c, 0); int maxInitTasks = std::min(g_threadManager.GetNumLooperThreads(), MAX_POSSIBLE_TASKS); maxTasks_ = FORCE_SINGLE_THREAD ? 1 : maxInitTasks; @@ -163,12 +156,12 @@ BinManager::BinManager() { PickTileSize(maxInitTasks); tiles_ = (Tile *)AllocateAlignedMemory(sizeof(Tile) * tilesX_ * tilesY_, 64); for (int i = 0; i < tilesX_ * tilesY_; ++i) { - tiles_[i].head = 0; - tiles_[i].tail = 0; - tiles_[i].busy = false; + retro_atomic_int_init(&tiles_[i].head, 0); + retro_atomic_int_init(&tiles_[i].tail, 0); + retro_atomic_int_init(&tiles_[i].busy, 0); } for (auto &r : itemRefs_) - r = 0; + retro_atomic_int_init(&r, 0); states_.Setup(); cluts_.Setup(); queue_.Setup(); @@ -577,7 +570,7 @@ void BinManager::Drain() { PROFILE_THIS_SCOPE("bin_drain"); // Also when switching to one thread: what's left goes to the tiles, which hold the earlier items. - if (maxTasks_ > 1 || activeCount_ != 0) { + if (maxTasks_ > 1 || retro_atomic_load_relaxed_int(&activeCount_) != 0) { DistributeItems(); // Waking threads costs more than drawing a little: the flush draws that itself. if (entriesSinceWake_ >= WAKE_ENTRIES) { @@ -649,20 +642,20 @@ void BinManager::DistributeItems(size_t end) { const int ty1 = std::clamp(item.range.y1 >> tileShiftY_, 0, tilesY_ - 1); const int ty2 = std::clamp(item.range.y2 >> tileShiftY_, 0, tilesY_ - 1); // Set before any tile can draw it. - itemRefs_[index].store((tx2 - tx1 + 1) * (ty2 - ty1 + 1), std::memory_order_relaxed); + retro_atomic_store_relaxed_int(&itemRefs_[index], (tx2 - tx1 + 1) * (ty2 - ty1 + 1)); for (int ty = ty1; ty <= ty2; ++ty) { for (int tx = tx1; tx <= tx2; ++tx) { const int t = ty * tilesX_ + tx; Tile &tile = tiles_[t]; - const uint32_t tail = tile.tail.load(std::memory_order_relaxed); + const uint32_t tail = (uint32_t)retro_atomic_load_relaxed_int(&tile.tail); tile.items[tail % QUEUED_PRIMS] = (uint16_t)index; entriesSinceWake_++; - tile.tail.store(tail + 1, std::memory_order_release); + retro_atomic_store_release_int(&tile.tail, (int)(tail + 1)); if (!tileActive_[t]) { tileActive_[t] = true; - const int count = activeCount_.load(std::memory_order_relaxed); + const int count = retro_atomic_load_relaxed_int(&activeCount_); activeTiles_[count] = (uint16_t)t; - activeCount_.store(count + 1, std::memory_order_release); + retro_atomic_store_release_int(&activeCount_, count + 1); } } } @@ -671,43 +664,46 @@ void BinManager::DistributeItems(size_t end) { } void BinManager::ResetTiles() { - const int activeCount = activeCount_; + // Workers are all done here (waitable_), so relaxed is enough; their next run is enqueued after this. + const int activeCount = retro_atomic_load_relaxed_int(&activeCount_); for (int i = 0; i < activeCount; ++i) { const int t = activeTiles_[i]; - tiles_[t].head = 0; - tiles_[t].tail = 0; + retro_atomic_store_relaxed_int(&tiles_[t].head, 0); + retro_atomic_store_relaxed_int(&tiles_[t].tail, 0); tileActive_[t] = false; } - activeCount_ = 0; + retro_atomic_store_relaxed_int(&activeCount_, 0); entriesSinceWake_ = 0; tileGen_++; } void BinManager::ReclaimItems() { - while (!queue_.Empty() && queue_.head_ != distributePos_ && itemRefs_[queue_.head_].load(std::memory_order_acquire) == 0) + while (!queue_.Empty() && queue_.head_ != distributePos_ && retro_atomic_load_acquire_int(&itemRefs_[queue_.head_]) == 0) queue_.SkipNext(); } void BinManager::WakeTasks() { // No more threads than tiles with work. int pending = 0; - const int count = activeCount_.load(std::memory_order_relaxed); + // Paired with the fence in DrawBinItemsTask::Run(): either it sees the new items or we see it running. + retro_atomic_thread_fence_seq_cst(); + const int count = retro_atomic_load_relaxed_int(&activeCount_); for (int n = 0; n < count && pending < maxTasks_; ++n) { - const Tile &tile = tiles_[activeTiles_[n]]; - if (tile.head.load(std::memory_order_relaxed) != tile.tail.load(std::memory_order_relaxed)) + Tile &tile = tiles_[activeTiles_[n]]; + if (retro_atomic_load_relaxed_int(&tile.head) != retro_atomic_load_relaxed_int(&tile.tail)) pending++; } int first = -1; uint64_t chain = 0; for (int i = 0; i < maxTasks_ && pending > 0; ++i) { - if (taskStatus_[i]) { + if (retro_atomic_load_acquire_int(&taskStatus_[i])) { pending--; continue; } waitable_->Fill(); - taskStatus_[i] = true; + retro_atomic_store_relaxed_int(&taskStatus_[i], 1); if (first < 0) first = i; else @@ -717,18 +713,27 @@ void BinManager::WakeTasks() { } if (first < 0) return; - if (chain != 0) - chainWake_.fetch_or(chain, std::memory_order_release); + for (int w = 0; w < (int)ARRAY_SIZE(chainWake_); ++w) { + const uint32_t bits = (uint32_t)(chain >> (w * 32)); + if (bits != 0) { + retro_atomic_fetch_or_int(&chainWake_[w], (int)bits); + } + } g_threadManager.EnqueueTaskOnThread(first, taskLists_[first].Next()); mostThreads_ = std::max(mostThreads_, maxTasks_); } void BinManager::WakeChained() { - u64 chain = chainWake_.exchange(0, std::memory_order_acquire); - while (chain != 0) { - const int i = LeastSignificantSetBit(chain); - chain &= chain - 1; - g_threadManager.EnqueueTaskOnThread(i, taskLists_[i].Next()); + for (int w = 0; w < (int)ARRAY_SIZE(chainWake_); ++w) { + if (retro_atomic_load_relaxed_int(&chainWake_[w]) == 0) { + continue; + } + uint32_t chain = (uint32_t)retro_atomic_exchange_int(&chainWake_[w], 0); + while (chain != 0) { + const int i = w * 32 + LeastSignificantSetBit(chain); + chain &= chain - 1; + g_threadManager.EnqueueTaskOnThread(i, taskLists_[i].Next()); + } } } @@ -744,22 +749,25 @@ void BinManager::MakeRoom() { if (!ProcessTiles(0)) { // Every tile with work is held by a worker. Park until one lets go of a tile // (workers only notify while roomWaiting_ is up) or room frees up. - roomWaiting_.store(true, std::memory_order_seq_cst); + retro_atomic_store_release_int(&roomWaiting_, 1); + retro_atomic_thread_fence_seq_cst(); ParkingLotWait(&roomWaiting_, [&] { ReclaimItems(); return !queue_.Full() || HasIdleTileWithWork(); }); - roomWaiting_.store(false, std::memory_order_relaxed); + retro_atomic_store_relaxed_int(&roomWaiting_, 0); } ReclaimItems(); } } bool BinManager::HasIdleTileWithWork() const { - const int count = activeCount_.load(std::memory_order_acquire); + // The GPU thread's own view: it is the only writer of activeCount_ and tail. + BinManager *self = const_cast(this); + const int count = retro_atomic_load_relaxed_int(&self->activeCount_); for (int n = 0; n < count; ++n) { - const Tile &tile = tiles_[activeTiles_[n]]; - if (tile.head.load(std::memory_order_relaxed) != tile.tail.load(std::memory_order_acquire) && !tile.busy.load(std::memory_order_seq_cst)) + Tile &tile = self->tiles_[activeTiles_[n]]; + if (retro_atomic_load_acquire_int(&tile.head) != retro_atomic_load_relaxed_int(&tile.tail) && !retro_atomic_load_acquire_int(&tile.busy)) return true; } return false; @@ -770,13 +778,13 @@ bool BinManager::ProcessTiles(int start) { bool found; do { found = false; - const int count = activeCount_.load(std::memory_order_acquire); + const int count = retro_atomic_load_acquire_int(&activeCount_); for (int n = 0; n < count; ++n) { const int t = activeTiles_[(start + n) % count]; Tile &tile = tiles_[t]; - if (tile.head.load(std::memory_order_relaxed) == tile.tail.load(std::memory_order_acquire)) + if (retro_atomic_load_relaxed_int(&tile.head) == retro_atomic_load_acquire_int(&tile.tail)) continue; - if (tile.busy.load(std::memory_order_relaxed) || tile.busy.exchange(true, std::memory_order_acquire)) + if (retro_atomic_load_relaxed_int(&tile.busy) || retro_atomic_exchange_int(&tile.busy, 1)) continue; const int tx = t % tilesX_, ty = t / tilesX_; @@ -784,9 +792,9 @@ bool BinManager::ProcessTiles(int start) { tx << tileShiftX_, ty << tileShiftY_, ((tx + 1) << tileShiftX_) - 1, ((ty + 1) << tileShiftY_) - 1, }; - uint32_t head = tile.head.load(std::memory_order_relaxed); + uint32_t head = (uint32_t)retro_atomic_load_relaxed_int(&tile.head); uint32_t tail; - while (head != (tail = tile.tail.load(std::memory_order_acquire))) { + while (head != (tail = (uint32_t)retro_atomic_load_acquire_int(&tile.tail))) { for (; head != tail; ++head) { const uint16_t index = tile.items[head % QUEUED_PRIMS]; const BinItem &item = queue_[index]; @@ -794,13 +802,14 @@ bool BinManager::ProcessTiles(int start) { const BinCoords range = tileRange.Intersect(item.range); if (!range.Invalid()) DrawBinItem(item, range, states_[item.stateIndex]); - itemRefs_[index].fetch_sub(1, std::memory_order_release); + retro_atomic_fetch_sub_int(&itemRefs_[index], 1); } - tile.head.store(head, std::memory_order_release); + retro_atomic_store_release_int(&tile.head, (int)head); } - // Paired with the seq_cst store in MakeRoom(): either it sees this tile idle, or we see it waiting. - tile.busy.store(false, std::memory_order_seq_cst); - if (roomWaiting_.load(std::memory_order_seq_cst)) + retro_atomic_store_release_int(&tile.busy, 0); + // Paired with the fence in MakeRoom(): either it sees this tile idle, or we see it waiting. + retro_atomic_thread_fence_seq_cst(); + if (retro_atomic_load_relaxed_int(&roomWaiting_)) ParkingLotNotify(&roomWaiting_); found = true; any = true; @@ -828,7 +837,7 @@ void BinManager::Flush(const char *reason) { st = time_now_d(); } Drain(); - if (maxTasks_ > 1 || activeCount_ != 0) { + if (maxTasks_ > 1 || retro_atomic_load_relaxed_int(&activeCount_) != 0) { // Help with the drawing, then wait for what the threads are still on. while (ProcessTiles(0)) { } @@ -1245,7 +1254,7 @@ void BinManager::DrainDependent() { pendingStateIndex_ = stateIndex_; if (distributePos_ != last) DistributeItems(last); - if (activeCount_ != 0) { + if (retro_atomic_load_relaxed_int(&activeCount_) != 0) { if (entriesSinceWake_ >= WAKE_ENTRIES) WakeTasks(); while (ProcessTiles(0)) { diff --git a/GPU/Software/BinManager.h b/GPU/Software/BinManager.h index 02353e43e7..ece1bd8623 100644 --- a/GPU/Software/BinManager.h +++ b/GPU/Software/BinManager.h @@ -17,8 +17,8 @@ #pragma once -#include #include +#include #include "GPU/Software/Rasterizer.h" struct BinWaitable; @@ -168,10 +168,11 @@ struct BinQueue { return items_[index]; } + // Indices are the GPU thread's; workers only read items it published through the tiles. T *items_ = nullptr; - std::atomic head_; - std::atomic tail_ ; - std::atomic size_; + size_t head_; + size_t tail_; + size_t size_; size_t capacity_ = N; }; @@ -283,11 +284,12 @@ private: int maxTasks_ = 1; BinTaskList taskLists_[MAX_POSSIBLE_TASKS]; - std::atomic taskStatus_[MAX_POSSIBLE_TASKS]; + retro_atomic_int_t taskStatus_[MAX_POSSIBLE_TASKS]; // Threads whose tasks the first one woken enqueues: waking a thread is a system call, kept off this one. - std::atomic chainWake_{ 0 }; + // A bitmask, 32 tasks per word. + retro_atomic_int_t chainWake_[(MAX_POSSIBLE_TASKS + 31) / 32]; // Up while MakeRoom() is parked; workers letting go of a tile then wake it. - std::atomic roomWaiting_{ false }; + retro_atomic_int_t roomWaiting_{ 0 }; // With threads, queued items are binned into screen tiles. Any thread can take a tile with work and // draws its items in order; only one at a time, so each pixel still sees the primitives in order. @@ -307,17 +309,17 @@ private: int tilesY_ = TILES_Y; struct Tile { // Indices into queue_, as a ring: head_ is how many have been drawn, tail_ how many were pushed. - std::atomic head; - std::atomic tail; - std::atomic busy; + retro_atomic_int_t head; + retro_atomic_int_t tail; + retro_atomic_int_t busy; uint16_t items[QUEUED_PRIMS]; }; Tile *tiles_ = nullptr; // For each queued item, how many tiles still have to draw it. It's reclaimed at zero. - std::atomic itemRefs_[QUEUED_PRIMS]; + retro_atomic_int_t itemRefs_[QUEUED_PRIMS]; // The tiles given work since the last flush, for the threads to look through. uint16_t activeTiles_[TILES_X * TILES_Y]; - std::atomic activeCount_{ 0 }; + retro_atomic_int_t activeCount_{ 0 }; bool tileActive_[TILES_X * TILES_Y]{}; // The queue_ index of the first item not yet put in tiles, and how many have been added since. size_t distributePos_ = 0; diff --git a/GPU/Software/DrawPixel.cpp b/GPU/Software/DrawPixel.cpp index 1438661cb0..7b50cc46b8 100644 --- a/GPU/Software/DrawPixel.cpp +++ b/GPU/Software/DrawPixel.cpp @@ -16,7 +16,6 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include "ppsspp_config.h" -#include #include "Common/Common.h" #include "Common/Data/Convert/ColorConv.h" #include "Core/Config.h" @@ -30,7 +29,6 @@ using namespace Math3D; namespace Rasterizer { -std::mutex jitCacheLock; PixelJitCache *jitCache = nullptr; void Init() { @@ -839,16 +837,16 @@ SingleFunc PixelJitCache::GenericSingle(const PixelFuncID &id) { } thread_local PixelJitCache::LastCache PixelJitCache::lastSingle_; -int PixelJitCache::clearGen_ = 0; +retro_atomic_int_t PixelJitCache::clearGen_{ 0 }; // 256k should be plenty of space for plenty of variations. -PixelJitCache::PixelJitCache() : CodeBlock(1024 * 64 * 4), cache_(64) { +PixelJitCache::PixelJitCache() : CodeBlock(1024 * 64 * 4) { lastSingle_.gen = -1; - clearGen_++; + retro_atomic_fetch_add_int(&clearGen_, 1); } void PixelJitCache::Clear() { - clearGen_++; + retro_atomic_fetch_add_int(&clearGen_, 1); CodeBlock::Clear(); cache_.Clear(); addresses_.clear(); @@ -876,15 +874,15 @@ std::string PixelJitCache::DescribeCodePtr(const u8 *ptr) { return CodeBlock::DescribeCodePtr(ptr); } +// GPU thread. void PixelJitCache::Flush() { - std::unique_lock guard(jitCacheLock); - for (const auto &queued : compileQueue_) { + compileQueue_.Drain([this](PixelFuncID &&queued) { // Might've been compiled after enqueue, but before now. size_t queuedKey = std::hash()(queued); - if (!cache_.ContainsKey(queuedKey)) + if (!cache_.ContainsKey(queuedKey)) { Compile(queued); - } - compileQueue_.clear(); + } + }); } // Without a backend nothing ever compiles, and a lookup would flush the binner for nothing. @@ -899,40 +897,31 @@ SingleFunc PixelJitCache::GetSingle(const PixelFuncID &id, BinManager *binner) { return nullptr; const size_t key = std::hash()(id); - if (lastSingle_.Match(key, clearGen_)) + if (lastSingle_.Match(key, ClearGeneration())) return lastSingle_.func; - std::unique_lock guard(jitCacheLock); SingleFunc singleFunc; if (cache_.Get(key, &singleFunc)) { - lastSingle_.Set(key, singleFunc, clearGen_); + lastSingle_.Set(key, singleFunc, ClearGeneration()); return singleFunc; } if (!binner) { // Can't compile, let's try to do it later when there's an opportunity. - compileQueue_.insert(id); + compileQueue_.Push(id); return nullptr; } - guard.unlock(); + // The GPU thread, with nothing drawing after this. binner->Flush("compile"); - guard.lock(); - - for (const auto &queued : compileQueue_) { - // Might've been compiled after enqueue, but before now. - size_t queuedKey = std::hash()(queued); - if (!cache_.ContainsKey(queuedKey)) - Compile(queued); - } - compileQueue_.clear(); + Flush(); // Might've been in the queue. if (!cache_.ContainsKey(key)) Compile(id); if (cache_.Get(key, &singleFunc)) { - lastSingle_.Set(key, singleFunc, clearGen_); + lastSingle_.Set(key, singleFunc, ClearGeneration()); return singleFunc; } else { return nullptr; @@ -941,7 +930,7 @@ SingleFunc PixelJitCache::GetSingle(const PixelFuncID &id, BinManager *binner) { void PixelJitCache::Compile(const PixelFuncID &id) { // x64 is typically 200-500 bytes, but let's be safe. - if (GetSpaceLeft() < 65536) { + if (GetSpaceLeft() < 65536 || cache_.NearFull()) { Clear(); } diff --git a/GPU/Software/DrawPixel.h b/GPU/Software/DrawPixel.h index 729d96bedc..e3cc469709 100644 --- a/GPU/Software/DrawPixel.h +++ b/GPU/Software/DrawPixel.h @@ -22,8 +22,8 @@ #include #include #include -#include -#include "Common/Data/Collections/Hashmaps.h" +#include +#include "Common/Thread/MpscQueue.h" #include "GPU/Math3D.h" #include "GPU/Software/FuncId.h" #include "GPU/Software/RasterizerRegCache.h" @@ -71,7 +71,7 @@ public: void Clear() override; void Flush(); // Changes whenever the code space is cleared, which frees all previously returned functions. - static int ClearGeneration() { return clearGen_; } + static int ClearGeneration() { return retro_atomic_load_relaxed_int(&clearGen_); } std::string DescribeCodePtr(const u8 *ptr) override; @@ -128,10 +128,13 @@ private: } }; - DenseHashMap cache_; + // Compiled and cleared on the GPU thread, looked up by any (see JitFuncTable). + JitFuncTable cache_; + // GPU thread only. std::unordered_map addresses_; - std::unordered_set compileQueue_; - static int clearGen_; + // Lookups that missed without a binner to flush, compiled on the GPU thread at the next chance. + MpscQueue compileQueue_; + static retro_atomic_int_t clearGen_; static thread_local LastCache lastSingle_; const u8 *constBlendHalf_11_4s_ = nullptr; diff --git a/GPU/Software/RasterizerRegCache.h b/GPU/Software/RasterizerRegCache.h index 8b76260d00..827db1a172 100644 --- a/GPU/Software/RasterizerRegCache.h +++ b/GPU/Software/RasterizerRegCache.h @@ -24,6 +24,8 @@ #include #include +#include + #include "Common/Common.h" #include "Common/Math/SIMDHeaders.h" @@ -267,4 +269,86 @@ private: std::vector prologGen_; }; +// Compiled functions by ID hash, open addressed and insert-only. One thread (the GPU thread) inserts +// and clears; any thread looks up without locking. Clear() only while no other thread looks up, +// since it also goes with freeing the code. A null function is cached as failed. +template +class JitFuncTable { + static_assert((N & (N - 1)) == 0, "N must be a power of two"); + +public: + JitFuncTable() { + for (Slot &slot : slots_) { + slot.key = 0; + retro_atomic_ptr_init(&slot.value, nullptr); + } + } + JitFuncTable(const JitFuncTable &) = delete; + JitFuncTable &operator=(const JitFuncTable &) = delete; + + bool Get(size_t key, F *func) { + size_t i = key & (N - 1); + for (int n = 0; n < N; ++n) { + void *value = retro_atomic_load_acquire_ptr(&slots_[i].value); + if (!value) { + return false; + } + if (slots_[i].key == key) { + *func = value == Failed() ? nullptr : reinterpret_cast(value); + return true; + } + i = (i + 1) & (N - 1); + } + return false; + } + + bool ContainsKey(size_t key) { + F func; + return Get(key, &func); + } + + // Inserting thread only. Compilers clear first when NearFull(), so there's always a free slot. + void Insert(size_t key, F func) { + size_t i = key & (N - 1); + for (int n = 0; n < N; ++n) { + void *value = retro_atomic_load_relaxed_ptr(&slots_[i].value); + if (!value) { + slots_[i].key = key; + retro_atomic_store_release_ptr(&slots_[i].value, func ? reinterpret_cast(func) : Failed()); + count_++; + return; + } + if (slots_[i].key == key) { + return; + } + i = (i + 1) & (N - 1); + } + } + + bool NearFull() const { + return count_ >= N - N / 4; + } + + void Clear() { + for (Slot &slot : slots_) { + retro_atomic_store_relaxed_ptr(&slot.value, nullptr); + } + count_ = 0; + } + +private: + static void *Failed() { + static char failed; + return &failed; + } + + struct Slot { + // Written before value is published. + size_t key; + retro_atomic_ptr_t value; + }; + Slot slots_[N]; + int count_ = 0; +}; + }; diff --git a/GPU/Software/Sampler.cpp b/GPU/Software/Sampler.cpp index e07cfd55f3..4bfce5014f 100644 --- a/GPU/Software/Sampler.cpp +++ b/GPU/Software/Sampler.cpp @@ -17,7 +17,6 @@ #include "ppsspp_config.h" #include -#include #include "Common/Common.h" #include "Common/Data/Convert/ColorConv.h" #include "Common/LogReporting.h" @@ -38,7 +37,6 @@ static Vec4IntResult SOFTRAST_CALL SampleNearest(float s, float t, Vec4IntArg pr static Vec4IntResult SOFTRAST_CALL SampleLinear(float s, float t, Vec4IntArg prim_color, const u8 *const *tptr, const uint16_t *bufw, int level, int levelFrac, const SamplerID &samplerID); static Vec4IntResult SOFTRAST_CALL SampleFetch(int u, int v, const u8 *tptr, int bufw, int level, const SamplerID &samplerID); -std::mutex jitCacheLock; SamplerJitCache *jitCache = nullptr; void Init() { @@ -100,18 +98,18 @@ FetchFunc GetFetchFunc(SamplerID id, BinManager *binner) { thread_local SamplerJitCache::LastCache SamplerJitCache::lastFetch_; thread_local SamplerJitCache::LastCache SamplerJitCache::lastNearest_; thread_local SamplerJitCache::LastCache SamplerJitCache::lastLinear_; -int SamplerJitCache::clearGen_ = 0; +retro_atomic_int_t SamplerJitCache::clearGen_{ 0 }; // 256k should be enough. -SamplerJitCache::SamplerJitCache() : Rasterizer::CodeBlock(1024 * 64 * 4), cache_(64) { +SamplerJitCache::SamplerJitCache() : Rasterizer::CodeBlock(1024 * 64 * 4) { lastFetch_.gen = -1; lastNearest_.gen = -1; lastLinear_.gen = -1; - clearGen_++; + retro_atomic_fetch_add_int(&clearGen_, 1); } void SamplerJitCache::Clear() { - clearGen_++; + retro_atomic_fetch_add_int(&clearGen_, 1); CodeBlock::Clear(); cache_.Clear(); addresses_.clear(); @@ -153,14 +151,17 @@ std::string SamplerJitCache::DescribeCodePtr(const u8 *ptr) { } void SamplerJitCache::Flush() { - std::unique_lock guard(jitCacheLock); - for (const auto &queued : compileQueue_) { + CompileQueued(); +} + +void SamplerJitCache::CompileQueued() { + compileQueue_.Drain([this](SamplerID &&queued) { // Might've been compiled after enqueue, but before now. size_t queuedKey = std::hash()(queued); - if (!cache_.ContainsKey(queuedKey)) + if (!cache_.ContainsKey(queuedKey)) { Compile(queued); - } - compileQueue_.clear(); + } + }); } // Without a backend nothing ever compiles, and a lookup would flush the binner for nothing. @@ -177,8 +178,6 @@ static bool CanJit(const SamplerID &id) { } NearestFunc SamplerJitCache::GetByID(const SamplerID &id, size_t key, BinManager *binner) { - std::unique_lock guard(jitCacheLock); - NearestFunc func; if (cache_.Get(key, &func)) { return func; @@ -186,21 +185,13 @@ NearestFunc SamplerJitCache::GetByID(const SamplerID &id, size_t key, BinManager if (!binner) { // Can't compile, let's try to do it later when there's an opportunity. - compileQueue_.insert(id); + compileQueue_.Push(id); return nullptr; } - guard.unlock(); + // The GPU thread, with nothing drawing after this. binner->Flush("compile"); - guard.lock(); - - for (const auto &queued : compileQueue_) { - // Might've been compiled after enqueue, but before now. - size_t queuedKey = std::hash()(queued); - if (!cache_.ContainsKey(queuedKey)) - Compile(queued); - } - compileQueue_.clear(); + CompileQueued(); if (!cache_.ContainsKey(key)) Compile(id); @@ -218,11 +209,11 @@ NearestFunc SamplerJitCache::GetNearest(const SamplerID &id, BinManager *binner) return nullptr; const size_t key = std::hash()(id); - if (lastNearest_.Match(key, clearGen_)) + if (lastNearest_.Match(key, ClearGeneration())) return (NearestFunc)lastNearest_.func; auto func = GetByID(id, key, binner); - lastNearest_.Set(key, func, clearGen_); + lastNearest_.Set(key, func, ClearGeneration()); return (NearestFunc)func; } @@ -231,11 +222,11 @@ LinearFunc SamplerJitCache::GetLinear(const SamplerID &id, BinManager *binner) { return nullptr; const size_t key = std::hash()(id); - if (lastLinear_.Match(key, clearGen_)) + if (lastLinear_.Match(key, ClearGeneration())) return (LinearFunc)lastLinear_.func; auto func = GetByID(id, key, binner); - lastLinear_.Set(key, func, clearGen_); + lastLinear_.Set(key, func, ClearGeneration()); return (LinearFunc)func; } @@ -244,17 +235,17 @@ FetchFunc SamplerJitCache::GetFetch(const SamplerID &id, BinManager *binner) { return nullptr; const size_t key = std::hash()(id); - if (lastFetch_.Match(key, clearGen_)) + if (lastFetch_.Match(key, ClearGeneration())) return (FetchFunc)lastFetch_.func; auto func = GetByID(id, key, binner); - lastFetch_.Set(key, func, clearGen_); + lastFetch_.Set(key, func, ClearGeneration()); return (FetchFunc)func; } void SamplerJitCache::Compile(const SamplerID &id) { // This should be sufficient. - if (GetSpaceLeft() < 16384) { + if (GetSpaceLeft() < 16384 || cache_.NearFull()) { Clear(); } diff --git a/GPU/Software/Sampler.h b/GPU/Software/Sampler.h index 60058a1512..2f51cf3e70 100644 --- a/GPU/Software/Sampler.h +++ b/GPU/Software/Sampler.h @@ -20,8 +20,8 @@ #include "ppsspp_config.h" #include -#include -#include "Common/Data/Collections/Hashmaps.h" +#include +#include "Common/Thread/MpscQueue.h" #include "GPU/Math3D.h" #include "GPU/Software/FuncId.h" #include "GPU/Software/RasterizerRegCache.h" @@ -64,12 +64,13 @@ public: void Clear() override; void Flush(); // Changes whenever the code space is cleared, which frees all previously returned functions. - static int ClearGeneration() { return clearGen_; } + static int ClearGeneration() { return retro_atomic_load_relaxed_int(&clearGen_); } std::string DescribeCodePtr(const u8 *ptr) override; private: void Compile(const SamplerID &id); + void CompileQueued(); NearestFunc GetByID(const SamplerID &id, size_t key, BinManager *binner); FetchFunc CompileFetch(const SamplerID &id); NearestFunc CompileNearest(const SamplerID &id); @@ -147,10 +148,13 @@ private: } }; - DenseHashMap cache_; + // Compiled and cleared on the GPU thread, looked up by any (see JitFuncTable). + Rasterizer::JitFuncTable cache_; + // GPU thread only. std::unordered_map addresses_; - std::unordered_set compileQueue_; - static int clearGen_; + // Lookups that missed without a binner to flush, compiled on the GPU thread at the next chance. + MpscQueue compileQueue_; + static retro_atomic_int_t clearGen_; static thread_local LastCache lastFetch_; static thread_local LastCache lastNearest_; static thread_local LastCache lastLinear_; diff --git a/GPU/Vulkan/GPU_Vulkan.cpp b/GPU/Vulkan/GPU_Vulkan.cpp index 5d69cc61fc..3f6fd12372 100644 --- a/GPU/Vulkan/GPU_Vulkan.cpp +++ b/GPU/Vulkan/GPU_Vulkan.cpp @@ -16,7 +16,6 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include #include "Common/Profiler/Profiler.h" diff --git a/GPU/Vulkan/GPU_Vulkan.h b/GPU/Vulkan/GPU_Vulkan.h index 264a0b51db..88d8dbebe5 100644 --- a/GPU/Vulkan/GPU_Vulkan.h +++ b/GPU/Vulkan/GPU_Vulkan.h @@ -19,7 +19,6 @@ #include #include -#include #include "Common/File/Path.h" diff --git a/GPU/Vulkan/ShaderManagerVulkan.h b/GPU/Vulkan/ShaderManagerVulkan.h index 86848b60ad..368e2a0b3e 100644 --- a/GPU/Vulkan/ShaderManagerVulkan.h +++ b/GPU/Vulkan/ShaderManagerVulkan.h @@ -19,7 +19,6 @@ #include #include -#include #include "Common/Thread/Promise.h" #include "Common/Data/Collections/Hashmaps.h" diff --git a/docs/DebuggerThreading.md b/docs/DebuggerThreading.md index 2975476c35..09ce297e86 100644 --- a/docs/DebuggerThreading.md +++ b/docs/DebuggerThreading.md @@ -1,132 +1,52 @@ -# Debugger threading model (Core_RunOnCPUThread / g_frameMutex) +# Debugger threading model -Which thread may touch CPU-thread-owned debugger state (breakpoints, symbol map, registers, memory, -kernel threads), and in which order the locks have to be taken. Read this before touching -`Windows/Debugger/*`, `Core/Debugger/WebSocket*` or anything else that reaches into core state from -outside the CPU thread. +Which thread may touch what in the WebSocket debugger (headless builds; the libretro core doesn't +build it). Read this before touching `Core/Debugger/WebSocket*`, `Core/WebServer.cpp` or anything +else that reaches into core state from outside the CPU thread. -CPU-thread-owned debugger state (`g_breakpoints`, `g_symbolMap`, `g_disassemblyManager`, registers, -memory, kernel threads) used to be touched directly from other threads (the WebSocket handler -thread, and the legacy Win32 debugger's message-pump thread) with no synchronization. Two -mechanisms now exist for doing this safely - pick based on whether you're mutating or just reading: +There are no locks here, and none should come back. State has one owner; other threads hand it +work or messages. -- **`Core_RunOnCPUThread(func)`** (`Core.h`/`Core.cpp`) - queues `func` to run on the CPU thread, - blocking the caller until it's done. Use for *mutations* (breakpoint add/remove, register/memory - writes, symbol map edits, stepping requests, thread wake/kill). Drained at the top of every - `Core_RunLoopUntil()` iteration, so it's reached whether the CPU is running or stepping/paused; - runs immediately if already called from the CPU thread. Two hard rules learned the hard way: - never put a modal Win32 dialog call (`MessageBox`, `DialogBoxParam`, `InputBox_GetString`, - `SomeDialog::exec()`) inside the queued lambda - it would block the CPU thread on user input, so - split the function into "read/decide", "show modal", "mutate" pieces instead. And never call - `SendMessage()` targeting one of your own GUI-thread windows from inside the lambda - the calling - GUI thread is blocked waiting on the CPU thread rather than pumping messages, so a cross-thread - `SendMessage()` back to it deadlocks; keep such calls outside the lambda instead. -- **`g_frameMutex`** (`Core.h`/`Core.cpp`) - a plain `std::mutex`, held by `NativeFrame()` - (`UI/NativeApp.cpp`) only across the span where it actually touches that state - (`g_breakpoints.Frame()` through `g_screenManager->render()` - where `Core_RunLoopUntil()`/actual - CPU stepping happens - through `runImDebugger`/`renderImDebugger`), not across input handling or - the present/frame-pacing waits. Use for *reads* invoked very frequently (`WM_PAINT`, a list - reload triggered on every debugger-state-changed notification) where routing through - `Core_RunOnCPUThread` would be too slow/heavy. The legacy Win32 debugger windows do this now - - see `CtrlRegisterList::onPaint`, `CtrlDisAsmView::onPaint`, `CtrlMemView::onPaint`, - `CtrlBreakpointList::reloadBreakpoints`, `CtrlThreadList::reloadThreads`, etc. in - `Windows/Debugger/*.cpp`. +## Threads -Architecture fact that makes `g_frameMutex` correct: regardless of graphics backend, `NativeFrame()` -(and thus CPU emulation via `Core_RunLoopUntil()`, and the Dear ImGui debugger) always runs on the -*same* thread - see `Core/EmuThread.cpp`. When a backend needs its own thread for actual graphics -API calls (`GraphicsContext::NeedsSeparateEmuThread()` - true for OpenGL, SDL, headless, libretro, -Qt; false for D3D11/Vulkan on Windows), the *original* thread stays behind purely to pump -`graphicsContext->ThreadFrame()` (i.e. just executes queued graphics API calls), and a *newly -spawned* thread takes over `NativeFrame()`/game logic/CPU duty. So `UI/ImDebugger/*.cpp` is always -safe to read/write this state directly, without either mechanism - it's always on the same thread -as `Core_RunLoopUntil()`. The legacy Win32 debugger is different: its dialogs are pumped by the -*original* `WinMain` message-loop thread, which is a genuinely separate OS thread from whichever -thread ends up running `NativeFrame()`/CPU, regardless of backend (this split happens one level -above the `NeedsSeparateEmuThread()` branch) - that's the whole reason it needed this mechanism. +- **CPU thread** - runs `Core_RunLoopUntil()` and owns all emulator state, including the debugger's + (`g_breakpoints`, `g_symbolMap`, `g_disassemblyManager`, `MIPSAnalyst`'s function tables, the + `MemBlockInfo` slab maps, registers, memory, kernel objects). +- **Web server thread** (`Core/WebServer.cpp`) - blocks in `http::Server::RunSlice()` on the + listening socket and the server's `net::CancelToken`, with no timeout. `ShutdownWebServer()` + cancels the token, which wakes the accept loop and every connection, then joins the thread. +- **Connection threads** - one per request (`NewThreadExecutor`). A debugger session runs + `HandleDebuggerRequest()` there. The server thread joins them all before it exits. -**Update (2026-08-08): both `BreakpointManager`'s and `SymbolMap`'s internal mutexes have been -removed** after auditing every touchpoint across the codebase (WebSocket subscribers, -`Windows/Debugger/*.cpp`, `Windows/MainWindowMenu.cpp`/`Windows/MainWindow.cpp` main-window menu -items, `Core/Core.cpp`'s free-threaded `Core_Break`/`Core_Resume`, `UI/ImDebugger/*.cpp`, -JIT/interpreter backends, `Core/Debugger/MemBlockInfo.cpp`) and confirming each is covered by one of -the two mechanisms above or is already on the CPU/NativeFrame thread. Notably, `Windows/main.cpp`'s -`SortSymbols()` calls (fired from `System_Notify(BOOT_DONE)`/`System_Notify(SYMBOL_MAP_UPDATED)`) -turned out to already be safe without any change - both notifications are only ever fired from the -CPU/NativeFrame thread (`UI/EmuScreen.cpp`, `Core/HLE/sceKernelModule.cpp`), despite an old comment -there claiming reliance on the (now-removed) internal lock. `Qt/mainwindow.cpp`/`Qt/QtMain.cpp` -still poke at `g_symbolMap` directly and unguarded on the Qt UI thread - a pre-existing issue, -deliberately left alone since Qt isn't a maintained backend and is slated for removal; removing the -lock doesn't change `SymbolMap`'s public API, so Qt still builds, just without that safety net. -`GPU/Common/GPUDebugInterface.cpp` (GE debugger expression evaluation) and `Core/MemFault.cpp` -(crash-time diagnostics) also touch `g_symbolMap` and were deliberately not audited this round - -different subsystem / best-effort-by-nature respectively, follow up if they ever come up. +## Rules -Two more things found while doing this: -- **`Core_RunOnCPUThread()`'s queue is only drained where `Core_RunLoopUntil()` runs, which requires - a game to be loaded** (it's called from `EmuScreen::render()`). Calling `Core_RunOnCPUThread()` - while at the main menu with nothing loaded used to hang forever. Fixed by also calling the - (now public) `Core_ProcessCPUQueue()` directly from `NativeFrame()`, right before - `g_screenManager->render()`, inside the same `g_frameMutex`-locked span - so it always runs, not - just while a game is active. -- **Lock-ordering rule**: because `Core_ProcessCPUQueue()` is called from inside `NativeFrame()`'s - `g_frameMutex`-locked span, any `Core_RunOnCPUThread()` lambda that itself tries to lock - `g_frameMutex` (directly, or indirectly - e.g. by calling something like - `CDisasm::NotifyMapLoaded()`, which locks it internally) will deadlock. Keep such calls outside - the queued lambda, same as the modal-dialog and `SendMessage()` rules above. +- **Emulator state is accessed only inside `Core_RunOnCPUThread(func)`** (`Core/Core.h`). It queues + `func` for the CPU thread and blocks until it has run (inline if already on the CPU thread). Put + the validity checks (`isAlive()`, `IsValidAddress()`) inside the callback too; answering them + outside means acting on a stale answer. +- **Never make the CPU thread wait for a thread that may be waiting on the CPU thread.** A + `Core_RunOnCPUThread()` callback must not block on a connection. The queue is drained only while + the CPU loop runs, so don't rely on it where the loop may never run again (shutdown paths). +- **Events the emulator produces on its own are pushed, never polled.** Each connection has a + `DebuggerEventSink` (a `DebuggerMailbox`, see `WebSocketUtils.h`): an MPSC inbox of formatted + events plus a wake socket. `WebSocketDebuggerTick()`, called from `Core_ProcessCPUQueue()` on the + CPU thread, notices transitions (`game.*`, `cpu.stepping`, `input.buttons`/`input.analog`, + finished `input.buttons.press`) and posts them; breakpoint hits are posted from + `WebSocketNotifyBreakpointHit()`. Don't add a broadcaster that reads emulator state from the + connection thread. +- **A connection blocks until something happens**: socket readable/writable, its sink woken, or the + server stopping (`WebSocketServer::Process(wakeFds, count)`, no timeout). Producers publish first, + then call `mailbox->Wake()`; wakes are coalesced with a pending flag, and the connection rearms + (drains the socket, then clears the flag) before looking at its queues. +- Subscribers fed from another thread (`GPUStatsSubscriber`'s flip listener, `GPURecordSubscriber`'s + record callback) push into their own MPSC queue and call `mailbox->Wake()`; their `Broadcast()` + drains it on the connection thread. Log messages arrive the same way (`LogBroadcaster`). +- Sinks are refcounted: the connection and the CPU thread's list each hold one reference, so a + callback on the CPU thread can still post after the connection has left. -## Lock ordering: `g_frameMutex` before `Core_LockAgainstShutdown()`, always - -`Core_LockAgainstShutdown()` / `CoreShutdownLock` (a `recursive_mutex`, `g_shutdownLock` in -`Core/Core.cpp`) is held across `CPU_Shutdown()` and `Memory::Reinit()`, i.e. while the core is -going away. Take it on any thread other than the CPU thread before reading core state - emulated -memory, the symbol map, kernel objects - so none of it is freed mid-read. It was called -`Memory::Lock()` and only covered the memory map; the name misled people into thinking it was about -memory access. When a function needs both it and `g_frameMutex`, **take `g_frameMutex` first**. - -The CPU thread's order is structural and can't be changed: `NativeFrame()` wraps everything below it -in `g_frameMutex`, and several things under there lock memory - `Core_ProcessCPUQueue()` running a -queued WebSocket handler, and `runImDebugger()` -> `ImMemView` -> `DisassembleRange()`. So the -GUI-thread side is the one that has to match. (Getting it backwards deadlocked for real: a paint -handler held the memory lock and waited for `g_frameMutex` while the CPU thread did the reverse.) - -Also: **a `Core_RunOnCPUThread()` callback does not need `Core_LockAgainstShutdown()`** - teardown only happens -on the CPU thread itself (`Memory::Shutdown()` via `CPU_Shutdown()` <- `PSP_Shutdown()`, all callers -on that thread; `Memory::Reinit()` from `Memory::DoState()` on savestate load). Don't add one. - -**The general rule behind both of these: never make the CPU thread wait for a thread that is (or may -be) waiting on the CPU thread.** `Core_RunOnCPUThread()` blocks until the CPU thread drains the -queue, so anything the CPU thread might block on must not be held across such a call. The WebSocket -debugger's `lifecycleLock` hit exactly this - it was held across a whole event handler, and the CPU -thread took it in `Core_NotifyLifecycle(STOPPING)`, so stopping a game with a debugger request in -flight hung both threads. That lock is gone now; the rule is what's left of it. - -**The WebSocket debugger no longer has any lock guarding it against startup/shutdown, and must not -grow one back.** The invariant instead is: a handler either does its emulator-state access inside -`Core_RunOnCPUThread()` - which serializes it against startup and teardown, since those run on the -CPU thread too - or touches only state that carries its own lock (the log ring buffer, `ctrlMutex`, -`GPUStepping`'s pause-action rendezvous). When adding a subscriber, put the core access in the -queued callback, including the `isAlive()`/`IsValidAddress()` checks: answering those outside it -just means acting on an answer that may already be stale. - -`game.*` and `cpu.stepping`/`cpu.resume` are pushed rather than polled - `WebSocketDebuggerTick()` -(called from `Core_ProcessCPUQueue()`) notices the transition on the CPU thread, formats the event -there, and drops it in a per-connection mailbox. Don't add a broadcaster that reads emulator state -from the connection's own thread; produce the event on the CPU thread and push it instead. - -The Win32 debugger's GUI-thread readers *do* still need it, so don't "simplify" those away: -teardown is not yet fully inside the `g_frameMutex` span. `EmuScreen::render()`'s `PSP_Shutdown()` is -inside it, but the ones in `EmuScreen::sendMessage()` (`REQUEST_GAME_RESET`, loading a new game) run -from `g_screenManager->sendMessage()` in `NativeFrame()`, which sits *above* where the guard is -taken. Closing that hole - moving those shutdowns inside the span, or deferring them to render time - -is the prerequisite for dropping the shutdown lock from the debugger entirely. - -Painting-problem design history, in case a similar tradeoff comes up elsewhere: routing every paint -through `Core_RunOnCPUThread` was rejected as too slow for something invoked continuously. A -per-window snapshot/cache with a per-row-rechecked `Core_IsStepping()` guard was tried first and -worked, but still had a narrow TOCTOU race (the CPU could resume between the check and that row's -reads) and the per-row-recheck pattern itself wasn't liked. Settled on `g_frameMutex` instead - -simpler, and actually race-free rather than just lower-risk. `CtrlRegisterList` shows live values -always now, grayed out by color alone (not cached) while the core is running, since a -constantly-moving value isn't meaningful to read closely anyway. +## Memory tagging +`NotifyMemInfo*()` may be called from any thread (CPU thread, software rasterizer workers, the SAS +thread). Notifications go into an MPSC queue; the slab maps belong to the emulation thread, which +drains the queue before every lookup (`FindMemInfo*`, `FormatMemWriteTagAt`), on every flip, and for +savestates. Lookups are emulation-thread only. diff --git a/ext/CMakeLists.txt b/ext/CMakeLists.txt index ebfd2a0824..86c4684673 100644 --- a/ext/CMakeLists.txt +++ b/ext/CMakeLists.txt @@ -50,7 +50,6 @@ add_library(xxhash STATIC set(aemu_postoffice_client_obj aemu_postoffice_client/postoffice.c aemu_postoffice_client/postoffice_mem_stdc.c - aemu_postoffice_client/mutex_impl_cpp.cpp aemu_postoffice_client/log_impl_ppsspp.cpp ) diff --git a/ext/aemu_postoffice_client/mutex_impl.h b/ext/aemu_postoffice_client/mutex_impl.h deleted file mode 100644 index 40b0b176ec..0000000000 --- a/ext/aemu_postoffice_client/mutex_impl.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef __MUTEX_IMPL_H -#define __MUTEX_IMPL_H - -#ifdef __cplusplus -extern "C" { -#endif - -void init_drain_mutex(); -void lock_drain_mutex(); -void unlock_drain_mutex(); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/ext/aemu_postoffice_client/mutex_impl_cpp.cpp b/ext/aemu_postoffice_client/mutex_impl_cpp.cpp deleted file mode 100644 index 1b925e68f3..0000000000 --- a/ext/aemu_postoffice_client/mutex_impl_cpp.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include - -extern "C" { - -static std::mutex drain_mutex; -void init_drain_mutex(){ -} -void lock_drain_mutex(){ - drain_mutex.lock(); -} -void unlock_drain_mutex(){ - drain_mutex.unlock(); -} - -} diff --git a/ext/aemu_postoffice_client/postoffice.c b/ext/aemu_postoffice_client/postoffice.c index e8c3c7a734..3c5477642d 100644 --- a/ext/aemu_postoffice_client/postoffice.c +++ b/ext/aemu_postoffice_client/postoffice.c @@ -6,7 +6,6 @@ #include "log_impl.h" #include "sock_impl.h" -#include "mutex_impl.h" #include "postoffice_mem.h" #include "aemu_postoffice_packets.h" @@ -94,7 +93,6 @@ int aemu_post_office_init(){ retro_atomic_int_init(&ptp_sessions[i].claimed, 0); } - init_drain_mutex(); retro_eventcount_init(&close_ec); }else{ // re-run, close all opened sessions @@ -371,13 +369,12 @@ static int pdp_drain_blocks_to_ring_buf(struct pdp_session *session){ } } -static int pdp_drain_blocks_to_ring_buf_locked(struct pdp_session *session){ +/* A pdp session is used by one thread at a time (the emulator's), so draining needs no lock. */ +static int pdp_drain_blocks_to_ring_buf_guarded(struct pdp_session *session){ if (!op_begin(&session->inflight, &session->abort)){ return AEMU_POSTOFFICE_CLIENT_SESSION_DEAD; } - lock_drain_mutex(); int result = pdp_drain_blocks_to_ring_buf(session); - unlock_drain_mutex(); op_end(&session->inflight, &session->abort); return result; } @@ -395,7 +392,7 @@ int pdp_recv(void *pdp_handle, char *pdp_mac, int *pdp_port, char *buf, int *len if (ABORTING(session)){ return AEMU_POSTOFFICE_CLIENT_SESSION_DEAD; } - int drain_result = pdp_drain_blocks_to_ring_buf_locked(session); + int drain_result = pdp_drain_blocks_to_ring_buf_guarded(session); if (drain_result == AEMU_POSTOFFICE_CLIENT_SESSION_DEAD){ return AEMU_POSTOFFICE_CLIENT_SESSION_DEAD; } @@ -469,7 +466,7 @@ int pdp_peek_next_size(void *pdp_handle){ return AEMU_POSTOFFICE_CLIENT_SESSION_DEAD; } - int drain_result = pdp_drain_blocks_to_ring_buf_locked(session); + int drain_result = pdp_drain_blocks_to_ring_buf_guarded(session); if (drain_result == AEMU_POSTOFFICE_CLIENT_SESSION_DEAD){ return AEMU_POSTOFFICE_CLIENT_SESSION_DEAD; } @@ -493,7 +490,7 @@ int pdp_buffered_data_size(void *pdp_handle){ return AEMU_POSTOFFICE_CLIENT_SESSION_DEAD; } - int drain_result = pdp_drain_blocks_to_ring_buf_locked(session); + int drain_result = pdp_drain_blocks_to_ring_buf_guarded(session); if (drain_result == AEMU_POSTOFFICE_CLIENT_SESSION_DEAD){ return AEMU_POSTOFFICE_CLIENT_SESSION_DEAD; } @@ -914,13 +911,12 @@ static int ptp_drain_blocks_to_ring_buf(struct ptp_session *session){ } } -static int ptp_drain_blocks_to_ring_buf_locked(struct ptp_session *session){ +/* Like pdp sessions, a ptp session is used by one thread at a time once it's connected. */ +static int ptp_drain_blocks_to_ring_buf_guarded(struct ptp_session *session){ if (!op_begin(&session->inflight, &session->abort)){ return AEMU_POSTOFFICE_CLIENT_SESSION_DEAD; } - lock_drain_mutex(); int result = ptp_drain_blocks_to_ring_buf(session); - unlock_drain_mutex(); op_end(&session->inflight, &session->abort); return result; } @@ -939,7 +935,7 @@ int ptp_recv(void *ptp_handle, char *buf, int *len, bool non_block){ if (ABORTING(session)){ return AEMU_POSTOFFICE_CLIENT_SESSION_DEAD; } - int drain_result = ptp_drain_blocks_to_ring_buf_locked(session); + int drain_result = ptp_drain_blocks_to_ring_buf_guarded(session); if (drain_result == AEMU_POSTOFFICE_CLIENT_SESSION_DEAD){ return AEMU_POSTOFFICE_CLIENT_SESSION_DEAD; } @@ -1021,7 +1017,7 @@ int ptp_peek_next_size(void *ptp_handle){ return AEMU_POSTOFFICE_CLIENT_SESSION_DEAD; } - int drain_result = ptp_drain_blocks_to_ring_buf_locked(session); + int drain_result = ptp_drain_blocks_to_ring_buf_guarded(session); if (drain_result == AEMU_POSTOFFICE_CLIENT_SESSION_DEAD){ return AEMU_POSTOFFICE_CLIENT_SESSION_DEAD; } diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 3860ba7794..aa43e8db9b 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -50,6 +50,7 @@ #include "Core/Config.h" #include "Core/ConfigValues.h" #include "Core/Core.h" +#include "Core/Debugger/WebSocket.h" #include "Core/CoreTiming.h" #include "Core/EmuThread.h" #include "Core/HLE/HLE.h" @@ -433,6 +434,7 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar double emulatedElapsed = 0.0; double lastEmulatedTime = CoreTiming::GetGlobalTimeUs() / 1000000.0; coreState = coreParameter.startBreak ? CORE_STEPPING_CPU : CORE_RUNNING_CPU; + bool debuggerClientSeen = false; while (coreState == CORE_RUNNING_CPU || coreState == CORE_STEPPING_CPU) { // Savestate loads/saves are queued and applied here, same as EmuScreen::render does in the // app. Without this, --state silently did nothing at all. @@ -453,6 +455,7 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar }); } + const int cpuWorkSeen = Core_CPUWorkSeen(); int blockTicks = (int)usToCycles(1000000 / 10); PSP_RunLoopFor(blockTicks); @@ -494,6 +497,20 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar // burns real seconds but no emulated ones, so the emulated limit can't misfire that way. const bool wallTimedOut = time_now_d() > wallDeadline && !debugger; const bool emulatedTimedOut = emulatedElapsed > opt.timeoutEmulated; + // Stopped for a debugger client: wait for it to queue something rather than spin. There's + // no deadline while waiting for the first client. Once every client has gone, nothing can + // resume us, so that ends the run like a stop without a debugger. + if (WebSocketDebuggerHasClients()) { + debuggerClientSeen = true; + } + const bool stoppedForDebugger = opt.debugger && (coreState == CORE_STEPPING_CPU || coreState == CORE_STEPPING_GE); + if (stoppedForDebugger) { + if (debuggerClientSeen && !WebSocketDebuggerHasClients()) { + break; + } + Core_WaitForCPUWork(cpuWorkSeen); + continue; + } if (wallTimedOut || emulatedTimedOut) { // Don't compare, print the output at least up to this point, and bail. if (!opt.bench) { @@ -1168,7 +1185,7 @@ int main(int argc, const char* argv[]) { // hangs until the timeout. Better to say why and bail - see WebServerSetRequireExactPort(). if (!WebServerWaitForStartup()) { fprintf(stderr, "Failed to start the debugger web server on port %d\n", cmdLineOptions.DebuggerPort().value()); - // The server thread has exited but is still joinable - without this, its std::thread + // The server thread has exited but is still joinable - without this, its Thread // destructor would call std::terminate() on the way out and we'd abort instead of // returning a useful exit code. ShutdownWebServer(); diff --git a/headless/README.md b/headless/README.md index 63866f869e..ea98d15a7c 100644 --- a/headless/README.md +++ b/headless/README.md @@ -54,7 +54,7 @@ PPSSPPHeadless file.elf|file.prx|file.ppdmp [...] [options] | `-i` | Use interpreter CPU core. | | `--ir` | Use IR interpreter CPU core. | | `-j` | Use JIT CPU core (default). | -| `--debugger=` | Enable WebSocket debugger and break at start. | +| `--debugger=` | Enable WebSocket debugger and break at start. While stopped, the run waits for a client (no timeout); it ends once the last client disconnects. | | `--state=` | Load a save state before running. | | `--old-atrac` | Use the old Atrac3+ audio decoder. | | `--ignore ` | Skip the specified test file. | diff --git a/libretro/LibretroGLContext.h b/libretro/LibretroGLContext.h index 259a1a111b..3f910d0387 100644 --- a/libretro/LibretroGLContext.h +++ b/libretro/LibretroGLContext.h @@ -21,14 +21,23 @@ public: bool InitAPI(void *wnd, std::string *deviceName, std::string *error_message) override; void CreateDrawContext() override; void DestroyDrawContext() override; + // The render thread uses the framebuffer, so it fetches it: on the emu thread when there's no + // separate one, else at the start of each render thread pass. void SetRenderTarget() override { - extern GLuint g_defaultFBO; - g_defaultFBO = hw_render_.get_current_framebuffer(); + if (!Libretro::useEmuThread) { + UpdateDefaultFBO(); + } } void ThreadStart() override { renderManager_->ThreadStart(draw_); } - bool ThreadFrame() override { return renderManager_->ThreadFrame(); } - bool ThreadRunUntilPaused() override { return renderManager_->ThreadRunUntilPaused(); } + bool ThreadFrame() override { + UpdateDefaultFBO(); + return renderManager_->ThreadFrame(); + } + bool ThreadRunUntilPaused() override { + UpdateDefaultFBO(); + return renderManager_->ThreadRunUntilPaused(); + } void NotifyEmuThreadPaused() override { renderManager_->NotifyEmuThreadPaused(); } void ThreadEnd() override { renderManager_->ThreadEnd(); } @@ -39,6 +48,11 @@ public: void NotifyEmuThreadExit() override; private: + void UpdateDefaultFBO() { + extern GLuint g_defaultFBO; + g_defaultFBO = hw_render_.get_current_framebuffer(); + } + GLRenderManager *renderManager_ = nullptr; bool glewInitDone = false; }; diff --git a/libretro/LibretroGLCoreContext.h b/libretro/LibretroGLCoreContext.h index f1f85e5e02..4f142ece2a 100644 --- a/libretro/LibretroGLCoreContext.h +++ b/libretro/LibretroGLCoreContext.h @@ -15,14 +15,23 @@ public: bool InitAPI(void *wnd, std::string *deviceName, std::string *error_message) override; void CreateDrawContext() override; void DestroyDrawContext() override; + // The render thread uses the framebuffer, so it fetches it: on the emu thread when there's no + // separate one, else at the start of each render thread pass. void SetRenderTarget() override { - extern GLuint g_defaultFBO; - g_defaultFBO = hw_render_.get_current_framebuffer(); + if (!Libretro::useEmuThread) { + UpdateDefaultFBO(); + } } void ThreadStart() override { renderManager_->ThreadStart(draw_); } - bool ThreadFrame() override { return renderManager_->ThreadFrame(); } - bool ThreadRunUntilPaused() override { return renderManager_->ThreadRunUntilPaused(); } + bool ThreadFrame() override { + UpdateDefaultFBO(); + return renderManager_->ThreadFrame(); + } + bool ThreadRunUntilPaused() override { + UpdateDefaultFBO(); + return renderManager_->ThreadRunUntilPaused(); + } void NotifyEmuThreadPaused() override { renderManager_->NotifyEmuThreadPaused(); } void ThreadEnd() override { renderManager_->ThreadEnd(); } @@ -33,6 +42,11 @@ public: const char *Ident() override { return "OpenGL Core"; } private: + void UpdateDefaultFBO() { + extern GLuint g_defaultFBO; + g_defaultFBO = hw_render_.get_current_framebuffer(); + } + GLRenderManager *renderManager_ = nullptr; bool glewInitDone = false; }; diff --git a/libretro/LibretroGraphicsContext.cpp b/libretro/LibretroGraphicsContext.cpp index 26592b94d6..6c3b464bcf 100644 --- a/libretro/LibretroGraphicsContext.cpp +++ b/libretro/LibretroGraphicsContext.cpp @@ -78,7 +78,7 @@ void LibretroHWRenderContext::ContextDestroy() { gpu->DeviceLost(); } - if (!hw_render_.cache_context && Libretro::useEmuThread && draw_ && Libretro::emuThreadState != Libretro::EmuThreadState::PAUSED) { + if (!hw_render_.cache_context && Libretro::useEmuThread && draw_ && Libretro::EmuThreadStateGet() != Libretro::EmuThreadState::PAUSED) { DestroyDrawContext(); } diff --git a/libretro/LibretroGraphicsContext.h b/libretro/LibretroGraphicsContext.h index 7e84ec70b5..bc3a1d37c3 100644 --- a/libretro/LibretroGraphicsContext.h +++ b/libretro/LibretroGraphicsContext.h @@ -1,5 +1,5 @@ #pragma once -#include +#include #include #include "Common/GPU/GraphicsContext.h" @@ -119,7 +119,9 @@ enum class EmuThreadState { STOPPED, }; extern bool useEmuThread; -extern std::atomic emuThreadState; +extern retro_atomic_int_t emuThreadState; +inline EmuThreadState EmuThreadStateGet() { return (EmuThreadState)retro_atomic_load_acquire_int(&emuThreadState); } +inline void EmuThreadStateSet(EmuThreadState state) { retro_atomic_store_release_int(&emuThreadState, (int)state); } void EmuThreadStart(); void EmuThreadStop(); void EmuThreadPause(); diff --git a/libretro/LibretroVulkanPresentation.cpp b/libretro/LibretroVulkanPresentation.cpp index d841d4db00..f4f013f9ee 100644 --- a/libretro/LibretroVulkanPresentation.cpp +++ b/libretro/LibretroVulkanPresentation.cpp @@ -1,3 +1,4 @@ +#include "Common/Thread/ParkingLot.h" #include "libretro/LibretroVulkanPresentation.h" #include "Common/GPU/Vulkan/VulkanContext.h" @@ -105,9 +106,14 @@ void LibretroVulkanPresentation::Destroy(VulkanContext *context) { } images_.clear(); syncIndexMask_ = 0; - std::lock_guard lock(mutex_); - presentPending_ = false; - condVar_.notify_all(); + SetPresentPending(false); +} + +void LibretroVulkanPresentation::SetPresentPending(bool pending) { + retro_atomic_store_release_int(&presentPending_, pending ? 1 : 0); + if (!pending) { + ParkingLotNotify(&presentPending_); + } } bool LibretroVulkanPresentation::NeedsRecreate() const { @@ -137,20 +143,16 @@ VkResult LibretroVulkanPresentation::QueuePresent(VulkanContext *vulkan, VkQueue if (vulkan_->get_sync_index_mask(vulkan_->handle) != syncIndexMask_ || !IsValidImageIndex(imageIndex)) { return VK_ERROR_OUT_OF_DATE_KHR; } - std::unique_lock lock(mutex_); vulkan_->set_image(vulkan_->handle, &images_[imageIndex].retroImage, 0, nullptr, vulkan_->queue_index); return VK_SUCCESS; } void LibretroVulkanPresentation::BeginPresent() { - std::lock_guard lock(mutex_); - presentPending_ = true; + SetPresentPending(true); } void LibretroVulkanPresentation::EndPresent() { - std::lock_guard lock(mutex_); - presentPending_ = false; - condVar_.notify_all(); + SetPresentPending(false); } void LibretroVulkanPresentation::LockQueue() { @@ -169,6 +171,5 @@ void LibretroVulkanPresentation::PrepareSubmit(VkSubmitInfo &submitInfo) { } void LibretroVulkanPresentation::WaitForPresentation() { - std::unique_lock lock(mutex_); - condVar_.wait(lock, [this] { return !presentPending_; }); + ParkingLotWait(&presentPending_, [this] { return retro_atomic_load_acquire_int(&presentPending_) == 0; }); } diff --git a/libretro/LibretroVulkanPresentation.h b/libretro/LibretroVulkanPresentation.h index 8c160d8593..e5835eb1d5 100644 --- a/libretro/LibretroVulkanPresentation.h +++ b/libretro/LibretroVulkanPresentation.h @@ -1,8 +1,8 @@ #pragma once #include -#include -#include + +#include // Must come before : this is what defines VK_USE_PLATFORM_WIN32_KHR (etc.) before // the first inclusion of ext/vulkan/vulkan.h - since that header is include-guarded, whichever include @@ -65,9 +65,10 @@ private: std::vector images_; - std::mutex mutex_; - std::condition_variable condVar_; - bool presentPending_ = false; + // Set by BeginPresent on the emu thread, cleared by EndPresent on the render thread once + // set_image has been called. WaitForPresentation parks on it. + retro_atomic_int_t presentPending_{ 0 }; + void SetPresentPending(bool pending); static uint32_t ImageCountFromMask(uint32_t mask); bool IsValidImageIndex(uint32_t imageIndex) const; diff --git a/libretro/Makefile.common b/libretro/Makefile.common index 55138176ad..97ce5c1341 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -272,6 +272,7 @@ SOURCES_CXX += \ $(COMMONDIR)/File/DiskFree.cpp \ $(COMMONDIR)/File/Path.cpp \ $(COMMONDIR)/File/FileUtil.cpp \ + $(COMMONDIR)/File/PositionalFile.cpp \ $(COMMONDIR)/File/FileDescriptor.cpp \ $(COMMONDIR)/File/DirListing.cpp \ $(COMMONDIR)/GPU/thin3d.cpp \ @@ -326,6 +327,7 @@ SOURCES_CXX += \ $(COMMONDIR)/Thread/ThreadUtil.cpp \ $(COMMONDIR)/Thread/ParallelLoop.cpp \ $(COMMONDIR)/Thread/ParkingLot.cpp \ + $(COMMONDIR)/Thread/Thread.cpp \ $(COMMONDIR)/Thread/ThreadManager.cpp \ $(COMMONDIR)/System/Display.cpp \ $(COMMONDIR)/System/Request.cpp \ @@ -494,7 +496,6 @@ SOURCES_C += \ $(EXTDIR)/aemu_postoffice_client/postoffice_mem_stdc.c SOURCES_CXX += \ - $(EXTDIR)/aemu_postoffice_client/mutex_impl_cpp.cpp \ $(EXTDIR)/aemu_postoffice_client/log_impl_ppsspp.cpp ifeq ($(PLATFORM_EXT), win32) @@ -876,7 +877,6 @@ SOURCES_C += $(LIBRETROCOMMONDIR)/compat/compat_posix_string.c \ $(LIBRETROCOMMONDIR)/streams/file_stream.c \ $(LIBRETROCOMMONDIR)/streams/file_stream_transforms.c \ $(LIBRETROCOMMONDIR)/string/rstrtod.c \ - $(LIBRETROCOMMONDIR)/time/rtime.c \ $(LIBRETROCOMMONDIR)/vfs/vfs_implementation.c # Codecs: zlib-format deflate, zstd, CHD, zip and PNG. @@ -904,7 +904,8 @@ SOURCES_C += $(LIBRETROCOMMONDIR)/features/features_cpu.c \ $(LIBRETROCOMMONDIR)/rthreads/retro_eventcount.c \ $(LIBRETROCOMMONDIR)/rthreads/retro_procbarrier.c \ $(LIBRETROCOMMONDIR)/rthreads/rthreads.c \ - $(LIBRETROCOMMONDIR)/rthreads/tpool.c + $(LIBRETROCOMMONDIR)/rthreads/tpool.c \ + $(LIBRETROCOMMONDIR)/time/rtime.c INCFLAGS += -I$(LIBRETROCOMMONDIR)/include diff --git a/libretro/libretro.cpp b/libretro/libretro.cpp index de91f5f3e5..be5fd85f4c 100644 --- a/libretro/libretro.cpp +++ b/libretro/libretro.cpp @@ -1,11 +1,12 @@ #include "ppsspp_config.h" #include +#include #include -#include -#include #include #include -#include + +#include +#include #include "Common/CPUDetect.h" #include "Common/Log.h" @@ -25,6 +26,7 @@ #include "Common/StringUtils.h" #include "Common/Thread/ParkingLot.h" +#include "Common/Thread/Thread.h" #include "Core/Config.h" #include "Core/ConfigValues.h" #include "Core/Core.h" @@ -68,18 +70,11 @@ #define SAMPLERATE 44100 -/* AUDIO output buffer */ -static struct { - int16_t *data; - int32_t size; - int32_t capacity; -} output_audio_buffer = {NULL, 0, 0}; -// output_audio_buffer is filled by System_AudioPushSamples() on the emu -// thread (used with the GL backends, where PSP-side audio pushes arrive -// via the GL command queue) and drained + realloc'd on the libretro -// thread in retro_run()/init/shutdown — every access must hold this -// mutex or a realloc can free the pointer mid-read on the other thread. -static std::mutex output_audio_buffer_mutex; +// Audio output: System_AudioPushSamples() fills it on whichever thread runs the emulator (the +// emu thread with the GL backends), retro_run() drains it to the frontend. Stereo int16 frames. +static retro_spsc_t output_audio_ring; +static bool output_audio_ring_ok = false; +static const size_t OUTPUT_AUDIO_RING_FRAMES = 32768; // Calculated swap interval is 'stable' if the same // value is recorded for a number of retro_run() @@ -130,51 +125,33 @@ namespace Libretro static float runSpeed = 0.0f; static s64 runTicksLast = 0; - // Must be called with output_audio_buffer_mutex held. - static bool ensure_output_audio_buffer_capacity(int32_t capacity) + // With neither side running. + static void init_output_audio_buffer() { - if (capacity <= output_audio_buffer.capacity) { - return true; - } - - int16_t *data = (int16_t*)realloc(output_audio_buffer.data, capacity * sizeof(*output_audio_buffer.data)); - if (!data) { - // Keep the old buffer, drop the new samples. - if (log_cb) - log_cb(RETRO_LOG_ERROR, "Failed to grow output audio buffer to %d samples\n", capacity); - return false; - } - output_audio_buffer.data = data; - output_audio_buffer.capacity = capacity; - if (log_cb) - log_cb(RETRO_LOG_DEBUG, "Output audio buffer capacity set to %d\n", capacity); - return true; - } - - static void init_output_audio_buffer(int32_t capacity) - { - std::lock_guard lock(output_audio_buffer_mutex); - output_audio_buffer.data = NULL; - output_audio_buffer.size = 0; - output_audio_buffer.capacity = 0; - ensure_output_audio_buffer_capacity(capacity); + output_audio_ring_ok = retro_spsc_init(&output_audio_ring, OUTPUT_AUDIO_RING_FRAMES * 4); + if (!output_audio_ring_ok && log_cb) + log_cb(RETRO_LOG_ERROR, "Failed to allocate the output audio buffer\n"); } static void free_output_audio_buffer() { - std::lock_guard lock(output_audio_buffer_mutex); - free(output_audio_buffer.data); - output_audio_buffer.data = NULL; - output_audio_buffer.size = 0; - output_audio_buffer.capacity = 0; + retro_spsc_free(&output_audio_ring); + output_audio_ring_ok = false; } + // The frontend gets the ring in place, in up to two contiguous runs. static void upload_output_audio_buffer() { - std::lock_guard lock(output_audio_buffer_mutex); - if (output_audio_buffer.data && output_audio_buffer.size > 0) - audio_batch_cb(output_audio_buffer.data, output_audio_buffer.size / 2); - output_audio_buffer.size = 0; + if (!output_audio_ring_ok) + return; + const void *ptr; + size_t bytes; + while ((bytes = retro_spsc_read_begin(&output_audio_ring, &ptr)) >= 4) + { + bytes &= ~(size_t)3; + audio_batch_cb((const int16_t *)ptr, bytes / 4); + retro_spsc_read_end(&output_audio_ring, bytes); + } } @@ -1267,7 +1244,7 @@ void retro_init(void) g_threadManager.Init(cpu_info.num_cores, cpu_info.logical_cpu_count); - init_output_audio_buffer(2048); + init_output_audio_buffer(); } void retro_deinit(void) @@ -1324,9 +1301,9 @@ unsigned retro_api_version(void) { return RETRO_API_VERSION; } namespace Libretro { bool useEmuThread = false; - std::atomic emuThreadState(EmuThreadState::DISABLED); + retro_atomic_int_t emuThreadState{ (int)EmuThreadState::DISABLED }; - static std::thread emuThread; + static Thread emuThread; static void EmuFrame() { ctx->SetRenderTarget(); Draw::DrawContext *draw = ctx->GetDrawContext(); @@ -1378,7 +1355,7 @@ namespace Libretro { SetCurrentThreadName("EmuThread"); for (;;) { - EmuThreadState state = emuThreadState; + EmuThreadState state = EmuThreadStateGet(); switch (state) { case EmuThreadState::RUNNING: @@ -1387,15 +1364,15 @@ namespace Libretro { case EmuThreadState::PAUSE_REQUESTED: // CAS so a concurrent QUIT_REQUESTED is not clobbered; the loop re-reads the state. // The marker follows this frame's work, so the frontend has drained it on seeing it. - if (emuThreadState.compare_exchange_strong(state, EmuThreadState::PAUSED)) + if (retro_atomic_cas_int(&emuThreadState, (int)state, (int)EmuThreadState::PAUSED)) ctx->NotifyEmuThreadPaused(); break; case EmuThreadState::PAUSED: - ParkingLotWait(&emuThreadState, [] { return emuThreadState != EmuThreadState::PAUSED; }); + ParkingLotWait(&emuThreadState, [] { return EmuThreadStateGet() != EmuThreadState::PAUSED; }); break; case EmuThreadState::QUIT_REQUESTED: ctx->NotifyEmuThreadExit(); - emuThreadState = EmuThreadState::STOPPED; + EmuThreadStateSet(EmuThreadState::STOPPED); return; default: _dbg_assert_(false); @@ -1406,32 +1383,32 @@ namespace Libretro { } void EmuThreadStart() { - EmuThreadState state = emuThreadState; + EmuThreadState state = EmuThreadStateGet(); bool wasPaused = state == EmuThreadState::PAUSED; if (state == EmuThreadState::RUNNING || (emuThread.joinable() && !wasPaused)) return; - emuThreadState = EmuThreadState::RUNNING; + EmuThreadStateSet(EmuThreadState::RUNNING); if (wasPaused) ParkingLotNotify(&emuThreadState); else { ctx->ThreadStart(); - emuThread = std::thread(&EmuThreadFunc); + emuThread = Thread(&EmuThreadFunc); } } void EmuThreadStop() { // Stop and join from any live state, not just RUNNING: unloading while // PAUSED (the state retro_serialize_size() leaves behind) must not leave - // a live thread over freed engine state or a joinable std::thread. + // a live thread over freed engine state or a joinable Thread. if (!emuThread.joinable()) return; - if (emuThreadState != EmuThreadState::STOPPED) { - emuThreadState = EmuThreadState::QUIT_REQUESTED; + if (EmuThreadStateGet() != EmuThreadState::STOPPED) { + EmuThreadStateSet(EmuThreadState::QUIT_REQUESTED); ParkingLotNotify(&emuThreadState); } @@ -1443,10 +1420,10 @@ namespace Libretro { } void EmuThreadPause() { - if (emuThreadState != EmuThreadState::RUNNING) + if (EmuThreadStateGet() != EmuThreadState::RUNNING) return; - emuThreadState = EmuThreadState::PAUSE_REQUESTED; + EmuThreadStateSet(EmuThreadState::PAUSE_REQUESTED); // Run queued work, readback syncs included, up to the marker the emu // thread queues once it has parked at a frame boundary. @@ -1728,7 +1705,7 @@ void retro_run(void) { if (useEmuThread) { // Also resumes from PAUSED, so a retro_serialize_size() without a // following retro_serialize() cannot freeze emulation. - if (emuThreadState != EmuThreadState::RUNNING) { + if (EmuThreadStateGet() != EmuThreadState::RUNNING) { EmuThreadStart(); } @@ -2021,19 +1998,25 @@ void System_AudioPushSamples(const int32_t *audio, int numSamples, float volume) if (!audio || numSamples <= 0) return; - std::lock_guard lock(output_audio_buffer_mutex); + if (!output_audio_ring_ok) + return; - int32_t values = numSamples * 2; // stereo - if (output_audio_buffer.capacity - output_audio_buffer.size < values) { - if (!ensure_output_audio_buffer_capacity((int32_t)((output_audio_buffer.capacity + values) * 1.5f))) - return; + // Convert straight into the ring. Whole frames only, so every run stays frame-aligned; + // what doesn't fit is dropped. + int frames = numSamples; + while (frames > 0) { + void *ptr; + const size_t room = retro_spsc_write_begin(&output_audio_ring, &ptr) / 4; + if (room == 0) + break; + const int n = (int)std::min(room, (size_t)frames); + int16_t *dst = (int16_t *)ptr; + for (int i = 0; i < n * 2; i++) + dst[i] = Clamp16(audio[i]); + retro_spsc_write_end(&output_audio_ring, (size_t)n * 4); + audio += n * 2; + frames -= n; } - - // Convert straight into the output buffer. - int16_t *dst = output_audio_buffer.data + output_audio_buffer.size; - for (int32_t i = 0; i < values; i++) - dst[i] = Clamp16(audio[i]); - output_audio_buffer.size += values; } void System_AudioGetDebugStats(char *buf, size_t bufSize) { if (buf) buf[0] = '\0'; } diff --git a/unittest/TestNetWait.cpp b/unittest/TestNetWait.cpp index 99d74ca386..677a6125a2 100644 --- a/unittest/TestNetWait.cpp +++ b/unittest/TestNetWait.cpp @@ -2,7 +2,6 @@ #include "Common/Net/SocketCompat.h" -#include #include "Common/Net/Cancel.h" #include "Common/Net/HTTPClient.h" @@ -10,6 +9,7 @@ #include "Common/TimeUtil.h" #include "UnitTest.h" +#include "Common/Thread/Thread.h" static uintptr_t LoopbackUDP(sockaddr_in *addr) { uintptr_t s = (uintptr_t)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); @@ -29,7 +29,7 @@ bool TestNetWait() { { net::WakeSocket wake; EXPECT_TRUE(wake.Fd() != -1); - std::thread waker([&] { wake.Wake(); }); + Thread waker([&] { wake.Wake(); }); EXPECT_EQ_INT((int)net::WaitSocketsOrWake(nullptr, nullptr, 0, false, -1.0, &wake), (int)net::WaitResult::CANCELLED); waker.join(); wake.Drain(); @@ -55,7 +55,7 @@ bool TestNetWait() { sockaddr_in addr; uintptr_t a = LoopbackUDP(&addr); net::CancelToken cancel; - std::thread canceller([&] { cancel.Cancel(); }); + Thread canceller([&] { cancel.Cancel(); }); EXPECT_EQ_INT((int)net::WaitSocket(a, false, -1.0, &cancel), (int)net::WaitResult::CANCELLED); canceller.join(); EXPECT_EQ_INT((int)net::WaitSocket(a, false, -1.0, &cancel), (int)net::WaitResult::CANCELLED); @@ -68,7 +68,7 @@ bool TestNetWait() { if (client.Resolve("10.255.255.1", 9)) { net::CancelToken cancel; const double start = time_now_d(); - std::thread canceller([&] { cancel.Cancel(); }); + Thread canceller([&] { cancel.Cancel(); }); EXPECT_FALSE(client.Connect(1, 30.0, &cancel)); canceller.join(); EXPECT_TRUE(time_now_d() - start < 5.0); diff --git a/unittest/TestThreadManager.cpp b/unittest/TestThreadManager.cpp index 094c6f090d..e3eee548f1 100644 --- a/unittest/TestThreadManager.cpp +++ b/unittest/TestThreadManager.cpp @@ -1,4 +1,3 @@ -#include #include #include @@ -13,6 +12,7 @@ #include "Common/Thread/Waitable.h" #include "UnitTest.h" +#include "Common/Thread/Thread.h" struct ResultObject { bool ok; @@ -67,7 +67,7 @@ bool TestParallelLoop(ThreadManager *threadMan) { const size_t THREAD_COUNT = 9; const size_t ITERATIONS = 40000; -static std::atomic g_atomicCounter; +static retro_atomic_int_t g_atomicCounter{ 0 }; static ThreadManager *g_threadMan; static CountingBarrier g_barrier(THREAD_COUNT + 1); @@ -80,7 +80,7 @@ public: return TaskPriority::NORMAL; } void Run() override { - g_atomicCounter++; + retro_atomic_fetch_add_int(&g_atomicCounter, 1); waitable_->Notify(); } private: @@ -98,20 +98,20 @@ void ThreadFunc() { } bool TestMultithreadedScheduling() { - g_atomicCounter = 0; + retro_atomic_store_release_int(&g_atomicCounter, 0); auto start = Instant::Now(); - std::vector threads; + std::vector threads; for (int i = 0; i < THREAD_COUNT; i++) { - threads.push_back(std::thread(ThreadFunc)); + threads.push_back(Thread(ThreadFunc)); } // Just testing the barrier g_barrier.Arrive(); // OK, all are done. - EXPECT_EQ_INT(g_atomicCounter, THREAD_COUNT * ITERATIONS); + EXPECT_EQ_INT(retro_atomic_load_acquire_int(&g_atomicCounter), THREAD_COUNT * ITERATIONS); for (int i = 0; i < THREAD_COUNT; i++) { threads[i].join(); diff --git a/unittest/TestVFS.cpp b/unittest/TestVFS.cpp index 8eb57886d0..2dc54214d6 100644 --- a/unittest/TestVFS.cpp +++ b/unittest/TestVFS.cpp @@ -1,4 +1,3 @@ -#include #include #include "Common/Log.h" diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index a628045d68..3539c19a0e 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -37,7 +37,7 @@ #include #include -#include +#include #include #include #include @@ -2044,13 +2044,13 @@ bool TestParseLBN() { // The read counter lives outside, since CachingFileLoader deletes its backend. class PatternFileLoader : public FileLoader { public: - PatternFileLoader(s64 size, std::atomic *reads) : size_(size), reads_(reads) {} + PatternFileLoader(s64 size, retro_atomic_int_t *reads) : size_(size), reads_(reads) {} bool Exists() override { return true; } bool IsDirectory() override { return false; } s64 FileSize() override { return size_; } Path GetPath() const override { return Path(); } size_t ReadAt(s64 pos, size_t bytes, size_t count, void *data, Flags flags) override { - (*reads_)++; + retro_atomic_fetch_add_int(reads_, 1); s64 end = std::min(pos + (s64)(bytes * count), size_); for (s64 i = pos; i < end; i++) { ((u8 *)data)[i - pos] = (u8)(i * 31 + 7); @@ -2060,7 +2060,7 @@ public: private: s64 size_; - std::atomic *reads_; + retro_atomic_int_t *reads_; }; static bool MatchesPattern(const u8 *data, s64 pos, size_t bytes) { @@ -2075,7 +2075,7 @@ static bool MatchesPattern(const u8 *data, s64 pos, size_t bytes) { static bool TestCachingFileLoader() { // The last 64 KB block of the file is short. const s64 size = 3 * 65536 + 1234; - std::atomic reads{}; + retro_atomic_int_t reads{ 0 }; std::unique_ptr loader(new CachingFileLoader(new PatternFileLoader(size, &reads))); std::vector buf(65536 * 2); @@ -2088,14 +2088,14 @@ static bool TestCachingFileLoader() { // Both reads ended in the last block, so everything they touched is cached and there's // nothing left to read ahead. Nothing further should reach the backend, even past EOF. - int readsBefore = reads; + int readsBefore = retro_atomic_load_acquire_int(&reads); pos = 3 * 65536 + 100; for (int i = 0; i < 100; i++) { EXPECT_EQ_INT(loader->ReadAt(pos, 100, buf.data()), 100); } // Waits for any read-ahead. loader.reset(); - EXPECT_EQ_INT(reads, readsBefore); + EXPECT_EQ_INT(retro_atomic_load_acquire_int(&reads), readsBefore); return true; }