mikage-dev/source/os.hpp
Tony Wasserka c2b79585ac OS: Make ServerPort acquisition idempotent
This is required for the ssl and http modules and probably others.
2024-03-30 18:33:49 +01:00

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"