Browse Source
Merge pull request #5131 from bunnei/scheduler-rewrite
Merge pull request #5131 from bunnei/scheduler-rewrite
Rewrite Kernel scheduler based on Atmospherence_cpp
committed by
GitHub
41 changed files with 2216 additions and 1872 deletions
-
2src/common/CMakeLists.txt
-
99src/common/bit_set.h
-
345src/common/multi_level_queue.h
-
11src/core/CMakeLists.txt
-
3src/core/arm/dynarmic/arm_dynarmic_32.cpp
-
5src/core/arm/dynarmic/arm_dynarmic_64.cpp
-
26src/core/core.cpp
-
20src/core/core.h
-
98src/core/cpu_manager.cpp
-
21src/core/hle/kernel/address_arbiter.cpp
-
52src/core/hle/kernel/global_scheduler_context.cpp
-
81src/core/hle/kernel/global_scheduler_context.h
-
4src/core/hle/kernel/handle_table.cpp
-
10src/core/hle/kernel/hle_ipc.cpp
-
58src/core/hle/kernel/k_affinity_mask.h
-
449src/core/hle/kernel/k_priority_queue.h
-
784src/core/hle/kernel/k_scheduler.cpp
-
201src/core/hle/kernel/k_scheduler.h
-
74src/core/hle/kernel/k_scheduler_lock.h
-
41src/core/hle/kernel/k_scoped_lock.h
-
50src/core/hle/kernel/k_scoped_scheduler_lock_and_sleep.h
-
63src/core/hle/kernel/kernel.cpp
-
17src/core/hle/kernel/kernel.h
-
12src/core/hle/kernel/mutex.cpp
-
8src/core/hle/kernel/physical_core.cpp
-
13src/core/hle/kernel/physical_core.h
-
14src/core/hle/kernel/process.cpp
-
13src/core/hle/kernel/process.h
-
4src/core/hle/kernel/readable_event.cpp
-
819src/core/hle/kernel/scheduler.cpp
-
320src/core/hle/kernel/scheduler.h
-
4src/core/hle/kernel/server_session.cpp
-
78src/core/hle/kernel/svc.cpp
-
11src/core/hle/kernel/synchronization.cpp
-
79src/core/hle/kernel/thread.cpp
-
114src/core/hle/kernel/thread.h
-
17src/core/hle/kernel/time_manager.cpp
-
2src/core/hle/service/time/time.cpp
-
1src/tests/CMakeLists.txt
-
55src/tests/common/multi_level_queue.cpp
-
10src/yuzu/debugger/wait_tree.cpp
@ -0,0 +1,99 @@ |
|||||
|
/* |
||||
|
* Copyright (c) 2018-2020 Atmosphère-NX |
||||
|
* |
||||
|
* This program is free software; you can redistribute it and/or modify it |
||||
|
* under the terms and conditions of the GNU General Public License, |
||||
|
* version 2, as published by the Free Software Foundation. |
||||
|
* |
||||
|
* This program is distributed in the hope 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 for |
||||
|
* more details. |
||||
|
* |
||||
|
* You should have received a copy of the GNU General Public License |
||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
*/ |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <array> |
||||
|
#include <bit> |
||||
|
|
||||
|
#include "common/alignment.h" |
||||
|
#include "common/bit_util.h" |
||||
|
#include "common/common_types.h" |
||||
|
|
||||
|
namespace Common { |
||||
|
|
||||
|
namespace impl { |
||||
|
|
||||
|
template <typename Storage, size_t N> |
||||
|
class BitSet { |
||||
|
|
||||
|
public: |
||||
|
constexpr BitSet() = default; |
||||
|
|
||||
|
constexpr void SetBit(size_t i) { |
||||
|
this->words[i / FlagsPerWord] |= GetBitMask(i % FlagsPerWord); |
||||
|
} |
||||
|
|
||||
|
constexpr void ClearBit(size_t i) { |
||||
|
this->words[i / FlagsPerWord] &= ~GetBitMask(i % FlagsPerWord); |
||||
|
} |
||||
|
|
||||
|
constexpr size_t CountLeadingZero() const { |
||||
|
for (size_t i = 0; i < NumWords; i++) { |
||||
|
if (this->words[i]) { |
||||
|
return FlagsPerWord * i + CountLeadingZeroImpl(this->words[i]); |
||||
|
} |
||||
|
} |
||||
|
return FlagsPerWord * NumWords; |
||||
|
} |
||||
|
|
||||
|
constexpr size_t GetNextSet(size_t n) const { |
||||
|
for (size_t i = (n + 1) / FlagsPerWord; i < NumWords; i++) { |
||||
|
Storage word = this->words[i]; |
||||
|
if (!IsAligned(n + 1, FlagsPerWord)) { |
||||
|
word &= GetBitMask(n % FlagsPerWord) - 1; |
||||
|
} |
||||
|
if (word) { |
||||
|
return FlagsPerWord * i + CountLeadingZeroImpl(word); |
||||
|
} |
||||
|
} |
||||
|
return FlagsPerWord * NumWords; |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
static_assert(std::is_unsigned_v<Storage>); |
||||
|
static_assert(sizeof(Storage) <= sizeof(u64)); |
||||
|
|
||||
|
static constexpr size_t FlagsPerWord = BitSize<Storage>(); |
||||
|
static constexpr size_t NumWords = AlignUp(N, FlagsPerWord) / FlagsPerWord; |
||||
|
|
||||
|
static constexpr auto CountLeadingZeroImpl(Storage word) { |
||||
|
return std::countl_zero(static_cast<unsigned long long>(word)) - |
||||
|
(BitSize<unsigned long long>() - FlagsPerWord); |
||||
|
} |
||||
|
|
||||
|
static constexpr Storage GetBitMask(size_t bit) { |
||||
|
return Storage(1) << (FlagsPerWord - 1 - bit); |
||||
|
} |
||||
|
|
||||
|
std::array<Storage, NumWords> words{}; |
||||
|
}; |
||||
|
|
||||
|
} // namespace impl |
||||
|
|
||||
|
template <size_t N> |
||||
|
using BitSet8 = impl::BitSet<u8, N>; |
||||
|
|
||||
|
template <size_t N> |
||||
|
using BitSet16 = impl::BitSet<u16, N>; |
||||
|
|
||||
|
template <size_t N> |
||||
|
using BitSet32 = impl::BitSet<u32, N>; |
||||
|
|
||||
|
template <size_t N> |
||||
|
using BitSet64 = impl::BitSet<u64, N>; |
||||
|
|
||||
|
} // namespace Common |
||||
@ -1,345 +0,0 @@ |
|||||
// Copyright 2019 TuxSH |
|
||||
// Licensed under GPLv2 or any later version |
|
||||
// Refer to the license.txt file included. |
|
||||
|
|
||||
#pragma once |
|
||||
|
|
||||
#include <array> |
|
||||
#include <iterator> |
|
||||
#include <list> |
|
||||
#include <utility> |
|
||||
|
|
||||
#include "common/bit_util.h" |
|
||||
#include "common/common_types.h" |
|
||||
|
|
||||
namespace Common { |
|
||||
|
|
||||
/** |
|
||||
* A MultiLevelQueue is a type of priority queue which has the following characteristics: |
|
||||
* - iteratable through each of its elements. |
|
||||
* - back can be obtained. |
|
||||
* - O(1) add, lookup (both front and back) |
|
||||
* - discrete priorities and a max of 64 priorities (limited domain) |
|
||||
* This type of priority queue is normaly used for managing threads within an scheduler |
|
||||
*/ |
|
||||
template <typename T, std::size_t Depth> |
|
||||
class MultiLevelQueue { |
|
||||
public: |
|
||||
using value_type = T; |
|
||||
using reference = value_type&; |
|
||||
using const_reference = const value_type&; |
|
||||
using pointer = value_type*; |
|
||||
using const_pointer = const value_type*; |
|
||||
|
|
||||
using difference_type = typename std::pointer_traits<pointer>::difference_type; |
|
||||
using size_type = std::size_t; |
|
||||
|
|
||||
template <bool is_constant> |
|
||||
class iterator_impl { |
|
||||
public: |
|
||||
using iterator_category = std::bidirectional_iterator_tag; |
|
||||
using value_type = T; |
|
||||
using pointer = std::conditional_t<is_constant, T*, const T*>; |
|
||||
using reference = std::conditional_t<is_constant, const T&, T&>; |
|
||||
using difference_type = typename std::pointer_traits<pointer>::difference_type; |
|
||||
|
|
||||
friend bool operator==(const iterator_impl& lhs, const iterator_impl& rhs) { |
|
||||
if (lhs.IsEnd() && rhs.IsEnd()) |
|
||||
return true; |
|
||||
return std::tie(lhs.current_priority, lhs.it) == std::tie(rhs.current_priority, rhs.it); |
|
||||
} |
|
||||
|
|
||||
friend bool operator!=(const iterator_impl& lhs, const iterator_impl& rhs) { |
|
||||
return !operator==(lhs, rhs); |
|
||||
} |
|
||||
|
|
||||
reference operator*() const { |
|
||||
return *it; |
|
||||
} |
|
||||
|
|
||||
pointer operator->() const { |
|
||||
return it.operator->(); |
|
||||
} |
|
||||
|
|
||||
iterator_impl& operator++() { |
|
||||
if (IsEnd()) { |
|
||||
return *this; |
|
||||
} |
|
||||
|
|
||||
++it; |
|
||||
|
|
||||
if (it == GetEndItForPrio()) { |
|
||||
u64 prios = mlq.used_priorities; |
|
||||
prios &= ~((1ULL << (current_priority + 1)) - 1); |
|
||||
if (prios == 0) { |
|
||||
current_priority = static_cast<u32>(mlq.depth()); |
|
||||
} else { |
|
||||
current_priority = CountTrailingZeroes64(prios); |
|
||||
it = GetBeginItForPrio(); |
|
||||
} |
|
||||
} |
|
||||
return *this; |
|
||||
} |
|
||||
|
|
||||
iterator_impl& operator--() { |
|
||||
if (IsEnd()) { |
|
||||
if (mlq.used_priorities != 0) { |
|
||||
current_priority = 63 - CountLeadingZeroes64(mlq.used_priorities); |
|
||||
it = GetEndItForPrio(); |
|
||||
--it; |
|
||||
} |
|
||||
} else if (it == GetBeginItForPrio()) { |
|
||||
u64 prios = mlq.used_priorities; |
|
||||
prios &= (1ULL << current_priority) - 1; |
|
||||
if (prios != 0) { |
|
||||
current_priority = CountTrailingZeroes64(prios); |
|
||||
it = GetEndItForPrio(); |
|
||||
--it; |
|
||||
} |
|
||||
} else { |
|
||||
--it; |
|
||||
} |
|
||||
return *this; |
|
||||
} |
|
||||
|
|
||||
iterator_impl operator++(int) { |
|
||||
const iterator_impl v{*this}; |
|
||||
++(*this); |
|
||||
return v; |
|
||||
} |
|
||||
|
|
||||
iterator_impl operator--(int) { |
|
||||
const iterator_impl v{*this}; |
|
||||
--(*this); |
|
||||
return v; |
|
||||
} |
|
||||
|
|
||||
// allow implicit const->non-const |
|
||||
iterator_impl(const iterator_impl<false>& other) |
|
||||
: mlq(other.mlq), it(other.it), current_priority(other.current_priority) {} |
|
||||
|
|
||||
iterator_impl(const iterator_impl<true>& other) |
|
||||
: mlq(other.mlq), it(other.it), current_priority(other.current_priority) {} |
|
||||
|
|
||||
iterator_impl& operator=(const iterator_impl<false>& other) { |
|
||||
mlq = other.mlq; |
|
||||
it = other.it; |
|
||||
current_priority = other.current_priority; |
|
||||
return *this; |
|
||||
} |
|
||||
|
|
||||
friend class iterator_impl<true>; |
|
||||
iterator_impl() = default; |
|
||||
|
|
||||
private: |
|
||||
friend class MultiLevelQueue; |
|
||||
using container_ref = |
|
||||
std::conditional_t<is_constant, const MultiLevelQueue&, MultiLevelQueue&>; |
|
||||
using list_iterator = std::conditional_t<is_constant, typename std::list<T>::const_iterator, |
|
||||
typename std::list<T>::iterator>; |
|
||||
|
|
||||
explicit iterator_impl(container_ref mlq, list_iterator it, u32 current_priority) |
|
||||
: mlq(mlq), it(it), current_priority(current_priority) {} |
|
||||
explicit iterator_impl(container_ref mlq, u32 current_priority) |
|
||||
: mlq(mlq), it(), current_priority(current_priority) {} |
|
||||
|
|
||||
bool IsEnd() const { |
|
||||
return current_priority == mlq.depth(); |
|
||||
} |
|
||||
|
|
||||
list_iterator GetBeginItForPrio() const { |
|
||||
return mlq.levels[current_priority].begin(); |
|
||||
} |
|
||||
|
|
||||
list_iterator GetEndItForPrio() const { |
|
||||
return mlq.levels[current_priority].end(); |
|
||||
} |
|
||||
|
|
||||
container_ref mlq; |
|
||||
list_iterator it; |
|
||||
u32 current_priority; |
|
||||
}; |
|
||||
|
|
||||
using iterator = iterator_impl<false>; |
|
||||
using const_iterator = iterator_impl<true>; |
|
||||
|
|
||||
void add(const T& element, u32 priority, bool send_back = true) { |
|
||||
if (send_back) |
|
||||
levels[priority].push_back(element); |
|
||||
else |
|
||||
levels[priority].push_front(element); |
|
||||
used_priorities |= 1ULL << priority; |
|
||||
} |
|
||||
|
|
||||
void remove(const T& element, u32 priority) { |
|
||||
auto it = ListIterateTo(levels[priority], element); |
|
||||
if (it == levels[priority].end()) |
|
||||
return; |
|
||||
levels[priority].erase(it); |
|
||||
if (levels[priority].empty()) { |
|
||||
used_priorities &= ~(1ULL << priority); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void adjust(const T& element, u32 old_priority, u32 new_priority, bool adjust_front = false) { |
|
||||
remove(element, old_priority); |
|
||||
add(element, new_priority, !adjust_front); |
|
||||
} |
|
||||
void adjust(const_iterator it, u32 old_priority, u32 new_priority, bool adjust_front = false) { |
|
||||
adjust(*it, old_priority, new_priority, adjust_front); |
|
||||
} |
|
||||
|
|
||||
void transfer_to_front(const T& element, u32 priority, MultiLevelQueue& other) { |
|
||||
ListSplice(other.levels[priority], other.levels[priority].begin(), levels[priority], |
|
||||
ListIterateTo(levels[priority], element)); |
|
||||
|
|
||||
other.used_priorities |= 1ULL << priority; |
|
||||
|
|
||||
if (levels[priority].empty()) { |
|
||||
used_priorities &= ~(1ULL << priority); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void transfer_to_front(const_iterator it, u32 priority, MultiLevelQueue& other) { |
|
||||
transfer_to_front(*it, priority, other); |
|
||||
} |
|
||||
|
|
||||
void transfer_to_back(const T& element, u32 priority, MultiLevelQueue& other) { |
|
||||
ListSplice(other.levels[priority], other.levels[priority].end(), levels[priority], |
|
||||
ListIterateTo(levels[priority], element)); |
|
||||
|
|
||||
other.used_priorities |= 1ULL << priority; |
|
||||
|
|
||||
if (levels[priority].empty()) { |
|
||||
used_priorities &= ~(1ULL << priority); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void transfer_to_back(const_iterator it, u32 priority, MultiLevelQueue& other) { |
|
||||
transfer_to_back(*it, priority, other); |
|
||||
} |
|
||||
|
|
||||
void yield(u32 priority, std::size_t n = 1) { |
|
||||
ListShiftForward(levels[priority], n); |
|
||||
} |
|
||||
|
|
||||
[[nodiscard]] std::size_t depth() const { |
|
||||
return Depth; |
|
||||
} |
|
||||
|
|
||||
[[nodiscard]] std::size_t size(u32 priority) const { |
|
||||
return levels[priority].size(); |
|
||||
} |
|
||||
|
|
||||
[[nodiscard]] std::size_t size() const { |
|
||||
u64 priorities = used_priorities; |
|
||||
std::size_t size = 0; |
|
||||
while (priorities != 0) { |
|
||||
const u64 current_priority = CountTrailingZeroes64(priorities); |
|
||||
size += levels[current_priority].size(); |
|
||||
priorities &= ~(1ULL << current_priority); |
|
||||
} |
|
||||
return size; |
|
||||
} |
|
||||
|
|
||||
[[nodiscard]] bool empty() const { |
|
||||
return used_priorities == 0; |
|
||||
} |
|
||||
|
|
||||
[[nodiscard]] bool empty(u32 priority) const { |
|
||||
return (used_priorities & (1ULL << priority)) == 0; |
|
||||
} |
|
||||
|
|
||||
[[nodiscard]] u32 highest_priority_set(u32 max_priority = 0) const { |
|
||||
const u64 priorities = |
|
||||
max_priority == 0 ? used_priorities : (used_priorities & ~((1ULL << max_priority) - 1)); |
|
||||
return priorities == 0 ? Depth : static_cast<u32>(CountTrailingZeroes64(priorities)); |
|
||||
} |
|
||||
|
|
||||
[[nodiscard]] u32 lowest_priority_set(u32 min_priority = Depth - 1) const { |
|
||||
const u64 priorities = min_priority >= Depth - 1 |
|
||||
? used_priorities |
|
||||
: (used_priorities & ((1ULL << (min_priority + 1)) - 1)); |
|
||||
return priorities == 0 ? Depth : 63 - CountLeadingZeroes64(priorities); |
|
||||
} |
|
||||
|
|
||||
[[nodiscard]] const_iterator cbegin(u32 max_prio = 0) const { |
|
||||
const u32 priority = highest_priority_set(max_prio); |
|
||||
return priority == Depth ? cend() |
|
||||
: const_iterator{*this, levels[priority].cbegin(), priority}; |
|
||||
} |
|
||||
[[nodiscard]] const_iterator begin(u32 max_prio = 0) const { |
|
||||
return cbegin(max_prio); |
|
||||
} |
|
||||
[[nodiscard]] iterator begin(u32 max_prio = 0) { |
|
||||
const u32 priority = highest_priority_set(max_prio); |
|
||||
return priority == Depth ? end() : iterator{*this, levels[priority].begin(), priority}; |
|
||||
} |
|
||||
|
|
||||
[[nodiscard]] const_iterator cend(u32 min_prio = Depth - 1) const { |
|
||||
return min_prio == Depth - 1 ? const_iterator{*this, Depth} : cbegin(min_prio + 1); |
|
||||
} |
|
||||
[[nodiscard]] const_iterator end(u32 min_prio = Depth - 1) const { |
|
||||
return cend(min_prio); |
|
||||
} |
|
||||
[[nodiscard]] iterator end(u32 min_prio = Depth - 1) { |
|
||||
return min_prio == Depth - 1 ? iterator{*this, Depth} : begin(min_prio + 1); |
|
||||
} |
|
||||
|
|
||||
[[nodiscard]] T& front(u32 max_priority = 0) { |
|
||||
const u32 priority = highest_priority_set(max_priority); |
|
||||
return levels[priority == Depth ? 0 : priority].front(); |
|
||||
} |
|
||||
[[nodiscard]] const T& front(u32 max_priority = 0) const { |
|
||||
const u32 priority = highest_priority_set(max_priority); |
|
||||
return levels[priority == Depth ? 0 : priority].front(); |
|
||||
} |
|
||||
|
|
||||
[[nodiscard]] T& back(u32 min_priority = Depth - 1) { |
|
||||
const u32 priority = lowest_priority_set(min_priority); // intended |
|
||||
return levels[priority == Depth ? 63 : priority].back(); |
|
||||
} |
|
||||
[[nodiscard]] const T& back(u32 min_priority = Depth - 1) const { |
|
||||
const u32 priority = lowest_priority_set(min_priority); // intended |
|
||||
return levels[priority == Depth ? 63 : priority].back(); |
|
||||
} |
|
||||
|
|
||||
void clear() { |
|
||||
used_priorities = 0; |
|
||||
for (std::size_t i = 0; i < Depth; i++) { |
|
||||
levels[i].clear(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private: |
|
||||
using const_list_iterator = typename std::list<T>::const_iterator; |
|
||||
|
|
||||
static void ListShiftForward(std::list<T>& list, const std::size_t shift = 1) { |
|
||||
if (shift >= list.size()) { |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
const auto begin_range = list.begin(); |
|
||||
const auto end_range = std::next(begin_range, shift); |
|
||||
list.splice(list.end(), list, begin_range, end_range); |
|
||||
} |
|
||||
|
|
||||
static void ListSplice(std::list<T>& in_list, const_list_iterator position, |
|
||||
std::list<T>& out_list, const_list_iterator element) { |
|
||||
in_list.splice(position, out_list, element); |
|
||||
} |
|
||||
|
|
||||
[[nodiscard]] static const_list_iterator ListIterateTo(const std::list<T>& list, |
|
||||
const T& element) { |
|
||||
auto it = list.cbegin(); |
|
||||
while (it != list.cend() && *it != element) { |
|
||||
++it; |
|
||||
} |
|
||||
return it; |
|
||||
} |
|
||||
|
|
||||
std::array<std::list<T>, Depth> levels; |
|
||||
u64 used_priorities = 0; |
|
||||
}; |
|
||||
|
|
||||
} // namespace Common |
|
||||
@ -0,0 +1,52 @@ |
|||||
|
// Copyright 2020 yuzu Emulator Project
|
||||
|
// Licensed under GPLv2 or any later version
|
||||
|
// Refer to the license.txt file included.
|
||||
|
|
||||
|
#include <mutex>
|
||||
|
|
||||
|
#include "common/assert.h"
|
||||
|
#include "core/core.h"
|
||||
|
#include "core/hle/kernel/global_scheduler_context.h"
|
||||
|
#include "core/hle/kernel/k_scheduler.h"
|
||||
|
#include "core/hle/kernel/kernel.h"
|
||||
|
|
||||
|
namespace Kernel { |
||||
|
|
||||
|
GlobalSchedulerContext::GlobalSchedulerContext(KernelCore& kernel) |
||||
|
: kernel{kernel}, scheduler_lock{kernel} {} |
||||
|
|
||||
|
GlobalSchedulerContext::~GlobalSchedulerContext() = default; |
||||
|
|
||||
|
void GlobalSchedulerContext::AddThread(std::shared_ptr<Thread> thread) { |
||||
|
std::scoped_lock lock{global_list_guard}; |
||||
|
thread_list.push_back(std::move(thread)); |
||||
|
} |
||||
|
|
||||
|
void GlobalSchedulerContext::RemoveThread(std::shared_ptr<Thread> thread) { |
||||
|
std::scoped_lock lock{global_list_guard}; |
||||
|
thread_list.erase(std::remove(thread_list.begin(), thread_list.end(), thread), |
||||
|
thread_list.end()); |
||||
|
} |
||||
|
|
||||
|
void GlobalSchedulerContext::PreemptThreads() { |
||||
|
// The priority levels at which the global scheduler preempts threads every 10 ms. They are
|
||||
|
// ordered from Core 0 to Core 3.
|
||||
|
static constexpr std::array<u32, Core::Hardware::NUM_CPU_CORES> preemption_priorities{ |
||||
|
59, |
||||
|
59, |
||||
|
59, |
||||
|
63, |
||||
|
}; |
||||
|
|
||||
|
ASSERT(IsLocked()); |
||||
|
for (u32 core_id = 0; core_id < Core::Hardware::NUM_CPU_CORES; core_id++) { |
||||
|
const u32 priority = preemption_priorities[core_id]; |
||||
|
kernel.Scheduler(core_id).RotateScheduledQueue(core_id, priority); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
bool GlobalSchedulerContext::IsLocked() const { |
||||
|
return scheduler_lock.IsLockedByCurrentThread(); |
||||
|
} |
||||
|
|
||||
|
} // namespace Kernel
|
||||
@ -0,0 +1,81 @@ |
|||||
|
// Copyright 2020 yuzu Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <atomic> |
||||
|
#include <vector> |
||||
|
|
||||
|
#include "common/common_types.h" |
||||
|
#include "common/spin_lock.h" |
||||
|
#include "core/hardware_properties.h" |
||||
|
#include "core/hle/kernel/k_priority_queue.h" |
||||
|
#include "core/hle/kernel/k_scheduler_lock.h" |
||||
|
#include "core/hle/kernel/thread.h" |
||||
|
|
||||
|
namespace Kernel { |
||||
|
|
||||
|
class KernelCore; |
||||
|
class SchedulerLock; |
||||
|
|
||||
|
using KSchedulerPriorityQueue = |
||||
|
KPriorityQueue<Thread, Core::Hardware::NUM_CPU_CORES, THREADPRIO_LOWEST, THREADPRIO_HIGHEST>; |
||||
|
constexpr s32 HighestCoreMigrationAllowedPriority = 2; |
||||
|
|
||||
|
class GlobalSchedulerContext final { |
||||
|
friend class KScheduler; |
||||
|
|
||||
|
public: |
||||
|
using LockType = KAbstractSchedulerLock<KScheduler>; |
||||
|
|
||||
|
explicit GlobalSchedulerContext(KernelCore& kernel); |
||||
|
~GlobalSchedulerContext(); |
||||
|
|
||||
|
/// Adds a new thread to the scheduler |
||||
|
void AddThread(std::shared_ptr<Thread> thread); |
||||
|
|
||||
|
/// Removes a thread from the scheduler |
||||
|
void RemoveThread(std::shared_ptr<Thread> thread); |
||||
|
|
||||
|
/// Returns a list of all threads managed by the scheduler |
||||
|
[[nodiscard]] const std::vector<std::shared_ptr<Thread>>& GetThreadList() const { |
||||
|
return thread_list; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Rotates the scheduling queues of threads at a preemption priority and then does |
||||
|
* some core rebalancing. Preemption priorities can be found in the array |
||||
|
* 'preemption_priorities'. |
||||
|
* |
||||
|
* @note This operation happens every 10ms. |
||||
|
*/ |
||||
|
void PreemptThreads(); |
||||
|
|
||||
|
/// Returns true if the global scheduler lock is acquired |
||||
|
bool IsLocked() const; |
||||
|
|
||||
|
[[nodiscard]] LockType& SchedulerLock() { |
||||
|
return scheduler_lock; |
||||
|
} |
||||
|
|
||||
|
[[nodiscard]] const LockType& SchedulerLock() const { |
||||
|
return scheduler_lock; |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
friend class KScopedSchedulerLock; |
||||
|
friend class KScopedSchedulerLockAndSleep; |
||||
|
|
||||
|
KernelCore& kernel; |
||||
|
|
||||
|
std::atomic_bool scheduler_update_needed{}; |
||||
|
KSchedulerPriorityQueue priority_queue; |
||||
|
LockType scheduler_lock; |
||||
|
|
||||
|
/// Lists all thread ids that aren't deleted/etc. |
||||
|
std::vector<std::shared_ptr<Thread>> thread_list; |
||||
|
Common::SpinLock global_list_guard{}; |
||||
|
}; |
||||
|
|
||||
|
} // namespace Kernel |
||||
@ -0,0 +1,58 @@ |
|||||
|
// Copyright 2020 yuzu Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
// This file references various implementation details from Atmosphere, an open-source firmware for |
||||
|
// the Nintendo Switch. Copyright 2018-2020 Atmosphere-NX. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include "common/assert.h" |
||||
|
#include "common/common_types.h" |
||||
|
#include "core/hardware_properties.h" |
||||
|
|
||||
|
namespace Kernel { |
||||
|
|
||||
|
class KAffinityMask { |
||||
|
public: |
||||
|
constexpr KAffinityMask() = default; |
||||
|
|
||||
|
[[nodiscard]] constexpr u64 GetAffinityMask() const { |
||||
|
return this->mask; |
||||
|
} |
||||
|
|
||||
|
constexpr void SetAffinityMask(u64 new_mask) { |
||||
|
ASSERT((new_mask & ~AllowedAffinityMask) == 0); |
||||
|
this->mask = new_mask; |
||||
|
} |
||||
|
|
||||
|
[[nodiscard]] constexpr bool GetAffinity(s32 core) const { |
||||
|
return this->mask & GetCoreBit(core); |
||||
|
} |
||||
|
|
||||
|
constexpr void SetAffinity(s32 core, bool set) { |
||||
|
ASSERT(0 <= core && core < static_cast<s32>(Core::Hardware::NUM_CPU_CORES)); |
||||
|
|
||||
|
if (set) { |
||||
|
this->mask |= GetCoreBit(core); |
||||
|
} else { |
||||
|
this->mask &= ~GetCoreBit(core); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constexpr void SetAll() { |
||||
|
this->mask = AllowedAffinityMask; |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
[[nodiscard]] static constexpr u64 GetCoreBit(s32 core) { |
||||
|
ASSERT(0 <= core && core < static_cast<s32>(Core::Hardware::NUM_CPU_CORES)); |
||||
|
return (1ULL << core); |
||||
|
} |
||||
|
|
||||
|
static constexpr u64 AllowedAffinityMask = (1ULL << Core::Hardware::NUM_CPU_CORES) - 1; |
||||
|
|
||||
|
u64 mask{}; |
||||
|
}; |
||||
|
|
||||
|
} // namespace Kernel |
||||
@ -0,0 +1,449 @@ |
|||||
|
// Copyright 2020 yuzu Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
// This file references various implementation details from Atmosphere, an open-source firmware for |
||||
|
// the Nintendo Switch. Copyright 2018-2020 Atmosphere-NX. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <array> |
||||
|
|
||||
|
#include "common/assert.h" |
||||
|
#include "common/bit_set.h" |
||||
|
#include "common/bit_util.h" |
||||
|
#include "common/common_types.h" |
||||
|
|
||||
|
namespace Kernel { |
||||
|
|
||||
|
class Thread; |
||||
|
|
||||
|
template <typename T> |
||||
|
concept KPriorityQueueAffinityMask = !std::is_reference_v<T> && requires(T & t) { |
||||
|
{ t.GetAffinityMask() } |
||||
|
->std::convertible_to<u64>; |
||||
|
{t.SetAffinityMask(std::declval<u64>())}; |
||||
|
|
||||
|
{ t.GetAffinity(std::declval<int32_t>()) } |
||||
|
->std::same_as<bool>; |
||||
|
{t.SetAffinity(std::declval<int32_t>(), std::declval<bool>())}; |
||||
|
{t.SetAll()}; |
||||
|
}; |
||||
|
|
||||
|
template <typename T> |
||||
|
concept KPriorityQueueMember = !std::is_reference_v<T> && requires(T & t) { |
||||
|
{typename T::QueueEntry()}; |
||||
|
{(typename T::QueueEntry()).Initialize()}; |
||||
|
{(typename T::QueueEntry()).SetPrev(std::addressof(t))}; |
||||
|
{(typename T::QueueEntry()).SetNext(std::addressof(t))}; |
||||
|
{ (typename T::QueueEntry()).GetNext() } |
||||
|
->std::same_as<T*>; |
||||
|
{ (typename T::QueueEntry()).GetPrev() } |
||||
|
->std::same_as<T*>; |
||||
|
{ t.GetPriorityQueueEntry(std::declval<s32>()) } |
||||
|
->std::same_as<typename T::QueueEntry&>; |
||||
|
|
||||
|
{t.GetAffinityMask()}; |
||||
|
{ typename std::remove_cvref<decltype(t.GetAffinityMask())>::type() } |
||||
|
->KPriorityQueueAffinityMask; |
||||
|
|
||||
|
{ t.GetActiveCore() } |
||||
|
->std::convertible_to<s32>; |
||||
|
{ t.GetPriority() } |
||||
|
->std::convertible_to<s32>; |
||||
|
}; |
||||
|
|
||||
|
template <typename Member, size_t _NumCores, int LowestPriority, int HighestPriority> |
||||
|
requires KPriorityQueueMember<Member> class KPriorityQueue { |
||||
|
public: |
||||
|
using AffinityMaskType = typename std::remove_cv_t< |
||||
|
typename std::remove_reference<decltype(std::declval<Member>().GetAffinityMask())>::type>; |
||||
|
|
||||
|
static_assert(LowestPriority >= 0); |
||||
|
static_assert(HighestPriority >= 0); |
||||
|
static_assert(LowestPriority >= HighestPriority); |
||||
|
static constexpr size_t NumPriority = LowestPriority - HighestPriority + 1; |
||||
|
static constexpr size_t NumCores = _NumCores; |
||||
|
|
||||
|
static constexpr bool IsValidCore(s32 core) { |
||||
|
return 0 <= core && core < static_cast<s32>(NumCores); |
||||
|
} |
||||
|
|
||||
|
static constexpr bool IsValidPriority(s32 priority) { |
||||
|
return HighestPriority <= priority && priority <= LowestPriority + 1; |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
using Entry = typename Member::QueueEntry; |
||||
|
|
||||
|
public: |
||||
|
class KPerCoreQueue { |
||||
|
private: |
||||
|
std::array<Entry, NumCores> root{}; |
||||
|
|
||||
|
public: |
||||
|
constexpr KPerCoreQueue() { |
||||
|
for (auto& per_core_root : root) { |
||||
|
per_core_root.Initialize(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constexpr bool PushBack(s32 core, Member* member) { |
||||
|
// Get the entry associated with the member. |
||||
|
Entry& member_entry = member->GetPriorityQueueEntry(core); |
||||
|
|
||||
|
// Get the entry associated with the end of the queue. |
||||
|
Member* tail = this->root[core].GetPrev(); |
||||
|
Entry& tail_entry = |
||||
|
(tail != nullptr) ? tail->GetPriorityQueueEntry(core) : this->root[core]; |
||||
|
|
||||
|
// Link the entries. |
||||
|
member_entry.SetPrev(tail); |
||||
|
member_entry.SetNext(nullptr); |
||||
|
tail_entry.SetNext(member); |
||||
|
this->root[core].SetPrev(member); |
||||
|
|
||||
|
return tail == nullptr; |
||||
|
} |
||||
|
|
||||
|
constexpr bool PushFront(s32 core, Member* member) { |
||||
|
// Get the entry associated with the member. |
||||
|
Entry& member_entry = member->GetPriorityQueueEntry(core); |
||||
|
|
||||
|
// Get the entry associated with the front of the queue. |
||||
|
Member* head = this->root[core].GetNext(); |
||||
|
Entry& head_entry = |
||||
|
(head != nullptr) ? head->GetPriorityQueueEntry(core) : this->root[core]; |
||||
|
|
||||
|
// Link the entries. |
||||
|
member_entry.SetPrev(nullptr); |
||||
|
member_entry.SetNext(head); |
||||
|
head_entry.SetPrev(member); |
||||
|
this->root[core].SetNext(member); |
||||
|
|
||||
|
return (head == nullptr); |
||||
|
} |
||||
|
|
||||
|
constexpr bool Remove(s32 core, Member* member) { |
||||
|
// Get the entry associated with the member. |
||||
|
Entry& member_entry = member->GetPriorityQueueEntry(core); |
||||
|
|
||||
|
// Get the entries associated with next and prev. |
||||
|
Member* prev = member_entry.GetPrev(); |
||||
|
Member* next = member_entry.GetNext(); |
||||
|
Entry& prev_entry = |
||||
|
(prev != nullptr) ? prev->GetPriorityQueueEntry(core) : this->root[core]; |
||||
|
Entry& next_entry = |
||||
|
(next != nullptr) ? next->GetPriorityQueueEntry(core) : this->root[core]; |
||||
|
|
||||
|
// Unlink. |
||||
|
prev_entry.SetNext(next); |
||||
|
next_entry.SetPrev(prev); |
||||
|
|
||||
|
return (this->GetFront(core) == nullptr); |
||||
|
} |
||||
|
|
||||
|
constexpr Member* GetFront(s32 core) const { |
||||
|
return this->root[core].GetNext(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
class KPriorityQueueImpl { |
||||
|
public: |
||||
|
constexpr KPriorityQueueImpl() = default; |
||||
|
|
||||
|
constexpr void PushBack(s32 priority, s32 core, Member* member) { |
||||
|
ASSERT(IsValidCore(core)); |
||||
|
ASSERT(IsValidPriority(priority)); |
||||
|
|
||||
|
if (priority > LowestPriority) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (this->queues[priority].PushBack(core, member)) { |
||||
|
this->available_priorities[core].SetBit(priority); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constexpr void PushFront(s32 priority, s32 core, Member* member) { |
||||
|
ASSERT(IsValidCore(core)); |
||||
|
ASSERT(IsValidPriority(priority)); |
||||
|
|
||||
|
if (priority > LowestPriority) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (this->queues[priority].PushFront(core, member)) { |
||||
|
this->available_priorities[core].SetBit(priority); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constexpr void Remove(s32 priority, s32 core, Member* member) { |
||||
|
ASSERT(IsValidCore(core)); |
||||
|
ASSERT(IsValidPriority(priority)); |
||||
|
|
||||
|
if (priority > LowestPriority) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (this->queues[priority].Remove(core, member)) { |
||||
|
this->available_priorities[core].ClearBit(priority); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constexpr Member* GetFront(s32 core) const { |
||||
|
ASSERT(IsValidCore(core)); |
||||
|
|
||||
|
const s32 priority = |
||||
|
static_cast<s32>(this->available_priorities[core].CountLeadingZero()); |
||||
|
if (priority <= LowestPriority) { |
||||
|
return this->queues[priority].GetFront(core); |
||||
|
} else { |
||||
|
return nullptr; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constexpr Member* GetFront(s32 priority, s32 core) const { |
||||
|
ASSERT(IsValidCore(core)); |
||||
|
ASSERT(IsValidPriority(priority)); |
||||
|
|
||||
|
if (priority <= LowestPriority) { |
||||
|
return this->queues[priority].GetFront(core); |
||||
|
} else { |
||||
|
return nullptr; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constexpr Member* GetNext(s32 core, const Member* member) const { |
||||
|
ASSERT(IsValidCore(core)); |
||||
|
|
||||
|
Member* next = member->GetPriorityQueueEntry(core).GetNext(); |
||||
|
if (next == nullptr) { |
||||
|
const s32 priority = static_cast<s32>( |
||||
|
this->available_priorities[core].GetNextSet(member->GetPriority())); |
||||
|
if (priority <= LowestPriority) { |
||||
|
next = this->queues[priority].GetFront(core); |
||||
|
} |
||||
|
} |
||||
|
return next; |
||||
|
} |
||||
|
|
||||
|
constexpr void MoveToFront(s32 priority, s32 core, Member* member) { |
||||
|
ASSERT(IsValidCore(core)); |
||||
|
ASSERT(IsValidPriority(priority)); |
||||
|
|
||||
|
if (priority <= LowestPriority) { |
||||
|
this->queues[priority].Remove(core, member); |
||||
|
this->queues[priority].PushFront(core, member); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constexpr Member* MoveToBack(s32 priority, s32 core, Member* member) { |
||||
|
ASSERT(IsValidCore(core)); |
||||
|
ASSERT(IsValidPriority(priority)); |
||||
|
|
||||
|
if (priority <= LowestPriority) { |
||||
|
this->queues[priority].Remove(core, member); |
||||
|
this->queues[priority].PushBack(core, member); |
||||
|
return this->queues[priority].GetFront(core); |
||||
|
} else { |
||||
|
return nullptr; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
std::array<KPerCoreQueue, NumPriority> queues{}; |
||||
|
std::array<Common::BitSet64<NumPriority>, NumCores> available_priorities{}; |
||||
|
}; |
||||
|
|
||||
|
private: |
||||
|
KPriorityQueueImpl scheduled_queue; |
||||
|
KPriorityQueueImpl suggested_queue; |
||||
|
|
||||
|
private: |
||||
|
constexpr void ClearAffinityBit(u64& affinity, s32 core) { |
||||
|
affinity &= ~(u64(1) << core); |
||||
|
} |
||||
|
|
||||
|
constexpr s32 GetNextCore(u64& affinity) { |
||||
|
const s32 core = Common::CountTrailingZeroes64(affinity); |
||||
|
ClearAffinityBit(affinity, core); |
||||
|
return core; |
||||
|
} |
||||
|
|
||||
|
constexpr void PushBack(s32 priority, Member* member) { |
||||
|
ASSERT(IsValidPriority(priority)); |
||||
|
|
||||
|
// Push onto the scheduled queue for its core, if we can. |
||||
|
u64 affinity = member->GetAffinityMask().GetAffinityMask(); |
||||
|
if (const s32 core = member->GetActiveCore(); core >= 0) { |
||||
|
this->scheduled_queue.PushBack(priority, core, member); |
||||
|
ClearAffinityBit(affinity, core); |
||||
|
} |
||||
|
|
||||
|
// And suggest the thread for all other cores. |
||||
|
while (affinity) { |
||||
|
this->suggested_queue.PushBack(priority, GetNextCore(affinity), member); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constexpr void PushFront(s32 priority, Member* member) { |
||||
|
ASSERT(IsValidPriority(priority)); |
||||
|
|
||||
|
// Push onto the scheduled queue for its core, if we can. |
||||
|
u64 affinity = member->GetAffinityMask().GetAffinityMask(); |
||||
|
if (const s32 core = member->GetActiveCore(); core >= 0) { |
||||
|
this->scheduled_queue.PushFront(priority, core, member); |
||||
|
ClearAffinityBit(affinity, core); |
||||
|
} |
||||
|
|
||||
|
// And suggest the thread for all other cores. |
||||
|
// Note: Nintendo pushes onto the back of the suggested queue, not the front. |
||||
|
while (affinity) { |
||||
|
this->suggested_queue.PushBack(priority, GetNextCore(affinity), member); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constexpr void Remove(s32 priority, Member* member) { |
||||
|
ASSERT(IsValidPriority(priority)); |
||||
|
|
||||
|
// Remove from the scheduled queue for its core. |
||||
|
u64 affinity = member->GetAffinityMask().GetAffinityMask(); |
||||
|
if (const s32 core = member->GetActiveCore(); core >= 0) { |
||||
|
this->scheduled_queue.Remove(priority, core, member); |
||||
|
ClearAffinityBit(affinity, core); |
||||
|
} |
||||
|
|
||||
|
// Remove from the suggested queue for all other cores. |
||||
|
while (affinity) { |
||||
|
this->suggested_queue.Remove(priority, GetNextCore(affinity), member); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public: |
||||
|
constexpr KPriorityQueue() = default; |
||||
|
|
||||
|
// Getters. |
||||
|
constexpr Member* GetScheduledFront(s32 core) const { |
||||
|
return this->scheduled_queue.GetFront(core); |
||||
|
} |
||||
|
|
||||
|
constexpr Member* GetScheduledFront(s32 core, s32 priority) const { |
||||
|
return this->scheduled_queue.GetFront(priority, core); |
||||
|
} |
||||
|
|
||||
|
constexpr Member* GetSuggestedFront(s32 core) const { |
||||
|
return this->suggested_queue.GetFront(core); |
||||
|
} |
||||
|
|
||||
|
constexpr Member* GetSuggestedFront(s32 core, s32 priority) const { |
||||
|
return this->suggested_queue.GetFront(priority, core); |
||||
|
} |
||||
|
|
||||
|
constexpr Member* GetScheduledNext(s32 core, const Member* member) const { |
||||
|
return this->scheduled_queue.GetNext(core, member); |
||||
|
} |
||||
|
|
||||
|
constexpr Member* GetSuggestedNext(s32 core, const Member* member) const { |
||||
|
return this->suggested_queue.GetNext(core, member); |
||||
|
} |
||||
|
|
||||
|
constexpr Member* GetSamePriorityNext(s32 core, const Member* member) const { |
||||
|
return member->GetPriorityQueueEntry(core).GetNext(); |
||||
|
} |
||||
|
|
||||
|
// Mutators. |
||||
|
constexpr void PushBack(Member* member) { |
||||
|
this->PushBack(member->GetPriority(), member); |
||||
|
} |
||||
|
|
||||
|
constexpr void Remove(Member* member) { |
||||
|
this->Remove(member->GetPriority(), member); |
||||
|
} |
||||
|
|
||||
|
constexpr void MoveToScheduledFront(Member* member) { |
||||
|
this->scheduled_queue.MoveToFront(member->GetPriority(), member->GetActiveCore(), member); |
||||
|
} |
||||
|
|
||||
|
constexpr Thread* MoveToScheduledBack(Member* member) { |
||||
|
return this->scheduled_queue.MoveToBack(member->GetPriority(), member->GetActiveCore(), |
||||
|
member); |
||||
|
} |
||||
|
|
||||
|
// First class fancy operations. |
||||
|
constexpr void ChangePriority(s32 prev_priority, bool is_running, Member* member) { |
||||
|
ASSERT(IsValidPriority(prev_priority)); |
||||
|
|
||||
|
// Remove the member from the queues. |
||||
|
const s32 new_priority = member->GetPriority(); |
||||
|
this->Remove(prev_priority, member); |
||||
|
|
||||
|
// And enqueue. If the member is running, we want to keep it running. |
||||
|
if (is_running) { |
||||
|
this->PushFront(new_priority, member); |
||||
|
} else { |
||||
|
this->PushBack(new_priority, member); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constexpr void ChangeAffinityMask(s32 prev_core, const AffinityMaskType& prev_affinity, |
||||
|
Member* member) { |
||||
|
// Get the new information. |
||||
|
const s32 priority = member->GetPriority(); |
||||
|
const AffinityMaskType& new_affinity = member->GetAffinityMask(); |
||||
|
const s32 new_core = member->GetActiveCore(); |
||||
|
|
||||
|
// Remove the member from all queues it was in before. |
||||
|
for (s32 core = 0; core < static_cast<s32>(NumCores); core++) { |
||||
|
if (prev_affinity.GetAffinity(core)) { |
||||
|
if (core == prev_core) { |
||||
|
this->scheduled_queue.Remove(priority, core, member); |
||||
|
} else { |
||||
|
this->suggested_queue.Remove(priority, core, member); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// And add the member to all queues it should be in now. |
||||
|
for (s32 core = 0; core < static_cast<s32>(NumCores); core++) { |
||||
|
if (new_affinity.GetAffinity(core)) { |
||||
|
if (core == new_core) { |
||||
|
this->scheduled_queue.PushBack(priority, core, member); |
||||
|
} else { |
||||
|
this->suggested_queue.PushBack(priority, core, member); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constexpr void ChangeCore(s32 prev_core, Member* member, bool to_front = false) { |
||||
|
// Get the new information. |
||||
|
const s32 new_core = member->GetActiveCore(); |
||||
|
const s32 priority = member->GetPriority(); |
||||
|
|
||||
|
// We don't need to do anything if the core is the same. |
||||
|
if (prev_core != new_core) { |
||||
|
// Remove from the scheduled queue for the previous core. |
||||
|
if (prev_core >= 0) { |
||||
|
this->scheduled_queue.Remove(priority, prev_core, member); |
||||
|
} |
||||
|
|
||||
|
// Remove from the suggested queue and add to the scheduled queue for the new core. |
||||
|
if (new_core >= 0) { |
||||
|
this->suggested_queue.Remove(priority, new_core, member); |
||||
|
if (to_front) { |
||||
|
this->scheduled_queue.PushFront(priority, new_core, member); |
||||
|
} else { |
||||
|
this->scheduled_queue.PushBack(priority, new_core, member); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Add to the suggested queue for the previous core. |
||||
|
if (prev_core >= 0) { |
||||
|
this->suggested_queue.PushBack(priority, prev_core, member); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
} // namespace Kernel |
||||
@ -0,0 +1,784 @@ |
|||||
|
// Copyright 2020 yuzu Emulator Project
|
||||
|
// Licensed under GPLv2 or any later version
|
||||
|
// Refer to the license.txt file included.
|
||||
|
|
||||
|
// This file references various implementation details from Atmosphere, an open-source firmware for
|
||||
|
// the Nintendo Switch. Copyright 2018-2020 Atmosphere-NX.
|
||||
|
|
||||
|
#include "common/assert.h"
|
||||
|
#include "common/bit_util.h"
|
||||
|
#include "common/fiber.h"
|
||||
|
#include "common/logging/log.h"
|
||||
|
#include "core/arm/arm_interface.h"
|
||||
|
#include "core/core.h"
|
||||
|
#include "core/core_timing.h"
|
||||
|
#include "core/cpu_manager.h"
|
||||
|
#include "core/hle/kernel/k_scheduler.h"
|
||||
|
#include "core/hle/kernel/k_scoped_scheduler_lock_and_sleep.h"
|
||||
|
#include "core/hle/kernel/kernel.h"
|
||||
|
#include "core/hle/kernel/physical_core.h"
|
||||
|
#include "core/hle/kernel/process.h"
|
||||
|
#include "core/hle/kernel/thread.h"
|
||||
|
#include "core/hle/kernel/time_manager.h"
|
||||
|
|
||||
|
namespace Kernel { |
||||
|
|
||||
|
static void IncrementScheduledCount(Kernel::Thread* thread) { |
||||
|
if (auto process = thread->GetOwnerProcess(); process) { |
||||
|
process->IncrementScheduledCount(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KScheduler::RescheduleCores(KernelCore& kernel, u64 cores_pending_reschedule, |
||||
|
Core::EmuThreadHandle global_thread) { |
||||
|
u32 current_core = global_thread.host_handle; |
||||
|
bool must_context_switch = global_thread.guest_handle != InvalidHandle && |
||||
|
(current_core < Core::Hardware::NUM_CPU_CORES); |
||||
|
|
||||
|
while (cores_pending_reschedule != 0) { |
||||
|
u32 core = Common::CountTrailingZeroes64(cores_pending_reschedule); |
||||
|
ASSERT(core < Core::Hardware::NUM_CPU_CORES); |
||||
|
if (!must_context_switch || core != current_core) { |
||||
|
auto& phys_core = kernel.PhysicalCore(core); |
||||
|
phys_core.Interrupt(); |
||||
|
} else { |
||||
|
must_context_switch = true; |
||||
|
} |
||||
|
cores_pending_reschedule &= ~(1ULL << core); |
||||
|
} |
||||
|
if (must_context_switch) { |
||||
|
auto core_scheduler = kernel.CurrentScheduler(); |
||||
|
kernel.ExitSVCProfile(); |
||||
|
core_scheduler->RescheduleCurrentCore(); |
||||
|
kernel.EnterSVCProfile(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
u64 KScheduler::UpdateHighestPriorityThread(Thread* highest_thread) { |
||||
|
std::scoped_lock lock{guard}; |
||||
|
if (Thread* prev_highest_thread = this->state.highest_priority_thread; |
||||
|
prev_highest_thread != highest_thread) { |
||||
|
if (prev_highest_thread != nullptr) { |
||||
|
IncrementScheduledCount(prev_highest_thread); |
||||
|
prev_highest_thread->SetLastScheduledTick(system.CoreTiming().GetCPUTicks()); |
||||
|
} |
||||
|
if (this->state.should_count_idle) { |
||||
|
if (highest_thread != nullptr) { |
||||
|
// if (Process* process = highest_thread->GetOwnerProcess(); process != nullptr) {
|
||||
|
// process->SetRunningThread(this->core_id, highest_thread,
|
||||
|
// this->state.idle_count);
|
||||
|
//}
|
||||
|
} else { |
||||
|
this->state.idle_count++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this->state.highest_priority_thread = highest_thread; |
||||
|
this->state.needs_scheduling = true; |
||||
|
return (1ULL << this->core_id); |
||||
|
} else { |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore& kernel) { |
||||
|
ASSERT(kernel.GlobalSchedulerContext().IsLocked()); |
||||
|
|
||||
|
// Clear that we need to update.
|
||||
|
ClearSchedulerUpdateNeeded(kernel); |
||||
|
|
||||
|
u64 cores_needing_scheduling = 0, idle_cores = 0; |
||||
|
Thread* top_threads[Core::Hardware::NUM_CPU_CORES]; |
||||
|
auto& priority_queue = GetPriorityQueue(kernel); |
||||
|
|
||||
|
/// We want to go over all cores, finding the highest priority thread and determining if
|
||||
|
/// scheduling is needed for that core.
|
||||
|
for (size_t core_id = 0; core_id < Core::Hardware::NUM_CPU_CORES; core_id++) { |
||||
|
Thread* top_thread = priority_queue.GetScheduledFront(static_cast<s32>(core_id)); |
||||
|
if (top_thread != nullptr) { |
||||
|
// If the thread has no waiters, we need to check if the process has a thread pinned.
|
||||
|
// TODO(bunnei): Implement thread pinning
|
||||
|
} else { |
||||
|
idle_cores |= (1ULL << core_id); |
||||
|
} |
||||
|
|
||||
|
top_threads[core_id] = top_thread; |
||||
|
cores_needing_scheduling |= |
||||
|
kernel.Scheduler(core_id).UpdateHighestPriorityThread(top_threads[core_id]); |
||||
|
} |
||||
|
|
||||
|
// Idle cores are bad. We're going to try to migrate threads to each idle core in turn.
|
||||
|
while (idle_cores != 0) { |
||||
|
u32 core_id = Common::CountTrailingZeroes64(idle_cores); |
||||
|
if (Thread* suggested = priority_queue.GetSuggestedFront(core_id); suggested != nullptr) { |
||||
|
s32 migration_candidates[Core::Hardware::NUM_CPU_CORES]; |
||||
|
size_t num_candidates = 0; |
||||
|
|
||||
|
// While we have a suggested thread, try to migrate it!
|
||||
|
while (suggested != nullptr) { |
||||
|
// Check if the suggested thread is the top thread on its core.
|
||||
|
const s32 suggested_core = suggested->GetActiveCore(); |
||||
|
if (Thread* top_thread = |
||||
|
(suggested_core >= 0) ? top_threads[suggested_core] : nullptr; |
||||
|
top_thread != suggested) { |
||||
|
// Make sure we're not dealing with threads too high priority for migration.
|
||||
|
if (top_thread != nullptr && |
||||
|
top_thread->GetPriority() < HighestCoreMigrationAllowedPriority) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// The suggested thread isn't bound to its core, so we can migrate it!
|
||||
|
suggested->SetActiveCore(core_id); |
||||
|
priority_queue.ChangeCore(suggested_core, suggested); |
||||
|
|
||||
|
top_threads[core_id] = suggested; |
||||
|
cores_needing_scheduling |= |
||||
|
kernel.Scheduler(core_id).UpdateHighestPriorityThread(top_threads[core_id]); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// Note this core as a candidate for migration.
|
||||
|
ASSERT(num_candidates < Core::Hardware::NUM_CPU_CORES); |
||||
|
migration_candidates[num_candidates++] = suggested_core; |
||||
|
suggested = priority_queue.GetSuggestedNext(core_id, suggested); |
||||
|
} |
||||
|
|
||||
|
// If suggested is nullptr, we failed to migrate a specific thread. So let's try all our
|
||||
|
// candidate cores' top threads.
|
||||
|
if (suggested == nullptr) { |
||||
|
for (size_t i = 0; i < num_candidates; i++) { |
||||
|
// Check if there's some other thread that can run on the candidate core.
|
||||
|
const s32 candidate_core = migration_candidates[i]; |
||||
|
suggested = top_threads[candidate_core]; |
||||
|
if (Thread* next_on_candidate_core = |
||||
|
priority_queue.GetScheduledNext(candidate_core, suggested); |
||||
|
next_on_candidate_core != nullptr) { |
||||
|
// The candidate core can run some other thread! We'll migrate its current
|
||||
|
// top thread to us.
|
||||
|
top_threads[candidate_core] = next_on_candidate_core; |
||||
|
cores_needing_scheduling |= |
||||
|
kernel.Scheduler(candidate_core) |
||||
|
.UpdateHighestPriorityThread(top_threads[candidate_core]); |
||||
|
|
||||
|
// Perform the migration.
|
||||
|
suggested->SetActiveCore(core_id); |
||||
|
priority_queue.ChangeCore(candidate_core, suggested); |
||||
|
|
||||
|
top_threads[core_id] = suggested; |
||||
|
cores_needing_scheduling |= |
||||
|
kernel.Scheduler(core_id).UpdateHighestPriorityThread( |
||||
|
top_threads[core_id]); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
idle_cores &= ~(1ULL << core_id); |
||||
|
} |
||||
|
|
||||
|
return cores_needing_scheduling; |
||||
|
} |
||||
|
|
||||
|
void KScheduler::OnThreadStateChanged(KernelCore& kernel, Thread* thread, u32 old_state) { |
||||
|
ASSERT(kernel.GlobalSchedulerContext().IsLocked()); |
||||
|
|
||||
|
// Check if the state has changed, because if it hasn't there's nothing to do.
|
||||
|
const auto cur_state = thread->scheduling_state; |
||||
|
if (cur_state == old_state) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Update the priority queues.
|
||||
|
if (old_state == static_cast<u32>(ThreadSchedStatus::Runnable)) { |
||||
|
// If we were previously runnable, then we're not runnable now, and we should remove.
|
||||
|
GetPriorityQueue(kernel).Remove(thread); |
||||
|
IncrementScheduledCount(thread); |
||||
|
SetSchedulerUpdateNeeded(kernel); |
||||
|
} else if (cur_state == static_cast<u32>(ThreadSchedStatus::Runnable)) { |
||||
|
// If we're now runnable, then we weren't previously, and we should add.
|
||||
|
GetPriorityQueue(kernel).PushBack(thread); |
||||
|
IncrementScheduledCount(thread); |
||||
|
SetSchedulerUpdateNeeded(kernel); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KScheduler::OnThreadPriorityChanged(KernelCore& kernel, Thread* thread, Thread* current_thread, |
||||
|
u32 old_priority) { |
||||
|
|
||||
|
ASSERT(kernel.GlobalSchedulerContext().IsLocked()); |
||||
|
|
||||
|
// If the thread is runnable, we want to change its priority in the queue.
|
||||
|
if (thread->scheduling_state == static_cast<u32>(ThreadSchedStatus::Runnable)) { |
||||
|
GetPriorityQueue(kernel).ChangePriority( |
||||
|
old_priority, thread == kernel.CurrentScheduler()->GetCurrentThread(), thread); |
||||
|
IncrementScheduledCount(thread); |
||||
|
SetSchedulerUpdateNeeded(kernel); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KScheduler::OnThreadAffinityMaskChanged(KernelCore& kernel, Thread* thread, |
||||
|
const KAffinityMask& old_affinity, s32 old_core) { |
||||
|
ASSERT(kernel.GlobalSchedulerContext().IsLocked()); |
||||
|
|
||||
|
// If the thread is runnable, we want to change its affinity in the queue.
|
||||
|
if (thread->scheduling_state == static_cast<u32>(ThreadSchedStatus::Runnable)) { |
||||
|
GetPriorityQueue(kernel).ChangeAffinityMask(old_core, old_affinity, thread); |
||||
|
IncrementScheduledCount(thread); |
||||
|
SetSchedulerUpdateNeeded(kernel); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KScheduler::RotateScheduledQueue(s32 core_id, s32 priority) { |
||||
|
ASSERT(system.GlobalSchedulerContext().IsLocked()); |
||||
|
|
||||
|
// Get a reference to the priority queue.
|
||||
|
auto& kernel = system.Kernel(); |
||||
|
auto& priority_queue = GetPriorityQueue(kernel); |
||||
|
|
||||
|
// Rotate the front of the queue to the end.
|
||||
|
Thread* top_thread = priority_queue.GetScheduledFront(core_id, priority); |
||||
|
Thread* next_thread = nullptr; |
||||
|
if (top_thread != nullptr) { |
||||
|
next_thread = priority_queue.MoveToScheduledBack(top_thread); |
||||
|
if (next_thread != top_thread) { |
||||
|
IncrementScheduledCount(top_thread); |
||||
|
IncrementScheduledCount(next_thread); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// While we have a suggested thread, try to migrate it!
|
||||
|
{ |
||||
|
Thread* suggested = priority_queue.GetSuggestedFront(core_id, priority); |
||||
|
while (suggested != nullptr) { |
||||
|
// Check if the suggested thread is the top thread on its core.
|
||||
|
const s32 suggested_core = suggested->GetActiveCore(); |
||||
|
if (Thread* top_on_suggested_core = |
||||
|
(suggested_core >= 0) ? priority_queue.GetScheduledFront(suggested_core) |
||||
|
: nullptr; |
||||
|
top_on_suggested_core != suggested) { |
||||
|
// If the next thread is a new thread that has been waiting longer than our
|
||||
|
// suggestion, we prefer it to our suggestion.
|
||||
|
if (top_thread != next_thread && next_thread != nullptr && |
||||
|
next_thread->GetLastScheduledTick() < suggested->GetLastScheduledTick()) { |
||||
|
suggested = nullptr; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// If we're allowed to do a migration, do one.
|
||||
|
// NOTE: Unlike migrations in UpdateHighestPriorityThread, this moves the suggestion
|
||||
|
// to the front of the queue.
|
||||
|
if (top_on_suggested_core == nullptr || |
||||
|
top_on_suggested_core->GetPriority() >= HighestCoreMigrationAllowedPriority) { |
||||
|
suggested->SetActiveCore(core_id); |
||||
|
priority_queue.ChangeCore(suggested_core, suggested, true); |
||||
|
IncrementScheduledCount(suggested); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Get the next suggestion.
|
||||
|
suggested = priority_queue.GetSamePriorityNext(core_id, suggested); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Now that we might have migrated a thread with the same priority, check if we can do better.
|
||||
|
|
||||
|
{ |
||||
|
Thread* best_thread = priority_queue.GetScheduledFront(core_id); |
||||
|
if (best_thread == GetCurrentThread()) { |
||||
|
best_thread = priority_queue.GetScheduledNext(core_id, best_thread); |
||||
|
} |
||||
|
|
||||
|
// If the best thread we can choose has a priority the same or worse than ours, try to
|
||||
|
// migrate a higher priority thread.
|
||||
|
if (best_thread != nullptr && best_thread->GetPriority() >= static_cast<u32>(priority)) { |
||||
|
Thread* suggested = priority_queue.GetSuggestedFront(core_id); |
||||
|
while (suggested != nullptr) { |
||||
|
// If the suggestion's priority is the same as ours, don't bother.
|
||||
|
if (suggested->GetPriority() >= best_thread->GetPriority()) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// Check if the suggested thread is the top thread on its core.
|
||||
|
const s32 suggested_core = suggested->GetActiveCore(); |
||||
|
if (Thread* top_on_suggested_core = |
||||
|
(suggested_core >= 0) ? priority_queue.GetScheduledFront(suggested_core) |
||||
|
: nullptr; |
||||
|
top_on_suggested_core != suggested) { |
||||
|
// If we're allowed to do a migration, do one.
|
||||
|
// NOTE: Unlike migrations in UpdateHighestPriorityThread, this moves the
|
||||
|
// suggestion to the front of the queue.
|
||||
|
if (top_on_suggested_core == nullptr || |
||||
|
top_on_suggested_core->GetPriority() >= |
||||
|
HighestCoreMigrationAllowedPriority) { |
||||
|
suggested->SetActiveCore(core_id); |
||||
|
priority_queue.ChangeCore(suggested_core, suggested, true); |
||||
|
IncrementScheduledCount(suggested); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Get the next suggestion.
|
||||
|
suggested = priority_queue.GetSuggestedNext(core_id, suggested); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// After a rotation, we need a scheduler update.
|
||||
|
SetSchedulerUpdateNeeded(kernel); |
||||
|
} |
||||
|
|
||||
|
bool KScheduler::CanSchedule(KernelCore& kernel) { |
||||
|
return kernel.CurrentScheduler()->GetCurrentThread()->GetDisableDispatchCount() <= 1; |
||||
|
} |
||||
|
|
||||
|
bool KScheduler::IsSchedulerUpdateNeeded(const KernelCore& kernel) { |
||||
|
return kernel.GlobalSchedulerContext().scheduler_update_needed.load(std::memory_order_acquire); |
||||
|
} |
||||
|
|
||||
|
void KScheduler::SetSchedulerUpdateNeeded(KernelCore& kernel) { |
||||
|
kernel.GlobalSchedulerContext().scheduler_update_needed.store(true, std::memory_order_release); |
||||
|
} |
||||
|
|
||||
|
void KScheduler::ClearSchedulerUpdateNeeded(KernelCore& kernel) { |
||||
|
kernel.GlobalSchedulerContext().scheduler_update_needed.store(false, std::memory_order_release); |
||||
|
} |
||||
|
|
||||
|
void KScheduler::DisableScheduling(KernelCore& kernel) { |
||||
|
if (auto* scheduler = kernel.CurrentScheduler(); scheduler) { |
||||
|
ASSERT(scheduler->GetCurrentThread()->GetDisableDispatchCount() >= 0); |
||||
|
scheduler->GetCurrentThread()->DisableDispatch(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KScheduler::EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduling, |
||||
|
Core::EmuThreadHandle global_thread) { |
||||
|
if (auto* scheduler = kernel.CurrentScheduler(); scheduler) { |
||||
|
scheduler->GetCurrentThread()->EnableDispatch(); |
||||
|
} |
||||
|
RescheduleCores(kernel, cores_needing_scheduling, global_thread); |
||||
|
} |
||||
|
|
||||
|
u64 KScheduler::UpdateHighestPriorityThreads(KernelCore& kernel) { |
||||
|
if (IsSchedulerUpdateNeeded(kernel)) { |
||||
|
return UpdateHighestPriorityThreadsImpl(kernel); |
||||
|
} else { |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
KSchedulerPriorityQueue& KScheduler::GetPriorityQueue(KernelCore& kernel) { |
||||
|
return kernel.GlobalSchedulerContext().priority_queue; |
||||
|
} |
||||
|
|
||||
|
void KScheduler::YieldWithoutCoreMigration() { |
||||
|
auto& kernel = system.Kernel(); |
||||
|
|
||||
|
// Validate preconditions.
|
||||
|
ASSERT(CanSchedule(kernel)); |
||||
|
ASSERT(kernel.CurrentProcess() != nullptr); |
||||
|
|
||||
|
// Get the current thread and process.
|
||||
|
Thread& cur_thread = *GetCurrentThread(); |
||||
|
Process& cur_process = *kernel.CurrentProcess(); |
||||
|
|
||||
|
// If the thread's yield count matches, there's nothing for us to do.
|
||||
|
if (cur_thread.GetYieldScheduleCount() == cur_process.GetScheduledCount()) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Get a reference to the priority queue.
|
||||
|
auto& priority_queue = GetPriorityQueue(kernel); |
||||
|
|
||||
|
// Perform the yield.
|
||||
|
{ |
||||
|
KScopedSchedulerLock lock(kernel); |
||||
|
|
||||
|
const auto cur_state = cur_thread.scheduling_state; |
||||
|
if (cur_state == static_cast<u32>(ThreadSchedStatus::Runnable)) { |
||||
|
// Put the current thread at the back of the queue.
|
||||
|
Thread* next_thread = priority_queue.MoveToScheduledBack(std::addressof(cur_thread)); |
||||
|
IncrementScheduledCount(std::addressof(cur_thread)); |
||||
|
|
||||
|
// If the next thread is different, we have an update to perform.
|
||||
|
if (next_thread != std::addressof(cur_thread)) { |
||||
|
SetSchedulerUpdateNeeded(kernel); |
||||
|
} else { |
||||
|
// Otherwise, set the thread's yield count so that we won't waste work until the
|
||||
|
// process is scheduled again.
|
||||
|
cur_thread.SetYieldScheduleCount(cur_process.GetScheduledCount()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KScheduler::YieldWithCoreMigration() { |
||||
|
auto& kernel = system.Kernel(); |
||||
|
|
||||
|
// Validate preconditions.
|
||||
|
ASSERT(CanSchedule(kernel)); |
||||
|
ASSERT(kernel.CurrentProcess() != nullptr); |
||||
|
|
||||
|
// Get the current thread and process.
|
||||
|
Thread& cur_thread = *GetCurrentThread(); |
||||
|
Process& cur_process = *kernel.CurrentProcess(); |
||||
|
|
||||
|
// If the thread's yield count matches, there's nothing for us to do.
|
||||
|
if (cur_thread.GetYieldScheduleCount() == cur_process.GetScheduledCount()) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Get a reference to the priority queue.
|
||||
|
auto& priority_queue = GetPriorityQueue(kernel); |
||||
|
|
||||
|
// Perform the yield.
|
||||
|
{ |
||||
|
KScopedSchedulerLock lock(kernel); |
||||
|
|
||||
|
const auto cur_state = cur_thread.scheduling_state; |
||||
|
if (cur_state == static_cast<u32>(ThreadSchedStatus::Runnable)) { |
||||
|
// Get the current active core.
|
||||
|
const s32 core_id = cur_thread.GetActiveCore(); |
||||
|
|
||||
|
// Put the current thread at the back of the queue.
|
||||
|
Thread* next_thread = priority_queue.MoveToScheduledBack(std::addressof(cur_thread)); |
||||
|
IncrementScheduledCount(std::addressof(cur_thread)); |
||||
|
|
||||
|
// While we have a suggested thread, try to migrate it!
|
||||
|
bool recheck = false; |
||||
|
Thread* suggested = priority_queue.GetSuggestedFront(core_id); |
||||
|
while (suggested != nullptr) { |
||||
|
// Check if the suggested thread is the thread running on its core.
|
||||
|
const s32 suggested_core = suggested->GetActiveCore(); |
||||
|
|
||||
|
if (Thread* running_on_suggested_core = |
||||
|
(suggested_core >= 0) |
||||
|
? kernel.Scheduler(suggested_core).state.highest_priority_thread |
||||
|
: nullptr; |
||||
|
running_on_suggested_core != suggested) { |
||||
|
// If the current thread's priority is higher than our suggestion's we prefer
|
||||
|
// the next thread to the suggestion. We also prefer the next thread when the
|
||||
|
// current thread's priority is equal to the suggestions, but the next thread
|
||||
|
// has been waiting longer.
|
||||
|
if ((suggested->GetPriority() > cur_thread.GetPriority()) || |
||||
|
(suggested->GetPriority() == cur_thread.GetPriority() && |
||||
|
next_thread != std::addressof(cur_thread) && |
||||
|
next_thread->GetLastScheduledTick() < suggested->GetLastScheduledTick())) { |
||||
|
suggested = nullptr; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// If we're allowed to do a migration, do one.
|
||||
|
// NOTE: Unlike migrations in UpdateHighestPriorityThread, this moves the
|
||||
|
// suggestion to the front of the queue.
|
||||
|
if (running_on_suggested_core == nullptr || |
||||
|
running_on_suggested_core->GetPriority() >= |
||||
|
HighestCoreMigrationAllowedPriority) { |
||||
|
suggested->SetActiveCore(core_id); |
||||
|
priority_queue.ChangeCore(suggested_core, suggested, true); |
||||
|
IncrementScheduledCount(suggested); |
||||
|
break; |
||||
|
} else { |
||||
|
// We couldn't perform a migration, but we should check again on a future
|
||||
|
// yield.
|
||||
|
recheck = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Get the next suggestion.
|
||||
|
suggested = priority_queue.GetSuggestedNext(core_id, suggested); |
||||
|
} |
||||
|
|
||||
|
// If we still have a suggestion or the next thread is different, we have an update to
|
||||
|
// perform.
|
||||
|
if (suggested != nullptr || next_thread != std::addressof(cur_thread)) { |
||||
|
SetSchedulerUpdateNeeded(kernel); |
||||
|
} else if (!recheck) { |
||||
|
// Otherwise if we don't need to re-check, set the thread's yield count so that we
|
||||
|
// won't waste work until the process is scheduled again.
|
||||
|
cur_thread.SetYieldScheduleCount(cur_process.GetScheduledCount()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KScheduler::YieldToAnyThread() { |
||||
|
auto& kernel = system.Kernel(); |
||||
|
|
||||
|
// Validate preconditions.
|
||||
|
ASSERT(CanSchedule(kernel)); |
||||
|
ASSERT(kernel.CurrentProcess() != nullptr); |
||||
|
|
||||
|
// Get the current thread and process.
|
||||
|
Thread& cur_thread = *GetCurrentThread(); |
||||
|
Process& cur_process = *kernel.CurrentProcess(); |
||||
|
|
||||
|
// If the thread's yield count matches, there's nothing for us to do.
|
||||
|
if (cur_thread.GetYieldScheduleCount() == cur_process.GetScheduledCount()) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Get a reference to the priority queue.
|
||||
|
auto& priority_queue = GetPriorityQueue(kernel); |
||||
|
|
||||
|
// Perform the yield.
|
||||
|
{ |
||||
|
KScopedSchedulerLock lock(kernel); |
||||
|
|
||||
|
const auto cur_state = cur_thread.scheduling_state; |
||||
|
if (cur_state == static_cast<u32>(ThreadSchedStatus::Runnable)) { |
||||
|
// Get the current active core.
|
||||
|
const s32 core_id = cur_thread.GetActiveCore(); |
||||
|
|
||||
|
// Migrate the current thread to core -1.
|
||||
|
cur_thread.SetActiveCore(-1); |
||||
|
priority_queue.ChangeCore(core_id, std::addressof(cur_thread)); |
||||
|
IncrementScheduledCount(std::addressof(cur_thread)); |
||||
|
|
||||
|
// If there's nothing scheduled, we can try to perform a migration.
|
||||
|
if (priority_queue.GetScheduledFront(core_id) == nullptr) { |
||||
|
// While we have a suggested thread, try to migrate it!
|
||||
|
Thread* suggested = priority_queue.GetSuggestedFront(core_id); |
||||
|
while (suggested != nullptr) { |
||||
|
// Check if the suggested thread is the top thread on its core.
|
||||
|
const s32 suggested_core = suggested->GetActiveCore(); |
||||
|
if (Thread* top_on_suggested_core = |
||||
|
(suggested_core >= 0) ? priority_queue.GetScheduledFront(suggested_core) |
||||
|
: nullptr; |
||||
|
top_on_suggested_core != suggested) { |
||||
|
// If we're allowed to do a migration, do one.
|
||||
|
if (top_on_suggested_core == nullptr || |
||||
|
top_on_suggested_core->GetPriority() >= |
||||
|
HighestCoreMigrationAllowedPriority) { |
||||
|
suggested->SetActiveCore(core_id); |
||||
|
priority_queue.ChangeCore(suggested_core, suggested); |
||||
|
IncrementScheduledCount(suggested); |
||||
|
} |
||||
|
|
||||
|
// Regardless of whether we migrated, we had a candidate, so we're done.
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// Get the next suggestion.
|
||||
|
suggested = priority_queue.GetSuggestedNext(core_id, suggested); |
||||
|
} |
||||
|
|
||||
|
// If the suggestion is different from the current thread, we need to perform an
|
||||
|
// update.
|
||||
|
if (suggested != std::addressof(cur_thread)) { |
||||
|
SetSchedulerUpdateNeeded(kernel); |
||||
|
} else { |
||||
|
// Otherwise, set the thread's yield count so that we won't waste work until the
|
||||
|
// process is scheduled again.
|
||||
|
cur_thread.SetYieldScheduleCount(cur_process.GetScheduledCount()); |
||||
|
} |
||||
|
} else { |
||||
|
// Otherwise, we have an update to perform.
|
||||
|
SetSchedulerUpdateNeeded(kernel); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
KScheduler::KScheduler(Core::System& system, std::size_t core_id) |
||||
|
: system(system), core_id(core_id) { |
||||
|
switch_fiber = std::make_shared<Common::Fiber>(OnSwitch, this); |
||||
|
this->state.needs_scheduling = true; |
||||
|
this->state.interrupt_task_thread_runnable = false; |
||||
|
this->state.should_count_idle = false; |
||||
|
this->state.idle_count = 0; |
||||
|
this->state.idle_thread_stack = nullptr; |
||||
|
this->state.highest_priority_thread = nullptr; |
||||
|
} |
||||
|
|
||||
|
KScheduler::~KScheduler() = default; |
||||
|
|
||||
|
Thread* KScheduler::GetCurrentThread() const { |
||||
|
if (current_thread) { |
||||
|
return current_thread; |
||||
|
} |
||||
|
return idle_thread; |
||||
|
} |
||||
|
|
||||
|
u64 KScheduler::GetLastContextSwitchTicks() const { |
||||
|
return last_context_switch_time; |
||||
|
} |
||||
|
|
||||
|
void KScheduler::RescheduleCurrentCore() { |
||||
|
ASSERT(GetCurrentThread()->GetDisableDispatchCount() == 1); |
||||
|
|
||||
|
auto& phys_core = system.Kernel().PhysicalCore(core_id); |
||||
|
if (phys_core.IsInterrupted()) { |
||||
|
phys_core.ClearInterrupt(); |
||||
|
} |
||||
|
guard.lock(); |
||||
|
if (this->state.needs_scheduling) { |
||||
|
Schedule(); |
||||
|
} else { |
||||
|
guard.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KScheduler::OnThreadStart() { |
||||
|
SwitchContextStep2(); |
||||
|
} |
||||
|
|
||||
|
void KScheduler::Unload(Thread* thread) { |
||||
|
if (thread) { |
||||
|
thread->SetIsRunning(false); |
||||
|
if (thread->IsContinuousOnSVC() && !thread->IsHLEThread()) { |
||||
|
system.ArmInterface(core_id).ExceptionalExit(); |
||||
|
thread->SetContinuousOnSVC(false); |
||||
|
} |
||||
|
if (!thread->IsHLEThread() && !thread->HasExited()) { |
||||
|
Core::ARM_Interface& cpu_core = system.ArmInterface(core_id); |
||||
|
cpu_core.SaveContext(thread->GetContext32()); |
||||
|
cpu_core.SaveContext(thread->GetContext64()); |
||||
|
// Save the TPIDR_EL0 system register in case it was modified.
|
||||
|
thread->SetTPIDR_EL0(cpu_core.GetTPIDR_EL0()); |
||||
|
cpu_core.ClearExclusiveState(); |
||||
|
} |
||||
|
thread->context_guard.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KScheduler::Reload(Thread* thread) { |
||||
|
if (thread) { |
||||
|
ASSERT_MSG(thread->GetSchedulingStatus() == ThreadSchedStatus::Runnable, |
||||
|
"Thread must be runnable."); |
||||
|
|
||||
|
// Cancel any outstanding wakeup events for this thread
|
||||
|
thread->SetIsRunning(true); |
||||
|
thread->SetWasRunning(false); |
||||
|
|
||||
|
auto* const thread_owner_process = thread->GetOwnerProcess(); |
||||
|
if (thread_owner_process != nullptr) { |
||||
|
system.Kernel().MakeCurrentProcess(thread_owner_process); |
||||
|
} |
||||
|
if (!thread->IsHLEThread()) { |
||||
|
Core::ARM_Interface& cpu_core = system.ArmInterface(core_id); |
||||
|
cpu_core.LoadContext(thread->GetContext32()); |
||||
|
cpu_core.LoadContext(thread->GetContext64()); |
||||
|
cpu_core.SetTlsAddress(thread->GetTLSAddress()); |
||||
|
cpu_core.SetTPIDR_EL0(thread->GetTPIDR_EL0()); |
||||
|
cpu_core.ClearExclusiveState(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KScheduler::SwitchContextStep2() { |
||||
|
// Load context of new thread
|
||||
|
Reload(current_thread); |
||||
|
|
||||
|
RescheduleCurrentCore(); |
||||
|
} |
||||
|
|
||||
|
void KScheduler::ScheduleImpl() { |
||||
|
Thread* previous_thread = current_thread; |
||||
|
current_thread = state.highest_priority_thread; |
||||
|
|
||||
|
this->state.needs_scheduling = false; |
||||
|
|
||||
|
if (current_thread == previous_thread) { |
||||
|
guard.unlock(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
Process* const previous_process = system.Kernel().CurrentProcess(); |
||||
|
|
||||
|
UpdateLastContextSwitchTime(previous_thread, previous_process); |
||||
|
|
||||
|
// Save context for previous thread
|
||||
|
Unload(previous_thread); |
||||
|
|
||||
|
std::shared_ptr<Common::Fiber>* old_context; |
||||
|
if (previous_thread != nullptr) { |
||||
|
old_context = &previous_thread->GetHostContext(); |
||||
|
} else { |
||||
|
old_context = &idle_thread->GetHostContext(); |
||||
|
} |
||||
|
guard.unlock(); |
||||
|
|
||||
|
Common::Fiber::YieldTo(*old_context, switch_fiber); |
||||
|
/// When a thread wakes up, the scheduler may have changed to other in another core.
|
||||
|
auto& next_scheduler = *system.Kernel().CurrentScheduler(); |
||||
|
next_scheduler.SwitchContextStep2(); |
||||
|
} |
||||
|
|
||||
|
void KScheduler::OnSwitch(void* this_scheduler) { |
||||
|
KScheduler* sched = static_cast<KScheduler*>(this_scheduler); |
||||
|
sched->SwitchToCurrent(); |
||||
|
} |
||||
|
|
||||
|
void KScheduler::SwitchToCurrent() { |
||||
|
while (true) { |
||||
|
{ |
||||
|
std::scoped_lock lock{guard}; |
||||
|
current_thread = state.highest_priority_thread; |
||||
|
this->state.needs_scheduling = false; |
||||
|
} |
||||
|
const auto is_switch_pending = [this] { |
||||
|
std::scoped_lock lock{guard}; |
||||
|
return state.needs_scheduling.load(std::memory_order_relaxed); |
||||
|
}; |
||||
|
do { |
||||
|
if (current_thread != nullptr && !current_thread->IsHLEThread()) { |
||||
|
current_thread->context_guard.lock(); |
||||
|
if (!current_thread->IsRunnable()) { |
||||
|
current_thread->context_guard.unlock(); |
||||
|
break; |
||||
|
} |
||||
|
if (static_cast<u32>(current_thread->GetProcessorID()) != core_id) { |
||||
|
current_thread->context_guard.unlock(); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
std::shared_ptr<Common::Fiber>* next_context; |
||||
|
if (current_thread != nullptr) { |
||||
|
next_context = ¤t_thread->GetHostContext(); |
||||
|
} else { |
||||
|
next_context = &idle_thread->GetHostContext(); |
||||
|
} |
||||
|
Common::Fiber::YieldTo(switch_fiber, *next_context); |
||||
|
} while (!is_switch_pending()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KScheduler::UpdateLastContextSwitchTime(Thread* thread, Process* process) { |
||||
|
const u64 prev_switch_ticks = last_context_switch_time; |
||||
|
const u64 most_recent_switch_ticks = system.CoreTiming().GetCPUTicks(); |
||||
|
const u64 update_ticks = most_recent_switch_ticks - prev_switch_ticks; |
||||
|
|
||||
|
if (thread != nullptr) { |
||||
|
thread->UpdateCPUTimeTicks(update_ticks); |
||||
|
} |
||||
|
|
||||
|
if (process != nullptr) { |
||||
|
process->UpdateCPUTimeTicks(update_ticks); |
||||
|
} |
||||
|
|
||||
|
last_context_switch_time = most_recent_switch_ticks; |
||||
|
} |
||||
|
|
||||
|
void KScheduler::Initialize() { |
||||
|
std::string name = "Idle Thread Id:" + std::to_string(core_id); |
||||
|
std::function<void(void*)> init_func = Core::CpuManager::GetIdleThreadStartFunc(); |
||||
|
void* init_func_parameter = system.GetCpuManager().GetStartFuncParamater(); |
||||
|
ThreadType type = static_cast<ThreadType>(THREADTYPE_KERNEL | THREADTYPE_HLE | THREADTYPE_IDLE); |
||||
|
auto thread_res = Thread::Create(system, type, name, 0, 64, 0, static_cast<u32>(core_id), 0, |
||||
|
nullptr, std::move(init_func), init_func_parameter); |
||||
|
idle_thread = thread_res.Unwrap().get(); |
||||
|
|
||||
|
{ |
||||
|
KScopedSchedulerLock lock{system.Kernel()}; |
||||
|
idle_thread->SetStatus(ThreadStatus::Ready); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
KScopedSchedulerLock::KScopedSchedulerLock(KernelCore& kernel) |
||||
|
: KScopedLock(kernel.GlobalSchedulerContext().SchedulerLock()) {} |
||||
|
|
||||
|
KScopedSchedulerLock::~KScopedSchedulerLock() = default; |
||||
|
|
||||
|
} // namespace Kernel
|
||||
@ -0,0 +1,201 @@ |
|||||
|
// Copyright 2020 yuzu Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
// This file references various implementation details from Atmosphere, an open-source firmware for |
||||
|
// the Nintendo Switch. Copyright 2018-2020 Atmosphere-NX. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <atomic> |
||||
|
|
||||
|
#include "common/common_types.h" |
||||
|
#include "common/spin_lock.h" |
||||
|
#include "core/hle/kernel/global_scheduler_context.h" |
||||
|
#include "core/hle/kernel/k_priority_queue.h" |
||||
|
#include "core/hle/kernel/k_scheduler_lock.h" |
||||
|
#include "core/hle/kernel/k_scoped_lock.h" |
||||
|
|
||||
|
namespace Common { |
||||
|
class Fiber; |
||||
|
} |
||||
|
|
||||
|
namespace Core { |
||||
|
class System; |
||||
|
} |
||||
|
|
||||
|
namespace Kernel { |
||||
|
|
||||
|
class KernelCore; |
||||
|
class Process; |
||||
|
class SchedulerLock; |
||||
|
class Thread; |
||||
|
|
||||
|
class KScheduler final { |
||||
|
public: |
||||
|
explicit KScheduler(Core::System& system, std::size_t core_id); |
||||
|
~KScheduler(); |
||||
|
|
||||
|
/// Reschedules to the next available thread (call after current thread is suspended) |
||||
|
void RescheduleCurrentCore(); |
||||
|
|
||||
|
/// Reschedules cores pending reschedule, to be called on EnableScheduling. |
||||
|
static void RescheduleCores(KernelCore& kernel, u64 cores_pending_reschedule, |
||||
|
Core::EmuThreadHandle global_thread); |
||||
|
|
||||
|
/// The next two are for SingleCore Only. |
||||
|
/// Unload current thread before preempting core. |
||||
|
void Unload(Thread* thread); |
||||
|
|
||||
|
/// Reload current thread after core preemption. |
||||
|
void Reload(Thread* thread); |
||||
|
|
||||
|
/// Gets the current running thread |
||||
|
[[nodiscard]] Thread* GetCurrentThread() const; |
||||
|
|
||||
|
/// Gets the timestamp for the last context switch in ticks. |
||||
|
[[nodiscard]] u64 GetLastContextSwitchTicks() const; |
||||
|
|
||||
|
[[nodiscard]] bool ContextSwitchPending() const { |
||||
|
return state.needs_scheduling.load(std::memory_order_relaxed); |
||||
|
} |
||||
|
|
||||
|
void Initialize(); |
||||
|
|
||||
|
void OnThreadStart(); |
||||
|
|
||||
|
[[nodiscard]] std::shared_ptr<Common::Fiber>& ControlContext() { |
||||
|
return switch_fiber; |
||||
|
} |
||||
|
|
||||
|
[[nodiscard]] const std::shared_ptr<Common::Fiber>& ControlContext() const { |
||||
|
return switch_fiber; |
||||
|
} |
||||
|
|
||||
|
[[nodiscard]] u64 UpdateHighestPriorityThread(Thread* highest_thread); |
||||
|
|
||||
|
/** |
||||
|
* Takes a thread and moves it to the back of the it's priority list. |
||||
|
* |
||||
|
* @note This operation can be redundant and no scheduling is changed if marked as so. |
||||
|
*/ |
||||
|
void YieldWithoutCoreMigration(); |
||||
|
|
||||
|
/** |
||||
|
* Takes a thread and moves it to the back of the it's priority list. |
||||
|
* Afterwards, tries to pick a suggested thread from the suggested queue that has worse time or |
||||
|
* a better priority than the next thread in the core. |
||||
|
* |
||||
|
* @note This operation can be redundant and no scheduling is changed if marked as so. |
||||
|
*/ |
||||
|
void YieldWithCoreMigration(); |
||||
|
|
||||
|
/** |
||||
|
* Takes a thread and moves it out of the scheduling queue. |
||||
|
* and into the suggested queue. If no thread can be scheduled afterwards in that core, |
||||
|
* a suggested thread is obtained instead. |
||||
|
* |
||||
|
* @note This operation can be redundant and no scheduling is changed if marked as so. |
||||
|
*/ |
||||
|
void YieldToAnyThread(); |
||||
|
|
||||
|
/// Notify the scheduler a thread's status has changed. |
||||
|
static void OnThreadStateChanged(KernelCore& kernel, Thread* thread, u32 old_state); |
||||
|
|
||||
|
/// Notify the scheduler a thread's priority has changed. |
||||
|
static void OnThreadPriorityChanged(KernelCore& kernel, Thread* thread, Thread* current_thread, |
||||
|
u32 old_priority); |
||||
|
|
||||
|
/// Notify the scheduler a thread's core and/or affinity mask has changed. |
||||
|
static void OnThreadAffinityMaskChanged(KernelCore& kernel, Thread* thread, |
||||
|
const KAffinityMask& old_affinity, s32 old_core); |
||||
|
|
||||
|
static bool CanSchedule(KernelCore& kernel); |
||||
|
static bool IsSchedulerUpdateNeeded(const KernelCore& kernel); |
||||
|
static void SetSchedulerUpdateNeeded(KernelCore& kernel); |
||||
|
static void ClearSchedulerUpdateNeeded(KernelCore& kernel); |
||||
|
static void DisableScheduling(KernelCore& kernel); |
||||
|
static void EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduling, |
||||
|
Core::EmuThreadHandle global_thread); |
||||
|
[[nodiscard]] static u64 UpdateHighestPriorityThreads(KernelCore& kernel); |
||||
|
|
||||
|
private: |
||||
|
friend class GlobalSchedulerContext; |
||||
|
|
||||
|
/** |
||||
|
* Takes care of selecting the new scheduled threads in three steps: |
||||
|
* |
||||
|
* 1. First a thread is selected from the top of the priority queue. If no thread |
||||
|
* is obtained then we move to step two, else we are done. |
||||
|
* |
||||
|
* 2. Second we try to get a suggested thread that's not assigned to any core or |
||||
|
* that is not the top thread in that core. |
||||
|
* |
||||
|
* 3. Third is no suggested thread is found, we do a second pass and pick a running |
||||
|
* thread in another core and swap it with its current thread. |
||||
|
* |
||||
|
* returns the cores needing scheduling. |
||||
|
*/ |
||||
|
[[nodiscard]] static u64 UpdateHighestPriorityThreadsImpl(KernelCore& kernel); |
||||
|
|
||||
|
[[nodiscard]] static KSchedulerPriorityQueue& GetPriorityQueue(KernelCore& kernel); |
||||
|
|
||||
|
void RotateScheduledQueue(s32 core_id, s32 priority); |
||||
|
|
||||
|
void Schedule() { |
||||
|
ASSERT(GetCurrentThread()->GetDisableDispatchCount() == 1); |
||||
|
this->ScheduleImpl(); |
||||
|
} |
||||
|
|
||||
|
/// Switches the CPU's active thread context to that of the specified thread |
||||
|
void ScheduleImpl(); |
||||
|
|
||||
|
/// When a thread wakes up, it must run this through it's new scheduler |
||||
|
void SwitchContextStep2(); |
||||
|
|
||||
|
/** |
||||
|
* Called on every context switch to update the internal timestamp |
||||
|
* This also updates the running time ticks for the given thread and |
||||
|
* process using the following difference: |
||||
|
* |
||||
|
* ticks += most_recent_ticks - last_context_switch_ticks |
||||
|
* |
||||
|
* The internal tick timestamp for the scheduler is simply the |
||||
|
* most recent tick count retrieved. No special arithmetic is |
||||
|
* applied to it. |
||||
|
*/ |
||||
|
void UpdateLastContextSwitchTime(Thread* thread, Process* process); |
||||
|
|
||||
|
static void OnSwitch(void* this_scheduler); |
||||
|
void SwitchToCurrent(); |
||||
|
|
||||
|
Thread* current_thread{}; |
||||
|
Thread* idle_thread{}; |
||||
|
|
||||
|
std::shared_ptr<Common::Fiber> switch_fiber{}; |
||||
|
|
||||
|
struct SchedulingState { |
||||
|
std::atomic<bool> needs_scheduling; |
||||
|
bool interrupt_task_thread_runnable{}; |
||||
|
bool should_count_idle{}; |
||||
|
u64 idle_count{}; |
||||
|
Thread* highest_priority_thread{}; |
||||
|
void* idle_thread_stack{}; |
||||
|
}; |
||||
|
|
||||
|
SchedulingState state; |
||||
|
|
||||
|
Core::System& system; |
||||
|
u64 last_context_switch_time{}; |
||||
|
const std::size_t core_id; |
||||
|
|
||||
|
Common::SpinLock guard{}; |
||||
|
}; |
||||
|
|
||||
|
class KScopedSchedulerLock : KScopedLock<GlobalSchedulerContext::LockType> { |
||||
|
public: |
||||
|
explicit KScopedSchedulerLock(KernelCore& kernel); |
||||
|
~KScopedSchedulerLock(); |
||||
|
}; |
||||
|
|
||||
|
} // namespace Kernel |
||||
@ -0,0 +1,74 @@ |
|||||
|
// Copyright 2020 yuzu Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
// This file references various implementation details from Atmosphere, an open-source firmware for |
||||
|
// the Nintendo Switch. Copyright 2018-2020 Atmosphere-NX. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include "common/assert.h" |
||||
|
#include "common/spin_lock.h" |
||||
|
#include "core/hardware_properties.h" |
||||
|
|
||||
|
namespace Kernel { |
||||
|
|
||||
|
class KernelCore; |
||||
|
|
||||
|
template <typename SchedulerType> |
||||
|
class KAbstractSchedulerLock { |
||||
|
public: |
||||
|
explicit KAbstractSchedulerLock(KernelCore& kernel) : kernel{kernel} {} |
||||
|
|
||||
|
bool IsLockedByCurrentThread() const { |
||||
|
return this->owner_thread == kernel.GetCurrentEmuThreadID(); |
||||
|
} |
||||
|
|
||||
|
void Lock() { |
||||
|
if (this->IsLockedByCurrentThread()) { |
||||
|
// If we already own the lock, we can just increment the count. |
||||
|
ASSERT(this->lock_count > 0); |
||||
|
this->lock_count++; |
||||
|
} else { |
||||
|
// Otherwise, we want to disable scheduling and acquire the spinlock. |
||||
|
SchedulerType::DisableScheduling(kernel); |
||||
|
this->spin_lock.lock(); |
||||
|
|
||||
|
// For debug, ensure that our state is valid. |
||||
|
ASSERT(this->lock_count == 0); |
||||
|
ASSERT(this->owner_thread == Core::EmuThreadHandle::InvalidHandle()); |
||||
|
|
||||
|
// Increment count, take ownership. |
||||
|
this->lock_count = 1; |
||||
|
this->owner_thread = kernel.GetCurrentEmuThreadID(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void Unlock() { |
||||
|
ASSERT(this->IsLockedByCurrentThread()); |
||||
|
ASSERT(this->lock_count > 0); |
||||
|
|
||||
|
// Release an instance of the lock. |
||||
|
if ((--this->lock_count) == 0) { |
||||
|
// We're no longer going to hold the lock. Take note of what cores need scheduling. |
||||
|
const u64 cores_needing_scheduling = |
||||
|
SchedulerType::UpdateHighestPriorityThreads(kernel); |
||||
|
Core::EmuThreadHandle leaving_thread = owner_thread; |
||||
|
|
||||
|
// Note that we no longer hold the lock, and unlock the spinlock. |
||||
|
this->owner_thread = Core::EmuThreadHandle::InvalidHandle(); |
||||
|
this->spin_lock.unlock(); |
||||
|
|
||||
|
// Enable scheduling, and perform a rescheduling operation. |
||||
|
SchedulerType::EnableScheduling(kernel, cores_needing_scheduling, leaving_thread); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
KernelCore& kernel; |
||||
|
Common::SpinLock spin_lock{}; |
||||
|
s32 lock_count{}; |
||||
|
Core::EmuThreadHandle owner_thread{Core::EmuThreadHandle::InvalidHandle()}; |
||||
|
}; |
||||
|
|
||||
|
} // namespace Kernel |
||||
@ -0,0 +1,41 @@ |
|||||
|
// Copyright 2020 yuzu Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
// This file references various implementation details from Atmosphere, an open-source firmware for |
||||
|
// the Nintendo Switch. Copyright 2018-2020 Atmosphere-NX. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include "common/common_types.h" |
||||
|
|
||||
|
namespace Kernel { |
||||
|
|
||||
|
template <typename T> |
||||
|
concept KLockable = !std::is_reference_v<T> && requires(T & t) { |
||||
|
{ t.Lock() } |
||||
|
->std::same_as<void>; |
||||
|
{ t.Unlock() } |
||||
|
->std::same_as<void>; |
||||
|
}; |
||||
|
|
||||
|
template <typename T> |
||||
|
requires KLockable<T> class KScopedLock { |
||||
|
public: |
||||
|
explicit KScopedLock(T* l) : lock_ptr(l) { |
||||
|
this->lock_ptr->Lock(); |
||||
|
} |
||||
|
explicit KScopedLock(T& l) : KScopedLock(std::addressof(l)) { /* ... */ |
||||
|
} |
||||
|
~KScopedLock() { |
||||
|
this->lock_ptr->Unlock(); |
||||
|
} |
||||
|
|
||||
|
KScopedLock(const KScopedLock&) = delete; |
||||
|
KScopedLock(KScopedLock&&) = delete; |
||||
|
|
||||
|
private: |
||||
|
T* lock_ptr; |
||||
|
}; |
||||
|
|
||||
|
} // namespace Kernel |
||||
@ -0,0 +1,50 @@ |
|||||
|
// Copyright 2020 yuzu Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
// This file references various implementation details from Atmosphere, an open-source firmware for |
||||
|
// the Nintendo Switch. Copyright 2018-2020 Atmosphere-NX. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include "common/common_types.h" |
||||
|
#include "core/hle/kernel/handle_table.h" |
||||
|
#include "core/hle/kernel/kernel.h" |
||||
|
#include "core/hle/kernel/thread.h" |
||||
|
#include "core/hle/kernel/time_manager.h" |
||||
|
|
||||
|
namespace Kernel { |
||||
|
|
||||
|
class KScopedSchedulerLockAndSleep { |
||||
|
public: |
||||
|
explicit KScopedSchedulerLockAndSleep(KernelCore& kernel, Handle& event_handle, Thread* t, |
||||
|
s64 timeout) |
||||
|
: kernel(kernel), event_handle(event_handle), thread(t), timeout_tick(timeout) { |
||||
|
event_handle = InvalidHandle; |
||||
|
|
||||
|
// Lock the scheduler. |
||||
|
kernel.GlobalSchedulerContext().scheduler_lock.Lock(); |
||||
|
} |
||||
|
|
||||
|
~KScopedSchedulerLockAndSleep() { |
||||
|
// Register the sleep. |
||||
|
if (this->timeout_tick > 0) { |
||||
|
kernel.TimeManager().ScheduleTimeEvent(event_handle, this->thread, this->timeout_tick); |
||||
|
} |
||||
|
|
||||
|
// Unlock the scheduler. |
||||
|
kernel.GlobalSchedulerContext().scheduler_lock.Unlock(); |
||||
|
} |
||||
|
|
||||
|
void CancelSleep() { |
||||
|
this->timeout_tick = 0; |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
KernelCore& kernel; |
||||
|
Handle& event_handle; |
||||
|
Thread* thread{}; |
||||
|
s64 timeout_tick{}; |
||||
|
}; |
||||
|
|
||||
|
} // namespace Kernel |
||||
@ -1,819 +0,0 @@ |
|||||
// Copyright 2018 yuzu emulator team
|
|
||||
// Licensed under GPLv2 or any later version
|
|
||||
// Refer to the license.txt file included.
|
|
||||
//
|
|
||||
// SelectThreads, Yield functions originally by TuxSH.
|
|
||||
// licensed under GPLv2 or later under exception provided by the author.
|
|
||||
|
|
||||
#include <algorithm>
|
|
||||
#include <mutex>
|
|
||||
#include <set>
|
|
||||
#include <unordered_set>
|
|
||||
#include <utility>
|
|
||||
|
|
||||
#include "common/assert.h"
|
|
||||
#include "common/bit_util.h"
|
|
||||
#include "common/fiber.h"
|
|
||||
#include "common/logging/log.h"
|
|
||||
#include "core/arm/arm_interface.h"
|
|
||||
#include "core/core.h"
|
|
||||
#include "core/core_timing.h"
|
|
||||
#include "core/cpu_manager.h"
|
|
||||
#include "core/hle/kernel/kernel.h"
|
|
||||
#include "core/hle/kernel/physical_core.h"
|
|
||||
#include "core/hle/kernel/process.h"
|
|
||||
#include "core/hle/kernel/scheduler.h"
|
|
||||
#include "core/hle/kernel/time_manager.h"
|
|
||||
|
|
||||
namespace Kernel { |
|
||||
|
|
||||
GlobalScheduler::GlobalScheduler(KernelCore& kernel) : kernel{kernel} {} |
|
||||
|
|
||||
GlobalScheduler::~GlobalScheduler() = default; |
|
||||
|
|
||||
void GlobalScheduler::AddThread(std::shared_ptr<Thread> thread) { |
|
||||
std::scoped_lock lock{global_list_guard}; |
|
||||
thread_list.push_back(std::move(thread)); |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::RemoveThread(std::shared_ptr<Thread> thread) { |
|
||||
std::scoped_lock lock{global_list_guard}; |
|
||||
thread_list.erase(std::remove(thread_list.begin(), thread_list.end(), thread), |
|
||||
thread_list.end()); |
|
||||
} |
|
||||
|
|
||||
u32 GlobalScheduler::SelectThreads() { |
|
||||
ASSERT(is_locked); |
|
||||
const auto update_thread = [](Thread* thread, Scheduler& sched) { |
|
||||
std::scoped_lock lock{sched.guard}; |
|
||||
if (thread != sched.selected_thread_set.get()) { |
|
||||
if (thread == nullptr) { |
|
||||
++sched.idle_selection_count; |
|
||||
} |
|
||||
sched.selected_thread_set = SharedFrom(thread); |
|
||||
} |
|
||||
const bool reschedule_pending = |
|
||||
sched.is_context_switch_pending || (sched.selected_thread_set != sched.current_thread); |
|
||||
sched.is_context_switch_pending = reschedule_pending; |
|
||||
std::atomic_thread_fence(std::memory_order_seq_cst); |
|
||||
return reschedule_pending; |
|
||||
}; |
|
||||
if (!is_reselection_pending.load()) { |
|
||||
return 0; |
|
||||
} |
|
||||
std::array<Thread*, Core::Hardware::NUM_CPU_CORES> top_threads{}; |
|
||||
|
|
||||
u32 idle_cores{}; |
|
||||
|
|
||||
// Step 1: Get top thread in schedule queue.
|
|
||||
for (u32 core = 0; core < Core::Hardware::NUM_CPU_CORES; core++) { |
|
||||
Thread* top_thread = |
|
||||
scheduled_queue[core].empty() ? nullptr : scheduled_queue[core].front(); |
|
||||
if (top_thread != nullptr) { |
|
||||
// TODO(Blinkhawk): Implement Thread Pinning
|
|
||||
} else { |
|
||||
idle_cores |= (1U << core); |
|
||||
} |
|
||||
top_threads[core] = top_thread; |
|
||||
} |
|
||||
|
|
||||
while (idle_cores != 0) { |
|
||||
u32 core_id = Common::CountTrailingZeroes32(idle_cores); |
|
||||
|
|
||||
if (!suggested_queue[core_id].empty()) { |
|
||||
std::array<s32, Core::Hardware::NUM_CPU_CORES> migration_candidates{}; |
|
||||
std::size_t num_candidates = 0; |
|
||||
auto iter = suggested_queue[core_id].begin(); |
|
||||
Thread* suggested = nullptr; |
|
||||
// Step 2: Try selecting a suggested thread.
|
|
||||
while (iter != suggested_queue[core_id].end()) { |
|
||||
suggested = *iter; |
|
||||
iter++; |
|
||||
s32 suggested_core_id = suggested->GetProcessorID(); |
|
||||
Thread* top_thread = |
|
||||
suggested_core_id >= 0 ? top_threads[suggested_core_id] : nullptr; |
|
||||
if (top_thread != suggested) { |
|
||||
if (top_thread != nullptr && |
|
||||
top_thread->GetPriority() < THREADPRIO_MAX_CORE_MIGRATION) { |
|
||||
suggested = nullptr; |
|
||||
break; |
|
||||
// There's a too high thread to do core migration, cancel
|
|
||||
} |
|
||||
TransferToCore(suggested->GetPriority(), static_cast<s32>(core_id), suggested); |
|
||||
break; |
|
||||
} |
|
||||
suggested = nullptr; |
|
||||
migration_candidates[num_candidates++] = suggested_core_id; |
|
||||
} |
|
||||
// Step 3: Select a suggested thread from another core
|
|
||||
if (suggested == nullptr) { |
|
||||
for (std::size_t i = 0; i < num_candidates; i++) { |
|
||||
s32 candidate_core = migration_candidates[i]; |
|
||||
suggested = top_threads[candidate_core]; |
|
||||
auto it = scheduled_queue[candidate_core].begin(); |
|
||||
it++; |
|
||||
Thread* next = it != scheduled_queue[candidate_core].end() ? *it : nullptr; |
|
||||
if (next != nullptr) { |
|
||||
TransferToCore(suggested->GetPriority(), static_cast<s32>(core_id), |
|
||||
suggested); |
|
||||
top_threads[candidate_core] = next; |
|
||||
break; |
|
||||
} else { |
|
||||
suggested = nullptr; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
top_threads[core_id] = suggested; |
|
||||
} |
|
||||
|
|
||||
idle_cores &= ~(1U << core_id); |
|
||||
} |
|
||||
u32 cores_needing_context_switch{}; |
|
||||
for (u32 core = 0; core < Core::Hardware::NUM_CPU_CORES; core++) { |
|
||||
Scheduler& sched = kernel.Scheduler(core); |
|
||||
ASSERT(top_threads[core] == nullptr || |
|
||||
static_cast<u32>(top_threads[core]->GetProcessorID()) == core); |
|
||||
if (update_thread(top_threads[core], sched)) { |
|
||||
cores_needing_context_switch |= (1U << core); |
|
||||
} |
|
||||
} |
|
||||
return cores_needing_context_switch; |
|
||||
} |
|
||||
|
|
||||
bool GlobalScheduler::YieldThread(Thread* yielding_thread) { |
|
||||
ASSERT(is_locked); |
|
||||
// Note: caller should use critical section, etc.
|
|
||||
if (!yielding_thread->IsRunnable()) { |
|
||||
// Normally this case shouldn't happen except for SetThreadActivity.
|
|
||||
is_reselection_pending.store(true, std::memory_order_release); |
|
||||
return false; |
|
||||
} |
|
||||
const u32 core_id = static_cast<u32>(yielding_thread->GetProcessorID()); |
|
||||
const u32 priority = yielding_thread->GetPriority(); |
|
||||
|
|
||||
// Yield the thread
|
|
||||
Reschedule(priority, core_id, yielding_thread); |
|
||||
const Thread* const winner = scheduled_queue[core_id].front(); |
|
||||
if (kernel.GetCurrentHostThreadID() != core_id) { |
|
||||
is_reselection_pending.store(true, std::memory_order_release); |
|
||||
} |
|
||||
|
|
||||
return AskForReselectionOrMarkRedundant(yielding_thread, winner); |
|
||||
} |
|
||||
|
|
||||
bool GlobalScheduler::YieldThreadAndBalanceLoad(Thread* yielding_thread) { |
|
||||
ASSERT(is_locked); |
|
||||
// Note: caller should check if !thread.IsSchedulerOperationRedundant and use critical section,
|
|
||||
// etc.
|
|
||||
if (!yielding_thread->IsRunnable()) { |
|
||||
// Normally this case shouldn't happen except for SetThreadActivity.
|
|
||||
is_reselection_pending.store(true, std::memory_order_release); |
|
||||
return false; |
|
||||
} |
|
||||
const u32 core_id = static_cast<u32>(yielding_thread->GetProcessorID()); |
|
||||
const u32 priority = yielding_thread->GetPriority(); |
|
||||
|
|
||||
// Yield the thread
|
|
||||
Reschedule(priority, core_id, yielding_thread); |
|
||||
|
|
||||
std::array<Thread*, Core::Hardware::NUM_CPU_CORES> current_threads; |
|
||||
for (std::size_t i = 0; i < current_threads.size(); i++) { |
|
||||
current_threads[i] = scheduled_queue[i].empty() ? nullptr : scheduled_queue[i].front(); |
|
||||
} |
|
||||
|
|
||||
Thread* next_thread = scheduled_queue[core_id].front(priority); |
|
||||
Thread* winner = nullptr; |
|
||||
for (auto& thread : suggested_queue[core_id]) { |
|
||||
const s32 source_core = thread->GetProcessorID(); |
|
||||
if (source_core >= 0) { |
|
||||
if (current_threads[source_core] != nullptr) { |
|
||||
if (thread == current_threads[source_core] || |
|
||||
current_threads[source_core]->GetPriority() < min_regular_priority) { |
|
||||
continue; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
if (next_thread->GetLastRunningTicks() >= thread->GetLastRunningTicks() || |
|
||||
next_thread->GetPriority() < thread->GetPriority()) { |
|
||||
if (thread->GetPriority() <= priority) { |
|
||||
winner = thread; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (winner != nullptr) { |
|
||||
if (winner != yielding_thread) { |
|
||||
TransferToCore(winner->GetPriority(), s32(core_id), winner); |
|
||||
} |
|
||||
} else { |
|
||||
winner = next_thread; |
|
||||
} |
|
||||
|
|
||||
if (kernel.GetCurrentHostThreadID() != core_id) { |
|
||||
is_reselection_pending.store(true, std::memory_order_release); |
|
||||
} |
|
||||
|
|
||||
return AskForReselectionOrMarkRedundant(yielding_thread, winner); |
|
||||
} |
|
||||
|
|
||||
bool GlobalScheduler::YieldThreadAndWaitForLoadBalancing(Thread* yielding_thread) { |
|
||||
ASSERT(is_locked); |
|
||||
// Note: caller should check if !thread.IsSchedulerOperationRedundant and use critical section,
|
|
||||
// etc.
|
|
||||
if (!yielding_thread->IsRunnable()) { |
|
||||
// Normally this case shouldn't happen except for SetThreadActivity.
|
|
||||
is_reselection_pending.store(true, std::memory_order_release); |
|
||||
return false; |
|
||||
} |
|
||||
Thread* winner = nullptr; |
|
||||
const u32 core_id = static_cast<u32>(yielding_thread->GetProcessorID()); |
|
||||
|
|
||||
// Remove the thread from its scheduled mlq, put it on the corresponding "suggested" one instead
|
|
||||
TransferToCore(yielding_thread->GetPriority(), -1, yielding_thread); |
|
||||
|
|
||||
// If the core is idle, perform load balancing, excluding the threads that have just used this
|
|
||||
// function...
|
|
||||
if (scheduled_queue[core_id].empty()) { |
|
||||
// Here, "current_threads" is calculated after the ""yield"", unlike yield -1
|
|
||||
std::array<Thread*, Core::Hardware::NUM_CPU_CORES> current_threads; |
|
||||
for (std::size_t i = 0; i < current_threads.size(); i++) { |
|
||||
current_threads[i] = scheduled_queue[i].empty() ? nullptr : scheduled_queue[i].front(); |
|
||||
} |
|
||||
for (auto& thread : suggested_queue[core_id]) { |
|
||||
const s32 source_core = thread->GetProcessorID(); |
|
||||
if (source_core < 0 || thread == current_threads[source_core]) { |
|
||||
continue; |
|
||||
} |
|
||||
if (current_threads[source_core] == nullptr || |
|
||||
current_threads[source_core]->GetPriority() >= min_regular_priority) { |
|
||||
winner = thread; |
|
||||
} |
|
||||
break; |
|
||||
} |
|
||||
if (winner != nullptr) { |
|
||||
if (winner != yielding_thread) { |
|
||||
TransferToCore(winner->GetPriority(), static_cast<s32>(core_id), winner); |
|
||||
} |
|
||||
} else { |
|
||||
winner = yielding_thread; |
|
||||
} |
|
||||
} else { |
|
||||
winner = scheduled_queue[core_id].front(); |
|
||||
} |
|
||||
|
|
||||
if (kernel.GetCurrentHostThreadID() != core_id) { |
|
||||
is_reselection_pending.store(true, std::memory_order_release); |
|
||||
} |
|
||||
|
|
||||
return AskForReselectionOrMarkRedundant(yielding_thread, winner); |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::PreemptThreads() { |
|
||||
ASSERT(is_locked); |
|
||||
for (std::size_t core_id = 0; core_id < Core::Hardware::NUM_CPU_CORES; core_id++) { |
|
||||
const u32 priority = preemption_priorities[core_id]; |
|
||||
|
|
||||
if (scheduled_queue[core_id].size(priority) > 0) { |
|
||||
if (scheduled_queue[core_id].size(priority) > 1) { |
|
||||
scheduled_queue[core_id].front(priority)->IncrementYieldCount(); |
|
||||
} |
|
||||
scheduled_queue[core_id].yield(priority); |
|
||||
if (scheduled_queue[core_id].size(priority) > 1) { |
|
||||
scheduled_queue[core_id].front(priority)->IncrementYieldCount(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Thread* current_thread = |
|
||||
scheduled_queue[core_id].empty() ? nullptr : scheduled_queue[core_id].front(); |
|
||||
Thread* winner = nullptr; |
|
||||
for (auto& thread : suggested_queue[core_id]) { |
|
||||
const s32 source_core = thread->GetProcessorID(); |
|
||||
if (thread->GetPriority() != priority) { |
|
||||
continue; |
|
||||
} |
|
||||
if (source_core >= 0) { |
|
||||
Thread* next_thread = scheduled_queue[source_core].empty() |
|
||||
? nullptr |
|
||||
: scheduled_queue[source_core].front(); |
|
||||
if (next_thread != nullptr && next_thread->GetPriority() < 2) { |
|
||||
break; |
|
||||
} |
|
||||
if (next_thread == thread) { |
|
||||
continue; |
|
||||
} |
|
||||
} |
|
||||
if (current_thread != nullptr && |
|
||||
current_thread->GetLastRunningTicks() >= thread->GetLastRunningTicks()) { |
|
||||
winner = thread; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (winner != nullptr) { |
|
||||
TransferToCore(winner->GetPriority(), s32(core_id), winner); |
|
||||
current_thread = |
|
||||
winner->GetPriority() <= current_thread->GetPriority() ? winner : current_thread; |
|
||||
} |
|
||||
|
|
||||
if (current_thread != nullptr && current_thread->GetPriority() > priority) { |
|
||||
for (auto& thread : suggested_queue[core_id]) { |
|
||||
const s32 source_core = thread->GetProcessorID(); |
|
||||
if (thread->GetPriority() < priority) { |
|
||||
continue; |
|
||||
} |
|
||||
if (source_core >= 0) { |
|
||||
Thread* next_thread = scheduled_queue[source_core].empty() |
|
||||
? nullptr |
|
||||
: scheduled_queue[source_core].front(); |
|
||||
if (next_thread != nullptr && next_thread->GetPriority() < 2) { |
|
||||
break; |
|
||||
} |
|
||||
if (next_thread == thread) { |
|
||||
continue; |
|
||||
} |
|
||||
} |
|
||||
if (current_thread != nullptr && |
|
||||
current_thread->GetLastRunningTicks() >= thread->GetLastRunningTicks()) { |
|
||||
winner = thread; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (winner != nullptr) { |
|
||||
TransferToCore(winner->GetPriority(), s32(core_id), winner); |
|
||||
current_thread = winner; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
is_reselection_pending.store(true, std::memory_order_release); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::EnableInterruptAndSchedule(u32 cores_pending_reschedule, |
|
||||
Core::EmuThreadHandle global_thread) { |
|
||||
u32 current_core = global_thread.host_handle; |
|
||||
bool must_context_switch = global_thread.guest_handle != InvalidHandle && |
|
||||
(current_core < Core::Hardware::NUM_CPU_CORES); |
|
||||
while (cores_pending_reschedule != 0) { |
|
||||
u32 core = Common::CountTrailingZeroes32(cores_pending_reschedule); |
|
||||
ASSERT(core < Core::Hardware::NUM_CPU_CORES); |
|
||||
if (!must_context_switch || core != current_core) { |
|
||||
auto& phys_core = kernel.PhysicalCore(core); |
|
||||
phys_core.Interrupt(); |
|
||||
} else { |
|
||||
must_context_switch = true; |
|
||||
} |
|
||||
cores_pending_reschedule &= ~(1U << core); |
|
||||
} |
|
||||
if (must_context_switch) { |
|
||||
auto& core_scheduler = kernel.CurrentScheduler(); |
|
||||
kernel.ExitSVCProfile(); |
|
||||
core_scheduler.TryDoContextSwitch(); |
|
||||
kernel.EnterSVCProfile(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::Suggest(u32 priority, std::size_t core, Thread* thread) { |
|
||||
ASSERT(is_locked); |
|
||||
suggested_queue[core].add(thread, priority); |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::Unsuggest(u32 priority, std::size_t core, Thread* thread) { |
|
||||
ASSERT(is_locked); |
|
||||
suggested_queue[core].remove(thread, priority); |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::Schedule(u32 priority, std::size_t core, Thread* thread) { |
|
||||
ASSERT(is_locked); |
|
||||
ASSERT_MSG(thread->GetProcessorID() == s32(core), "Thread must be assigned to this core."); |
|
||||
scheduled_queue[core].add(thread, priority); |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::SchedulePrepend(u32 priority, std::size_t core, Thread* thread) { |
|
||||
ASSERT(is_locked); |
|
||||
ASSERT_MSG(thread->GetProcessorID() == s32(core), "Thread must be assigned to this core."); |
|
||||
scheduled_queue[core].add(thread, priority, false); |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::Reschedule(u32 priority, std::size_t core, Thread* thread) { |
|
||||
ASSERT(is_locked); |
|
||||
scheduled_queue[core].remove(thread, priority); |
|
||||
scheduled_queue[core].add(thread, priority); |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::Unschedule(u32 priority, std::size_t core, Thread* thread) { |
|
||||
ASSERT(is_locked); |
|
||||
scheduled_queue[core].remove(thread, priority); |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::TransferToCore(u32 priority, s32 destination_core, Thread* thread) { |
|
||||
ASSERT(is_locked); |
|
||||
const bool schedulable = thread->GetPriority() < THREADPRIO_COUNT; |
|
||||
const s32 source_core = thread->GetProcessorID(); |
|
||||
if (source_core == destination_core || !schedulable) { |
|
||||
return; |
|
||||
} |
|
||||
thread->SetProcessorID(destination_core); |
|
||||
if (source_core >= 0) { |
|
||||
Unschedule(priority, static_cast<u32>(source_core), thread); |
|
||||
} |
|
||||
if (destination_core >= 0) { |
|
||||
Unsuggest(priority, static_cast<u32>(destination_core), thread); |
|
||||
Schedule(priority, static_cast<u32>(destination_core), thread); |
|
||||
} |
|
||||
if (source_core >= 0) { |
|
||||
Suggest(priority, static_cast<u32>(source_core), thread); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
bool GlobalScheduler::AskForReselectionOrMarkRedundant(Thread* current_thread, |
|
||||
const Thread* winner) { |
|
||||
if (current_thread == winner) { |
|
||||
current_thread->IncrementYieldCount(); |
|
||||
return true; |
|
||||
} else { |
|
||||
is_reselection_pending.store(true, std::memory_order_release); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::AdjustSchedulingOnStatus(Thread* thread, u32 old_flags) { |
|
||||
if (old_flags == thread->scheduling_state) { |
|
||||
return; |
|
||||
} |
|
||||
ASSERT(is_locked); |
|
||||
|
|
||||
if (old_flags == static_cast<u32>(ThreadSchedStatus::Runnable)) { |
|
||||
// In this case the thread was running, now it's pausing/exitting
|
|
||||
if (thread->processor_id >= 0) { |
|
||||
Unschedule(thread->current_priority, static_cast<u32>(thread->processor_id), thread); |
|
||||
} |
|
||||
|
|
||||
for (u32 core = 0; core < Core::Hardware::NUM_CPU_CORES; core++) { |
|
||||
if (core != static_cast<u32>(thread->processor_id) && |
|
||||
((thread->affinity_mask >> core) & 1) != 0) { |
|
||||
Unsuggest(thread->current_priority, core, thread); |
|
||||
} |
|
||||
} |
|
||||
} else if (thread->scheduling_state == static_cast<u32>(ThreadSchedStatus::Runnable)) { |
|
||||
// The thread is now set to running from being stopped
|
|
||||
if (thread->processor_id >= 0) { |
|
||||
Schedule(thread->current_priority, static_cast<u32>(thread->processor_id), thread); |
|
||||
} |
|
||||
|
|
||||
for (u32 core = 0; core < Core::Hardware::NUM_CPU_CORES; core++) { |
|
||||
if (core != static_cast<u32>(thread->processor_id) && |
|
||||
((thread->affinity_mask >> core) & 1) != 0) { |
|
||||
Suggest(thread->current_priority, core, thread); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
SetReselectionPending(); |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::AdjustSchedulingOnPriority(Thread* thread, u32 old_priority) { |
|
||||
if (thread->scheduling_state != static_cast<u32>(ThreadSchedStatus::Runnable)) { |
|
||||
return; |
|
||||
} |
|
||||
ASSERT(is_locked); |
|
||||
if (thread->processor_id >= 0) { |
|
||||
Unschedule(old_priority, static_cast<u32>(thread->processor_id), thread); |
|
||||
} |
|
||||
|
|
||||
for (u32 core = 0; core < Core::Hardware::NUM_CPU_CORES; core++) { |
|
||||
if (core != static_cast<u32>(thread->processor_id) && |
|
||||
((thread->affinity_mask >> core) & 1) != 0) { |
|
||||
Unsuggest(old_priority, core, thread); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (thread->processor_id >= 0) { |
|
||||
if (thread == kernel.CurrentScheduler().GetCurrentThread()) { |
|
||||
SchedulePrepend(thread->current_priority, static_cast<u32>(thread->processor_id), |
|
||||
thread); |
|
||||
} else { |
|
||||
Schedule(thread->current_priority, static_cast<u32>(thread->processor_id), thread); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
for (u32 core = 0; core < Core::Hardware::NUM_CPU_CORES; core++) { |
|
||||
if (core != static_cast<u32>(thread->processor_id) && |
|
||||
((thread->affinity_mask >> core) & 1) != 0) { |
|
||||
Suggest(thread->current_priority, core, thread); |
|
||||
} |
|
||||
} |
|
||||
thread->IncrementYieldCount(); |
|
||||
SetReselectionPending(); |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::AdjustSchedulingOnAffinity(Thread* thread, u64 old_affinity_mask, |
|
||||
s32 old_core) { |
|
||||
if (thread->scheduling_state != static_cast<u32>(ThreadSchedStatus::Runnable) || |
|
||||
thread->current_priority >= THREADPRIO_COUNT) { |
|
||||
return; |
|
||||
} |
|
||||
ASSERT(is_locked); |
|
||||
|
|
||||
for (u32 core = 0; core < Core::Hardware::NUM_CPU_CORES; core++) { |
|
||||
if (((old_affinity_mask >> core) & 1) != 0) { |
|
||||
if (core == static_cast<u32>(old_core)) { |
|
||||
Unschedule(thread->current_priority, core, thread); |
|
||||
} else { |
|
||||
Unsuggest(thread->current_priority, core, thread); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
for (u32 core = 0; core < Core::Hardware::NUM_CPU_CORES; core++) { |
|
||||
if (((thread->affinity_mask >> core) & 1) != 0) { |
|
||||
if (core == static_cast<u32>(thread->processor_id)) { |
|
||||
Schedule(thread->current_priority, core, thread); |
|
||||
} else { |
|
||||
Suggest(thread->current_priority, core, thread); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
thread->IncrementYieldCount(); |
|
||||
SetReselectionPending(); |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::Shutdown() { |
|
||||
for (std::size_t core = 0; core < Core::Hardware::NUM_CPU_CORES; core++) { |
|
||||
scheduled_queue[core].clear(); |
|
||||
suggested_queue[core].clear(); |
|
||||
} |
|
||||
thread_list.clear(); |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::Lock() { |
|
||||
Core::EmuThreadHandle current_thread = kernel.GetCurrentEmuThreadID(); |
|
||||
ASSERT(!current_thread.IsInvalid()); |
|
||||
if (current_thread == current_owner) { |
|
||||
++scope_lock; |
|
||||
} else { |
|
||||
inner_lock.lock(); |
|
||||
is_locked = true; |
|
||||
current_owner = current_thread; |
|
||||
ASSERT(current_owner != Core::EmuThreadHandle::InvalidHandle()); |
|
||||
scope_lock = 1; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void GlobalScheduler::Unlock() { |
|
||||
if (--scope_lock != 0) { |
|
||||
ASSERT(scope_lock > 0); |
|
||||
return; |
|
||||
} |
|
||||
u32 cores_pending_reschedule = SelectThreads(); |
|
||||
Core::EmuThreadHandle leaving_thread = current_owner; |
|
||||
current_owner = Core::EmuThreadHandle::InvalidHandle(); |
|
||||
scope_lock = 1; |
|
||||
is_locked = false; |
|
||||
inner_lock.unlock(); |
|
||||
EnableInterruptAndSchedule(cores_pending_reschedule, leaving_thread); |
|
||||
} |
|
||||
|
|
||||
Scheduler::Scheduler(Core::System& system, std::size_t core_id) : system(system), core_id(core_id) { |
|
||||
switch_fiber = std::make_shared<Common::Fiber>(std::function<void(void*)>(OnSwitch), this); |
|
||||
} |
|
||||
|
|
||||
Scheduler::~Scheduler() = default; |
|
||||
|
|
||||
bool Scheduler::HaveReadyThreads() const { |
|
||||
return system.GlobalScheduler().HaveReadyThreads(core_id); |
|
||||
} |
|
||||
|
|
||||
Thread* Scheduler::GetCurrentThread() const { |
|
||||
if (current_thread) { |
|
||||
return current_thread.get(); |
|
||||
} |
|
||||
return idle_thread.get(); |
|
||||
} |
|
||||
|
|
||||
Thread* Scheduler::GetSelectedThread() const { |
|
||||
return selected_thread.get(); |
|
||||
} |
|
||||
|
|
||||
u64 Scheduler::GetLastContextSwitchTicks() const { |
|
||||
return last_context_switch_time; |
|
||||
} |
|
||||
|
|
||||
void Scheduler::TryDoContextSwitch() { |
|
||||
auto& phys_core = system.Kernel().CurrentPhysicalCore(); |
|
||||
if (phys_core.IsInterrupted()) { |
|
||||
phys_core.ClearInterrupt(); |
|
||||
} |
|
||||
guard.lock(); |
|
||||
if (is_context_switch_pending) { |
|
||||
SwitchContext(); |
|
||||
} else { |
|
||||
guard.unlock(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void Scheduler::OnThreadStart() { |
|
||||
SwitchContextStep2(); |
|
||||
} |
|
||||
|
|
||||
void Scheduler::Unload(Thread* thread) { |
|
||||
if (thread) { |
|
||||
thread->last_running_ticks = system.CoreTiming().GetCPUTicks(); |
|
||||
thread->SetIsRunning(false); |
|
||||
if (thread->IsContinuousOnSVC() && !thread->IsHLEThread()) { |
|
||||
system.ArmInterface(core_id).ExceptionalExit(); |
|
||||
thread->SetContinuousOnSVC(false); |
|
||||
} |
|
||||
if (!thread->IsHLEThread() && !thread->HasExited()) { |
|
||||
Core::ARM_Interface& cpu_core = system.ArmInterface(core_id); |
|
||||
cpu_core.SaveContext(thread->GetContext32()); |
|
||||
cpu_core.SaveContext(thread->GetContext64()); |
|
||||
// Save the TPIDR_EL0 system register in case it was modified.
|
|
||||
thread->SetTPIDR_EL0(cpu_core.GetTPIDR_EL0()); |
|
||||
cpu_core.ClearExclusiveState(); |
|
||||
} |
|
||||
thread->context_guard.unlock(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void Scheduler::Unload() { |
|
||||
Unload(current_thread.get()); |
|
||||
} |
|
||||
|
|
||||
void Scheduler::Reload(Thread* thread) { |
|
||||
if (thread) { |
|
||||
ASSERT_MSG(thread->GetSchedulingStatus() == ThreadSchedStatus::Runnable, |
|
||||
"Thread must be runnable."); |
|
||||
|
|
||||
// Cancel any outstanding wakeup events for this thread
|
|
||||
thread->SetIsRunning(true); |
|
||||
thread->SetWasRunning(false); |
|
||||
thread->last_running_ticks = system.CoreTiming().GetCPUTicks(); |
|
||||
|
|
||||
auto* const thread_owner_process = thread->GetOwnerProcess(); |
|
||||
if (thread_owner_process != nullptr) { |
|
||||
system.Kernel().MakeCurrentProcess(thread_owner_process); |
|
||||
} |
|
||||
if (!thread->IsHLEThread()) { |
|
||||
Core::ARM_Interface& cpu_core = system.ArmInterface(core_id); |
|
||||
cpu_core.LoadContext(thread->GetContext32()); |
|
||||
cpu_core.LoadContext(thread->GetContext64()); |
|
||||
cpu_core.SetTlsAddress(thread->GetTLSAddress()); |
|
||||
cpu_core.SetTPIDR_EL0(thread->GetTPIDR_EL0()); |
|
||||
cpu_core.ClearExclusiveState(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void Scheduler::Reload() { |
|
||||
Reload(current_thread.get()); |
|
||||
} |
|
||||
|
|
||||
void Scheduler::SwitchContextStep2() { |
|
||||
// Load context of new thread
|
|
||||
Reload(selected_thread.get()); |
|
||||
|
|
||||
TryDoContextSwitch(); |
|
||||
} |
|
||||
|
|
||||
void Scheduler::SwitchContext() { |
|
||||
current_thread_prev = current_thread; |
|
||||
selected_thread = selected_thread_set; |
|
||||
Thread* previous_thread = current_thread_prev.get(); |
|
||||
Thread* new_thread = selected_thread.get(); |
|
||||
current_thread = selected_thread; |
|
||||
|
|
||||
is_context_switch_pending = false; |
|
||||
|
|
||||
if (new_thread == previous_thread) { |
|
||||
guard.unlock(); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
Process* const previous_process = system.Kernel().CurrentProcess(); |
|
||||
|
|
||||
UpdateLastContextSwitchTime(previous_thread, previous_process); |
|
||||
|
|
||||
// Save context for previous thread
|
|
||||
Unload(previous_thread); |
|
||||
|
|
||||
std::shared_ptr<Common::Fiber>* old_context; |
|
||||
if (previous_thread != nullptr) { |
|
||||
old_context = &previous_thread->GetHostContext(); |
|
||||
} else { |
|
||||
old_context = &idle_thread->GetHostContext(); |
|
||||
} |
|
||||
guard.unlock(); |
|
||||
|
|
||||
Common::Fiber::YieldTo(*old_context, switch_fiber); |
|
||||
/// When a thread wakes up, the scheduler may have changed to other in another core.
|
|
||||
auto& next_scheduler = system.Kernel().CurrentScheduler(); |
|
||||
next_scheduler.SwitchContextStep2(); |
|
||||
} |
|
||||
|
|
||||
void Scheduler::OnSwitch(void* this_scheduler) { |
|
||||
Scheduler* sched = static_cast<Scheduler*>(this_scheduler); |
|
||||
sched->SwitchToCurrent(); |
|
||||
} |
|
||||
|
|
||||
void Scheduler::SwitchToCurrent() { |
|
||||
while (true) { |
|
||||
{ |
|
||||
std::scoped_lock lock{guard}; |
|
||||
selected_thread = selected_thread_set; |
|
||||
current_thread = selected_thread; |
|
||||
is_context_switch_pending = false; |
|
||||
} |
|
||||
const auto is_switch_pending = [this] { |
|
||||
std::scoped_lock lock{guard}; |
|
||||
return is_context_switch_pending; |
|
||||
}; |
|
||||
do { |
|
||||
if (current_thread != nullptr && !current_thread->IsHLEThread()) { |
|
||||
current_thread->context_guard.lock(); |
|
||||
if (!current_thread->IsRunnable()) { |
|
||||
current_thread->context_guard.unlock(); |
|
||||
break; |
|
||||
} |
|
||||
if (static_cast<u32>(current_thread->GetProcessorID()) != core_id) { |
|
||||
current_thread->context_guard.unlock(); |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
std::shared_ptr<Common::Fiber>* next_context; |
|
||||
if (current_thread != nullptr) { |
|
||||
next_context = ¤t_thread->GetHostContext(); |
|
||||
} else { |
|
||||
next_context = &idle_thread->GetHostContext(); |
|
||||
} |
|
||||
Common::Fiber::YieldTo(switch_fiber, *next_context); |
|
||||
} while (!is_switch_pending()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void Scheduler::UpdateLastContextSwitchTime(Thread* thread, Process* process) { |
|
||||
const u64 prev_switch_ticks = last_context_switch_time; |
|
||||
const u64 most_recent_switch_ticks = system.CoreTiming().GetCPUTicks(); |
|
||||
const u64 update_ticks = most_recent_switch_ticks - prev_switch_ticks; |
|
||||
|
|
||||
if (thread != nullptr) { |
|
||||
thread->UpdateCPUTimeTicks(update_ticks); |
|
||||
} |
|
||||
|
|
||||
if (process != nullptr) { |
|
||||
process->UpdateCPUTimeTicks(update_ticks); |
|
||||
} |
|
||||
|
|
||||
last_context_switch_time = most_recent_switch_ticks; |
|
||||
} |
|
||||
|
|
||||
void Scheduler::Initialize() { |
|
||||
std::string name = "Idle Thread Id:" + std::to_string(core_id); |
|
||||
std::function<void(void*)> init_func = Core::CpuManager::GetIdleThreadStartFunc(); |
|
||||
void* init_func_parameter = system.GetCpuManager().GetStartFuncParamater(); |
|
||||
ThreadType type = static_cast<ThreadType>(THREADTYPE_KERNEL | THREADTYPE_HLE | THREADTYPE_IDLE); |
|
||||
auto thread_res = Thread::Create(system, type, name, 0, 64, 0, static_cast<u32>(core_id), 0, |
|
||||
nullptr, std::move(init_func), init_func_parameter); |
|
||||
idle_thread = std::move(thread_res).Unwrap(); |
|
||||
} |
|
||||
|
|
||||
void Scheduler::Shutdown() { |
|
||||
current_thread = nullptr; |
|
||||
selected_thread = nullptr; |
|
||||
} |
|
||||
|
|
||||
SchedulerLock::SchedulerLock(KernelCore& kernel) : kernel{kernel} { |
|
||||
kernel.GlobalScheduler().Lock(); |
|
||||
} |
|
||||
|
|
||||
SchedulerLock::~SchedulerLock() { |
|
||||
kernel.GlobalScheduler().Unlock(); |
|
||||
} |
|
||||
|
|
||||
SchedulerLockAndSleep::SchedulerLockAndSleep(KernelCore& kernel, Handle& event_handle, |
|
||||
Thread* time_task, s64 nanoseconds) |
|
||||
: SchedulerLock{kernel}, event_handle{event_handle}, time_task{time_task}, nanoseconds{ |
|
||||
nanoseconds} { |
|
||||
event_handle = InvalidHandle; |
|
||||
} |
|
||||
|
|
||||
SchedulerLockAndSleep::~SchedulerLockAndSleep() { |
|
||||
if (sleep_cancelled) { |
|
||||
return; |
|
||||
} |
|
||||
auto& time_manager = kernel.TimeManager(); |
|
||||
time_manager.ScheduleTimeEvent(event_handle, time_task, nanoseconds); |
|
||||
} |
|
||||
|
|
||||
void SchedulerLockAndSleep::Release() { |
|
||||
if (sleep_cancelled) { |
|
||||
return; |
|
||||
} |
|
||||
auto& time_manager = kernel.TimeManager(); |
|
||||
time_manager.ScheduleTimeEvent(event_handle, time_task, nanoseconds); |
|
||||
sleep_cancelled = true; |
|
||||
} |
|
||||
|
|
||||
} // namespace Kernel
|
|
||||
@ -1,320 +0,0 @@ |
|||||
// Copyright 2018 yuzu emulator team |
|
||||
// Licensed under GPLv2 or any later version |
|
||||
// Refer to the license.txt file included. |
|
||||
|
|
||||
#pragma once |
|
||||
|
|
||||
#include <atomic> |
|
||||
#include <memory> |
|
||||
#include <mutex> |
|
||||
#include <vector> |
|
||||
|
|
||||
#include "common/common_types.h" |
|
||||
#include "common/multi_level_queue.h" |
|
||||
#include "common/spin_lock.h" |
|
||||
#include "core/hardware_properties.h" |
|
||||
#include "core/hle/kernel/thread.h" |
|
||||
|
|
||||
namespace Common { |
|
||||
class Fiber; |
|
||||
} |
|
||||
|
|
||||
namespace Core { |
|
||||
class ARM_Interface; |
|
||||
class System; |
|
||||
} // namespace Core |
|
||||
|
|
||||
namespace Kernel { |
|
||||
|
|
||||
class KernelCore; |
|
||||
class Process; |
|
||||
class SchedulerLock; |
|
||||
|
|
||||
class GlobalScheduler final { |
|
||||
public: |
|
||||
explicit GlobalScheduler(KernelCore& kernel); |
|
||||
~GlobalScheduler(); |
|
||||
|
|
||||
/// Adds a new thread to the scheduler |
|
||||
void AddThread(std::shared_ptr<Thread> thread); |
|
||||
|
|
||||
/// Removes a thread from the scheduler |
|
||||
void RemoveThread(std::shared_ptr<Thread> thread); |
|
||||
|
|
||||
/// Returns a list of all threads managed by the scheduler |
|
||||
const std::vector<std::shared_ptr<Thread>>& GetThreadList() const { |
|
||||
return thread_list; |
|
||||
} |
|
||||
|
|
||||
/// Notify the scheduler a thread's status has changed. |
|
||||
void AdjustSchedulingOnStatus(Thread* thread, u32 old_flags); |
|
||||
|
|
||||
/// Notify the scheduler a thread's priority has changed. |
|
||||
void AdjustSchedulingOnPriority(Thread* thread, u32 old_priority); |
|
||||
|
|
||||
/// Notify the scheduler a thread's core and/or affinity mask has changed. |
|
||||
void AdjustSchedulingOnAffinity(Thread* thread, u64 old_affinity_mask, s32 old_core); |
|
||||
|
|
||||
/** |
|
||||
* Takes care of selecting the new scheduled threads in three steps: |
|
||||
* |
|
||||
* 1. First a thread is selected from the top of the priority queue. If no thread |
|
||||
* is obtained then we move to step two, else we are done. |
|
||||
* |
|
||||
* 2. Second we try to get a suggested thread that's not assigned to any core or |
|
||||
* that is not the top thread in that core. |
|
||||
* |
|
||||
* 3. Third is no suggested thread is found, we do a second pass and pick a running |
|
||||
* thread in another core and swap it with its current thread. |
|
||||
* |
|
||||
* returns the cores needing scheduling. |
|
||||
*/ |
|
||||
u32 SelectThreads(); |
|
||||
|
|
||||
bool HaveReadyThreads(std::size_t core_id) const { |
|
||||
return !scheduled_queue[core_id].empty(); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Takes a thread and moves it to the back of the it's priority list. |
|
||||
* |
|
||||
* @note This operation can be redundant and no scheduling is changed if marked as so. |
|
||||
*/ |
|
||||
bool YieldThread(Thread* thread); |
|
||||
|
|
||||
/** |
|
||||
* Takes a thread and moves it to the back of the it's priority list. |
|
||||
* Afterwards, tries to pick a suggested thread from the suggested queue that has worse time or |
|
||||
* a better priority than the next thread in the core. |
|
||||
* |
|
||||
* @note This operation can be redundant and no scheduling is changed if marked as so. |
|
||||
*/ |
|
||||
bool YieldThreadAndBalanceLoad(Thread* thread); |
|
||||
|
|
||||
/** |
|
||||
* Takes a thread and moves it out of the scheduling queue. |
|
||||
* and into the suggested queue. If no thread can be scheduled afterwards in that core, |
|
||||
* a suggested thread is obtained instead. |
|
||||
* |
|
||||
* @note This operation can be redundant and no scheduling is changed if marked as so. |
|
||||
*/ |
|
||||
bool YieldThreadAndWaitForLoadBalancing(Thread* thread); |
|
||||
|
|
||||
/** |
|
||||
* Rotates the scheduling queues of threads at a preemption priority and then does |
|
||||
* some core rebalancing. Preemption priorities can be found in the array |
|
||||
* 'preemption_priorities'. |
|
||||
* |
|
||||
* @note This operation happens every 10ms. |
|
||||
*/ |
|
||||
void PreemptThreads(); |
|
||||
|
|
||||
u32 CpuCoresCount() const { |
|
||||
return Core::Hardware::NUM_CPU_CORES; |
|
||||
} |
|
||||
|
|
||||
void SetReselectionPending() { |
|
||||
is_reselection_pending.store(true, std::memory_order_release); |
|
||||
} |
|
||||
|
|
||||
bool IsReselectionPending() const { |
|
||||
return is_reselection_pending.load(std::memory_order_acquire); |
|
||||
} |
|
||||
|
|
||||
void Shutdown(); |
|
||||
|
|
||||
private: |
|
||||
friend class SchedulerLock; |
|
||||
|
|
||||
/// Lock the scheduler to the current thread. |
|
||||
void Lock(); |
|
||||
|
|
||||
/// Unlocks the scheduler, reselects threads, interrupts cores for rescheduling |
|
||||
/// and reschedules current core if needed. |
|
||||
void Unlock(); |
|
||||
|
|
||||
void EnableInterruptAndSchedule(u32 cores_pending_reschedule, |
|
||||
Core::EmuThreadHandle global_thread); |
|
||||
|
|
||||
/** |
|
||||
* Add a thread to the suggested queue of a cpu core. Suggested threads may be |
|
||||
* picked if no thread is scheduled to run on the core. |
|
||||
*/ |
|
||||
void Suggest(u32 priority, std::size_t core, Thread* thread); |
|
||||
|
|
||||
/** |
|
||||
* Remove a thread to the suggested queue of a cpu core. Suggested threads may be |
|
||||
* picked if no thread is scheduled to run on the core. |
|
||||
*/ |
|
||||
void Unsuggest(u32 priority, std::size_t core, Thread* thread); |
|
||||
|
|
||||
/** |
|
||||
* Add a thread to the scheduling queue of a cpu core. The thread is added at the |
|
||||
* back the queue in its priority level. |
|
||||
*/ |
|
||||
void Schedule(u32 priority, std::size_t core, Thread* thread); |
|
||||
|
|
||||
/** |
|
||||
* Add a thread to the scheduling queue of a cpu core. The thread is added at the |
|
||||
* front the queue in its priority level. |
|
||||
*/ |
|
||||
void SchedulePrepend(u32 priority, std::size_t core, Thread* thread); |
|
||||
|
|
||||
/// Reschedule an already scheduled thread based on a new priority |
|
||||
void Reschedule(u32 priority, std::size_t core, Thread* thread); |
|
||||
|
|
||||
/// Unschedules a thread. |
|
||||
void Unschedule(u32 priority, std::size_t core, Thread* thread); |
|
||||
|
|
||||
/** |
|
||||
* Transfers a thread into an specific core. If the destination_core is -1 |
|
||||
* it will be unscheduled from its source code and added into its suggested |
|
||||
* queue. |
|
||||
*/ |
|
||||
void TransferToCore(u32 priority, s32 destination_core, Thread* thread); |
|
||||
|
|
||||
bool AskForReselectionOrMarkRedundant(Thread* current_thread, const Thread* winner); |
|
||||
|
|
||||
static constexpr u32 min_regular_priority = 2; |
|
||||
std::array<Common::MultiLevelQueue<Thread*, THREADPRIO_COUNT>, Core::Hardware::NUM_CPU_CORES> |
|
||||
scheduled_queue; |
|
||||
std::array<Common::MultiLevelQueue<Thread*, THREADPRIO_COUNT>, Core::Hardware::NUM_CPU_CORES> |
|
||||
suggested_queue; |
|
||||
std::atomic<bool> is_reselection_pending{false}; |
|
||||
|
|
||||
// The priority levels at which the global scheduler preempts threads every 10 ms. They are |
|
||||
// ordered from Core 0 to Core 3. |
|
||||
std::array<u32, Core::Hardware::NUM_CPU_CORES> preemption_priorities = {59, 59, 59, 62}; |
|
||||
|
|
||||
/// Scheduler lock mechanisms. |
|
||||
bool is_locked{}; |
|
||||
std::mutex inner_lock; |
|
||||
std::atomic<s64> scope_lock{}; |
|
||||
Core::EmuThreadHandle current_owner{Core::EmuThreadHandle::InvalidHandle()}; |
|
||||
|
|
||||
Common::SpinLock global_list_guard{}; |
|
||||
|
|
||||
/// Lists all thread ids that aren't deleted/etc. |
|
||||
std::vector<std::shared_ptr<Thread>> thread_list; |
|
||||
KernelCore& kernel; |
|
||||
}; |
|
||||
|
|
||||
class Scheduler final { |
|
||||
public: |
|
||||
explicit Scheduler(Core::System& system, std::size_t core_id); |
|
||||
~Scheduler(); |
|
||||
|
|
||||
/// Returns whether there are any threads that are ready to run. |
|
||||
bool HaveReadyThreads() const; |
|
||||
|
|
||||
/// Reschedules to the next available thread (call after current thread is suspended) |
|
||||
void TryDoContextSwitch(); |
|
||||
|
|
||||
/// The next two are for SingleCore Only. |
|
||||
/// Unload current thread before preempting core. |
|
||||
void Unload(Thread* thread); |
|
||||
void Unload(); |
|
||||
/// Reload current thread after core preemption. |
|
||||
void Reload(Thread* thread); |
|
||||
void Reload(); |
|
||||
|
|
||||
/// Gets the current running thread |
|
||||
Thread* GetCurrentThread() const; |
|
||||
|
|
||||
/// Gets the currently selected thread from the top of the multilevel queue |
|
||||
Thread* GetSelectedThread() const; |
|
||||
|
|
||||
/// Gets the timestamp for the last context switch in ticks. |
|
||||
u64 GetLastContextSwitchTicks() const; |
|
||||
|
|
||||
bool ContextSwitchPending() const { |
|
||||
return is_context_switch_pending; |
|
||||
} |
|
||||
|
|
||||
void Initialize(); |
|
||||
|
|
||||
/// Shutdowns the scheduler. |
|
||||
void Shutdown(); |
|
||||
|
|
||||
void OnThreadStart(); |
|
||||
|
|
||||
std::shared_ptr<Common::Fiber>& ControlContext() { |
|
||||
return switch_fiber; |
|
||||
} |
|
||||
|
|
||||
const std::shared_ptr<Common::Fiber>& ControlContext() const { |
|
||||
return switch_fiber; |
|
||||
} |
|
||||
|
|
||||
private: |
|
||||
friend class GlobalScheduler; |
|
||||
|
|
||||
/// Switches the CPU's active thread context to that of the specified thread |
|
||||
void SwitchContext(); |
|
||||
|
|
||||
/// When a thread wakes up, it must run this through it's new scheduler |
|
||||
void SwitchContextStep2(); |
|
||||
|
|
||||
/** |
|
||||
* Called on every context switch to update the internal timestamp |
|
||||
* This also updates the running time ticks for the given thread and |
|
||||
* process using the following difference: |
|
||||
* |
|
||||
* ticks += most_recent_ticks - last_context_switch_ticks |
|
||||
* |
|
||||
* The internal tick timestamp for the scheduler is simply the |
|
||||
* most recent tick count retrieved. No special arithmetic is |
|
||||
* applied to it. |
|
||||
*/ |
|
||||
void UpdateLastContextSwitchTime(Thread* thread, Process* process); |
|
||||
|
|
||||
static void OnSwitch(void* this_scheduler); |
|
||||
void SwitchToCurrent(); |
|
||||
|
|
||||
std::shared_ptr<Thread> current_thread = nullptr; |
|
||||
std::shared_ptr<Thread> selected_thread = nullptr; |
|
||||
std::shared_ptr<Thread> current_thread_prev = nullptr; |
|
||||
std::shared_ptr<Thread> selected_thread_set = nullptr; |
|
||||
std::shared_ptr<Thread> idle_thread = nullptr; |
|
||||
|
|
||||
std::shared_ptr<Common::Fiber> switch_fiber = nullptr; |
|
||||
|
|
||||
Core::System& system; |
|
||||
u64 last_context_switch_time = 0; |
|
||||
u64 idle_selection_count = 0; |
|
||||
const std::size_t core_id; |
|
||||
|
|
||||
Common::SpinLock guard{}; |
|
||||
|
|
||||
bool is_context_switch_pending = false; |
|
||||
}; |
|
||||
|
|
||||
class SchedulerLock { |
|
||||
public: |
|
||||
[[nodiscard]] explicit SchedulerLock(KernelCore& kernel); |
|
||||
~SchedulerLock(); |
|
||||
|
|
||||
protected: |
|
||||
KernelCore& kernel; |
|
||||
}; |
|
||||
|
|
||||
class SchedulerLockAndSleep : public SchedulerLock { |
|
||||
public: |
|
||||
explicit SchedulerLockAndSleep(KernelCore& kernel, Handle& event_handle, Thread* time_task, |
|
||||
s64 nanoseconds); |
|
||||
~SchedulerLockAndSleep(); |
|
||||
|
|
||||
void CancelSleep() { |
|
||||
sleep_cancelled = true; |
|
||||
} |
|
||||
|
|
||||
void Release(); |
|
||||
|
|
||||
private: |
|
||||
Handle& event_handle; |
|
||||
Thread* time_task; |
|
||||
s64 nanoseconds; |
|
||||
bool sleep_cancelled{}; |
|
||||
}; |
|
||||
|
|
||||
} // namespace Kernel |
|
||||
@ -1,55 +0,0 @@ |
|||||
// Copyright 2019 Yuzu Emulator Project
|
|
||||
// Licensed under GPLv2 or any later version
|
|
||||
// Refer to the license.txt file included.
|
|
||||
|
|
||||
#include <catch2/catch.hpp>
|
|
||||
#include <math.h>
|
|
||||
#include "common/common_types.h"
|
|
||||
#include "common/multi_level_queue.h"
|
|
||||
|
|
||||
namespace Common { |
|
||||
|
|
||||
TEST_CASE("MultiLevelQueue", "[common]") { |
|
||||
std::array<f32, 8> values = {0.0, 5.0, 1.0, 9.0, 8.0, 2.0, 6.0, 7.0}; |
|
||||
Common::MultiLevelQueue<f32, 64> mlq; |
|
||||
REQUIRE(mlq.empty()); |
|
||||
mlq.add(values[2], 2); |
|
||||
mlq.add(values[7], 7); |
|
||||
mlq.add(values[3], 3); |
|
||||
mlq.add(values[4], 4); |
|
||||
mlq.add(values[0], 0); |
|
||||
mlq.add(values[5], 5); |
|
||||
mlq.add(values[6], 6); |
|
||||
mlq.add(values[1], 1); |
|
||||
u32 index = 0; |
|
||||
bool all_set = true; |
|
||||
for (auto& f : mlq) { |
|
||||
all_set &= (f == values[index]); |
|
||||
index++; |
|
||||
} |
|
||||
REQUIRE(all_set); |
|
||||
REQUIRE(!mlq.empty()); |
|
||||
f32 v = 8.0; |
|
||||
mlq.add(v, 2); |
|
||||
v = -7.0; |
|
||||
mlq.add(v, 2, false); |
|
||||
REQUIRE(mlq.front(2) == -7.0); |
|
||||
mlq.yield(2); |
|
||||
REQUIRE(mlq.front(2) == values[2]); |
|
||||
REQUIRE(mlq.back(2) == -7.0); |
|
||||
REQUIRE(mlq.empty(8)); |
|
||||
v = 10.0; |
|
||||
mlq.add(v, 8); |
|
||||
mlq.adjust(v, 8, 9); |
|
||||
REQUIRE(mlq.front(9) == v); |
|
||||
REQUIRE(mlq.empty(8)); |
|
||||
REQUIRE(!mlq.empty(9)); |
|
||||
mlq.adjust(values[0], 0, 9); |
|
||||
REQUIRE(mlq.highest_priority_set() == 1); |
|
||||
REQUIRE(mlq.lowest_priority_set() == 9); |
|
||||
mlq.remove(values[1], 1); |
|
||||
REQUIRE(mlq.highest_priority_set() == 2); |
|
||||
REQUIRE(mlq.empty(1)); |
|
||||
} |
|
||||
|
|
||||
} // namespace Common
|
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue