mirror of
https://git.suyu.dev/suyu/suyu
synced 2024-11-01 04:47:53 +00:00
Kernel: Move WaitObject to a separate file
Now that HandleTable doesn't directly depend on WaitObject anymore, this can be separated from the main kernel.h header.
This commit is contained in:
parent
9453223075
commit
64ecf81a3c
15 changed files with 178 additions and 135 deletions
|
@ -10,6 +10,7 @@
|
||||||
#include "core/hle/kernel/semaphore.h"
|
#include "core/hle/kernel/semaphore.h"
|
||||||
#include "core/hle/kernel/thread.h"
|
#include "core/hle/kernel/thread.h"
|
||||||
#include "core/hle/kernel/timer.h"
|
#include "core/hle/kernel/timer.h"
|
||||||
|
#include "core/hle/kernel/wait_object.h"
|
||||||
|
|
||||||
WaitTreeItem::~WaitTreeItem() {}
|
WaitTreeItem::~WaitTreeItem() {}
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,10 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <boost/container/flat_set.hpp>
|
|
||||||
|
|
||||||
#include <QAbstractItemModel>
|
#include <QAbstractItemModel>
|
||||||
#include <QDockWidget>
|
#include <QDockWidget>
|
||||||
#include <QTreeView>
|
#include <QTreeView>
|
||||||
|
#include <boost/container/flat_set.hpp>
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/hle/kernel/kernel.h"
|
#include "core/hle/kernel/kernel.h"
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ set(SRCS
|
||||||
hle/kernel/thread.cpp
|
hle/kernel/thread.cpp
|
||||||
hle/kernel/timer.cpp
|
hle/kernel/timer.cpp
|
||||||
hle/kernel/vm_manager.cpp
|
hle/kernel/vm_manager.cpp
|
||||||
|
hle/kernel/wait_object.cpp
|
||||||
hle/service/ac/ac.cpp
|
hle/service/ac/ac.cpp
|
||||||
hle/service/ac/ac_i.cpp
|
hle/service/ac/ac_i.cpp
|
||||||
hle/service/ac/ac_u.cpp
|
hle/service/ac/ac_u.cpp
|
||||||
|
@ -249,6 +250,7 @@ set(HEADERS
|
||||||
hle/kernel/thread.h
|
hle/kernel/thread.h
|
||||||
hle/kernel/timer.h
|
hle/kernel/timer.h
|
||||||
hle/kernel/vm_manager.h
|
hle/kernel/vm_manager.h
|
||||||
|
hle/kernel/wait_object.h
|
||||||
hle/result.h
|
hle/result.h
|
||||||
hle/service/ac/ac.h
|
hle/service/ac/ac.h
|
||||||
hle/service/ac/ac_i.h
|
hle/service/ac/ac_i.h
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "core/hle/kernel/kernel.h"
|
#include "core/hle/kernel/kernel.h"
|
||||||
|
#include "core/hle/kernel/wait_object.h"
|
||||||
|
|
||||||
namespace Kernel {
|
namespace Kernel {
|
||||||
|
|
||||||
|
|
|
@ -20,85 +20,6 @@ namespace Kernel {
|
||||||
unsigned int Object::next_object_id;
|
unsigned int Object::next_object_id;
|
||||||
HandleTable g_handle_table;
|
HandleTable g_handle_table;
|
||||||
|
|
||||||
void WaitObject::AddWaitingThread(SharedPtr<Thread> thread) {
|
|
||||||
auto itr = std::find(waiting_threads.begin(), waiting_threads.end(), thread);
|
|
||||||
if (itr == waiting_threads.end())
|
|
||||||
waiting_threads.push_back(std::move(thread));
|
|
||||||
}
|
|
||||||
|
|
||||||
void WaitObject::RemoveWaitingThread(Thread* thread) {
|
|
||||||
auto itr = std::find(waiting_threads.begin(), waiting_threads.end(), thread);
|
|
||||||
// If a thread passed multiple handles to the same object,
|
|
||||||
// the kernel might attempt to remove the thread from the object's
|
|
||||||
// waiting threads list multiple times.
|
|
||||||
if (itr != waiting_threads.end())
|
|
||||||
waiting_threads.erase(itr);
|
|
||||||
}
|
|
||||||
|
|
||||||
SharedPtr<Thread> WaitObject::GetHighestPriorityReadyThread() {
|
|
||||||
Thread* candidate = nullptr;
|
|
||||||
s32 candidate_priority = THREADPRIO_LOWEST + 1;
|
|
||||||
|
|
||||||
for (const auto& thread : waiting_threads) {
|
|
||||||
// The list of waiting threads must not contain threads that are not waiting to be awakened.
|
|
||||||
ASSERT_MSG(thread->status == THREADSTATUS_WAIT_SYNCH_ANY ||
|
|
||||||
thread->status == THREADSTATUS_WAIT_SYNCH_ALL,
|
|
||||||
"Inconsistent thread statuses in waiting_threads");
|
|
||||||
|
|
||||||
if (thread->current_priority >= candidate_priority)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (ShouldWait(thread.get()))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// A thread is ready to run if it's either in THREADSTATUS_WAIT_SYNCH_ANY or
|
|
||||||
// in THREADSTATUS_WAIT_SYNCH_ALL and the rest of the objects it is waiting on are ready.
|
|
||||||
bool ready_to_run = true;
|
|
||||||
if (thread->status == THREADSTATUS_WAIT_SYNCH_ALL) {
|
|
||||||
ready_to_run = std::none_of(thread->wait_objects.begin(), thread->wait_objects.end(),
|
|
||||||
[&thread](const SharedPtr<WaitObject>& object) {
|
|
||||||
return object->ShouldWait(thread.get());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ready_to_run) {
|
|
||||||
candidate = thread.get();
|
|
||||||
candidate_priority = thread->current_priority;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WaitObject::WakeupAllWaitingThreads() {
|
|
||||||
while (auto thread = GetHighestPriorityReadyThread()) {
|
|
||||||
if (!thread->IsSleepingOnWaitAll()) {
|
|
||||||
Acquire(thread.get());
|
|
||||||
// Set the output index of the WaitSynchronizationN call to the index of this object.
|
|
||||||
if (thread->wait_set_output) {
|
|
||||||
thread->SetWaitSynchronizationOutput(thread->GetWaitObjectIndex(this));
|
|
||||||
thread->wait_set_output = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (auto& object : thread->wait_objects) {
|
|
||||||
object->Acquire(thread.get());
|
|
||||||
}
|
|
||||||
// Note: This case doesn't update the output index of WaitSynchronizationN.
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto& object : thread->wait_objects)
|
|
||||||
object->RemoveWaitingThread(thread.get());
|
|
||||||
thread->wait_objects.clear();
|
|
||||||
|
|
||||||
thread->SetWaitSynchronizationResult(RESULT_SUCCESS);
|
|
||||||
thread->ResumeFromWait();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<SharedPtr<Thread>>& WaitObject::GetWaitingThreads() const {
|
|
||||||
return waiting_threads;
|
|
||||||
}
|
|
||||||
|
|
||||||
HandleTable::HandleTable() {
|
HandleTable::HandleTable() {
|
||||||
next_generation = 1;
|
next_generation = 1;
|
||||||
Clear();
|
Clear();
|
||||||
|
|
|
@ -17,8 +17,6 @@ namespace Kernel {
|
||||||
|
|
||||||
using Handle = u32;
|
using Handle = u32;
|
||||||
|
|
||||||
class Thread;
|
|
||||||
|
|
||||||
enum KernelHandle : Handle {
|
enum KernelHandle : Handle {
|
||||||
CurrentThread = 0xFFFF8000,
|
CurrentThread = 0xFFFF8000,
|
||||||
CurrentProcess = 0xFFFF8001,
|
CurrentProcess = 0xFFFF8001,
|
||||||
|
@ -133,57 +131,6 @@ inline SharedPtr<T> DynamicObjectCast(SharedPtr<Object> object) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Class that represents a Kernel object that a thread can be waiting on
|
|
||||||
class WaitObject : public Object {
|
|
||||||
public:
|
|
||||||
/**
|
|
||||||
* Check if the specified thread should wait until the object is available
|
|
||||||
* @param thread The thread about which we're deciding.
|
|
||||||
* @return True if the current thread should wait due to this object being unavailable
|
|
||||||
*/
|
|
||||||
virtual bool ShouldWait(Thread* thread) const = 0;
|
|
||||||
|
|
||||||
/// Acquire/lock the object for the specified thread if it is available
|
|
||||||
virtual void Acquire(Thread* thread) = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a thread to wait on this object
|
|
||||||
* @param thread Pointer to thread to add
|
|
||||||
*/
|
|
||||||
virtual void AddWaitingThread(SharedPtr<Thread> thread);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a thread from waiting on this object (e.g. if it was resumed already)
|
|
||||||
* @param thread Pointer to thread to remove
|
|
||||||
*/
|
|
||||||
virtual void RemoveWaitingThread(Thread* thread);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wake up all threads waiting on this object that can be awoken, in priority order,
|
|
||||||
* and set the synchronization result and output of the thread.
|
|
||||||
*/
|
|
||||||
virtual void WakeupAllWaitingThreads();
|
|
||||||
|
|
||||||
/// Obtains the highest priority thread that is ready to run from this object's waiting list.
|
|
||||||
SharedPtr<Thread> GetHighestPriorityReadyThread();
|
|
||||||
|
|
||||||
/// Get a const reference to the waiting threads list for debug use
|
|
||||||
const std::vector<SharedPtr<Thread>>& GetWaitingThreads() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
/// Threads waiting for this object to become available
|
|
||||||
std::vector<SharedPtr<Thread>> waiting_threads;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Specialization of DynamicObjectCast for WaitObjects
|
|
||||||
template <>
|
|
||||||
inline SharedPtr<WaitObject> DynamicObjectCast<WaitObject>(SharedPtr<Object> object) {
|
|
||||||
if (object != nullptr && object->IsWaitable()) {
|
|
||||||
return boost::static_pointer_cast<WaitObject>(std::move(object));
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class allows the creation of Handles, which are references to objects that can be tested
|
* This class allows the creation of Handles, which are references to objects that can be tested
|
||||||
* for validity and looked up. Here they are used to pass references to kernel objects to/from the
|
* for validity and looked up. Here they are used to pass references to kernel objects to/from the
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "core/hle/kernel/kernel.h"
|
#include "core/hle/kernel/kernel.h"
|
||||||
|
#include "core/hle/kernel/wait_object.h"
|
||||||
|
|
||||||
namespace Kernel {
|
namespace Kernel {
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "core/hle/kernel/kernel.h"
|
#include "core/hle/kernel/kernel.h"
|
||||||
|
#include "core/hle/kernel/wait_object.h"
|
||||||
|
|
||||||
namespace Kernel {
|
namespace Kernel {
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "core/hle/kernel/kernel.h"
|
#include "core/hle/kernel/kernel.h"
|
||||||
|
#include "core/hle/kernel/wait_object.h"
|
||||||
|
|
||||||
namespace Service {
|
namespace Service {
|
||||||
class SessionRequestHandler;
|
class SessionRequestHandler;
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include "core/hle/kernel/kernel.h"
|
#include "core/hle/kernel/kernel.h"
|
||||||
#include "core/hle/kernel/session.h"
|
#include "core/hle/kernel/session.h"
|
||||||
#include "core/hle/kernel/thread.h"
|
#include "core/hle/kernel/thread.h"
|
||||||
|
#include "core/hle/kernel/wait_object.h"
|
||||||
#include "core/hle/result.h"
|
#include "core/hle/result.h"
|
||||||
#include "core/hle/service/service.h"
|
#include "core/hle/service/service.h"
|
||||||
#include "core/memory.h"
|
#include "core/memory.h"
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "core/arm/arm_interface.h"
|
#include "core/arm/arm_interface.h"
|
||||||
#include "core/hle/kernel/kernel.h"
|
#include "core/hle/kernel/kernel.h"
|
||||||
|
#include "core/hle/kernel/wait_object.h"
|
||||||
#include "core/hle/result.h"
|
#include "core/hle/result.h"
|
||||||
|
|
||||||
enum ThreadPriority : s32 {
|
enum ThreadPriority : s32 {
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "core/hle/kernel/kernel.h"
|
#include "core/hle/kernel/kernel.h"
|
||||||
|
#include "core/hle/kernel/wait_object.h"
|
||||||
|
|
||||||
namespace Kernel {
|
namespace Kernel {
|
||||||
|
|
||||||
|
|
99
src/core/hle/kernel/wait_object.cpp
Normal file
99
src/core/hle/kernel/wait_object.cpp
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright 2014 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include "common/assert.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/hle/config_mem.h"
|
||||||
|
#include "core/hle/kernel/errors.h"
|
||||||
|
#include "core/hle/kernel/kernel.h"
|
||||||
|
#include "core/hle/kernel/memory.h"
|
||||||
|
#include "core/hle/kernel/process.h"
|
||||||
|
#include "core/hle/kernel/resource_limit.h"
|
||||||
|
#include "core/hle/kernel/thread.h"
|
||||||
|
#include "core/hle/kernel/timer.h"
|
||||||
|
#include "core/hle/shared_page.h"
|
||||||
|
|
||||||
|
namespace Kernel {
|
||||||
|
|
||||||
|
void WaitObject::AddWaitingThread(SharedPtr<Thread> thread) {
|
||||||
|
auto itr = std::find(waiting_threads.begin(), waiting_threads.end(), thread);
|
||||||
|
if (itr == waiting_threads.end())
|
||||||
|
waiting_threads.push_back(std::move(thread));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaitObject::RemoveWaitingThread(Thread* thread) {
|
||||||
|
auto itr = std::find(waiting_threads.begin(), waiting_threads.end(), thread);
|
||||||
|
// If a thread passed multiple handles to the same object,
|
||||||
|
// the kernel might attempt to remove the thread from the object's
|
||||||
|
// waiting threads list multiple times.
|
||||||
|
if (itr != waiting_threads.end())
|
||||||
|
waiting_threads.erase(itr);
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPtr<Thread> WaitObject::GetHighestPriorityReadyThread() {
|
||||||
|
Thread* candidate = nullptr;
|
||||||
|
s32 candidate_priority = THREADPRIO_LOWEST + 1;
|
||||||
|
|
||||||
|
for (const auto& thread : waiting_threads) {
|
||||||
|
// The list of waiting threads must not contain threads that are not waiting to be awakened.
|
||||||
|
ASSERT_MSG(thread->status == THREADSTATUS_WAIT_SYNCH_ANY ||
|
||||||
|
thread->status == THREADSTATUS_WAIT_SYNCH_ALL,
|
||||||
|
"Inconsistent thread statuses in waiting_threads");
|
||||||
|
|
||||||
|
if (thread->current_priority >= candidate_priority)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (ShouldWait(thread.get()))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// A thread is ready to run if it's either in THREADSTATUS_WAIT_SYNCH_ANY or
|
||||||
|
// in THREADSTATUS_WAIT_SYNCH_ALL and the rest of the objects it is waiting on are ready.
|
||||||
|
bool ready_to_run = true;
|
||||||
|
if (thread->status == THREADSTATUS_WAIT_SYNCH_ALL) {
|
||||||
|
ready_to_run = std::none_of(thread->wait_objects.begin(), thread->wait_objects.end(),
|
||||||
|
[&thread](const SharedPtr<WaitObject>& object) {
|
||||||
|
return object->ShouldWait(thread.get());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ready_to_run) {
|
||||||
|
candidate = thread.get();
|
||||||
|
candidate_priority = thread->current_priority;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaitObject::WakeupAllWaitingThreads() {
|
||||||
|
while (auto thread = GetHighestPriorityReadyThread()) {
|
||||||
|
if (!thread->IsSleepingOnWaitAll()) {
|
||||||
|
Acquire(thread.get());
|
||||||
|
// Set the output index of the WaitSynchronizationN call to the index of this object.
|
||||||
|
if (thread->wait_set_output) {
|
||||||
|
thread->SetWaitSynchronizationOutput(thread->GetWaitObjectIndex(this));
|
||||||
|
thread->wait_set_output = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (auto& object : thread->wait_objects) {
|
||||||
|
object->Acquire(thread.get());
|
||||||
|
}
|
||||||
|
// Note: This case doesn't update the output index of WaitSynchronizationN.
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& object : thread->wait_objects)
|
||||||
|
object->RemoveWaitingThread(thread.get());
|
||||||
|
thread->wait_objects.clear();
|
||||||
|
|
||||||
|
thread->SetWaitSynchronizationResult(RESULT_SUCCESS);
|
||||||
|
thread->ResumeFromWait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<SharedPtr<Thread>>& WaitObject::GetWaitingThreads() const {
|
||||||
|
return waiting_threads;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Kernel
|
67
src/core/hle/kernel/wait_object.h
Normal file
67
src/core/hle/kernel/wait_object.h
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright 2014 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <boost/smart_ptr/intrusive_ptr.hpp>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "core/hle/kernel/kernel.h"
|
||||||
|
|
||||||
|
namespace Kernel {
|
||||||
|
|
||||||
|
class Thread;
|
||||||
|
|
||||||
|
/// Class that represents a Kernel object that a thread can be waiting on
|
||||||
|
class WaitObject : public Object {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Check if the specified thread should wait until the object is available
|
||||||
|
* @param thread The thread about which we're deciding.
|
||||||
|
* @return True if the current thread should wait due to this object being unavailable
|
||||||
|
*/
|
||||||
|
virtual bool ShouldWait(Thread* thread) const = 0;
|
||||||
|
|
||||||
|
/// Acquire/lock the object for the specified thread if it is available
|
||||||
|
virtual void Acquire(Thread* thread) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a thread to wait on this object
|
||||||
|
* @param thread Pointer to thread to add
|
||||||
|
*/
|
||||||
|
virtual void AddWaitingThread(SharedPtr<Thread> thread);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a thread from waiting on this object (e.g. if it was resumed already)
|
||||||
|
* @param thread Pointer to thread to remove
|
||||||
|
*/
|
||||||
|
virtual void RemoveWaitingThread(Thread* thread);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wake up all threads waiting on this object that can be awoken, in priority order,
|
||||||
|
* and set the synchronization result and output of the thread.
|
||||||
|
*/
|
||||||
|
virtual void WakeupAllWaitingThreads();
|
||||||
|
|
||||||
|
/// Obtains the highest priority thread that is ready to run from this object's waiting list.
|
||||||
|
SharedPtr<Thread> GetHighestPriorityReadyThread();
|
||||||
|
|
||||||
|
/// Get a const reference to the waiting threads list for debug use
|
||||||
|
const std::vector<SharedPtr<Thread>>& GetWaitingThreads() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// Threads waiting for this object to become available
|
||||||
|
std::vector<SharedPtr<Thread>> waiting_threads;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Specialization of DynamicObjectCast for WaitObjects
|
||||||
|
template <>
|
||||||
|
inline SharedPtr<WaitObject> DynamicObjectCast<WaitObject>(SharedPtr<Object> object) {
|
||||||
|
if (object != nullptr && object->IsWaitable()) {
|
||||||
|
return boost::static_pointer_cast<WaitObject>(std::move(object));
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Kernel
|
|
@ -27,6 +27,7 @@
|
||||||
#include "core/hle/kernel/thread.h"
|
#include "core/hle/kernel/thread.h"
|
||||||
#include "core/hle/kernel/timer.h"
|
#include "core/hle/kernel/timer.h"
|
||||||
#include "core/hle/kernel/vm_manager.h"
|
#include "core/hle/kernel/vm_manager.h"
|
||||||
|
#include "core/hle/kernel/wait_object.h"
|
||||||
#include "core/hle/result.h"
|
#include "core/hle/result.h"
|
||||||
#include "core/hle/service/service.h"
|
#include "core/hle/service/service.h"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue