diff --git a/Common/Thread/MpscQueue.h b/Common/Thread/MpscQueue.h index 48da77eba7..2db90b2dac 100644 --- a/Common/Thread/MpscQueue.h +++ b/Common/Thread/MpscQueue.h @@ -17,12 +17,15 @@ #pragma once +#include #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. +// oldest first. Nodes are recycled: a drained node goes onto a free stack shared by every +// queue of the same T, and a producer takes that stack whole when its own thread's list is +// empty, so once warmed up a push doesn't allocate. template class MpscQueue { public: @@ -37,8 +40,9 @@ public: // Any thread. void Push(T value) { - Node *node = new Node(std::move(value)); - mpsc_stack_push(&stack_, &node->link.node); + Node *node = TakeNode(); + new (node->storage) T(std::move(value)); + mpsc_stack_push(&stack_, &node->link); } // Any thread, as a hint; exact for the consumer. @@ -53,28 +57,53 @@ public: 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 *item = (Node *)node; + T *value = item->Value(); + f(std::move(*value)); + value->~T(); + mpsc_stack_push(&freeNodes_, &item->link); 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; - }; + // Standard layout with the stack node first, so a drained node converts back to its Node. struct Node { - explicit Node(T &&v) : value(std::move(v)) { - link.node.next = nullptr; - link.owner = this; - } - Link link; - T value; + mpsc_stack_node_t link; + alignas(T) unsigned char storage[sizeof(T)]; + T *Value() { return reinterpret_cast(storage); } }; + // Nodes this thread took from freeNodes_. Only this thread touches it. + struct LocalNodes { + mpsc_stack_node_t *head = nullptr; + ~LocalNodes() { + while (head) { + mpsc_stack_node_t *next = head->next; + delete (Node *)head; + head = next; + } + } + }; + + static Node *TakeNode() { + static thread_local LocalNodes local; + if (!local.head) { + local.head = mpsc_stack_drain(&freeNodes_); + } + if (!local.head) { + return new Node(); + } + Node *node = (Node *)local.head; + local.head = local.head->next; + return node; + } + mpsc_stack_t stack_; + // Zero-initialized, which is an empty stack. + static mpsc_stack_t freeNodes_; }; + +template +mpsc_stack_t MpscQueue::freeNodes_; diff --git a/Core/Dialog/PSPNpSigninDialog.cpp b/Core/Dialog/PSPNpSigninDialog.cpp index bbb740387d..6069d72f5f 100644 --- a/Core/Dialog/PSPNpSigninDialog.cpp +++ b/Core/Dialog/PSPNpSigninDialog.cpp @@ -16,10 +16,10 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include -#include "Common/TimeUtil.h" #include "Common/Data/Text/I18n.h" #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" +#include "Core/CoreTiming.h" #include "Core/MemMapHelpers.h" #include "Core/Util/PPGeDraw.h" #include "Core/HLE/sceKernelMemory.h" @@ -63,7 +63,7 @@ int PSPNpSigninDialog::Init(u32 paramAddr) { InitCommon(); //npSigninResult = -1; - startTime = (u64)(time_now_d() * 1000000.0); + startTime = CoreTiming::GetGlobalTimeUs(); step = 0; StartFade(true); @@ -100,7 +100,7 @@ int PSPNpSigninDialog::Update(int animSpeed) { UpdateButtons(); UpdateCommon(); auto err = GetI18NCategory(I18NCat::ERRORS); - u64 now = (u64)(time_now_d() * 1000000.0); + u64 now = CoreTiming::GetGlobalTimeUs(); if (request.npSigninStatus == NP_SIGNIN_STATUS_NONE) { auto di = GetI18NCategory(I18NCat::DIALOG); @@ -187,7 +187,7 @@ int PSPNpSigninDialog::Shutdown(bool force) { void PSPNpSigninDialog::DoState(PointerWrap &p) { PSPDialog::DoState(p); - auto s = p.Section("PSPNpSigninDialog", 1, 2); + auto s = p.Section("PSPNpSigninDialog", 1, 3); if (!s) return; @@ -198,9 +198,11 @@ void PSPNpSigninDialog::DoState(PointerWrap &p) { if (s >= 2) { Do(p, requestAddr); } - - if (p.mode == p.MODE_READ) { - startTime = (u64)(time_now_d() * 1000000.0); + // The delays run on emulated time. Older states restart them. + if (s >= 3) { + Do(p, startTime); + } else if (p.mode == p.MODE_READ) { + startTime = CoreTiming::GetGlobalTimeUs(); } } diff --git a/Core/Dialog/PSPSaveDialog.cpp b/Core/Dialog/PSPSaveDialog.cpp index 02cc0d4246..bc770ff571 100755 --- a/Core/Dialog/PSPSaveDialog.cpp +++ b/Core/Dialog/PSPSaveDialog.cpp @@ -29,7 +29,6 @@ #include "Core/FileSystems/MetaFileSystem.h" #include "Core/Util/PathUtil.h" #include "Core/Util/PPGeDraw.h" -#include "Common/TimeUtil.h" #include "Core/HLE/sceCtrl.h" #include "Core/HLE/sceUtility.h" #include "Core/HLE/ErrorCodes.h" @@ -39,13 +38,6 @@ #include "Core/Reporting.h" #include "Core/SaveState.h" -static double g_lastSaveTime = -1.0; - -// Actually this should be called on both saves and loads, since just after a load it's safe to exit. -void ResetSecondsSinceLastGameSave() { - g_lastSaveTime = time_now_d(); -} - void ShowSaveLoadIndicator(bool save) { if (g_Config.bShowSaveLoadIndicator) { g_OSD.Show(OSDType::STATUS_ICON, "", "", save ? "I_ROTATE_RIGHT" : "I_ROTATE_LEFT", 1.0f, "save_indicator"); @@ -53,14 +45,6 @@ void ShowSaveLoadIndicator(bool save) { } } -double SecondsSinceLastGameSave() { - if (g_lastSaveTime < 0) { - return -1.0; - } else { - return time_now_d() - g_lastSaveTime; - } -} - const static float FONT_SCALE = 0.55f; // These are rough, it seems to take at least 100ms or so to init, and shutdown depends on threads. @@ -1188,7 +1172,6 @@ void PSPSaveDialog::StartIO() { // Show save indicator. It's strange how "display" is just as much an action as what to display. if (display == DS_SAVE_SAVING || display == DS_LOAD_LOADING) { const bool save = display != DS_LOAD_LOADING; - ResetSecondsSinceLastGameSave(); ShowSaveLoadIndicator(save); } @@ -1234,15 +1217,9 @@ bool PSPSaveDialog::FinishIO() { switch (ioAction_) { case DS_LOAD_LOADING: - if (ioDisplay_ == DS_LOAD_DONE) { - g_lastSaveTime = time_now_d(); - } break; case DS_SAVE_SAVING: SaveState::NotifySaveData(); - if (ioDisplay_ == DS_SAVE_DONE) { - g_lastSaveTime = time_now_d(); - } break; case DS_NONE: switch ((SceUtilitySavedataType)(u32)request.mode) { @@ -1250,7 +1227,6 @@ bool PSPSaveDialog::FinishIO() { case SCE_UTILITY_SAVEDATA_TYPE_AUTOLOAD: case SCE_UTILITY_SAVEDATA_TYPE_READDATA: case SCE_UTILITY_SAVEDATA_TYPE_READDATASECURE: - ResetSecondsSinceLastGameSave(); ShowSaveLoadIndicator(false); break; case SCE_UTILITY_SAVEDATA_TYPE_MAKEDATA: @@ -1264,7 +1240,6 @@ bool PSPSaveDialog::FinishIO() { case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATA: case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATASECURE: SaveState::NotifySaveData(); - ResetSecondsSinceLastGameSave(); ShowSaveLoadIndicator(true); break; default: diff --git a/Core/Dialog/PSPSaveDialog.h b/Core/Dialog/PSPSaveDialog.h index 854daef476..6d0014e793 100644 --- a/Core/Dialog/PSPSaveDialog.h +++ b/Core/Dialog/PSPSaveDialog.h @@ -120,5 +120,3 @@ private: std::string ioDeleteDir_; }; -void ResetSecondsSinceLastGameSave(); -double SecondsSinceLastGameSave(); diff --git a/Core/HLE/sceUtility.cpp b/Core/HLE/sceUtility.cpp index 189bbea8e6..101777b27f 100644 --- a/Core/HLE/sceUtility.cpp +++ b/Core/HLE/sceUtility.cpp @@ -535,8 +535,6 @@ void __UtilityInit() { currentlyLoadedModules.clear(); swappedFirmwareModules.clear(); volatileUnlockEvent = CoreTiming::RegisterEvent("UtilityVolatileUnlock", UtilityVolatileUnlock); - - ResetSecondsSinceLastGameSave(); } void __UtilityDoState(PointerWrap &p) { diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index 66aae716fe..ef8ccdaabd 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -33,7 +33,6 @@ #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" #include "Common/StringUtils.h" -#include "Common/TimeUtil.h" #include "Core/SaveState.h" #include "Core/Config.h" @@ -80,7 +79,6 @@ static const char * const LOAD_UNDO_NAME = "load_undo.ppst"; namespace SaveState { // Used for "confirm exit if you haven't saved in a while" -double g_lastSaveTime = -1.0; // Set by Enqueue on any thread, taken by Process on the emulation thread. static retro_atomic_int_t needsProcess{ 0 }; @@ -97,7 +95,6 @@ static StateRingbuffer rewindStates; // Updated by Rescan. This is to avoid calling Exists a lot of times, which could be slow // on some Android devices when we allow many save states. -static double g_lastRescan = 0.0; static std::map> g_files; // the value is the mod-time. struct SaveStart { @@ -588,7 +585,6 @@ static retro_atomic_int_t g_screenshotFailures{ 0 }; g_files[f.name] = f.mtime; } - g_lastRescan = time_now_d(); } static bool SaveStateFileExists(std::string_view gamePrefix, int slot, const char *extension) { @@ -920,7 +916,6 @@ static retro_atomic_int_t g_screenshotFailures{ 0 }; } } #endif - g_lastSaveTime = time_now_d(); } else if (result == CChunkFileReader::ERROR_BROKEN_STATE) { HandleLoadFailure(false, &callbackMetadata); callbackMessage = std::string(i18nLoadFailure) + ": " + errorString; @@ -956,7 +951,6 @@ static retro_atomic_int_t g_screenshotFailures{ 0 }; } } #endif - g_lastSaveTime = time_now_d(); } else if (result == CChunkFileReader::ERROR_BROKEN_STATE) { // TODO: What else might we want to do here? This should be very unusual. callbackMessage = i18nSaveFailure; @@ -1049,20 +1043,10 @@ static retro_atomic_int_t g_screenshotFailures{ 0 }; saveDataGeneration = 0; lastSaveDataGeneration = 0; saveStateInitialGitVersion.clear(); - - g_lastSaveTime = time_now_d(); } void Shutdown() { rewindStates.Clear(); } - double SecondsSinceLastSavestate() { - if (g_lastSaveTime < 0) { - return -1.0; - } else { - return time_now_d() - g_lastSaveTime; - } - } - } // namespace SaveState diff --git a/Core/SaveState.h b/Core/SaveState.h index 746c4c3f11..277372a03a 100644 --- a/Core/SaveState.h +++ b/Core/SaveState.h @@ -128,5 +128,4 @@ namespace SaveState { bool PollRestartNeeded(); // Returns the time since last save. -1 if N/A. - double SecondsSinceLastSavestate(); } // namespace SaveState diff --git a/unittest/TestThreadManager.cpp b/unittest/TestThreadManager.cpp index 6b58ae2a21..3e30b1b39c 100644 --- a/unittest/TestThreadManager.cpp +++ b/unittest/TestThreadManager.cpp @@ -5,6 +5,8 @@ #include "Common/TimeUtil.h" #include "Common/Thread/Barrier.h" #include "Common/Thread/Event.h" +#include "Common/Thread/MpscQueue.h" +#include "Common/Thread/ParkingLot.h" #include "Common/Thread/ThreadManager.h" #include "Common/Thread/Channel.h" #include "Common/Thread/Promise.h" @@ -127,6 +129,57 @@ bool TestMultithreadedScheduling() { return true; } +struct QueueItem { + int producer; + int seq; +}; + +static const int QUEUE_PRODUCERS = 4; +static const int QUEUE_ITEMS = 20000; + +static void QueueProducer(MpscQueue *queue, EventCounter *pushed, int producer) { + for (int i = 0; i < QUEUE_ITEMS; i++) { + queue->Push(QueueItem{ producer, i }); + pushed->Notify(); + } +} + +// Several producers against one consumer, twice, so the second round runs on recycled nodes. +// Each producer's items must come out complete and in order. +bool TestMpscQueue() { + MpscQueue queue; + EventCounter pushed; + for (int round = 0; round < 2; round++) { + int next[QUEUE_PRODUCERS]{}; + bool ordered = true; + int received = 0; + std::vector producers; + for (int i = 0; i < QUEUE_PRODUCERS; i++) { + producers.push_back(Thread(QueueProducer, &queue, &pushed, i)); + } + while (received < QUEUE_PRODUCERS * QUEUE_ITEMS) { + const int seen = pushed.Seen(); + const bool any = queue.Drain([&](QueueItem &&item) { + if (item.seq != next[item.producer]) { + ordered = false; + } + next[item.producer] = item.seq + 1; + received++; + }); + if (!any) { + pushed.Wait(seen); + } + } + for (auto &t : producers) { + t.join(); + } + EXPECT_TRUE(ordered); + EXPECT_EQ_INT(received, QUEUE_PRODUCERS * QUEUE_ITEMS); + EXPECT_TRUE(queue.Empty()); + } + return true; +} + bool TestThreadManager() { ThreadManager manager; manager.Init(8, 1); @@ -158,5 +211,9 @@ bool TestThreadManager() { return false; } + if (!TestMpscQueue()) { + return false; + } + return true; }