mirror of
https://github.com/mikage-emu/mikage-dev.git
synced 2025-01-08 14:30:59 +01:00
c2b79585ac
This is required for the ssl and http modules and probably others.
2047 lines
70 KiB
C++
2047 lines
70 KiB
C++
#pragma once
|
|
|
|
#include "interpreter.h"
|
|
|
|
#include "os_hypervisor.hpp"
|
|
#include "os_types.hpp"
|
|
|
|
#include "framework/bit_field_new.hpp"
|
|
#include "framework/console.hpp"
|
|
#include "framework/settings.hpp"
|
|
|
|
//#include <spdlog/logger.h>
|
|
#include <spdlog/spdlog.h>
|
|
#include <fmt/ostream.h>
|
|
|
|
#include <boost/coroutine2/coroutine.hpp>
|
|
|
|
#include <boost/hana/define_struct.hpp>
|
|
#include <boost/hana/ext/std/tuple.hpp>
|
|
#include <boost/hana/functional/overload.hpp>
|
|
|
|
#include <list>
|
|
#include <map>
|
|
#include <unordered_map>
|
|
#include <memory>
|
|
#include <queue>
|
|
#include <thread>
|
|
|
|
#include "video_core/src/interrupt_listener.hpp"
|
|
|
|
class LogManager;
|
|
class PicaContext;
|
|
|
|
namespace Interpreter {
|
|
struct CPUContext;
|
|
struct Setup;
|
|
class GDBStub;
|
|
}
|
|
|
|
namespace Profiler {
|
|
class Profiler;
|
|
class Activity;
|
|
}
|
|
|
|
namespace HLE {
|
|
|
|
namespace OS {
|
|
|
|
// Abstract object with some unique ID.
|
|
class Object : public std::enable_shared_from_this<Object> {
|
|
public:
|
|
Object() = default;
|
|
Object(const Object&) = delete;
|
|
|
|
virtual ~Object() = default;
|
|
|
|
std::string name{};
|
|
|
|
std::string GetName() const {
|
|
if (!name.empty())
|
|
return name;
|
|
|
|
return typeid(*this).name();
|
|
}
|
|
};
|
|
|
|
// TODO: Add a reference counting base class?
|
|
|
|
class Thread;
|
|
|
|
// Called "KSynchronizationObject" on 3dbrew.
|
|
class ObserverSubject : public Object {
|
|
friend class OS;
|
|
public:
|
|
// List of observers. May include the same Thread multiple times.
|
|
std::list<std::weak_ptr<Thread>> observers;
|
|
|
|
// TODO: This should be made purely virtual once all ObserverSubject
|
|
// instances have this function implemented
|
|
virtual bool TryAcquireImpl(std::shared_ptr<Thread>) {
|
|
throw std::runtime_error("This should never be called!");
|
|
}
|
|
|
|
public:
|
|
virtual ~ObserverSubject() = default;
|
|
|
|
/**
|
|
* Tries to acquire the maintained resource in the given thread.
|
|
* If successful, thread will be unregistered from observers.
|
|
*/
|
|
bool TryAcquire(std::shared_ptr<Thread> thread);
|
|
|
|
/**
|
|
* Registers the given thread for notifications. This will make sure that
|
|
* threads waiting for this subject to become ready will be woken up when
|
|
* it does.
|
|
* The thread will be unregistered once the event has been acquired using
|
|
* TryAcquire.
|
|
*/
|
|
void Register(std::shared_ptr<Thread> thread);
|
|
|
|
/**
|
|
* Manually unregister the given thread from notifications.
|
|
*/
|
|
void Unregister(std::shared_ptr<Thread> thread);
|
|
};
|
|
|
|
/**
|
|
* A handle table is a collection of handles owned by a particular process.
|
|
* Handles are process-unique, but handles of different threads may refer to
|
|
* the same object. The objects are "owned" in the sense that deleting all
|
|
* handles that refer to a particular object will trigger destruction of the
|
|
* object.
|
|
*/
|
|
class HandleTable {
|
|
public: // TODO: Un-publicize. We currently need to do this to be able to find mutexes held by an exited thread :/
|
|
friend class ConsoleModule;
|
|
|
|
std::unordered_map<DebugHandle, std::shared_ptr<Object>> table;
|
|
|
|
Handle cur_handle = { 0 };
|
|
|
|
// Prints an internal logging message about a type mismatch.
|
|
void ErrorNotFound(Handle, const char* requested_type);
|
|
|
|
// Prints an internal logging message about a type mismatch.
|
|
void ErrorWrongType(std::shared_ptr<Object> object, const char* requested_type);
|
|
|
|
public:
|
|
template<typename Type>
|
|
using Entry = std::pair<DebugHandle, std::shared_ptr<Type>>;
|
|
|
|
/**
|
|
* Inserts the given Object into the handle table and returns a Handle to the object.
|
|
* @return Immutable pair of Handle and pointer the object.
|
|
*/
|
|
template<typename Type>
|
|
Entry<Type> CreateHandle(std::shared_ptr<Type> object, const DebugHandle::DebugInfo& debug_info) {
|
|
static_assert(std::is_base_of<Object,Type>::value, "object must be a shared_ptr to a derivative class of Object!");
|
|
++cur_handle.value; // TODO: Make sure this never becomes one of the reserved handles
|
|
DebugHandle debug_handle{cur_handle.value, debug_info};
|
|
table[debug_handle] = object;
|
|
return { debug_handle, object };
|
|
}
|
|
|
|
/**
|
|
* Inserts the given Object into the handle table with the specified Handle.
|
|
* Use this to set up reserved handles (e.g. 0xFFFF8001 for the handle of the current process)
|
|
*/
|
|
template<typename Type>
|
|
void CreateEntry(const DebugHandle& handle, std::shared_ptr<Type> object) {
|
|
static_assert(std::is_base_of<Object,Type>::value, "object must be a shared_ptr to a derivative class of Object!");
|
|
if (table.count(handle))
|
|
throw std::runtime_error("Handle already in table!");
|
|
|
|
table[handle] = object;
|
|
}
|
|
|
|
/**
|
|
* Returns a shared pointer to the kernel Object referenced by handle if
|
|
* it is in the table and if the object can be downcast to Type.
|
|
* @param fail_expected If true, we won't log an error when an object of different type is found with the given handle
|
|
* @return shared_ptr of the given Type. nullptr if the handle can't be used to obtain a pointer to Type.
|
|
*/
|
|
template<typename Type>
|
|
std::shared_ptr<Type> FindObject(Handle handle, bool fail_expected = false) {
|
|
static_assert(std::is_base_of<Object,Type>::value, "object must be a shared_ptr to a derivative class of Object!");
|
|
|
|
if (table.count(handle) == 0) {
|
|
if (!fail_expected) {
|
|
ErrorNotFound(handle, typeid(Type).name());
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
auto object_ptr = std::dynamic_pointer_cast<Type>(table[handle]);
|
|
if (!fail_expected && !object_ptr)
|
|
ErrorWrongType(table[handle], typeid(Type).name());
|
|
return object_ptr;
|
|
}
|
|
|
|
DebugHandle FindHandle(void* object) {
|
|
for (auto& entry : table) {
|
|
if (entry.second.get() == object) {
|
|
return entry.first;
|
|
}
|
|
}
|
|
return Handle{HANDLE_INVALID};
|
|
}
|
|
|
|
template<typename Type>
|
|
Handle FindHandle(std::shared_ptr<Type> object) {
|
|
static_assert(std::is_base_of<Object,Type>::value, "object must be a shared_ptr to a derivative class of Object!");
|
|
return FindHandle(object.get());
|
|
}
|
|
|
|
/**
|
|
* Sets the currently running thread (to be exposed as handle 0xFFFF8000)
|
|
* TODO: What does this return when multiple threads of the same process run on different CPUs?
|
|
*/
|
|
void SetCurrentThread(std::shared_ptr<Thread> thread) {
|
|
if (!thread) {
|
|
table.erase(Handle{0xFFFF8000});
|
|
} else {
|
|
table[Handle{0xFFFF8000}] = std::static_pointer_cast<Object>(thread);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes the given handle. Does not release any locks still hold on ObserverSubjects.
|
|
* @todo Implement error case?
|
|
*/
|
|
void CloseHandle(Handle handle);
|
|
};
|
|
|
|
class Process;
|
|
class OS;
|
|
class Session;
|
|
|
|
/// Returned as part of an SVCFuture to signalize that a Thread's wake_index should be returned upon next dispatch
|
|
struct PromisedWakeIndex {};
|
|
|
|
/// Returned as part of an SVCFuture to signalize that a Thread's pending_result should be returned upon next dispatch
|
|
struct PromisedResult {};
|
|
|
|
namespace detail {
|
|
template<typename T>
|
|
struct SVCFutureResultType {
|
|
using type = T;
|
|
};
|
|
|
|
template<>
|
|
struct SVCFutureResultType<PromisedWakeIndex> {
|
|
using type = uint32_t;
|
|
};
|
|
|
|
template<>
|
|
struct SVCFutureResultType<PromisedResult> {
|
|
using type = uint32_t;
|
|
};
|
|
|
|
} // namespace detail
|
|
|
|
// TODO: Move elsewhere
|
|
template<typename... T>
|
|
struct SVCFuture {
|
|
using DataType = std::tuple<T...>;
|
|
using ResultType = std::tuple<typename detail::SVCFutureResultType<T>::type...>;
|
|
|
|
SVCFuture() = default;
|
|
SVCFuture(DataType&& data) : data(data) {};
|
|
SVCFuture& operator =(const SVCFuture&) = default;
|
|
|
|
// TODO: This shouldn't be provided outside of the two functions that really need it
|
|
std::optional<DataType> data;
|
|
};
|
|
|
|
/**
|
|
* Utility type for returning the equivalent of "void" from a system call
|
|
* implementation
|
|
*/
|
|
using SVCEmptyFuture = SVCFuture<std::nullptr_t>;
|
|
|
|
using SVCCallbackType = std::function<void(std::shared_ptr<Thread>)>;
|
|
|
|
|
|
/**
|
|
* Mechanism that switches control flow between emulated OS threads and the emulated OS scheduler.
|
|
*
|
|
* Usually this is implemented using coroutines, but for Tracy-based profiling there exists a
|
|
* thread-based implementation.
|
|
*/
|
|
class ThreadControl {
|
|
public:
|
|
virtual ~ThreadControl() = default;
|
|
|
|
/**
|
|
* Move active control flow from this thread to the scheduler.
|
|
* Must only be called from the running thread
|
|
*/
|
|
virtual void YieldToScheduler() = 0;
|
|
|
|
/**
|
|
* Move active control flow from the scheduler to the thread.
|
|
* Must only be called from the scheduler
|
|
*/
|
|
virtual void ResumeFromScheduler() = 0;
|
|
|
|
/**
|
|
* Tear down any resources that may not be active when removing this thread
|
|
* from the scheduler
|
|
*/
|
|
virtual void PrepareForExit() {}
|
|
};
|
|
|
|
/// A single thread of execution (child classes may either be actual emulation threads or high-level emulated ones).
|
|
class Thread : public ObserverSubject {
|
|
uint32_t id;
|
|
|
|
public:
|
|
// TODO: Figure out how this actually affects behavior on the 3DS!
|
|
uint32_t priority;
|
|
|
|
bool pending_svc = false;
|
|
|
|
static const uint32_t PriorityDefault = 0x30;
|
|
|
|
/*
|
|
* When status is either WaitingForTimeout or WaitingForArbitration, this
|
|
* number denotes the system time (in nanoseconds) at which to wake up.
|
|
*/
|
|
int64_t timeout_at;
|
|
|
|
// Address that this thread is currently watching via an AddressArbiter
|
|
VAddr arbitration_address;
|
|
|
|
// Arbitration handle (stored for debugging)
|
|
Handle arbitration_handle;
|
|
|
|
Process& parent_process;
|
|
|
|
Profiler::Activity& activity;
|
|
/// Result to return from a pending system call
|
|
uint32_t promised_result;
|
|
|
|
/**
|
|
* Function to be called when a thread is interrupted due to a system call.
|
|
* All variables captured in this function are guaranteed to be valid until
|
|
* the thread execution is resumed or the thread exits.
|
|
*/
|
|
SVCCallbackType callback_for_svc;
|
|
|
|
// TODO: Make this private!
|
|
/* struct Context {
|
|
uint32_t regs[16];
|
|
uint32_t cpsr;
|
|
} context;*/
|
|
|
|
enum class Status {
|
|
Ready, // running or ready to run
|
|
Sleeping, // waiting until SignalResourceReady is called (TODO: SignalResourceReady is unused now!!!)
|
|
WaitingForTimeout, // waiting for a fixed amount of time
|
|
WaitingForArbitration, // waiting for another thread to signalize a particular address using ArbitrateAddress
|
|
Stopped, // thread exited and is waiting for its destruction
|
|
} status = Status::Ready;
|
|
|
|
/// List of resources that are being waited on as part of SVCWaitSynchronizationN.
|
|
std::list<std::shared_ptr<ObserverSubject>> wait_list;
|
|
|
|
/// If true, will not wake up the thread before wait_list is empty.
|
|
bool wait_for_all;
|
|
|
|
/**
|
|
* Index of the particular ObserverSubject in wait_list which has been
|
|
* acquired most recently. Note that the value of this variable is only
|
|
* intended to be read by emulated applications when wait_for_all is false.
|
|
*/
|
|
uint32_t wake_index;
|
|
|
|
/**
|
|
* weak_ptr to the most recently acquired ObserverSubject. This is only
|
|
* valid when wait_for_all is false.
|
|
*/
|
|
std::weak_ptr<Object> woken_object;
|
|
|
|
// Notify thread of a resource being ready for being TryAcquire'ed
|
|
// TODO: This function is unused now!
|
|
void SignalResourceReady();
|
|
|
|
std::unique_ptr<ThreadControl> control;
|
|
|
|
Thread(Process& owner, uint32_t id, uint32_t priority);
|
|
|
|
virtual ~Thread() = default;
|
|
|
|
// Guaranteed to be valid, since every thread must have a process.
|
|
Process& GetParentProcess() {
|
|
return parent_process;
|
|
}
|
|
|
|
std::shared_ptr<Thread> GetPointer();
|
|
|
|
HandleTable& GetProcessHandleTable();
|
|
|
|
uint32_t GetId() const;
|
|
|
|
uint32_t GetPriority() const;
|
|
|
|
OS& GetOS();
|
|
|
|
// Save the current CPU context into "context"
|
|
virtual void SaveContext() = 0;
|
|
|
|
// Restore the saved CPU context
|
|
virtual void RestoreContext() = 0;
|
|
|
|
/**
|
|
* Run the thread. Execution may yield back to the OS and consecutively other threads through the OS's internal scheduling coroutine.
|
|
* @note In order for execution to yield back to the OS, some system call must be invoked.
|
|
*/
|
|
virtual void Run() = 0;
|
|
|
|
/**
|
|
* Read a word from the TLS at the given byte offset (must be a multiple of 4)
|
|
* @todo Obsolete. Replace usage of this with ReadMemory
|
|
*/
|
|
virtual uint32_t ReadTLS(uint32_t byte_offset) = 0;
|
|
|
|
/**
|
|
* Write a word to the TLS at the given byte offset (must be a multiple of 4)
|
|
* @todo Obsolete. Replace usage of this with WriteMemory
|
|
*/
|
|
virtual void WriteTLS(uint32_t byte_offset, uint32_t value) = 0;
|
|
|
|
/**
|
|
* Write a byte to a location in this thread's parent process virtual memory
|
|
*/
|
|
void WriteMemory(VAddr addr, uint8_t value);
|
|
|
|
/**
|
|
* Write a word to a location in this thread's parent process virtual memory
|
|
*/
|
|
void WriteMemory32(VAddr addr, uint32_t value);
|
|
|
|
/**
|
|
* Read a byte from a location in this thread's parent process virtual memory
|
|
*/
|
|
uint8_t ReadMemory(VAddr addr);
|
|
|
|
/**
|
|
* Read a word from a location in this thread's parent process virtual memory
|
|
*/
|
|
uint32_t ReadMemory32(VAddr addr);
|
|
|
|
/**
|
|
* Get the value of the given CPU register in this thread's context.
|
|
* @param reg_index Index of the CPU register. r0-r15 are mapped to the first 16 values; CPSR is value 16.
|
|
*/
|
|
virtual uint32_t GetCPURegisterValue(unsigned reg_index) {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Set the value of the given CPU register in this thread's context.
|
|
* @param reg_index Index of the CPU register. r0-r15 are mapped to the first 16 values; CPSR is value 16.
|
|
* @param value New value for the specified register
|
|
*/
|
|
virtual void SetCPURegisterValue(unsigned reg_index, uint32_t value) {
|
|
// Do nothing by default
|
|
}
|
|
|
|
/**
|
|
* Add a breakpoint at the given address. The Thread implementation must
|
|
* ensure that whenever execution reaches the address, execution is paused
|
|
* and the GDB stub is notified about the event.
|
|
*/
|
|
virtual void AddBreakpoint(VAddr addr) {
|
|
// Do nothing by default
|
|
}
|
|
|
|
/**
|
|
* Removes any breakpoints from the given address.
|
|
*/
|
|
virtual void RemoveBreakpoint(VAddr addr) {
|
|
// Do nothing by default
|
|
}
|
|
|
|
/**
|
|
* Add a watchpoint at the given address. The Thread implementation must
|
|
* ensure that whenever execution reaches the address, execution is paused
|
|
* and the GDB stub is notified about the event.
|
|
*/
|
|
virtual void AddReadWatchpoint(VAddr addr) {
|
|
// Do nothing by default
|
|
}
|
|
|
|
/**
|
|
* Removes any read watchpoints from the given address.
|
|
*/
|
|
virtual void RemoveReadWatchpoint(VAddr addr) {
|
|
// Do nothing by default
|
|
}
|
|
|
|
/**
|
|
* Add a watchpoint at the given address. The Thread implementation must
|
|
* ensure that whenever execution reaches the address, execution is paused
|
|
* and the GDB stub is notified about the event.
|
|
*/
|
|
virtual void AddWriteWatchpoint(VAddr addr) {
|
|
// Do nothing by default
|
|
}
|
|
|
|
/**
|
|
* Removes any read watchpoints from the given address.
|
|
*/
|
|
virtual void RemoveWriteWatchpoint(VAddr addr) {
|
|
// Do nothing by default
|
|
}
|
|
|
|
std::shared_ptr<spdlog::logger> GetLogger();
|
|
|
|
/**
|
|
* Notifies this Thread about the resource being acquired for it. The
|
|
* resource will be removed from wait_list and the thread will be
|
|
* woken up if appropriate.
|
|
* @pre Thread state must be waiting for resources
|
|
* @pre The given resource must have been in wait_list
|
|
*/
|
|
void OnResourceAcquired(ObserverSubject& resource);
|
|
|
|
/**
|
|
* Signals a software interrupt to the HardwareScheduler and yields the
|
|
* Thread coroutine. The thread will not be dispatched back in until the
|
|
* OS has processed the system call. In particular, this means the current
|
|
* thread may be unscheduled and other threads may be dispatched before
|
|
* control returns to this thread.
|
|
*/
|
|
void YieldForSVC(uint32_t svc);
|
|
|
|
uint32_t GetPromise(PromisedResult) const {
|
|
return promised_result;
|
|
}
|
|
|
|
uint32_t GetPromise(PromisedWakeIndex) const {
|
|
return wake_index;
|
|
}
|
|
|
|
virtual bool TryAcquireImpl(std::shared_ptr<Thread>) override;
|
|
};
|
|
|
|
class TLSManager {
|
|
friend struct TLSSlot;
|
|
|
|
void Release(VAddr addr) {
|
|
slot_occupied[(addr - first_tls_addr) / tls_size] = false;
|
|
}
|
|
|
|
static constexpr uint32_t page_size = 0x1000;
|
|
static constexpr uint32_t tls_size = 0x200;
|
|
static constexpr uint32_t slots_per_page = page_size / tls_size;
|
|
|
|
static constexpr VAddr first_tls_addr = 0x1ff82000;
|
|
VAddr next_tls_addr = first_tls_addr;
|
|
std::vector<bool> slot_occupied;
|
|
|
|
bool NextSlotNeedsNewPage() const {
|
|
return (0 == (slot_occupied.size() % slots_per_page));
|
|
}
|
|
|
|
public:
|
|
struct TLSSlot GetFreePage(Process& process);
|
|
};
|
|
|
|
struct TLSSlot {
|
|
VAddr addr;
|
|
TLSManager& manager;
|
|
|
|
TLSSlot(VAddr addr, TLSManager& manager) : addr(addr), manager(manager) {
|
|
}
|
|
|
|
TLSSlot(TLSSlot&& slot) : manager(slot.manager) {
|
|
addr = std::exchange(slot.addr, 0);
|
|
}
|
|
|
|
~TLSSlot() {
|
|
if (addr) {
|
|
manager.Release(addr);
|
|
}
|
|
}
|
|
};
|
|
|
|
// A thread performing CPU emulation. This is as close as to a "true" emulated userland thread as it gets.
|
|
class EmuThread : public Thread {
|
|
public: // TODO: Required by SVCRaw for now. Should have a DumpRegister interface...
|
|
std::unique_ptr<Interpreter::ExecutionContext> context;
|
|
|
|
public: // TODO: Un-public-ize this!
|
|
TLSSlot tls;
|
|
|
|
public:
|
|
EmuThread(Process& owner, std::unique_ptr<Interpreter::ExecutionContext>, uint32_t id, uint32_t priority, VAddr entry, VAddr stack_top, TLSSlot tls, uint32_t r0, uint32_t fpscr);
|
|
|
|
virtual ~EmuThread() = default;
|
|
|
|
void SaveContext() override;
|
|
|
|
void RestoreContext() override;
|
|
|
|
virtual void Run() override;
|
|
|
|
virtual uint32_t ReadTLS(uint32_t offset) override;
|
|
|
|
virtual void WriteTLS(uint32_t offset, uint32_t value) override;
|
|
|
|
uint32_t GetCPURegisterValue(unsigned reg_index) override;
|
|
|
|
void SetCPURegisterValue(unsigned reg_index, uint32_t value) override;
|
|
|
|
void AddBreakpoint(VAddr addr) override;
|
|
|
|
void RemoveBreakpoint(VAddr addr) override;
|
|
|
|
void AddReadWatchpoint(VAddr addr) override;
|
|
|
|
void RemoveReadWatchpoint(VAddr addr) override;
|
|
|
|
void AddWriteWatchpoint(VAddr addr) override;
|
|
|
|
void RemoveWriteWatchpoint(VAddr addr) override;
|
|
};
|
|
|
|
class FakeProcess;
|
|
|
|
// A high-level emulated thread that omits any CPU emulation and instead executes a given C++ function
|
|
// Implementations must provide the Thread::Run() member function.
|
|
class FakeThread : public Thread {
|
|
// Fake thread local storage - initialized to zero to make sure all static buffer parameters are reset, etc.
|
|
// NOTE: This should actually be 0x200 bytes large.
|
|
std::array<uint32_t,0x200> tls{};
|
|
|
|
public:
|
|
FakeThread(FakeProcess& parent);
|
|
|
|
virtual ~FakeThread() = default;
|
|
|
|
void SaveContext() override {
|
|
// No context to save
|
|
// TODO: Actually, we might want to restore MMU configuration (e.g. to access IO registers)!
|
|
}
|
|
|
|
void RestoreContext() override {
|
|
// No context to restore
|
|
}
|
|
|
|
uint32_t ReadTLS(uint32_t offset) override {
|
|
return tls[offset / 4];
|
|
}
|
|
|
|
void WriteTLS(uint32_t offset, uint32_t value) override {
|
|
tls[offset / 4] = value;
|
|
}
|
|
|
|
// Overloaded from Thread for convenience to retrieve a FakeProcess directly rather than a Process
|
|
FakeProcess& GetParentProcess();
|
|
|
|
/**
|
|
* Yield back to the OS thread dispatcher and invoke the given system call
|
|
* implementation in its context. This function returns once the OS has
|
|
* dispatched this thread back in.
|
|
*/
|
|
template<typename... Results, typename... Args, typename... Args2>
|
|
auto CallSVC(SVCFuture<Results...> (OS::*svc_func)(Args2...), Args&&... args);
|
|
};
|
|
|
|
/**
|
|
* Wraps the given function in a FakeThread, enabling convenient implementation
|
|
* of threads using C++ functions.
|
|
*/
|
|
class WrappedFakeThread final : public FakeThread {
|
|
std::function<void(FakeThread&)> func;
|
|
|
|
void Run() override {
|
|
try {
|
|
func(*this);
|
|
} catch (HLE::OS::FakeThread*) { // TODO: Cleanup interface
|
|
// Do nothing
|
|
}
|
|
}
|
|
|
|
public:
|
|
WrappedFakeThread(FakeProcess& parent, std::function<void(FakeThread&)> func)
|
|
: FakeThread(parent), func(std::move(func)) {
|
|
}
|
|
};
|
|
|
|
/// See FakeDebugProcess.
|
|
class FakeDebugThread : public Thread {
|
|
public:
|
|
FakeDebugThread(Process& owner);
|
|
|
|
void SaveContext() override {
|
|
// No context to save
|
|
}
|
|
|
|
void RestoreContext() override {
|
|
// No context to restore
|
|
}
|
|
|
|
uint32_t ReadTLS(uint32_t offset) override {
|
|
// We don't maintain any TLS, so just return 0
|
|
return 0;
|
|
}
|
|
|
|
void WriteTLS(uint32_t offset, uint32_t value) override {
|
|
// We don't maintain any TLS, so just do nothing
|
|
}
|
|
|
|
void Run() override {
|
|
// Do nothing
|
|
}
|
|
|
|
/**
|
|
* Block until the given ProcessorControl requests execution to continue
|
|
*/
|
|
void WaitForContinue(Interpreter::ProcessorController& controller);
|
|
|
|
/**
|
|
* Reports that a new process was just created, and signals this to the
|
|
* given controller as a breakpoint. In return, the debugger will request
|
|
* execution to be paused and eventually to continue. This function blocks
|
|
* until the latter has happened.
|
|
* @param process Process that just launched
|
|
* @pre process must have its main thread created when calling this function.
|
|
*/
|
|
void ProcessLaunched(Process& process, Interpreter::ProcessorController& controller);
|
|
};
|
|
|
|
class MemoryManager;
|
|
class ResourceLimit;
|
|
|
|
/**
|
|
* Owner of a physical memory allocation: Either a Process, a SharedMemoryBlock, or a CodeSet
|
|
*/
|
|
class MemoryBlockOwner {
|
|
public:
|
|
virtual ~MemoryBlockOwner() = default;
|
|
};
|
|
|
|
enum class MemoryPermissions : uint32_t {
|
|
None = 0,
|
|
Read = 1,
|
|
Write = 2,
|
|
ReadWrite = 3,
|
|
Exec = 4,
|
|
ReadExec = 5,
|
|
};
|
|
|
|
/// A process (for a very broad definition of "process").
|
|
class Process : public ObserverSubject, public MemoryBlockOwner {
|
|
struct VirtualMemoryBlock {
|
|
uint32_t phys_start;
|
|
uint32_t size;
|
|
MemoryPermissions permissions;
|
|
};
|
|
|
|
virtual bool TryAcquireImpl(std::shared_ptr<Thread>) {
|
|
for (auto& thread : threads) {
|
|
if (thread->status != Thread::Status::Stopped) {
|
|
return false;
|
|
}
|
|
}
|
|
// Report that the process is terminated, since all of its threads stopped
|
|
return (status != Status::Created);
|
|
}
|
|
|
|
public: // TODO: Make this private again!
|
|
// Map from starting address to the virtual memory block
|
|
std::map<uint32_t, VirtualMemoryBlock> virtual_memory;
|
|
private:
|
|
|
|
OS& os;
|
|
public:
|
|
MemoryManager& memory_allocator;
|
|
|
|
public:
|
|
VAddr linear_base_addr = 0x14000000;
|
|
|
|
private:
|
|
// Process ID
|
|
ProcessId pid;
|
|
|
|
ThreadId next_tid;
|
|
|
|
protected:
|
|
public: // TODO: Un-public-ize this!
|
|
Profiler::Activity& activity;
|
|
|
|
/**
|
|
* Every Process needs a CPU interpreter Setup, even if the Process is
|
|
* high-level emulated: In particular, FakeProcesses might want to share
|
|
* memory with low-level emulated ones.
|
|
*/
|
|
Interpreter::Setup& interpreter_setup;
|
|
|
|
protected:
|
|
ThreadId MakeNewThreadId();
|
|
|
|
/**
|
|
* Called from MapVirtualMemory when a mapping took place.
|
|
* @param phys_addr Base physical address to map data from
|
|
* @param size Number of bytes to map.
|
|
* @param vaddr Virtual address at which to map the given physical memory
|
|
* @todo We might not actually need this!
|
|
*/
|
|
virtual void OnVirtualMemoryMapped(PAddr phys_addr, uint32_t size, VAddr vaddr) = 0;
|
|
|
|
/**
|
|
* Called from MapVirtualMemory when an unmapping took place.
|
|
* @param size Number of bytes to unmap
|
|
* @param vaddr Virtual address to start unmapping at
|
|
*/
|
|
virtual void OnVirtualMemoryUnmapped(VAddr vaddr, uint32_t size) = 0;
|
|
|
|
public:
|
|
// TODO: Consider making this private!
|
|
std::list<std::shared_ptr<Thread>> threads;
|
|
|
|
public:
|
|
HandleTable handle_table;
|
|
|
|
std::shared_ptr<ResourceLimit> limit;
|
|
|
|
enum class Status {
|
|
Created, // Just created (doesn't have any threads yet)
|
|
Running, // Process running with at least one thread (i.e. SVCRun has been called)
|
|
Stopping, // Stopping or stopped (if all threads are Stopped)
|
|
} status = Status::Created;
|
|
|
|
Process(OS& os, Profiler::Activity&, Interpreter::Setup& setup, ProcessId pid, MemoryManager& memory_allocator);
|
|
|
|
virtual ~Process() = default;
|
|
|
|
OS& GetOS() { return os; }
|
|
|
|
ProcessId GetId() const {
|
|
return pid;
|
|
}
|
|
|
|
std::shared_ptr<Thread> GetThreadFromId(ThreadId thread_id) const;
|
|
|
|
/**
|
|
* Set up a mapping from virtual memory to physical memory.
|
|
* @param phys_addr Base physical address to map data from
|
|
* @param size Number of bytes to map.
|
|
* @param vaddr Virtual address at which to map the given physical memory
|
|
* @return true on success, false otherwise
|
|
* @todo This may currently only be called on active processes (since it may modify the underlying's CPUContext MMU configuration)
|
|
*/
|
|
bool MapVirtualMemory(PAddr phys_addr, uint32_t size, VAddr vaddr, MemoryPermissions);
|
|
|
|
/**
|
|
* Unmaps the given virtual memory range from this process.
|
|
* @return true if the given range was successfully unmapped; false if parts of the range weren't unmapped
|
|
*/
|
|
bool UnmapVirtualMemory(VAddr vaddr, uint32_t size);
|
|
|
|
/**
|
|
* Translate the given virtual address to a physical one.
|
|
*/
|
|
std::optional<uint32_t> ResolveVirtualAddr(VAddr addr);
|
|
|
|
/**
|
|
* Finds the first unmapped virtual address range of the given size in bytes within the given bounds.
|
|
* @param size Minimum number of bytes in the given range
|
|
* @param vaddr_start Minimal virtual address at which to look for available locations
|
|
* @param vaddr_end Virtual address at which to stop looking for available locations
|
|
* @return On success, returns the base virtual address for the found range
|
|
* @todo This should actually be a Process matter!
|
|
*/
|
|
std::optional<uint32_t> FindAvailableVirtualMemory(uint32_t size, VAddr vaddr_start, VAddr vaddr_end);
|
|
|
|
MemoryManager& GetPhysicalMemoryManager();
|
|
|
|
/**
|
|
* Write a byte to a location in this process's virtual memory
|
|
* @todo Move to Process!
|
|
*/
|
|
virtual void WriteMemory(VAddr addr, uint8_t value) = 0;
|
|
|
|
virtual void WriteMemory32(VAddr addr, uint32_t value);
|
|
|
|
/**
|
|
* Read a byte from a location in this process's virtual memory
|
|
*/
|
|
virtual uint8_t ReadMemory(VAddr addr) = 0;
|
|
|
|
virtual uint32_t ReadMemory32(VAddr addr);
|
|
|
|
/**
|
|
* Read a byte from a location in physical memory. Only intended to be used
|
|
* by the PXI process
|
|
*/
|
|
uint8_t ReadPhysicalMemory(PAddr addr);
|
|
uint32_t ReadPhysicalMemory32(PAddr addr);
|
|
void WritePhysicalMemory(PAddr addr, uint8_t value);
|
|
void WritePhysicalMemory32(PAddr addr, uint32_t value);
|
|
|
|
std::shared_ptr<spdlog::logger> GetLogger();
|
|
};
|
|
|
|
class CodeSet;
|
|
|
|
class ResourceLimit;
|
|
|
|
// A low-level emulated process that is constituted of one or more EmuThreads
|
|
class EmuProcess : public Process {
|
|
public:
|
|
// TODO: Un-publish
|
|
std::unique_ptr<Interpreter::Processor> processor;
|
|
|
|
private:
|
|
TLSManager tls_manager;
|
|
|
|
void OnVirtualMemoryMapped(PAddr phys_addr, uint32_t size, VAddr vaddr) override;
|
|
|
|
void OnVirtualMemoryUnmapped(VAddr vaddr, uint32_t size) override;
|
|
|
|
public:
|
|
// The given CPUContext is used to initialize the virtual address mapping for this process
|
|
EmuProcess(OS& os, Interpreter::Setup& setup, uint32_t pid, std::shared_ptr<CodeSet> codeset, MemoryManager& memory_allocator);
|
|
~EmuProcess();
|
|
|
|
/**
|
|
* Creates a new EmuThread in this process (allocating memory for thread local storage along the way).
|
|
* The caller is responsible for registering the thread to the OS scheduler.
|
|
* @param entry_point Entry point of the thread.
|
|
*/
|
|
std::shared_ptr<EmuThread> SpawnThread(uint32_t priority, VAddr entry_point, VAddr stack_top, uint32_t r0, uint32_t fpscr);
|
|
|
|
std::shared_ptr<CodeSet> codeset;
|
|
|
|
void WriteMemory(VAddr addr, uint8_t value) override;
|
|
|
|
uint8_t ReadMemory(VAddr addr) override;
|
|
};
|
|
|
|
// A high-level emulated process that is constituted of one or more FakeThreads
|
|
class FakeProcess : public Process {
|
|
void OnVirtualMemoryMapped(PAddr phys_addr, uint32_t size, VAddr vaddr) override;
|
|
void OnVirtualMemoryUnmapped(VAddr vaddr, uint32_t size) override;
|
|
|
|
struct StaticBuffer {
|
|
std::vector<uint8_t> data;
|
|
};
|
|
|
|
/**
|
|
* Fake static buffers (maps from the fake start address to the data).
|
|
* Contained buffers are guaranteed to be non-overlapping.
|
|
* TODO: This can be turned into a generic buffer container
|
|
*/
|
|
std::map<uint32_t, StaticBuffer> static_buffers;
|
|
|
|
// Returns true if the given address points to process-internal storage.
|
|
// Returns false if the given address points to emulated memory.
|
|
bool IsInternalAddress(VAddr addr) const;
|
|
|
|
public:
|
|
FakeProcess(OS& os, Interpreter::Setup& setup, uint32_t pid, std::string_view name);
|
|
|
|
virtual ~FakeProcess() = default;
|
|
|
|
/**
|
|
* Attaches the given thread to the internal thread list and registers it to the OS scheduler.
|
|
*/
|
|
void AttachThread(std::shared_ptr<FakeThread> thread);
|
|
|
|
/**
|
|
* Allocate a static buffer for use in IPC requests.
|
|
* @return A fake address used to identify the static buffer data.
|
|
* @todo Actually, we can make this act as a generic "allocate buffer data" function
|
|
*/
|
|
uint32_t AllocateStaticBuffer(uint32_t size);
|
|
|
|
/**
|
|
* Allocate a static buffer for use in IPC requests
|
|
* @param address Address previously obtained by AllocateStaticBuffer, which is used to identify the buffer data.
|
|
*/
|
|
void FreeStaticBuffer(uint32_t address);
|
|
|
|
/**
|
|
* Allocate an internal buffer that is accessible from kernel functions
|
|
* @return a pair of an address where the buffer pretends to be mapped and a pointer to the buffer data
|
|
*/
|
|
std::pair<VAddr,uint8_t*> AllocateBuffer(uint32_t size);
|
|
|
|
void FreeBuffer(VAddr addr);
|
|
|
|
void WriteMemory(VAddr addr, uint8_t value) override;
|
|
|
|
void WriteMemory32(VAddr addr, uint32_t value) override;
|
|
|
|
uint8_t ReadMemory(VAddr addr) override;
|
|
};
|
|
|
|
/**
|
|
* A fake process similar in spirit to FakeProcess, but even more reduced to the bare minimum.
|
|
* FakeDebugProcess is not intended to actively participate in the OS operation; instead, it's
|
|
* mere purpose is to act as a dummy process that host debuggers can attach to such that they
|
|
* can listen for newly created EmuProcesses (which will report a SIGTRAP to debuggers).
|
|
* Other than that, FakeDebugProcess implements the memory reading interface to return always
|
|
* the same value (representing a "branch to self" ARM instruction), such that it appears to the
|
|
* debugger as if we were stuck in an infinite loop (since the alternative of returning garbage
|
|
* data would probably make the debugger go crazy).
|
|
*/
|
|
class FakeDebugProcess : public Process {
|
|
void OnVirtualMemoryMapped(PAddr, uint32_t, VAddr) override {
|
|
// Do nothing
|
|
}
|
|
|
|
void OnVirtualMemoryUnmapped(VAddr, uint32_t) override {
|
|
// Do nothing
|
|
}
|
|
|
|
public:
|
|
FakeDebugProcess(OS& os, Interpreter::Setup& setup, ProcessId pid);
|
|
|
|
void WriteMemory(VAddr, uint8_t) override {
|
|
// Do nothing
|
|
}
|
|
|
|
uint8_t ReadMemory(VAddr addr) override {
|
|
// Always return the ARM instruction 0xeafffffe, which is a branch to
|
|
// the current offset. To the debugger it will hence look as if we were
|
|
// stuck in an infinite loop (which is better than returning garbage
|
|
// instruction data).
|
|
switch (addr % 4) {
|
|
case 0: return 0xfe;
|
|
case 1: return 0xff;
|
|
case 2: return 0xff;
|
|
case 3: return 0xea;
|
|
default: return 0; // Silence compiler warning
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<FakeDebugThread> thread;
|
|
};
|
|
|
|
// Tags for WrappedFakeProcess
|
|
struct TagMapGPUMMIO {};
|
|
struct TagMapHIDMMIO {};
|
|
struct TagMapVRAM {};
|
|
struct TagMapDSPMemory {};
|
|
|
|
/**
|
|
* Wraps a FakeProcess around the C++ code providing for the process main
|
|
* thread logic. This logic is intended to be implemented in the State
|
|
* constructor. Effectively, a State object is created that represents the main
|
|
* process thread while it furthermore carries the entire process state (which
|
|
* may be used to spawn additional threads). This allows for high-level
|
|
* emulating stateful, multi-threaded Processes in a very natural manner.
|
|
* @note The State object creation is delayed until the FakeProcess's main
|
|
* thread is scheduled in, which allows the use of system calls in the
|
|
* State constructor, hence making for cleaner resource management.
|
|
* @todo Consider whether this should just be promoted to be our Thread class:
|
|
* Both EmuThread and FakeThread could possibly be implemented using
|
|
* an appropriate lambda expression.
|
|
*/
|
|
class WrappedFakeProcess : public FakeProcess {
|
|
std::function<void(FakeThread&)> functor;
|
|
|
|
WrappedFakeProcess(OS& os, Interpreter::Setup& setup, uint32_t pid, std::string_view name, std::function<void(FakeThread&)> functor)
|
|
: FakeProcess(os, setup, pid, name), functor(std::move(functor)) {
|
|
}
|
|
|
|
public:
|
|
void SpawnMainThread() {
|
|
// TODO: Should we call SVCExitThread after State{...} is done?
|
|
// If we do, we should assert that no more than one thread is left running!
|
|
auto thread = std::make_shared<WrappedFakeThread>(*this, std::move(functor));
|
|
AttachThread(thread);
|
|
}
|
|
|
|
static std::shared_ptr<WrappedFakeProcess> Create(OS& os, Interpreter::Setup& setup, uint32_t pid, const std::string& name, std::function<void(FakeThread&)> functor) {
|
|
auto process = std::shared_ptr<WrappedFakeProcess>(new WrappedFakeProcess(os, setup, pid, name, std::move(functor)));
|
|
// TODO: Attach debug information!
|
|
process->handle_table.CreateEntry(Handle{0xFFFF8001}, process);
|
|
return process;
|
|
}
|
|
|
|
template<typename Context, typename... Args>
|
|
static std::shared_ptr<WrappedFakeProcess> CreateWithContext(OS& os, Interpreter::Setup& setup, uint32_t pid, const std::string& name, Args&&... args) {
|
|
auto process = Create(os, setup, pid, name, [args...](FakeThread& thread) { Context{thread, args...}; });
|
|
// Map IO memory for HLE processes
|
|
if (std::is_base_of<TagMapGPUMMIO, Context>::value) {
|
|
process->MapVirtualMemory(Memory::IO_GPU::start, Memory::IO_GPU::size, 0x1EF00000, MemoryPermissions::ReadWrite);
|
|
}
|
|
// TODO: Add TagMapHashMMIO tag
|
|
// if (std::is_base_of<TagMapGPUMMIO, Context>::value) {
|
|
process->MapVirtualMemory(Memory::IO_HASH::start, Memory::IO_HASH::size, 0x1EC01000, MemoryPermissions::ReadWrite);
|
|
process->MapVirtualMemory(Memory::IO_HASH2::start, Memory::IO_HASH2::size, 0x1EE01000, MemoryPermissions::ReadWrite);
|
|
// }
|
|
if (std::is_base_of<TagMapHIDMMIO, Context>::value) {
|
|
process->MapVirtualMemory(Memory::IO_HID::start, Memory::IO_HID::size, 0x1EC46000, MemoryPermissions::ReadWrite);
|
|
}
|
|
if (std::is_base_of<TagMapVRAM, Context>::value) {
|
|
process->MapVirtualMemory(Memory::VRAM::start, Memory::VRAM::size, 0x1F000000, MemoryPermissions::ReadWrite);
|
|
}
|
|
// TODO: Migrate DSP to new service framework and apply this tag
|
|
/*if (std::is_base_of<TagMapDSPMemory, Context>::value)*/ {
|
|
process->MapVirtualMemory(Memory::DSP::start, Memory::DSP::size, 0x1FF00000, MemoryPermissions::ReadWrite);
|
|
}
|
|
return process;
|
|
}
|
|
};
|
|
|
|
template<typename Context, typename... Args>
|
|
std::shared_ptr<WrappedFakeProcess> CreateFakeProcessViaContext(OS& os, Interpreter::Setup& setup, uint32_t pid, const std::string& name, Args&&... args) {
|
|
return WrappedFakeProcess::CreateWithContext<Context>(os, setup, pid, name, std::forward<Args>(args)...);
|
|
}
|
|
|
|
struct CodeSetInfo {
|
|
BOOST_HANA_DEFINE_STRUCT(CodeSetInfo,
|
|
(std::array<uint8_t, 8>, app_name),
|
|
(std::array<uint8_t, 8>, unknown),
|
|
|
|
(VAddr, text_start),
|
|
(uint32_t, text_pages),
|
|
(VAddr, ro_start),
|
|
(uint32_t, ro_pages),
|
|
(VAddr, data_start),
|
|
(uint32_t, data_pages), // bss exclusive
|
|
|
|
// These fields seem to be the same as text_pages/ro_pages/data/pages
|
|
(uint32_t, text_size), // in 0x1000-pages
|
|
(uint32_t, ro_size), // in 0x1000-pages
|
|
(uint32_t, data_size), // in 0x1000-pages (bss inclusive)
|
|
|
|
(std::array<uint8_t, 12>, unknown2)
|
|
);
|
|
};
|
|
|
|
// TODO: Most of the contents of this struct need to be verified!
|
|
struct DMAConfig {
|
|
// There are 8 channels the application may chose from (values 0 through 7)
|
|
// 0xff means "any channel"
|
|
uint8_t channel;
|
|
|
|
// Value size (used for endian-swapping): 0=uint8, 2=uint16, 4=uint32, 8=uint64
|
|
uint8_t value_size;
|
|
|
|
struct {
|
|
uint8_t storage;
|
|
|
|
using Fields = v2::BitField::Fields<uint32_t>;
|
|
|
|
/// Load target configuration following this structure
|
|
auto LoadTargetConfig() const { return Fields::MakeOn<0, 1>(this); }
|
|
|
|
/// Load source configuration following this structure and the target configuration
|
|
auto LoadSourceConfig() const { return Fields::MakeOn<1, 1>(this); }
|
|
|
|
auto BlockUntilCompletion() const { return Fields::MakeOn<2, 1>(this); }
|
|
|
|
/// Load target configuration, but force its peripheral_id to be 0xff
|
|
auto LoadTargetAltConfig() const { return Fields::MakeOn<6, 1>(this); }
|
|
|
|
/// Load source configuration, but force its peripheral_id to be 0xff
|
|
auto LoadSourceAltConfig() const { return Fields::MakeOn<7, 1>(this); }
|
|
} flags;
|
|
|
|
uint8_t unknown2;
|
|
|
|
struct SubConfig {
|
|
uint8_t unknown;
|
|
|
|
// Seems to indicate the value size? TODO: how is this compatible with the member value_size above?
|
|
uint8_t type;
|
|
|
|
uint16_t unknown2;
|
|
|
|
uint16_t transfer_size;
|
|
|
|
uint16_t unknown3;
|
|
|
|
uint16_t stride;
|
|
|
|
static SubConfig Default() {
|
|
return { 0xff, 0xf, 0x80, 0x0, 0x80, 0x0 };
|
|
}
|
|
};
|
|
|
|
// NOTE: Possible 3dbrew erratum: Which SubConfig actually comes first?
|
|
SubConfig dest;
|
|
SubConfig source;
|
|
};
|
|
|
|
class CodeSet : public Object, public MemoryBlockOwner {
|
|
public:
|
|
char app_name[9]; // 8 characters + null-terminator
|
|
|
|
// NOTE: System version 10.x added pseudo address layout randomization,
|
|
// so the mapped physical memory provided by the caller may not be
|
|
// contiguous. We hence have to keep a list of mappings.
|
|
// The virtual address space is still contiguous, so a single start address is sufficient.
|
|
|
|
struct Mapping {
|
|
PAddr phys_start;
|
|
uint32_t num_pages;
|
|
};
|
|
|
|
std::vector<Mapping> text_phys;
|
|
VAddr text_vaddr;
|
|
|
|
std::vector<Mapping> ro_phys;
|
|
VAddr ro_vaddr;
|
|
|
|
// data excluding bss
|
|
std::vector<Mapping> data_phys;
|
|
VAddr data_vaddr;
|
|
|
|
PAddr bss_paddr;
|
|
uint32_t bss_size; // in pages
|
|
|
|
CodeSet(const CodeSetInfo& info);
|
|
|
|
// Cleans up the allocated data
|
|
virtual ~CodeSet() override;
|
|
};
|
|
|
|
class Port;
|
|
|
|
class ServerPort : public ObserverSubject {
|
|
friend class OS;
|
|
friend class ClientPort;
|
|
|
|
// Number of sessions that can be created in addition to the ones that are already open
|
|
uint32_t available_sessions;
|
|
|
|
public:
|
|
ServerPort(std::shared_ptr<Port> port, uint32_t max_sessions) : port(port), available_sessions(max_sessions) {
|
|
}
|
|
|
|
virtual ~ServerPort() = default;
|
|
|
|
bool TryAcquireImpl(std::shared_ptr<Thread> thread) override;
|
|
|
|
void OfferSession(std::shared_ptr<Session> session);
|
|
|
|
std::shared_ptr<Port> port;
|
|
|
|
// Queue of incoming sessions
|
|
std::queue<std::shared_ptr<Session>> session_queue;
|
|
};
|
|
|
|
class ClientPort : public ObserverSubject {
|
|
public:
|
|
ClientPort(std::shared_ptr<Port> port) : port(port) {
|
|
}
|
|
|
|
virtual ~ClientPort() = default;
|
|
|
|
std::shared_ptr<Port> port;
|
|
|
|
bool TryAcquireImpl(std::shared_ptr<Thread> thread) override;
|
|
};
|
|
|
|
class ServerSession : public ObserverSubject {
|
|
public: // TODO: Un-public-ize
|
|
uint32_t ipc_commands_pending = 0;
|
|
|
|
public:
|
|
ServerSession(std::shared_ptr<Session> session, std::shared_ptr<ServerPort> port) : session(session), port(port) {
|
|
}
|
|
|
|
virtual ~ServerSession() = default;
|
|
|
|
bool TryAcquireImpl(std::shared_ptr<Thread> thread) override;
|
|
|
|
void CommandReady();
|
|
|
|
std::shared_ptr<Session> session;
|
|
|
|
// May be nullptr for sessions that aren't bound to any port
|
|
std::shared_ptr<ServerPort> port;
|
|
};
|
|
|
|
class ClientSession : public ObserverSubject {
|
|
public:
|
|
ClientSession(std::shared_ptr<Session> session) : session(session) {
|
|
}
|
|
|
|
virtual ~ClientSession() = default;
|
|
|
|
// Returns bool if the session has been signalled for the thread pushed as the argument
|
|
// (thread = SVCSendSyncRequest sender)
|
|
bool TryAcquireImpl(std::shared_ptr<Thread> thread) override;
|
|
|
|
// Releases the first of the remaining waiting threads
|
|
void SetReady(std::shared_ptr<Thread> waiting_thread);
|
|
|
|
std::shared_ptr<Session> session;
|
|
|
|
// location of the thread that last sent an IPC request using this session (for the server to read IPC requests from its TLS)
|
|
std::list<std::weak_ptr<Thread>> threads;
|
|
};
|
|
|
|
class Port : public Object {
|
|
public:
|
|
virtual ~Port() = default;
|
|
|
|
// NOTE: Port is owned by its children, since one child is enough for a Port to exist, but if both children are released, the parent Port dies to.
|
|
std::weak_ptr<ServerPort> server;
|
|
std::weak_ptr<ClientPort> client;
|
|
};
|
|
|
|
class Session : public Object {
|
|
public:
|
|
virtual ~Session() = default;
|
|
|
|
// NOTE: Session is owned by its children, since one child is enough for a Session to exist, but if both children are released, the parent Session dies to.
|
|
std::weak_ptr<ClientSession> client;
|
|
std::weak_ptr<ServerSession> server;
|
|
};
|
|
|
|
class Mutex : public ObserverSubject {
|
|
uint32_t lock_count = 0;
|
|
|
|
// Thread currently holding a lock in this mutex
|
|
std::weak_ptr<Thread> owner;
|
|
|
|
public:
|
|
Mutex(bool locked, std::shared_ptr<Thread> initial_owner) : lock_count(locked ? 1 : 0), owner(initial_owner) {};
|
|
|
|
virtual ~Mutex() = default;
|
|
|
|
bool TryAcquireImpl(std::shared_ptr<Thread> thread) override;
|
|
|
|
void Release();
|
|
|
|
bool IsOwner(std::shared_ptr<Thread> thread) const;
|
|
|
|
bool IsReady() const {
|
|
return (lock_count == 0);
|
|
}
|
|
};
|
|
|
|
class Semaphore : public ObserverSubject {
|
|
public:
|
|
int32_t available_count;
|
|
|
|
private:
|
|
int32_t max_available;
|
|
|
|
public:
|
|
|
|
/// @pre 0 <= count <= max_count
|
|
Semaphore(int32_t count, int32_t max_count) : available_count(count), max_available(max_count) {
|
|
}
|
|
|
|
bool TryAcquireImpl(std::shared_ptr<Thread> thread) override;
|
|
|
|
/**
|
|
* Release this semaphore "times" times. This will allow "times" additional
|
|
* threads to acquire the thread. "times" needs to be larger than zero. The
|
|
* implied final count may not exceed the maximal count.
|
|
* @result RESULT_OK on success
|
|
*/
|
|
uint32_t Release(int32_t times);
|
|
};
|
|
|
|
enum class ResetType : uint32_t {
|
|
OneShot = 0, // Be acquirable once, then reset
|
|
Sticky = 1, // Be acquirable until explicitly reset
|
|
Pulse = 2, // Timer-only: Be acquirable once, then reset and restart the timer.
|
|
};
|
|
|
|
class Event : public ObserverSubject {
|
|
// TODO: Make this class thread-safe!
|
|
public: // TODO: Remove this ugly hack
|
|
bool signalled = false;
|
|
|
|
ResetType type;
|
|
|
|
public:
|
|
Event(ResetType type) : type(type) {
|
|
}
|
|
|
|
virtual ~Event() = default;
|
|
|
|
bool TryAcquireImpl(std::shared_ptr<Thread> thread) override;
|
|
|
|
void SignalEvent();
|
|
|
|
void ResetEvent();
|
|
};
|
|
|
|
class DMAObject : public ObserverSubject {
|
|
public:
|
|
DMAObject() = default;
|
|
virtual ~DMAObject() = default;
|
|
|
|
bool TryAcquireImpl(std::shared_ptr<Thread>) override {
|
|
// Our current DMA SVC implementation has DMAs complete instantly,
|
|
// so we always allow successful acquisition of this object
|
|
return true;
|
|
}
|
|
};
|
|
|
|
constexpr uint64_t cpu_clockrate = 268111856; // Clock rate as per constants used in 3DS kernel
|
|
using secs_per_tick = std::ratio<1,cpu_clockrate>;
|
|
using ticks = std::chrono::duration<uint64_t, secs_per_tick>;
|
|
// This is determined by LCD hardware configuration: cpu_clockrate / 24 / ((HTotal+1) * (VTotal+1))
|
|
using vblanks_per_sec = std::chrono::duration<uint64_t, std::ratio<24 * 451 * 414, cpu_clockrate>>; // approximately 59.83 Hz
|
|
|
|
class Timer : public ObserverSubject {
|
|
// TODO: Make this class thread-safe!
|
|
public: // TODO: remove this
|
|
bool active = false;
|
|
|
|
ResetType type;
|
|
|
|
// timeout when current_time >= timeout_time_ns
|
|
uint64_t timeout_time_ns = 0;
|
|
|
|
uint64_t period_ns = 0;
|
|
|
|
public:
|
|
Timer(ResetType type) : type(type) {
|
|
}
|
|
|
|
virtual ~Timer() = default;
|
|
|
|
bool TryAcquireImpl(std::shared_ptr<Thread> thread) override;
|
|
|
|
void Run(uint64_t timeout_time, uint64_t period);
|
|
|
|
void Reset();
|
|
|
|
bool Expired(uint64_t current_time) const;
|
|
};
|
|
|
|
/**
|
|
* These are used e.g. to implement light events (of which there may be
|
|
* arbitrarily many, but multiple light events need no more than a single
|
|
* kernel handle).
|
|
*/
|
|
class AddressArbiter : public Object {
|
|
public:
|
|
AddressArbiter() = default;
|
|
|
|
virtual ~AddressArbiter() = default;
|
|
};
|
|
|
|
struct MemoryBlock {
|
|
uint32_t size_bytes;
|
|
|
|
std::weak_ptr<MemoryBlockOwner> owner;
|
|
};
|
|
|
|
class MemoryManager {
|
|
// physical starting address of region
|
|
uint32_t region_start_paddr;
|
|
|
|
uint32_t region_size_bytes;
|
|
|
|
public:
|
|
// TODO: Needs an interface to check for ownership
|
|
// Maps describing the free/taken memory blocks (mapped by their starting physical address)
|
|
std::map<uint32_t, MemoryBlock> free;
|
|
std::map<uint32_t, MemoryBlock> taken;
|
|
|
|
public:
|
|
MemoryManager(PAddr start_address, uint32_t size);
|
|
|
|
/**
|
|
* Allocate a memory block of the given size.
|
|
* @return Starting address of the allocated buffer. boost::none on failure.
|
|
* @todo Provide means to deallocate the block again!
|
|
*/
|
|
std::optional<uint32_t> AllocateBlock(std::shared_ptr<MemoryBlockOwner>, uint32_t size_bytes);
|
|
|
|
void DeallocateBlock(std::shared_ptr<MemoryBlockOwner>, PAddr start_addr, uint32_t size_bytes);
|
|
|
|
void TransferOwnership(std::shared_ptr<MemoryBlockOwner> old_owner, std::shared_ptr<MemoryBlockOwner> new_owner, PAddr start_addr, uint32_t size_bytes);
|
|
|
|
void DeallocateBlock(PAddr start_addr, uint32_t size_bytes);
|
|
|
|
uint32_t UsedMemory() const;
|
|
|
|
uint32_t TotalSize() const;
|
|
|
|
PAddr RegionStart() const { return region_start_paddr; }
|
|
PAddr RegionEnd() const { return region_start_paddr + region_size_bytes; }
|
|
};
|
|
|
|
class SharedMemoryBlock : public Object, public MemoryBlockOwner {
|
|
public:
|
|
// Address to data stored in emulated memory
|
|
uint32_t phys_address;
|
|
|
|
uint32_t size;
|
|
|
|
bool owns_memory;
|
|
|
|
SharedMemoryBlock(uint32_t physical_address, uint32_t size, bool owns_memory)
|
|
: phys_address(physical_address), size(size), owns_memory(owns_memory) {
|
|
}
|
|
|
|
// TODO: On destruction, this must deallocate any pending memory!
|
|
virtual ~SharedMemoryBlock() = default;
|
|
};
|
|
|
|
// A service is an anonymous port that is queryable through the port srv, which needs service processes to provide the GetPrivateName method.
|
|
class Service : Port {
|
|
public:
|
|
virtual const char* GetPrivateName() = 0;
|
|
};
|
|
|
|
class OS final : public InterruptListener {
|
|
friend class ConsoleModule;
|
|
|
|
// Thread list for the scheduler (threads are owned by processes).
|
|
std::list<std::weak_ptr<Thread>> threads;
|
|
|
|
/* // GDB stub for Core 1
|
|
std::unique_ptr<Interpreter::GDBStub> gdbstub;
|
|
*/
|
|
// Thread for running the GDB stub
|
|
std::unique_ptr<std::thread> gdbthread;
|
|
|
|
// TODO: Currently, we never release any of the references hold here before shutdown!
|
|
// TODO: Consider storing ClientPorts here instead.
|
|
std::map<std::string, std::shared_ptr<Port>> ports;
|
|
|
|
|
|
Hypervisor hypervisor;
|
|
|
|
public: // TODO: Ugh.
|
|
PAddr configuration_memory = 0;
|
|
|
|
PAddr shared_memory_page = 0;
|
|
|
|
PAddr firm_launch_parameters = 0;
|
|
|
|
std::array<std::list<std::weak_ptr<Event>>, 0x76> bound_interrupts;
|
|
|
|
// Id of the next process that is going to be created
|
|
ProcessId next_pid;
|
|
|
|
// Number of CPU ticks since power-on
|
|
ticks system_tick = ticks::zero();
|
|
|
|
std::list<std::weak_ptr<Timer>> active_timers;
|
|
|
|
// Process handle table - all processes are owned exclusively by this table.
|
|
std::unordered_map<ProcessId, std::shared_ptr<Process>> process_handles;
|
|
|
|
struct FakeProcessInfo {
|
|
std::shared_ptr<WrappedFakeProcess> (*create)(OS&, const std::string&);
|
|
std::weak_ptr<Process> fake_process {};
|
|
};
|
|
std::unordered_map<std::string_view, FakeProcessInfo> hle_titles;
|
|
|
|
// TODO: Get rid of the parameter
|
|
DebugHandle::DebugInfo MakeHandleDebugInfo(DebugHandle::Source source = DebugHandle::Original) const;
|
|
DebugHandle::DebugInfo MakeHandleDebugInfoFromIPC(uint32_t ipc_command) const;
|
|
|
|
public: // TODO: privatize this again!
|
|
std::list<std::weak_ptr<Thread>> ready_queue; // Queue of threads that are ready to run
|
|
// std::list<std::weak_ptr<Thread>> priority_queue; // Queue of threads that are ready to run and should be prioritized over those in ready_queue (e.g. because they had been waiting on an event that was just signalled)
|
|
std::vector<std::weak_ptr<Thread>> waiting_queue; // List of threads waiting on a timeout
|
|
|
|
std::shared_ptr<MemoryBlockOwner> internal_memory_owner;
|
|
|
|
// APP, SYSTEM, and BASE
|
|
// For each application, all data (code + stack(?) + heap) is allocated
|
|
// in the region indicated by the memory type in the application's exheader
|
|
// kernel flags. The only known exception to this is Thread Local Storage,
|
|
// which is always allocated in the BASE region.
|
|
std::array<MemoryManager, 3> memory_regions;
|
|
[[deprecated]] MemoryManager& memory_app() { return memory_regions[0]; }
|
|
[[deprecated]] MemoryManager& memory_system() { return memory_regions[1]; }
|
|
[[deprecated]] MemoryManager& memory_base() { return memory_regions[2]; }
|
|
MemoryManager& FindMemoryRegionContaining(uint32_t paddr, uint32_t size);
|
|
|
|
OS(Profiler::Profiler&, Settings::Settings&, Interpreter::Setup&, LogManager&, PicaContext&, EmuDisplay::EmuDisplay&);
|
|
~OS();
|
|
|
|
Profiler::Profiler& profiler;
|
|
Profiler::Activity& activity;
|
|
|
|
PicaContext& pica_context;
|
|
EmuDisplay::EmuDisplay& display;
|
|
|
|
public:
|
|
Settings::Settings& settings;
|
|
|
|
Interpreter::Setup& setup;
|
|
|
|
ProcessId MakeNewProcessId();
|
|
|
|
/// Appends the given process to the global process list
|
|
void RegisterProcess(std::shared_ptr<Process> process);
|
|
|
|
// Process for debuggers to attach to initially
|
|
std::shared_ptr<FakeDebugProcess> debug_process;
|
|
|
|
static const int32_t MAX_SESSIONS = 0x1000; // Arbitrary choice
|
|
|
|
// NOTE: Required for SVCRaw (for some reason) and GDBStub
|
|
// TODO: Create an interface around this instead.
|
|
Thread* active_thread;
|
|
|
|
LogManager& log_manager;
|
|
std::shared_ptr<spdlog::logger> logger;
|
|
|
|
private:
|
|
std::atomic<bool> stop_requested { false };
|
|
|
|
public:
|
|
|
|
using Result = uint32_t;
|
|
|
|
template<typename... T>
|
|
using ResultAnd = std::tuple<Result, T...>;
|
|
|
|
enum class BreakReason : uint32_t {
|
|
Panic = 0,
|
|
Assert = 1,
|
|
User = 2
|
|
};
|
|
|
|
enum class ArbitrationType : uint32_t {
|
|
Signal = 0,
|
|
Acquire = 1,
|
|
DecrementAndAcquire = 2,
|
|
AcquireWithTimeout = 3,
|
|
DecrementAndAcquireWithTimeout = 4,
|
|
};
|
|
|
|
enum class MemoryRegion {
|
|
App = 1,
|
|
Sys = 2,
|
|
Base = 3
|
|
};
|
|
|
|
struct StartupInfo {
|
|
int32_t priority;
|
|
uint32_t stack_size;
|
|
int32_t argc;
|
|
VAddr argv_addr;
|
|
VAddr envp_addr;
|
|
};
|
|
|
|
struct KernelCapability {
|
|
uint32_t storage;
|
|
|
|
// The number of leading 1s in this field distinguishes each type of capability
|
|
auto identifier() const { return BitField::v3::MakeFieldOn<20, 12>(this); }
|
|
|
|
auto kernel_version() const {
|
|
struct {
|
|
uint32_t storage;
|
|
|
|
auto minor() const { return BitField::v3::MakeFieldOn<0, 8>(this); }
|
|
auto major() const { return BitField::v3::MakeFieldOn<8, 8>(this); }
|
|
} ret { storage };
|
|
return ret;
|
|
}
|
|
|
|
// TODO: Possible bugfix here... this used to be a struct of two BitFields, hence being uint64_t in total size???
|
|
auto map_io_page() const {
|
|
struct {
|
|
uint32_t storage;
|
|
|
|
auto page_index() const { return BitField::v3::MakeFieldOn<0, 20>(this); }
|
|
auto read_only() const { return BitField::v3::MakeFlagOn<20>(this); }
|
|
} ret { storage };
|
|
return ret;
|
|
}
|
|
};
|
|
|
|
// System calls begin here
|
|
/**
|
|
* TODOTEST: This has a PID==1 check on actual hardware, see https://www.3dbrew.org/w/index.php?title=3DS_System_Flaws&curid=95&diff=18930&oldid=18655
|
|
* TODO: Change permission parameter to MemoryPermissions type
|
|
*/
|
|
SVCFuture<OS::Result,uint32_t> SVCControlMemory(Thread& source, uint32_t addr0, uint32_t addr1, uint32_t size, uint32_t operation, uint32_t permissions);
|
|
|
|
SVCFuture<OS::Result,uint32_t> SVCControlProcessMemory(Thread& source, Process& process, uint32_t addr0, uint32_t addr1, uint32_t size, uint32_t operation, MemoryPermissions);
|
|
|
|
SVCFuture<OS::Result,HandleTable::Entry<Thread>> SVCCreateThread(Thread& source, uint32_t entry, uint32_t arg, uint32_t stack_top, uint32_t priority, uint32_t processor_id);
|
|
|
|
void ExitThread(Thread& thread);
|
|
SVCEmptyFuture SVCExitThread(Thread& source);
|
|
|
|
SVCEmptyFuture SVCSleepThread(Thread& source, int64_t duration);
|
|
|
|
// Allocate stack memory, set up the process main thread, and append the thread to the scheduler queue
|
|
SVCFuture<Result> SVCRun(Thread& source, Handle process, const StartupInfo& startup);
|
|
|
|
SVCFuture<Result,HandleTable::Entry<Mutex>> SVCCreateMutex(Thread& source, bool lock);
|
|
SVCFuture<Result> SVCReleaseMutex(Thread& source, Handle mutex);
|
|
|
|
/// @pre 0 <= initial_count <= max_count
|
|
SVCFuture<Result,HandleTable::Entry<Semaphore>> SVCCreateSemaphore(Thread& source, int32_t initial_count, int32_t max_count);
|
|
SVCFuture<OS::Result,int32_t> SVCReleaseSemaphore(Thread& source, Semaphore& sema, int32_t release_count);
|
|
|
|
SVCFuture<Result,HandleTable::Entry<Event>> SVCCreateEvent(Thread& source, ResetType type);
|
|
SVCFuture<Result> SVCSignalEvent(Thread& source, Handle event);
|
|
SVCFuture<Result> SVCClearEvent(Thread& source, Handle event);
|
|
|
|
SVCFuture<Result,HandleTable::Entry<Timer>> SVCCreateTimer(Thread& source, ResetType type);
|
|
/**
|
|
* Functional properties:
|
|
* - Timer starts waking up threads after the given initial time duration (in nanoseconds) has passed
|
|
* - ONESHOT timers will wake exactly one thread
|
|
* - STICKY timers will wake any number of threads until they are reset
|
|
* - PULSE timers will wake exactly one thread, and restart themselves to fire "interval" nanoseconds after the wakeup
|
|
* - In particular, this means that threads in general get woken later than "initial + interval * n", because the time of firing the timer will be after waking the thread.
|
|
*/
|
|
SVCFuture<Result> SVCSetTimer(Thread& source, Timer& timer, int64_t initial, int64_t period);
|
|
|
|
// TODO: This should transfer ownership of the shared pages to the memory block!
|
|
SVCFuture<Result,HandleTable::Entry<SharedMemoryBlock>> SVCCreateMemoryBlock(Thread& source, VAddr addr, uint32_t size, uint32_t owner_perms, uint32_t other_perms);
|
|
/**
|
|
* @todo Figure out what closing the block handle achieves: Does it unmap the memory?
|
|
*/
|
|
SVCFuture<Result> SVCMapMemoryBlock(Thread& source, Handle block_handle, VAddr addr, MemoryPermissions caller_perms, MemoryPermissions other_perms);
|
|
|
|
/**
|
|
* @todo Why does this take both a block and an address, when either would suffice?
|
|
*/
|
|
SVCFuture<Result> SVCUnmapMemoryBlock(Thread& source, SharedMemoryBlock& block, VAddr addr);
|
|
|
|
SVCFuture<Result,HandleTable::Entry<AddressArbiter>> SVCCreateAddressArbiter(Thread& source);
|
|
|
|
/**
|
|
* Functional properties:
|
|
* - Acquiring type: If the word at the given address is smaller than the
|
|
* given value, the thread is put to sleep until woken
|
|
* up by a call to this system call.
|
|
* - Signalling type: Wakes up "value"" threads waiting due to a call to
|
|
* this system call with the same address.
|
|
* (A negative "value" resumes all threads waiting on
|
|
* this arbiter.)
|
|
* @todo value should actually be of type int32_t, because value=-1 means to signalize all waiting threads
|
|
*/
|
|
SVCFuture<PromisedResult> SVCArbitrateAddress(Thread& source, Handle arbiter_handle, uint32_t address, ArbitrationType type, uint32_t value, int64_t timeout);
|
|
|
|
/**
|
|
* Things to test:
|
|
* - Does closing a mutex unlock it?
|
|
*/
|
|
SVCFuture<Result> SVCCloseHandle(Thread& source, Handle object);
|
|
|
|
Result CloseHandle(Thread& source, Handle object);
|
|
|
|
/**
|
|
* Things to test:
|
|
* - timeout = -1 implies indefinite sleep until resource is ready
|
|
* - timeout = 0 implies instant wakeup if resource is not ready
|
|
* - What about timeout < -2?
|
|
*/
|
|
SVCFuture<PromisedResult> SVCWaitSynchronization(Thread& source, Handle handle, int64_t timeout);
|
|
|
|
/**
|
|
* Things to test:
|
|
* - What happens if one of the given objects is being destroyed while waiting on it?
|
|
* - timeout = -1 implies indefinite sleep until resources are ready
|
|
* - timeout = 0 implies instant wakeup if resources are not ready
|
|
* - What about timeout < -2?
|
|
*/
|
|
SVCFuture<PromisedResult,PromisedWakeIndex> SVCWaitSynchronizationN(Thread& source, Handle* handles, uint32_t handle_count, bool wait_for_all, int64_t timeout);
|
|
|
|
// Create a new handle referencing the same object as the given handle.
|
|
SVCFuture<Result,Handle> SVCDuplicateHandle(Thread& source, Handle handle);
|
|
|
|
// Gets the time passed since turning on the system (measured in CPU ticks)
|
|
SVCFuture<uint64_t> SVCGetSystemTick(Thread& source);
|
|
|
|
/**
|
|
* Functional properties:
|
|
* - Returns success instantly as long as the port exists (the server need not have called ReplyAndReceive yet)
|
|
* - Returns 0xd88007fa if no port with the given name exists
|
|
*/
|
|
SVCFuture<Result,HandleTable::Entry<ClientSession>> SVCConnectToPort(Thread& source, const std::string& name);
|
|
|
|
/**
|
|
* Functional properties:
|
|
* - Behavior when two threads use this system call on the same handle:
|
|
* - The two requests get processed independently
|
|
* - It's unclear whether causal order is preserved when processing the two requests
|
|
* - SendSyncRequest can be called before the server has accepted the session in the first place (and SendSyncRequest will then block)
|
|
* - Blocks until a response arrives.
|
|
*/
|
|
SVCFuture<PromisedResult> SVCSendSyncRequest(Thread& source, Handle session);
|
|
|
|
/**
|
|
* Things to test:
|
|
* - May a process open itself?
|
|
* - Will circular dependencies between two processes that have opened each other be resolved when both of the processes have exited?
|
|
*/
|
|
SVCFuture<Result,HandleTable::Entry<Process>> SVCOpenProcess(Thread& source, ProcessId proc_id);
|
|
|
|
SVCFuture<Result,ProcessId> SVCGetProcessId(Thread& source, Process& process);
|
|
|
|
SVCFuture<Result,ThreadId> SVCGetThreadId(Thread& source, Thread& thread);
|
|
|
|
[[noreturn]] SVCEmptyFuture SVCBreak(Thread& source, BreakReason reason);
|
|
/**
|
|
* Functional properties:
|
|
* - A client port is only created when \p name is the empty string
|
|
* - Multiple ports with the same name may be created
|
|
* - No more than 7 ports may be created across the entire system
|
|
* - There seems to be no way to unregister a port (TODO: Verify)
|
|
*/
|
|
SVCFuture<Result,HandleTable::Entry<ServerPort>,HandleTable::Entry<ClientPort>> SVCCreatePort(Thread& source, const std::string& name, int32_t max_sessions);
|
|
/**
|
|
* Functional properties:
|
|
* - Returns before the server calls SVCReplyAndReceive
|
|
*/
|
|
SVCFuture<Result,HandleTable::Entry<ClientSession>> SVCCreateSessionToPort(Thread& source, Handle client_port);
|
|
SVCFuture<Result,HandleTable::Entry<ServerSession>,HandleTable::Entry<ClientSession>> SVCCreateSession(Thread& source);
|
|
SVCFuture<Result,HandleTable::Entry<ServerSession>> SVCAcceptSession(Thread& source, ServerPort& server_port); // returns server session
|
|
/**
|
|
* Functional properties:
|
|
* - When TLS@0x80 is 0xffff0000 and reply_target==0, the reply is omitted
|
|
* - handle_count==0 logically omits the "receive" part
|
|
* - handles==nullptr => ??
|
|
* - this function returns when a ServerPort handle that it's waiting on is being connected to. Does it also return when a session handle is closed?
|
|
*/
|
|
SVCFuture<PromisedResult,PromisedWakeIndex> SVCReplyAndReceive(Thread& source, Handle* handle_values, uint32_t handle_count, Handle reply_target); // returns index into @p handles
|
|
|
|
SVCFuture<Result> SVCBindInterrupt(Thread& source, uint32_t interrupt_index, std::shared_ptr<Event> signal_event, int32_t priority, uint32_t is_manual_clear);
|
|
|
|
/**
|
|
* @param start Range start adress (must be mapped in the source process)
|
|
*/
|
|
SVCFuture<Result> SVCInvalidateProcessDataCache(Thread& source, Handle process, uint32_t start, uint32_t num_bytes);
|
|
|
|
/**
|
|
* @param start Range start adress (must be mapped in the source process)
|
|
*/
|
|
SVCFuture<Result> SVCFlushProcessDataCache(Thread& source, Handle process, uint32_t start, uint32_t num_bytes);
|
|
|
|
SVCFuture<OS::Result,HandleTable::Entry<DMAObject>> SVCStartInterprocessDMA(Thread& source, Process& src_process, Process& dst_process, const VAddr src_address, const VAddr dst_address, uint32_t size, DMAConfig& dma_config);
|
|
|
|
/**
|
|
* Functional properties:
|
|
* - Unmaps the given memory from the virtual address space of the calling process (TODO: Verify)
|
|
*/
|
|
SVCFuture<Result,HandleTable::Entry<CodeSet>> SVCCreateCodeSet(Thread& source, const CodeSetInfo& info, VAddr text_data_addr, VAddr ro_data_addr, VAddr data_data_addr);
|
|
|
|
/**
|
|
* Functional properties:
|
|
* - Returns 0xe0a01bf5 when the code set contains a ro and rw address that are equal
|
|
*/
|
|
SVCFuture<Result,HandleTable::Entry<Process>> SVCCreateProcess(Thread& source, Handle codeset_handle, KernelCapability* kernel_caps, uint32_t num_kernel_caps);
|
|
|
|
/**
|
|
*
|
|
*/
|
|
SVCFuture<Result,HandleTable::Entry<ResourceLimit>> SVCCreateResourceLimit(Thread& source);
|
|
|
|
/**
|
|
*
|
|
*/
|
|
SVCFuture<Result> SVCSetResourceLimitValues(Thread& source, ResourceLimit& resource_limit, const std::vector<std::pair<uint32_t, uint64_t>>& limits);
|
|
|
|
// Fake SVCs begin here - these don't map to native SVCs but provide useful functionality
|
|
|
|
/**
|
|
* Invokes the system call handler for the given svc_id and decodes
|
|
* arguments appropriately from the given CPUContext.
|
|
* @return Function to call in the Thread context to encode the return
|
|
* values back into the CPUContext
|
|
*/
|
|
SVCCallbackType SVCRaw(Thread& source, unsigned svc_id, Interpreter::ExecutionContext&);
|
|
|
|
SVCEmptyFuture SVCDoNothing(Thread& source); // TODO: We don't really need this one, it was just for testing!
|
|
|
|
SVCFuture<Result,HandleTable::Entry<Object>> SVCCreateDummyObject(FakeThread& source, const std::string& name);
|
|
|
|
/**
|
|
* Add the given Thread to the internal thread list recognized by the scheduler.
|
|
* Intended to be used for FakeThreads, only. Native threads are added automatically when calling SVCCreateThread.
|
|
* TODO: This doesn't seem to be used anymore.
|
|
*/
|
|
SVCEmptyFuture SVCAddThread(std::shared_ptr<Thread> thread);
|
|
|
|
static std::pair<std::unique_ptr<OS>, std::unique_ptr<ConsoleModule>> Create(Settings::Settings& settings, Interpreter::Setup& setup, LogManager& log_manager, Profiler::Profiler&, PicaContext&, EmuDisplay::EmuDisplay&);
|
|
|
|
/**
|
|
* Initialized the OS, spawning all service processes along the way.
|
|
*/
|
|
void Initialize();
|
|
|
|
/**
|
|
* Creates a FakeProcess with no running threads.
|
|
*/
|
|
std::shared_ptr<FakeProcess> MakeFakeProcess(Interpreter::Setup& setup, const std::string& name);
|
|
|
|
/**
|
|
* Returns true if the given module should be high-level emulated, i.e. replaced with a FakeProcess
|
|
*/
|
|
bool ShouldHLEProcess(std::string_view module_name) const;
|
|
|
|
/**
|
|
* Registers a thread to the OS scheduler
|
|
*/
|
|
void RegisterToScheduler(std::shared_ptr<Thread> thread);
|
|
|
|
/**
|
|
* Runs the OS dispatcher.
|
|
* This function won't return unless the emulator encounters an exception
|
|
* or RequestStop is called.
|
|
*
|
|
* @note This must be called after Initialize()
|
|
* @note You have to add any user process explicitly before calling this if it should be ran, too.
|
|
*/
|
|
void Run(std::shared_ptr<Interpreter::Setup> setup);
|
|
|
|
/**
|
|
* Requests OS execution to stop.
|
|
*
|
|
* Resources will be cleaned up gracefully as far as C++ goes, but
|
|
* no guarantees are made with regards to emulated entities. Effectively,
|
|
* the state of emulated entities is considered undefined behavior upon
|
|
* calling this function.
|
|
*
|
|
* @note Thread-safe
|
|
*/
|
|
void RequestStop();
|
|
|
|
/**
|
|
* Runs the OS dispatcher. Does not return until all threads have exited.
|
|
* @todo This currently seems to overlap with Run in terms of functionality, but ultimately we want to move the whole OS scheduler/dispatcher to this function.
|
|
*/
|
|
// void StartScheduler(boost::coroutines::symmetric_coroutine<void>::yield_type& yield);
|
|
|
|
void EnterExecutionLoop();
|
|
void ElapseTime(std::chrono::nanoseconds time);
|
|
void Reschedule(std::shared_ptr<Thread> thread);
|
|
void RescheduleImmediately(std::shared_ptr<Thread> thread);
|
|
void TriggerThreadDestruction(std::shared_ptr<Thread>);
|
|
|
|
/**
|
|
* @pre The given thread must be the one currently running.
|
|
*/
|
|
void SwitchToSchedulerFromThread(Thread& thread);
|
|
|
|
std::vector<std::shared_ptr<Thread>> GetThreadList() const;
|
|
|
|
// TODO: This should eventually become SVCOpenProcess
|
|
std::shared_ptr<Process> GetProcessFromId(ProcessId proc_id) /*const*/;
|
|
|
|
/**
|
|
* Translate an IPC message (i.e. a request or a reply) from the source
|
|
* thread to the destination thread.
|
|
*/
|
|
void TranslateIPCMessage(Thread& source, Thread& dest, bool is_reply);
|
|
|
|
uint64_t GetTimeInNanoSeconds() const;
|
|
|
|
/**
|
|
* Signalize all events that are bound to the given interrupt
|
|
*/
|
|
void NotifyInterrupt(uint32_t index) override;
|
|
|
|
void OnResourceReady(ObserverSubject& resource);
|
|
};
|
|
|
|
const OS::Result RESULT_OK = 0;
|
|
|
|
template<typename... Results, typename... Args, typename... Args2>
|
|
auto FakeThread::CallSVC(SVCFuture<Results...> (OS::*svc_func)(Args2...), Args&&... args) {
|
|
if constexpr (std::is_same_v<decltype(svc_func), decltype(&OS::SVCBreak)>) {
|
|
if (svc_func == &OS::SVCBreak) {
|
|
// Throw immediately rather than switching to the OS coroutine and throwing from there.
|
|
// This makes error location much easier, since a debugger cannot print a relevant backtrace elsewhere.
|
|
throw std::runtime_error("Called SVCBreak");
|
|
}
|
|
}
|
|
|
|
SVCFuture<Results...> future;
|
|
callback_for_svc = [&future, svc_func, &args...](std::shared_ptr<Thread> thread) {
|
|
// Care must be taken when writing the result here. If we processed SVCExitThread (or similar), the outer context won't be valid anymore. Hence the result is stored externally in result_storage_for_svc
|
|
auto new_future = (thread->GetOS().*svc_func)(*std::static_pointer_cast<FakeThread>(thread), args...);
|
|
if (thread->status != Thread::Status::Stopped) {
|
|
future = std::move(new_future);
|
|
}
|
|
};
|
|
|
|
// Bounce between the OS dispatcher and the OS thread to execute callbacks
|
|
do {
|
|
// Switch to OS scheduler to process the callback
|
|
// (NOTE: This may cause other threads to be dispatched if a reschedule
|
|
// happens within the callback)
|
|
GetOS().SwitchToSchedulerFromThread(*this); // TODO: Rename!
|
|
|
|
if (status == Thread::Status::Stopped) {
|
|
// TODO: Come up with a cleaner interface
|
|
callback_for_svc = nullptr;
|
|
throw this;
|
|
}
|
|
|
|
// Repeat iff the callback added another callback
|
|
} while (callback_for_svc);
|
|
|
|
auto TransformTuple = [this](auto&&... args) {
|
|
auto Lookup = [this](auto arg) {
|
|
if constexpr (std::is_same_v<PromisedWakeIndex, decltype(arg)>) {
|
|
return wake_index;
|
|
} else if constexpr (std::is_same_v<PromisedResult, decltype(arg)>) {
|
|
return promised_result;
|
|
} else {
|
|
return arg;
|
|
}
|
|
};
|
|
return std::make_tuple(Lookup(args)...);
|
|
};
|
|
return std::apply(TransformTuple, *future.data);
|
|
}
|
|
|
|
inline std::string GetThreadObjectName(Thread& thread) {
|
|
return thread.GetName();
|
|
}
|
|
|
|
/// Utility structure to wrap a Process in a printable object
|
|
struct ProcessPrinter {
|
|
Process& process;
|
|
};
|
|
|
|
/// Utility structure to wrap a Thread in a printable object
|
|
struct ThreadPrinter {
|
|
Thread& thread;
|
|
};
|
|
|
|
/// Utility structure to wrap an Object reference in a printable object
|
|
struct ObjectRefPrinter {
|
|
Object& object;
|
|
};
|
|
|
|
/// Utility structure to wrap an std::shared_ptr<Object> in a printable object
|
|
struct ObjectPrinter {
|
|
const std::shared_ptr<Object>& object_ptr;
|
|
};
|
|
|
|
/// Utility structure to wrap a kernel handle in a printable object
|
|
struct HandlePrinter {
|
|
Thread& thread;
|
|
Handle handle;
|
|
};
|
|
|
|
std::ostream& operator<<(std::ostream& os, const ProcessPrinter& printer);
|
|
std::ostream& operator<<(std::ostream& os, const ThreadPrinter& printer);
|
|
std::ostream& operator<<(std::ostream& os, const ObjectRefPrinter& printer);
|
|
std::ostream& operator<<(std::ostream& os, const ObjectPrinter& printer);
|
|
std::ostream& operator<<(std::ostream& os, const HandlePrinter& printer);
|
|
|
|
} // namespace OS
|
|
|
|
} // namespace HLE
|
|
|
|
// Now that we're done with the definitions, include some definitions that are required to instantiate the above structs (e.g. due to unique_ptr being used on incomplete types)
|
|
#include "gdb_stub.h"
|