From af07b97f44ec8c29052432850b4a4dfa808da447 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Mon, 23 Mar 2026 20:49:40 +0100 Subject: [PATCH] core: kernel: Implement thread cpu time limit for core1 --- src/common/thread_queue_list.h | 17 +- src/core/file_sys/ncch_container.h | 9 +- src/core/hle/kernel/kernel.cpp | 14 ++ src/core/hle/kernel/kernel.h | 42 +++++ src/core/hle/kernel/resource_limit.cpp | 123 ++++++++++-- src/core/hle/kernel/resource_limit.h | 13 +- src/core/hle/kernel/thread.cpp | 249 ++++++++++++++++++++++--- src/core/hle/kernel/thread.h | 83 ++++++++- src/core/hle/service/apt/apt.cpp | 26 ++- src/core/hle/service/apt/apt.h | 2 - src/core/hle/service/pm/pm.cpp | 4 +- src/core/hle/service/pm/pm_app.cpp | 72 ++++++- src/core/hle/service/pm/pm_app.h | 18 +- src/core/loader/3dsx.cpp | 2 + src/core/loader/artic.cpp | 8 + src/core/loader/elf.cpp | 2 + src/core/loader/ncch.cpp | 8 + 17 files changed, 617 insertions(+), 75 deletions(-) diff --git a/src/common/thread_queue_list.h b/src/common/thread_queue_list.h index 5995d76f2..16abe0451 100644 --- a/src/common/thread_queue_list.h +++ b/src/common/thread_queue_list.h @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project / PPSSPP Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include "common/common_types.h" @@ -52,33 +53,35 @@ struct ThreadQueueList { return T(); } - T pop_first() { + std::pair pop_first() { Queue* cur = first; while (cur != nullptr) { if (!cur->data.empty()) { auto tmp = std::move(cur->data.front()); cur->data.pop_front(); - return tmp; + Priority prio = static_cast(ToIndex(cur)); + return {prio, tmp}; } cur = cur->next_nonempty; } - return T(); + return {Priority(), T()}; } - T pop_first_better(Priority priority) { + std::pair pop_first_better(Priority priority) { Queue* cur = first; Queue* stop = &queues[priority]; while (cur < stop) { if (!cur->data.empty()) { auto tmp = std::move(cur->data.front()); cur->data.pop_front(); - return tmp; + Priority prio = static_cast(ToIndex(cur)); + return {prio, tmp}; } cur = cur->next_nonempty; } - return T(); + return {Priority(), T()}; } void push_front(Priority priority, const T& thread_id) { diff --git a/src/core/file_sys/ncch_container.h b/src/core/file_sys/ncch_container.h index c7984d338..02f3bb0b1 100644 --- a/src/core/file_sys/ncch_container.h +++ b/src/core/file_sys/ncch_container.h @@ -182,7 +182,14 @@ struct ExHeader_ARM11_SystemLocalCaps { BitField<4, 4, u8> system_mode; }; u8 priority; - u8 resource_limit_descriptor[0x10][2]; + union { + u16 core1_schedule_flags; + BitField<0, 7, u16> max_cpu; + // Schedule mode flag, 0 -> "single", 1 -> "multi" + BitField<7, 1, u16> schedule_mode; + BitField<8, 8, u16> unknown; + }; + u8 resource_limit_descriptor[0xF][2]; // Always 0 (unused?) ExHeader_StorageInfo storage_info; u8 service_access_control[0x20][8]; u8 ex_service_access_control[0x2][8]; diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index 21ca655db..5c9e5b6ab 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -202,6 +202,14 @@ void KernelSystem::RestoreMemoryState(u64 title_id) { } } +void KernelSystem::SetCore1ScheduleMode(Core1ScheduleMode mode) { + GetThreadManager(1).SetScheduleMode(mode); +} + +void KernelSystem::UpdateCore1AppCpuLimit() { + GetThreadManager(1).UpdateAppCpuLimit(); +} + template void KernelSystem::serialize(Archive& ar, const unsigned int) { ar & memory_regions; @@ -246,4 +254,10 @@ void New3dsHwCapabilities::serialize(Archive& ar, const unsigned int) { } SERIALIZE_IMPL(New3dsHwCapabilities) +template +void Core1CpuTime::serialize(Archive& ar, const unsigned int) { + ar & raw; +} +SERIALIZE_IMPL(Core1CpuTime) + } // namespace Kernel diff --git a/src/core/hle/kernel/kernel.h b/src/core/hle/kernel/kernel.h index 53d88cddf..4637d1de0 100644 --- a/src/core/hle/kernel/kernel.h +++ b/src/core/hle/kernel/kernel.h @@ -136,6 +136,43 @@ private: friend class boost::serialization::access; }; +enum class Core1ScheduleMode : u32 { + // https://github.com/LumaTeam/Luma3DS/blob/e35972ea/sysmodules/pm/source/reslimit.c#L144 + // Starting by "sysmodule" threads, alternatively allow (if preemptible) only sysmodule + // threads, and then only application threads to run. This happens at a rate of 2ms * + // (cpuTime/100). + Multi, + // This divides the core1 time into slices of 25ms. Only one application thread is allowed to be + // created on core1. The "application" thread is given cpuTime% of the slice. The "sysmodules" + // threads are given a total of (90 - cpuTime)% of the slice. 10% remain for other threads. This + // mode is half-broken due to a kernel bug. + Single, +}; + +class Core1CpuTime { +public: + Core1CpuTime() = default; + constexpr Core1CpuTime(s32 value) : raw(value) {} + + static const Core1CpuTime PREEMPTION_DISABLED; + static const Core1CpuTime PREEMPTION_SYSMODULE; + static const Core1CpuTime PREEMPTION_EXCEMPTED; + + operator s32() const { + return raw; + } + +private: + s32 raw{}; + + template + void serialize(Archive& ar, const unsigned int); + friend class boost::serialization::access; +}; +inline constexpr Core1CpuTime Core1CpuTime::PREEMPTION_DISABLED{0}; +inline constexpr Core1CpuTime Core1CpuTime::PREEMPTION_SYSMODULE{1000}; +inline constexpr Core1CpuTime Core1CpuTime::PREEMPTION_EXCEMPTED{10000}; + class KernelSystem { public: explicit KernelSystem(Memory::MemorySystem& memory, Core::Timing& timing, @@ -378,6 +415,10 @@ public: void RestoreMemoryState(u64 title_id); + void SetCore1ScheduleMode(Core1ScheduleMode mode); + + void UpdateCore1AppCpuLimit(); + private: void MemoryInit(MemoryMode memory_mode, u64 override_init_time); @@ -447,3 +488,4 @@ private: } // namespace Kernel BOOST_CLASS_EXPORT_KEY(Kernel::New3dsHwCapabilities) +BOOST_CLASS_EXPORT_KEY(Kernel::Core1CpuTime) diff --git a/src/core/hle/kernel/resource_limit.cpp b/src/core/hle/kernel/resource_limit.cpp index e4b0dba0a..6cc309897 100644 --- a/src/core/hle/kernel/resource_limit.cpp +++ b/src/core/hle/kernel/resource_limit.cpp @@ -1,4 +1,4 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -9,19 +9,24 @@ #include "common/archives.h" #include "common/assert.h" #include "common/settings.h" +#include "core/hle/kernel/process.h" #include "core/hle/kernel/resource_limit.h" +#include "core/hle/kernel/thread.h" SERIALIZE_EXPORT_IMPL(Kernel::ResourceLimit) SERIALIZE_EXPORT_IMPL(Kernel::ResourceLimitList) namespace Kernel { -ResourceLimit::ResourceLimit(KernelSystem& kernel) : Object(kernel) {} +ResourceLimit::ResourceLimit(KernelSystem& _kernel) : Object(_kernel), kernel(_kernel) {} ResourceLimit::~ResourceLimit() = default; -std::shared_ptr ResourceLimit::Create(KernelSystem& kernel, std::string name) { +std::shared_ptr ResourceLimit::Create(KernelSystem& kernel, + ResourceLimitCategory category, + std::string name) { auto resource_limit = std::make_shared(kernel); + resource_limit->m_category = category; resource_limit->m_name = std::move(name); return resource_limit; } @@ -36,6 +41,11 @@ s32 ResourceLimit::GetLimitValue(ResourceLimitType type) const { return m_limit_values[index]; } +void ResourceLimit::SetCurrentValue(ResourceLimitType type, s32 value) { + const auto index = static_cast(type); + m_current_values[index] = value; +} + void ResourceLimit::SetLimitValue(ResourceLimitType type, s32 value) { const auto index = static_cast(type); m_limit_values[index] = value; @@ -66,9 +76,89 @@ bool ResourceLimit::Release(ResourceLimitType type, s32 amount) { return true; } +void ResourceLimit::ApplyAppMaxCPUSetting(std::shared_ptr& process, u8 exh_mode, + u8 exh_cpu_limit) { + + // List of 1.0 titles with max CPU time overrides. This is not a "hack" list, + // these values actually exist in the 3DS kernel. + static constexpr std::array, 16> cpu_time_overrides = {{ + // 3DS sound (JPN, USA, EUR) + {0x205, Core1CpuTime::PREEMPTION_EXCEMPTED}, + {0x215, Core1CpuTime::PREEMPTION_EXCEMPTED}, + {0x225, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + // Star Fox 64 3D (JPN) + {0x304, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + // Super Monkey Ball 3D (JPN) + {0x32E, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + // The Legend of Zelda: Ocarina of Time 3D (JPN, USA, EUR) + {0x334, 30}, + {0x335, 30}, + {0x336, 30}, + + // Doctor Lautrec to Boukyaku no Kishidan (JPN, USA, EUR) + {0x348, Core1CpuTime::PREEMPTION_EXCEMPTED}, + {0x368, Core1CpuTime::PREEMPTION_EXCEMPTED}, + {0x562, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + // Pro Yakyuu Spirits 2011 (JPN) + {0x349, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + // Super Monkey Ball 3D (USA, EUR) + {0x370, Core1CpuTime::PREEMPTION_EXCEMPTED}, + {0x389, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + // Star Fox 64 3D (USA, EUR) + {0x490, Core1CpuTime::PREEMPTION_EXCEMPTED}, + {0x491, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + }}; + + Core1ScheduleMode final_mode{}; + Core1CpuTime limit_cpu_time{}; + Core1CpuTime set_cpu_time = Core1CpuTime::PREEMPTION_DISABLED; + + if (exh_mode == 0 && exh_cpu_limit == 0) { + // This is a 1.0 app with the CPU time value not set. Use default or hardcoded value. + + final_mode = Core1ScheduleMode::Single; + limit_cpu_time = 80; + set_cpu_time = 25; + + u32 uID = static_cast(((process->codeset->program_id >> 8) & 0xFFFFF)); + + auto it = std::find_if(cpu_time_overrides.begin(), cpu_time_overrides.end(), + [uID](auto& v) { return v.first == uID; }); + + if (it != cpu_time_overrides.end()) { + const Core1CpuTime& time = it->second; + if (static_cast(time) > 100 && static_cast(time) < 200) { + // This code path is actually unused + final_mode = Core1ScheduleMode::Multi; + set_cpu_time = limit_cpu_time = static_cast(time) - 100; + } else { + final_mode = Core1ScheduleMode::Single; + set_cpu_time = limit_cpu_time = time; + } + } + } else { + final_mode = (exh_mode == 0) ? Core1ScheduleMode::Single : Core1ScheduleMode::Multi; + limit_cpu_time = exh_cpu_limit; + } + + // Normally done by PM through svcSetKernelState and svcSetResourceLimitValues. + SetLimitValue(ResourceLimitType::CpuTime, limit_cpu_time); + SetCurrentValue(ResourceLimitType::CpuTime, set_cpu_time); + kernel.SetCore1ScheduleMode(final_mode); + kernel.UpdateCore1AppCpuLimit(); +} + template void ResourceLimit::serialize(Archive& ar, const unsigned int) { ar& boost::serialization::base_object(*this); + ar & m_category; ar & m_name; ar & m_limit_values; ar & m_current_values; @@ -82,7 +172,8 @@ ResourceLimitList::ResourceLimitList(KernelSystem& kernel) { const auto& appmemalloc = kernel.GetMemoryRegion(MemoryRegion::APPLICATION); // Create the Application resource limit - auto resource_limit = ResourceLimit::Create(kernel, "Applications"); + auto resource_limit = + ResourceLimit::Create(kernel, ResourceLimitCategory::Application, "Applications"); resource_limit->SetLimitValue(ResourceLimitType::Priority, 0x18); resource_limit->SetLimitValue(ResourceLimitType::Commit, appmemalloc->size); resource_limit->SetLimitValue(ResourceLimitType::Thread, 0x20); @@ -92,11 +183,12 @@ ResourceLimitList::ResourceLimitList(KernelSystem& kernel) { resource_limit->SetLimitValue(ResourceLimitType::Timer, 0x8); resource_limit->SetLimitValue(ResourceLimitType::SharedMemory, 0x10); resource_limit->SetLimitValue(ResourceLimitType::AddressArbiter, 0x2); - resource_limit->SetLimitValue(ResourceLimitType::CpuTime, 0x0); - resource_limits[static_cast(ResourceLimitCategory::Application)] = resource_limit; + resource_limit->SetLimitValue(ResourceLimitType::CpuTime, Core1CpuTime::PREEMPTION_DISABLED); + resource_limits[static_cast(resource_limit->GetCategory())] = resource_limit; // Create the SysApplet resource limit - resource_limit = ResourceLimit::Create(kernel, "System Applets"); + resource_limit = + ResourceLimit::Create(kernel, ResourceLimitCategory::SysApplet, "System Applets"); resource_limit->SetLimitValue(ResourceLimitType::Priority, 0x4); resource_limit->SetLimitValue(ResourceLimitType::Commit, is_new_3ds ? 0x5E06000 : 0x2606000); resource_limit->SetLimitValue(ResourceLimitType::Thread, is_new_3ds ? 0x1D : 0xE); @@ -106,11 +198,12 @@ ResourceLimitList::ResourceLimitList(KernelSystem& kernel) { resource_limit->SetLimitValue(ResourceLimitType::Timer, 0x4); resource_limit->SetLimitValue(ResourceLimitType::SharedMemory, 0x8); resource_limit->SetLimitValue(ResourceLimitType::AddressArbiter, 0x3); - resource_limit->SetLimitValue(ResourceLimitType::CpuTime, 0x2710); - resource_limits[static_cast(ResourceLimitCategory::SysApplet)] = resource_limit; + resource_limit->SetLimitValue(ResourceLimitType::CpuTime, Core1CpuTime::PREEMPTION_EXCEMPTED); + resource_limits[static_cast(resource_limit->GetCategory())] = resource_limit; // Create the LibApplet resource limit - resource_limit = ResourceLimit::Create(kernel, "Library Applets"); + resource_limit = + ResourceLimit::Create(kernel, ResourceLimitCategory::LibApplet, "Library Applets"); resource_limit->SetLimitValue(ResourceLimitType::Priority, 0x4); resource_limit->SetLimitValue(ResourceLimitType::Commit, 0x602000); resource_limit->SetLimitValue(ResourceLimitType::Thread, 0xE); @@ -120,11 +213,11 @@ ResourceLimitList::ResourceLimitList(KernelSystem& kernel) { resource_limit->SetLimitValue(ResourceLimitType::Timer, 0x4); resource_limit->SetLimitValue(ResourceLimitType::SharedMemory, 0x8); resource_limit->SetLimitValue(ResourceLimitType::AddressArbiter, 0x1); - resource_limit->SetLimitValue(ResourceLimitType::CpuTime, 0x2710); - resource_limits[static_cast(ResourceLimitCategory::LibApplet)] = resource_limit; + resource_limit->SetLimitValue(ResourceLimitType::CpuTime, Core1CpuTime::PREEMPTION_EXCEMPTED); + resource_limits[static_cast(resource_limit->GetCategory())] = resource_limit; // Create the Other resource limit - resource_limit = ResourceLimit::Create(kernel, "Others"); + resource_limit = ResourceLimit::Create(kernel, ResourceLimitCategory::Other, "Others"); resource_limit->SetLimitValue(ResourceLimitType::Priority, 0x4); resource_limit->SetLimitValue(ResourceLimitType::Commit, is_new_3ds ? 0x2182000 : 0x1682000); resource_limit->SetLimitValue(ResourceLimitType::Thread, is_new_3ds ? 0xE1 : 0xCA); @@ -134,8 +227,8 @@ ResourceLimitList::ResourceLimitList(KernelSystem& kernel) { resource_limit->SetLimitValue(ResourceLimitType::Timer, is_new_3ds ? 0x2C : 0x2B); resource_limit->SetLimitValue(ResourceLimitType::SharedMemory, is_new_3ds ? 0x1F : 0x1E); resource_limit->SetLimitValue(ResourceLimitType::AddressArbiter, is_new_3ds ? 0x2D : 0x2B); - resource_limit->SetLimitValue(ResourceLimitType::CpuTime, 0x3E8); - resource_limits[static_cast(ResourceLimitCategory::Other)] = resource_limit; + resource_limit->SetLimitValue(ResourceLimitType::CpuTime, Core1CpuTime::PREEMPTION_SYSMODULE); + resource_limits[static_cast(resource_limit->GetCategory())] = resource_limit; } ResourceLimitList::~ResourceLimitList() = default; diff --git a/src/core/hle/kernel/resource_limit.h b/src/core/hle/kernel/resource_limit.h index 48235411e..63694a83c 100644 --- a/src/core/hle/kernel/resource_limit.h +++ b/src/core/hle/kernel/resource_limit.h @@ -1,4 +1,4 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -42,6 +42,7 @@ public: * Creates a resource limit object. */ static std::shared_ptr Create(KernelSystem& kernel, + ResourceLimitCategory category, std::string name = "Unknown"); std::string GetTypeName() const override { @@ -56,15 +57,25 @@ public: return HANDLE_TYPE; } + ResourceLimitCategory GetCategory() const { + return m_category; + } + s32 GetCurrentValue(ResourceLimitType type) const; s32 GetLimitValue(ResourceLimitType type) const; + void SetCurrentValue(ResourceLimitType name, s32 value); void SetLimitValue(ResourceLimitType name, s32 value); bool Reserve(ResourceLimitType type, s32 amount); bool Release(ResourceLimitType type, s32 amount); + void ApplyAppMaxCPUSetting(std::shared_ptr& process, u8 exh_mode, + u8 exh_cpu_limit); + private: + Kernel::KernelSystem& kernel; + ResourceLimitCategory m_category; using ResourceArray = std::array(ResourceLimitType::Max)>; ResourceArray m_limit_values{}; ResourceArray m_current_values{}; diff --git a/src/core/hle/kernel/thread.cpp b/src/core/hle/kernel/thread.cpp index 7c491309d..41a706fdd 100644 --- a/src/core/hle/kernel/thread.cpp +++ b/src/core/hle/kernel/thread.cpp @@ -37,6 +37,9 @@ void ThreadManager::serialize(Archive& ar, const unsigned int) { ar & ready_queue; ar & wakeup_callback_table; ar & thread_list; + ar & current_schedule_mode; + ar & single_time_limiter; + ar & multi_time_limiter; } SERIALIZE_IMPL(ThreadManager) @@ -56,6 +59,7 @@ void Thread::serialize(Archive& ar, const unsigned int file_version) { ar & held_mutexes; ar & pending_mutexes; ar & owner_process; + ar & resource_limit_category; ar & wait_objects; ar & wait_address; ar & name; @@ -179,33 +183,49 @@ void ThreadManager::SwitchContext(Thread* new_thread) { } Thread* ThreadManager::PopNextReadyThread() { - Thread* next = nullptr; + Thread* next; Thread* thread = GetCurrentThread(); - if (thread && thread->status == ThreadStatus::Running) { - do { - // We have to do better than the current thread. - // This call returns null when that's not possible. - next = ready_queue.pop_first_better(thread->current_priority); - if (!next) { - // Otherwise just keep going with the current thread - next = thread; - break; - } else if (!next->can_schedule) - unscheduled_ready_queue.push_back(next); - } while (!next->can_schedule); - } else { - do { - next = ready_queue.pop_first(); - if (next && !next->can_schedule) - unscheduled_ready_queue.push_back(next); - } while (next && !next->can_schedule); - } + while (true) { + std::vector> skipped; + u32 next_priority{}; + next = nullptr; - while (!unscheduled_ready_queue.empty()) { - auto t = std::move(unscheduled_ready_queue.back()); - ready_queue.push_back(t->current_priority, t); - unscheduled_ready_queue.pop_back(); + if (thread && thread->status == ThreadStatus::Running) { + do { + // We have to do better than the current thread. + // This call returns null when that's not possible. + std::tie(next_priority, next) = + ready_queue.pop_first_better(thread->current_priority); + if (!next) { + // Otherwise just keep going with the current thread + next = thread; + break; + } else if (!next->can_schedule) { + skipped.push_back({next_priority, next}); + } + + } while (!next->can_schedule); + } else { + do { + std::tie(next_priority, next) = ready_queue.pop_first(); + if (next && !next->can_schedule) { + skipped.push_back({next_priority, next}); + } + } while (next && !next->can_schedule); + } + + for (auto it = skipped.rbegin(); it != skipped.rend(); it++) { + ready_queue.push_front(it->first, it->second); + } + + // Try to time limit the selected thread on core 1 + if (core_id == 1 && next && GetCpuLimiter()->DoTimeLimit(next)) { + // If the thread is time limited, select the next one + continue; + } + + break; } return next; @@ -392,6 +412,7 @@ ResultVal> KernelSystem::CreateThread( thread->name = std::move(name); thread_managers[processor_id]->wakeup_callback_table[thread->thread_id] = thread.get(); thread->owner_process = owner_process; + thread->resource_limit_category = owner_process->resource_limit->GetCategory(); CASCADE_RESULT(thread->tls_address, owner_process->AllocateThreadLocalStorage()); // TODO(peachum): move to ScheduleThread() when scheduler is added so selected core is used @@ -516,10 +537,158 @@ VAddr Thread::GetCommandBufferAddress() const { return GetTLSAddress() + command_header_offset; } -ThreadManager::ThreadManager(Kernel::KernelSystem& kernel, u32 core_id) : kernel(kernel) { +CpuLimiter::~CpuLimiter() {} + +CpuLimiterMulti::CpuLimiterMulti(Kernel::KernelSystem& _kernel) : kernel(_kernel) {} + +void CpuLimiterMulti::Initialize(bool is_single) { + // TODO(PabloMK7): The is_single variable is needed to prevent + // registering an event twice with the same name. Once CpuLimiterSingle + // is implemented we can remove it. + tick_event = kernel.timing.RegisterEvent( + fmt::format("Kernel::{}::tick_event", is_single ? "CpuLimiterSingle" : "CpuLimiterMulti"), + [this](u64, s64 cycles_late) { this->OnTick(cycles_late); }); +} + +void CpuLimiterMulti::Start() { + if (ready) { + return; + } + ready = true; + active = false; + curr_state = SchedState::APP; // So that ChangeState starts with SYS + app_cpu_time = Core1CpuTime::PREEMPTION_DISABLED; +} + +void CpuLimiterMulti::End() { + if (!ready) { + return; + } + ready = false; + active = false; + kernel.timing.UnscheduleEvent(tick_event, 0); + WakeupSleepingThreads(); +} + +void CpuLimiterMulti::UpdateAppCpuLimit() { + if (!ready) { + return; + } + + app_cpu_time = static_cast(kernel.ResourceLimit() + .GetForCategory(Kernel::ResourceLimitCategory::Application) + ->GetCurrentValue(Kernel::ResourceLimitType::CpuTime)); + if (app_cpu_time == Core1CpuTime::PREEMPTION_DISABLED) { + // No preemption, disable event + active = false; + kernel.timing.UnscheduleEvent(tick_event, 0); + WakeupSleepingThreads(); + } else { + // Start preempting, enable event + if (active) { + // If we were active already, unschedule first + // so that the event is not scheduled twice. + // We could just not call ChangeState instead, + // but this allows adjusting the timing of the + // event sooner. + kernel.timing.UnscheduleEvent(tick_event, 0); + } + active = true; + ChangeState(0); + } +} + +bool CpuLimiterMulti::DoTimeLimit(Thread* thread) { + if (!ready || !active) { + // Preemption is not active, don't do anything. + return false; + } + if (kernel.ResourceLimit() + .GetForCategory(thread->resource_limit_category) + ->GetLimitValue(ResourceLimitType::CpuTime) == Core1CpuTime::PREEMPTION_EXCEMPTED) { + // The thread is excempted from preemption + return false; + } + + // On real hardware, the kernel uses a KPreemptionTimer to determine if a + // thread needs to be time limited. This properly uses the resource limit + // value to check if it is a sysmodule or not. We can do this instead and + // it should be good enough. TODO(PabloMK7): fix? + if (thread->resource_limit_category == ResourceLimitCategory::Application && + curr_state == SchedState::SYS || + thread->resource_limit_category == ResourceLimitCategory::Other && + curr_state == SchedState::APP) { + // Block thread as not in the current mode + thread->status = ThreadStatus::WaitSleep; + sleeping_thread_ids.push(thread->thread_id); + return true; + } + return false; +} + +void CpuLimiterMulti::OnTick(s64 cycles_late) { + WakeupSleepingThreads(); + ChangeState(cycles_late); +} + +void CpuLimiterMulti::ChangeState(s64 cycles_late) { + curr_state = (curr_state == SchedState::SYS) ? SchedState::APP : SchedState::SYS; + + s64 next_timer = base_tick_interval * (app_cpu_time / 100.f); + if (curr_state == SchedState::SYS) { + next_timer = base_tick_interval - next_timer; + } + if (next_timer > cycles_late) { + next_timer -= cycles_late; + } + kernel.timing.ScheduleEvent(next_timer, tick_event, 0, 1); +} + +void CpuLimiterMulti::WakeupSleepingThreads() { + while (!sleeping_thread_ids.empty()) { + u32 curr_id = sleeping_thread_ids.front(); + + auto thread = kernel.GetThreadManager(1).GetThreadByID(curr_id); + if (thread && thread->status == ThreadStatus::WaitSleep) { + thread->ResumeFromWait(); + } + + sleeping_thread_ids.pop(); + } +} + +template +void CpuLimiterMulti::serialize(Archive& ar, const unsigned int) { + ar & ready; + ar & active; + ar & app_cpu_time; + ar & curr_state; + std::vector v; + if (Archive::is_loading::value) { + ar & v; + for (auto it : v) { + sleeping_thread_ids.push(it); + } + } else { + std::queue temp = sleeping_thread_ids; + while (!temp.empty()) { + v.push_back(temp.front()); + temp.pop(); + } + ar & v; + } +} + +ThreadManager::ThreadManager(Kernel::KernelSystem& kernel, u32 core_id) + : kernel(kernel), core_id(core_id), current_schedule_mode(Core1ScheduleMode::Multi), + single_time_limiter(kernel), multi_time_limiter(kernel) { ThreadWakeupEventType = kernel.timing.RegisterEvent( "ThreadWakeupCallback_" + std::to_string(core_id), [this](u64 thread_id, s64 cycle_late) { ThreadWakeupCallback(thread_id, cycle_late); }); + if (core_id == 1) { + single_time_limiter.Initialize(true); + multi_time_limiter.Initialize(false); + } } ThreadManager::~ThreadManager() { @@ -532,13 +701,33 @@ std::span> ThreadManager::GetThreadList() const { return thread_list; } +std::shared_ptr ThreadManager::GetThreadByID(u32 thread_id) const { + for (auto& thread : thread_list) { + if (thread->thread_id == thread_id) { + return thread; + } + } + return nullptr; +} + +void ThreadManager::SetScheduleMode(Core1ScheduleMode mode) { + GetCpuLimiter()->End(); + current_schedule_mode = mode; + if (mode == Core1ScheduleMode::Single) { + LOG_WARNING(Kernel, "Unimplemented \"Single\" schedule mode."); + } + GetCpuLimiter()->Start(); +} + +void ThreadManager::UpdateAppCpuLimit() { + GetCpuLimiter()->UpdateAppCpuLimit(); +} + std::shared_ptr KernelSystem::GetThreadByID(u32 thread_id) const { for (u32 core_id = 0; core_id < Core::System::GetInstance().GetNumCores(); core_id++) { - const auto thread_list = GetThreadManager(core_id).GetThreadList(); - for (auto& thread : thread_list) { - if (thread->thread_id == thread_id) { - return thread; - } + auto ret = GetThreadManager(core_id).GetThreadByID(thread_id); + if (ret) { + return ret; } } return nullptr; diff --git a/src/core/hle/kernel/thread.h b/src/core/hle/kernel/thread.h index 8853384f4..be515319b 100644 --- a/src/core/hle/kernel/thread.h +++ b/src/core/hle/kernel/thread.h @@ -15,11 +15,13 @@ #include #include #include +#include #include "common/common_types.h" #include "common/thread_queue_list.h" #include "core/arm/arm_interface.h" #include "core/core_timing.h" #include "core/hle/kernel/object.h" +#include "core/hle/kernel/resource_limit.h" #include "core/hle/kernel/wait_object.h" #include "core/hle/result.h" @@ -49,7 +51,7 @@ enum class ThreadStatus { Running, ///< Currently running Ready, ///< Ready to run WaitArb, ///< Waiting on an address arbiter - WaitSleep, ///< Waiting due to a SleepThread SVC + WaitSleep, ///< Waiting due to a SleepThread SVC or time limited WaitIPC, ///< Waiting for the reply from an IPC request WaitSynchAny, ///< Waiting due to WaitSynch1 or WaitSynchN with wait_all = false WaitSynchAll, ///< Waiting due to WaitSynchronizationN with wait_all = true @@ -81,6 +83,64 @@ private: friend class boost::serialization::access; }; +class CpuLimiter { +public: + CpuLimiter() = default; + virtual ~CpuLimiter() = 0; + + virtual void Start() = 0; + virtual void End() = 0; + + virtual void UpdateAppCpuLimit() = 0; + + virtual bool DoTimeLimit(Thread* thread) = 0; +}; + +class CpuLimiterMulti : public CpuLimiter { +public: + CpuLimiterMulti(Kernel::KernelSystem& kernel); + ~CpuLimiterMulti() override = default; + + void Initialize(bool is_single); + + void Start() override; + void End() override; + + void UpdateAppCpuLimit() override; + + bool DoTimeLimit(Thread* thread) override; + +private: + enum class SchedState : u32 { + APP, + SYS, + }; + + void OnTick(s64 cycles_late); + + void ChangeState(s64 cycles_late); + + void WakeupSleepingThreads(); + + static constexpr u64 base_tick_interval = nsToCycles(2'000'000); // 2ms + + Kernel::KernelSystem& kernel; + Core::TimingEventType* tick_event{}; + + bool ready = false; + bool active = false; + Core1CpuTime app_cpu_time = Core1CpuTime::PREEMPTION_DISABLED; + SchedState curr_state{}; + std::queue sleeping_thread_ids; + + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned int); +}; + +// TODO(PabloMK7): Replace with proper implementation +using CpuLimiterSingle = CpuLimiterMulti; + class ThreadManager { public: explicit ThreadManager(Kernel::KernelSystem& kernel, u32 core_id); @@ -126,10 +186,21 @@ public: */ std::span> GetThreadList() const; + std::shared_ptr GetThreadByID(u32 thread_id) const; + void SetCPU(Core::ARM_Interface& cpu_) { cpu = &cpu_; } + void SetScheduleMode(Core1ScheduleMode mode); + + void UpdateAppCpuLimit(); + + CpuLimiter* GetCpuLimiter() { + return (current_schedule_mode == Core1ScheduleMode::Single) ? &single_time_limiter + : &multi_time_limiter; + } + private: /** * Switches the CPU's active thread context to that of the specified thread @@ -151,11 +222,11 @@ private: void ThreadWakeupCallback(u64 thread_id, s64 cycles_late); Kernel::KernelSystem& kernel; - Core::ARM_Interface* cpu; + u32 core_id; + Core::ARM_Interface* cpu{}; std::shared_ptr current_thread; Common::ThreadQueueList ready_queue; - std::deque unscheduled_ready_queue; std::unordered_map wakeup_callback_table; /// Event type for the thread wake up event @@ -164,6 +235,10 @@ private: // Lists all threadsthat aren't deleted. std::vector> thread_list; + Core1ScheduleMode current_schedule_mode{}; + CpuLimiterSingle single_time_limiter; + CpuLimiterMulti multi_time_limiter; + friend class Thread; friend class KernelSystem; @@ -316,6 +391,8 @@ public: std::weak_ptr owner_process{}; ///< Process that owns this thread + ResourceLimitCategory resource_limit_category{}; + /// Objects that the thread is waiting on, in the same order as they were /// passed to WaitSynchronization1/N. std::vector> wait_objects{}; diff --git a/src/core/hle/service/apt/apt.cpp b/src/core/hle/service/apt/apt.cpp index 6d0754c6c..355f98bab 100644 --- a/src/core/hle/service/apt/apt.cpp +++ b/src/core/hle/service/apt/apt.cpp @@ -29,6 +29,7 @@ #include "core/hle/service/cfg/cfg.h" #include "core/hle/service/fs/archive.h" #include "core/hle/service/fs/fs_user.h" +#include "core/hle/service/pm/pm_app.h" #include "core/hle/service/ptm/ptm.h" #include "core/hle/service/service.h" #include "core/hw/aes/ccm.h" @@ -47,7 +48,6 @@ void Module::serialize(Archive& ar, const unsigned int file_version) { ar & shared_font_mem; ar & shared_font_loaded; ar & shared_font_relocated; - ar & cpu_percent; ar & screen_capture_post_permission; ar & applet_manager; ar & wireless_reboot_info; @@ -731,29 +731,39 @@ void Module::APTInterface::SetAppCpuTimeLimit(Kernel::HLERequestContext& ctx) { const auto must_be_one = rp.Pop(); const auto value = rp.Pop(); - LOG_WARNING(Service_APT, "(STUBBED) called, must_be_one={}, value={}", must_be_one, value); if (must_be_one != 1) { LOG_ERROR(Service_APT, "This value should be one, but is actually {}!", must_be_one); } - apt->cpu_percent = value; - + auto pm_app = Service::PM::GetServiceAPP(apt->system); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); // No error + if (pm_app) { + rb.Push(pm_app->UpdateResourceLimit(Kernel::ResourceLimitType::CpuTime, value)); + } else { + LOG_ERROR(Service_APT, "Failed to get PM:APP module"); + rb.Push(ResultUnknown); + } } void Module::APTInterface::GetAppCpuTimeLimit(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const auto must_be_one = rp.Pop(); - LOG_WARNING(Service_APT, "(STUBBED) called, must_be_one={}", must_be_one); if (must_be_one != 1) { LOG_ERROR(Service_APT, "This value should be one, but is actually {}!", must_be_one); } + auto pm_app = Service::PM::GetServiceAPP(apt->system); IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); - rb.Push(ResultSuccess); // No error - rb.Push(apt->cpu_percent); + if (pm_app) { + auto res = pm_app->GetResourceLimit(Kernel::ResourceLimitType::CpuTime); + rb.Push(res.Code()); + rb.Push(res.ValueOr(u32{})); + } else { + LOG_ERROR(Service_APT, "Failed to get PM:APP module"); + rb.Push(ResultUnknown); + rb.Push(u32{}); + } } void Module::APTInterface::PrepareToStartLibraryApplet(Kernel::HLERequestContext& ctx) { diff --git a/src/core/hle/service/apt/apt.h b/src/core/hle/service/apt/apt.h index 85b92bb60..3eaf5fa65 100644 --- a/src/core/hle/service/apt/apt.h +++ b/src/core/hle/service/apt/apt.h @@ -1089,8 +1089,6 @@ private: bool shared_font_loaded = false; bool shared_font_relocated = false; - u32 cpu_percent = 0; ///< CPU time available to the running application - ScreencapPostPermission screen_capture_post_permission = ScreencapPostPermission::CleanThePermission; // TODO(JamePeng): verify the initial value diff --git a/src/core/hle/service/pm/pm.cpp b/src/core/hle/service/pm/pm.cpp index 6c9ddc7d9..ccb69ef2e 100644 --- a/src/core/hle/service/pm/pm.cpp +++ b/src/core/hle/service/pm/pm.cpp @@ -1,4 +1,4 @@ -// Copyright 2018 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -11,7 +11,7 @@ namespace Service::PM { void InstallInterfaces(Core::System& system) { auto& service_manager = system.ServiceManager(); - std::make_shared()->InstallAsService(service_manager); + std::make_shared(system)->InstallAsService(service_manager); std::make_shared()->InstallAsService(service_manager); } diff --git a/src/core/hle/service/pm/pm_app.cpp b/src/core/hle/service/pm/pm_app.cpp index ba14d1bdc..dc360c469 100644 --- a/src/core/hle/service/pm/pm_app.cpp +++ b/src/core/hle/service/pm/pm_app.cpp @@ -1,16 +1,19 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include "common/archives.h" +#include "core/core.h" #include "core/hle/ipc_helpers.h" +#include "core/hle/kernel/kernel.h" #include "core/hle/service/pm/pm_app.h" +SERVICE_CONSTRUCT_IMPL(Service::PM::PM_APP) SERIALIZE_EXPORT_IMPL(Service::PM::PM_APP) namespace Service::PM { -PM_APP::PM_APP() : ServiceFramework("pm:app", 3) { +PM_APP::PM_APP(Core::System& _system) : ServiceFramework("pm:app", 3), system(_system) { static const FunctionInfo functions[] = { // clang-format off {0x0001, nullptr, "LaunchTitle"}, @@ -22,8 +25,8 @@ PM_APP::PM_APP() : ServiceFramework("pm:app", 3) { {0x0007, nullptr, "GetFIRMLaunchParams"}, {0x0008, nullptr, "GetTitleExheaderFlags"}, {0x0009, nullptr, "SetFIRMLaunchParams"}, - {0x000A, nullptr, "SetAppResourceLimit"}, - {0x000B, nullptr, "GetAppResourceLimit"}, + {0x000A, &PM_APP::SetAppResourceLimit, "SetAppResourceLimit"}, + {0x000B, &PM_APP::GetAppResourceLimit, "GetAppResourceLimit"}, {0x000C, nullptr, "UnregisterProcess"}, {0x000D, nullptr, "LaunchTitleUpdate"}, // clang-format on @@ -32,4 +35,65 @@ PM_APP::PM_APP() : ServiceFramework("pm:app", 3) { RegisterHandlers(functions); } +Result PM_APP::UpdateResourceLimit(Kernel::ResourceLimitType type, u32 value) { + auto res_limit = + system.Kernel().ResourceLimit().GetForCategory(Kernel::ResourceLimitCategory::Application); + + if (type != Kernel::ResourceLimitType::CpuTime) { + return Result{ErrorDescription::NotImplemented, ErrorModule::PM, + ErrorSummary::InvalidArgument, ErrorLevel::Permanent}; + } + + if (value <= res_limit->GetLimitValue(Kernel::ResourceLimitType::CpuTime)) { + res_limit->SetCurrentValue(Kernel::ResourceLimitType::CpuTime, value); + system.Kernel().UpdateCore1AppCpuLimit(); + } + + return ResultSuccess; +} + +ResultVal PM_APP::GetResourceLimit(Kernel::ResourceLimitType type) { + auto res_limit = + system.Kernel().ResourceLimit().GetForCategory(Kernel::ResourceLimitCategory::Application); + + if (type != Kernel::ResourceLimitType::CpuTime) { + return Result{ErrorDescription::NotImplemented, ErrorModule::PM, + ErrorSummary::InvalidArgument, ErrorLevel::Permanent}; + } + + return static_cast(res_limit->GetCurrentValue(Kernel::ResourceLimitType::CpuTime)); +} + +void PM_APP::SetAppResourceLimit(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + rp.Skip(1, false); + auto type = static_cast(rp.Pop()); + u32 value = rp.Pop(); + rp.Skip(2, false); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(UpdateResourceLimit(type, value)); +} + +void PM_APP::GetAppResourceLimit(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + rp.Skip(1, false); + auto type = static_cast(rp.Pop()); + rp.Skip(3, false); + + u64 res_value = 0; + auto res = GetResourceLimit(type); + if (res.Succeeded()) { + res_value = static_cast(res.Unwrap()); + } + + IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); + rb.Push(res.Code()); + rb.Push(res_value); +} + +std::shared_ptr GetServiceAPP(Core::System& system) { + return system.ServiceManager().GetService("pm:app"); +} + } // namespace Service::PM diff --git a/src/core/hle/service/pm/pm_app.h b/src/core/hle/service/pm/pm_app.h index 9aefb0cee..decec0919 100644 --- a/src/core/hle/service/pm/pm_app.h +++ b/src/core/hle/service/pm/pm_app.h @@ -1,22 +1,36 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once +#include "core/hle/kernel/resource_limit.h" #include "core/hle/service/service.h" namespace Service::PM { class PM_APP final : public ServiceFramework { public: - PM_APP(); + explicit PM_APP(Core::System& system); ~PM_APP() = default; + Result UpdateResourceLimit(Kernel::ResourceLimitType type, u32 value); + + ResultVal GetResourceLimit(Kernel::ResourceLimitType type); + private: + Core::System& system; + + void SetAppResourceLimit(Kernel::HLERequestContext& ctx); + + void GetAppResourceLimit(Kernel::HLERequestContext& ctx); + SERVICE_SERIALIZATION_SIMPLE }; +std::shared_ptr GetServiceAPP(Core::System& system); + } // namespace Service::PM +SERVICE_CONSTRUCT(Service::PM::PM_APP) BOOST_CLASS_EXPORT_KEY(Service::PM::PM_APP) diff --git a/src/core/loader/3dsx.cpp b/src/core/loader/3dsx.cpp index d8c0beded..163a7d472 100644 --- a/src/core/loader/3dsx.cpp +++ b/src/core/loader/3dsx.cpp @@ -293,6 +293,8 @@ ResultStatus AppLoader_THREEDSX::Load(std::shared_ptr& process) process->resource_limit = system.Kernel().ResourceLimit().GetForCategory(Kernel::ResourceLimitCategory::Application); + process->resource_limit->ApplyAppMaxCPUSetting(process, 1, 89); + // On real HW this is done with FS:Reg, but we can be lazy auto fs_user = system.ServiceManager().GetService("fs:USER"); fs_user->RegisterProgramInfo(process->GetObjectId(), process->codeset->program_id, filepath); diff --git a/src/core/loader/artic.cpp b/src/core/loader/artic.cpp index 9c7aefbeb..fbcbe2376 100644 --- a/src/core/loader/artic.cpp +++ b/src/core/loader/artic.cpp @@ -203,6 +203,14 @@ ResultStatus Apploader_Artic::LoadExecImpl(std::shared_ptr& pro exheader.arm11_system_local_caps.resource_limit_category); process->resource_limit = system.Kernel().ResourceLimit().GetForCategory(category); + // Update application max cpu setting. PM module uses the launch flags to determine + // this, but using the resource limit category is close enough. + if (category == Kernel::ResourceLimitCategory::Application) { + process->resource_limit->ApplyAppMaxCPUSetting( + process, exheader.arm11_system_local_caps.schedule_mode, + exheader.arm11_system_local_caps.max_cpu); + } + // When running N3DS-unaware titles pm will lie about the amount of memory available. // This means RESLIMIT_COMMIT = APPMEMALLOC doesn't correspond to the actual size of // APPLICATION. See: diff --git a/src/core/loader/elf.cpp b/src/core/loader/elf.cpp index ebecb6712..916ec2fc9 100644 --- a/src/core/loader/elf.cpp +++ b/src/core/loader/elf.cpp @@ -393,6 +393,8 @@ ResultStatus AppLoader_ELF::Load(std::shared_ptr& process) { process->resource_limit = system.Kernel().ResourceLimit().GetForCategory(Kernel::ResourceLimitCategory::Application); + process->resource_limit->ApplyAppMaxCPUSetting(process, 1, 89); + process->Run(48, Kernel::DEFAULT_STACK_SIZE); is_loaded = true; diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index f61f5724b..2641b0c93 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -202,6 +202,14 @@ ResultStatus AppLoader_NCCH::LoadExec(std::shared_ptr& process) overlay_ncch->exheader_header.arm11_system_local_caps.resource_limit_category); process->resource_limit = system.Kernel().ResourceLimit().GetForCategory(category); + // Update application max cpu setting. PM module uses the launch flags to determine + // this, but using the resource limit category is close enough. + if (category == Kernel::ResourceLimitCategory::Application) { + process->resource_limit->ApplyAppMaxCPUSetting( + process, overlay_ncch->exheader_header.arm11_system_local_caps.schedule_mode, + overlay_ncch->exheader_header.arm11_system_local_caps.max_cpu); + } + // When running N3DS-unaware titles pm will lie about the amount of memory available. // This means RESLIMIT_COMMIT = APPMEMALLOC doesn't correspond to the actual size of // APPLICATION. See: