From 3f225d06c18a2b4243d23d0f95dbbc7563ba968f Mon Sep 17 00:00:00 2001 From: libretroadmin Date: Fri, 9 Oct 2026 07:47:30 +0200 Subject: [PATCH] Recycle MpscQueue nodes and keep host time out of emulated state A drained MpscQueue node goes onto a free stack shared by every queue of the same type, and a producer takes that stack whole when its own thread's list runs dry, so once warmed up a push no longer allocates. That takes the heap allocation back off memory block notifications, the CPU queue, Vulkan deletes and the GL render queue. A unit test runs several producers against one consumer over two rounds. The NpSignin dialog's delays run on emulated time and are saved with the state, so the dialog behaves the same at any speed and under run-ahead. The time since the last save and savestate goes; only the standalone frontends' exit prompts read it. --- Common/Thread/MpscQueue.h | 63 ++++++++++++++++++++++--------- Core/Dialog/PSPNpSigninDialog.cpp | 16 ++++---- Core/Dialog/PSPSaveDialog.cpp | 25 ------------ Core/Dialog/PSPSaveDialog.h | 2 - Core/HLE/sceUtility.cpp | 2 - Core/SaveState.cpp | 16 -------- Core/SaveState.h | 1 - unittest/TestThreadManager.cpp | 57 ++++++++++++++++++++++++++++ 8 files changed, 112 insertions(+), 70 deletions(-) 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; }