2024-03-07 22:05:16 +01:00
# 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)
2024-03-10 11:37:26 +01:00
std : : vector < std : : weak_ptr < Thread > > waiting_queue ; // List of threads waiting on a timeout
2024-03-07 22:05:16 +01:00
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 ) ;
2024-09-29 19:29:28 +02:00
OS ( Profiler : : Profiler & , Settings : : Settings & , Interpreter : : Setup & ,
LogManager & , AudioFrontend & , PicaContext & , EmuDisplay : : EmuDisplay & ) ;
2024-03-07 22:05:16 +01:00
~ 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 ) ;
2024-09-29 19:29:28 +02:00
static std : : pair < std : : unique_ptr < OS > , std : : unique_ptr < ConsoleModule > > Create ( Settings : : Settings & settings , Interpreter : : Setup & setup , LogManager & log_manager , Profiler : : Profiler & , AudioFrontend & , PicaContext & , EmuDisplay : : EmuDisplay & ) ;
2024-03-07 22:05:16 +01:00
/**
* 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"