Files
PS2Recomp/ps2xIOP/src/emulator/iop_emulator.cpp
T
Ranieri 75d729ce40 Feature/iop emulator (#244)
* refactor: from guest  threads to EE scheduler

* feat: bad wip mpeg fix for code veronica

* feat: cheap copy from host
feat: small perf o vsync tick

* feat: added EE clock Hz
fix: fix MPEG out of sync with new EE refactor

* fix: fix lotr tests

* fix: fix cri dtx loading
fix: fix wrong mmi instruction translation
fix: fix thread info params
feat: added EE  timers decoder and consumer
feat: split SFI and IOP memory to prevent collision and overrides

* feat: revert wrong changes

* refactor: change GS architecture

* feat: IOP emulator
refactor: codegen to catch callbacks on mips code
feat: added a lot of entries or IOP emulator

* feat: analyzer resolve the complete constant-producing sequence with five-instruction backward scan stopped at LUI and therefore

* feat: remove recompiled version of GetRomName
refactor: split IOP emulator logic
feat: added more HLE IOP modules
feat: added ps2_path

* eat: enhance ELF parser with improved callable entry detection and control flow analysis

* feat: update memory hint handling and enhance entry point discovery logic

* feat: add SET_GPR_ZE32 macro for zero-extending loads with unsigned semantics

* refactor: Refactor PS2 IOP Host Adapter and Memory Management
feat: Added PS2Vfs for virtual file system operations, including file opening, reading, writing, and path resolution.
feat: Improve VIF1 data processing to handle GIF image packets more efficiently.

* feat: added a lot of tests

* fix: fix texture caching
feat: wip multi version on dbcman

* feat: remove LLE IOPs
2026-09-19 21:31:44 -03:00

831 lines
29 KiB
C++

#include "iop_emulator.h"
#include "imports/iop_cdvd.h"
#include "core/iop_cpu.h"
#include "imports/iop_heaplib.h"
#include "imports/iop_imports.h"
#include "imports/iop_intrman.h"
#include "imports/iop_ioman.h"
#include "core/iop_kernel.h"
#include "imports/iop_loadcore.h"
#include "core/iop_memory.h"
#include "services/iop_module_loader.h"
#include "services/iop_rpc.h"
#include "imports/iop_stdio.h"
#include "imports/iop_sysclib.h"
#include "imports/iop_sysmem.h"
#include "imports/iop_timrman.h"
#include "imports/iop_vblank.h"
#include "iop_emulator_const.h"
#include <algorithm>
#include <cctype>
#include <map>
#include <optional>
#include <span>
#include <sstream>
#include <utility>
namespace ps2x::iop::detail
{
namespace
{
constexpr uint32_t kRamSize = IopMemory::RamSize;
constexpr uint32_t kKernelHeapBase = IopMemory::HeapBase;
constexpr uint32_t kKernelHeapLimit = IopMemory::HeapLimit;
constexpr uint32_t kCallStackBase = kKernelHeapLimit;
constexpr uint32_t kCallStackLimit = 0x001FFF00u;
constexpr uint32_t kCallStackSize = 0x2000u;
constexpr uint32_t kCallStackCapacity = (kCallStackLimit - kCallStackBase) / kCallStackSize;
constexpr uint64_t kCdvdCompletionCycles = 128u;
uint32_t physicalAddress(uint32_t address)
{
return IopMemory::physicalAddress(address);
}
int32_t sign16(uint32_t value)
{
return static_cast<int16_t>(value & 0xFFFFu);
}
bool iequals(std::string_view lhs, std::string_view rhs)
{
if (lhs.size() != rhs.size())
return false;
for (size_t i = 0; i < lhs.size(); ++i)
{
if (std::tolower(static_cast<unsigned char>(lhs[i])) !=
std::tolower(static_cast<unsigned char>(rhs[i])))
return false;
}
return true;
}
}
class IopEmulator::Impl final : public IopGuestExecutor
{
public:
using CpuState = IopCpuState;
struct Module
{
int id = 0;
std::string path;
std::string name;
uint32_t base = 0;
uint32_t size = 0;
uint32_t entry = 0;
uint32_t gp = 0;
bool resident = false;
};
struct GuestCallback
{
uint32_t function = 0;
uint32_t gp = 0;
};
struct ScheduledGuestCallback
{
uint32_t function = 0u;
uint32_t gp = 0u;
uint32_t argument = 0u;
};
explicit Impl(IopHost &hostRef)
: host(hostRef),
sysmem(host, memory),
kernel(memory),
cdvd(host, memory, kernel),
vblank(kernel),
rpc(host, memory, kernel),
sysclib(memory),
stdio(host, memory),
heaplib(memory),
intrman(memory),
timrman(),
ioman(memory),
cpuCore(memory),
imports(memory),
loadcore(memory, imports)
{
reset();
}
void reset()
{
memory.reset();
kernel.reset();
modules.clear();
imports.reset();
rpc.reset();
cdvd.reset();
intrman.reset();
timrman.reset();
ioman.reset();
pendingDmaInterrupts.clear();
pendingGuestCallbacks.clear();
nextModuleId = 1;
moduleCursor = kModuleLoadBase;
totalCycles = 0;
totalInstructions = 0;
eeCycleCarry = 0;
activeCpu = nullptr;
lastError.clear();
servicingDmaInterrupts = false;
servicingGuestCallbacks = false;
callDepth = 0u;
secrMcCommandHandler = {};
secrMcDevIdHandler = {};
checkKelfPathCallback = {};
}
uint8_t read8(uint32_t address) const
{
return memory.read8(address);
}
uint16_t read16(uint32_t address) const
{
return memory.read16(address);
}
uint32_t read32(uint32_t address) const
{
return memory.read32(address);
}
void write8(uint32_t address, uint8_t value)
{
memory.write8(address, value);
schedulePendingDma();
}
void write16(uint32_t address, uint16_t value)
{
memory.write16(address, value);
schedulePendingDma();
}
void write32(uint32_t address, uint32_t value)
{
memory.write32(address, value);
schedulePendingDma();
}
void schedulePendingDma()
{
if (const auto dma = memory.takeDmaStart())
pendingDmaInterrupts[dma->irq] = totalCycles + dma->delayCycles;
}
bool readRam(uint32_t address, void *destination, size_t size) const
{
return memory.readRam(address, destination, size);
}
bool writeRam(uint32_t address, const void *source, size_t size)
{
return memory.writeRam(address, source, size);
}
bool zeroRam(uint32_t address, size_t size)
{
return memory.zeroRam(address, size);
}
bool isHardwareAddress(uint32_t phys) const
{
return memory.isHardwareAddress(phys);
}
uint32_t allocate(uint32_t size, uint32_t alignment = 16u, std::optional<uint32_t> fixed = std::nullopt)
{
return memory.allocate(size, alignment, fixed);
}
bool freeAllocation(uint32_t address)
{
return memory.freeAllocation(address);
}
void log(LogLevel level, std::string_view text)
{
host.log(level, text);
}
bool checkInterrupt(CpuState &cpu)
{
const uint32_t status = cpu.cop0[12];
if ((status & 1u) == 0u)
return false;
if ((status & 0x2u) != 0u)
return false;
const bool pending = memory.interruptControl() != 0u && (memory.interruptStatus() & memory.interruptMask()) != 0u;
if (!pending)
return false;
cpu.cop0[13] |= 0x400u;
cpuCore.raiseException(cpu, 0u, cpu.pc, false);
return true;
}
enum class ImportDisposition
{
Handled,
JumpToGuest,
Missing,
};
ImportDisposition dispatchImport(const IopImportCall &call, CpuState &cpu)
{
const uint32_t a0 = cpu.gpr[4];
auto setV0 = [&](uint32_t value)
{
cpu.gpr[2] = value;
};
if (iequals(call.library, "sysmem") && sysmem.dispatchImport(call.ordinal, cpu))
return ImportDisposition::Handled;
if (iequals(call.library, "cdvdman") && cdvd.dispatchImport(call.ordinal, cpu))
{
if (const auto callback = cdvd.takeCompletionCallback())
{
pendingGuestCallbacks.emplace(
totalCycles + kCdvdCompletionCycles,
ScheduledGuestCallback{
callback->address,
callback->gp,
callback->reason,
});
}
return ImportDisposition::Handled;
}
if (iequals(call.library, "loadcore") && loadcore.dispatchImport(call.ordinal, cpu))
return ImportDisposition::Handled;
if (iequals(call.library, "thbase") || iequals(call.library, "threadman"))
{
return kernel.dispatchThreadImport(call.ordinal, cpu, totalCycles)
? ImportDisposition::Handled
: ImportDisposition::Missing;
}
if (iequals(call.library, "thsemap"))
{
return kernel.dispatchSemaphoreImport(call.ordinal, cpu)
? ImportDisposition::Handled
: ImportDisposition::Missing;
}
if (iequals(call.library, "thevent"))
{
return kernel.dispatchEventImport(call.ordinal, cpu)
? ImportDisposition::Handled
: ImportDisposition::Missing;
}
if (iequals(call.library, "sifcmd"))
{
return rpc.dispatchSifCmdImport(call.ordinal, cpu)
? ImportDisposition::Handled
: ImportDisposition::Missing;
}
if (iequals(call.library, "intrman") && intrman.dispatchImport(call.ordinal, cpu, *this))
return ImportDisposition::Handled;
if (iequals(call.library, "secrman"))
{
switch (call.ordinal)
{
case 4: // SecrSetMcCommandHandler
secrMcCommandHandler = {a0, cpu.gpr[28]};
setV0(0);
return ImportDisposition::Handled;
case 5: // SecrSetMcDevIDHandler
secrMcDevIdHandler = {a0, cpu.gpr[28]};
setV0(0);
return ImportDisposition::Handled;
default:
break;
}
}
if (iequals(call.library, "modload") && call.ordinal == 13u)
{
checkKelfPathCallback = {a0, cpu.gpr[28]};
setV0(0);
return ImportDisposition::Handled;
}
if (iequals(call.library, "ioman") && ioman.dispatchImport(call.ordinal, cpu, *this))
return ImportDisposition::Handled;
if (iequals(call.library, "sifman"))
{
return rpc.dispatchSifManImport(call.ordinal, cpu)
? ImportDisposition::Handled
: ImportDisposition::Missing;
}
if (iequals(call.library, "vblank") && vblank.dispatchImport(call.ordinal, cpu, totalCycles))
return ImportDisposition::Handled;
if (iequals(call.library, "timrman") && timrman.dispatchImport(call.ordinal, cpu, totalCycles))
return ImportDisposition::Handled;
if (iequals(call.library, "dmacman"))
{
setV0(0);
return ImportDisposition::Handled;
}
if (iequals(call.library, "stdio") && stdio.dispatchImport(call.ordinal, cpu))
return ImportDisposition::Handled;
if (iequals(call.library, "sysclib"))
{
return sysclib.dispatchImport(call.ordinal, cpu)
? ImportDisposition::Handled
: ImportDisposition::Missing;
}
if (iequals(call.library, "heaplib") && heaplib.dispatchImport(call.ordinal, cpu))
return ImportDisposition::Handled;
const uint32_t target = imports.resolve(call.library, call.ordinal, call.version);
if (target != 0u)
{
cpu.pc = target;
cpu.branchPending = false;
return ImportDisposition::JumpToGuest;
}
std::ostringstream out;
out << "[IOP] unhandled import " << call.library << ':' << call.ordinal
<< " version=0x" << std::hex << call.version << " pc=0x" << cpu.pc;
log(LogLevel::Warning, out.str());
setV0(0);
return ImportDisposition::Missing;
}
bool step(CpuState &cpu)
{
if (cpu.stopped)
return false;
if (cpu.pc == kThreadReturnSentinel || cpu.pc == kCallReturnSentinel)
{
cpu.stopped = true;
return false;
}
if (physicalAddress(cpu.pc) >= kRamSize)
{
std::ostringstream out;
out << "[IOP] execution outside RAM pc=0x" << std::hex << cpu.pc;
log(LogLevel::Error, out.str());
cpu.stopped = true;
return false;
}
if (checkInterrupt(cpu))
return true;
if (const auto import = imports.decode(cpu.pc))
{
const ImportDisposition disposition = dispatchImport(*import, cpu);
++totalInstructions;
++totalCycles;
if (disposition == ImportDisposition::JumpToGuest)
return true;
cpu.pc = cpu.gpr[31];
cpu.branchPending = false;
return !cpu.stopped;
}
const bool running = cpuCore.executeInstruction(cpu);
schedulePendingDma();
++totalInstructions;
++totalCycles;
return running;
}
uint32_t runCpu(CpuState &cpu, uint32_t instructionBudget)
{
CpuState *previous = activeCpu;
activeCpu = &cpu;
const uint64_t start = totalInstructions;
while (!cpu.stopped && !cpu.yielded && totalInstructions - start < instructionBudget)
{
if (!step(cpu))
break;
if (!servicingDmaInterrupts && !pendingDmaInterrupts.empty())
servicePendingDmaInterrupts();
if (!servicingGuestCallbacks && !pendingGuestCallbacks.empty())
servicePendingGuestCallbacks();
}
activeCpu = previous;
return static_cast<uint32_t>(totalInstructions - start);
}
uint32_t callFunction(uint32_t address,
uint32_t a0,
uint32_t a1,
uint32_t a2,
uint32_t a3,
uint32_t gp,
uint32_t budget = kMaxCallInstructions)
{
struct CallDepthGuard
{
uint32_t &depth;
~CallDepthGuard() { --depth; }
};
const uint32_t depth = callDepth++;
const CallDepthGuard depthGuard{callDepth};
CpuState cpu{};
cpu.pc = address;
cpu.gpr[4] = a0;
cpu.gpr[5] = a1;
cpu.gpr[6] = a2;
cpu.gpr[7] = a3;
cpu.gpr[28] = gp;
if (depth < kCallStackCapacity)
{
const uint32_t stackTop = kCallStackLimit - depth * kCallStackSize;
cpu.gpr[29] = stackTop - 32u;
}
else if (activeCpu && activeCpu->gpr[29] > kCallStackBase + kStackGuardBytes)
{
// Extremely deep re-entrancy borrows unused space below the
// suspended caller's live frame. Stack growth remains away
// from the caller, so its saved registers stay intact.
cpu.gpr[29] = (activeCpu->gpr[29] - kStackGuardBytes) & ~15u;
}
else
{
cpu.gpr[29] = kCallStackBase - 32u;
}
cpu.gpr[31] = kCallReturnSentinel;
runCpu(cpu, budget);
return cpu.gpr[2];
}
uint32_t executeGuestFunction(uint32_t address,
uint32_t a0,
uint32_t a1,
uint32_t a2,
uint32_t a3,
uint32_t gp) override
{
return callFunction(address, a0, a1, a2, a3, gp);
}
uint32_t executeGuestFunctionWithBudget(uint32_t address,
uint32_t a0,
uint32_t a1,
uint32_t a2,
uint32_t a3,
uint32_t gp,
uint32_t instructionBudget) override
{
return callFunction(address, a0, a1, a2, a3, gp, instructionBudget);
}
// Not that good to use exception handling for control flow but will do for now
void servicePendingDmaInterrupts()
{
if (servicingDmaInterrupts || pendingDmaInterrupts.empty())
return;
servicingDmaInterrupts = true;
std::vector<int> completed;
for (auto it = pendingDmaInterrupts.begin(); it != pendingDmaInterrupts.end();)
{
if (it->second > totalCycles)
{
++it;
continue;
}
completed.push_back(it->first);
it = pendingDmaInterrupts.erase(it);
}
try
{
for (const int irq : completed)
(void)intrman.dispatchInterrupt(irq, *this);
}
catch (...)
{
servicingDmaInterrupts = false;
throw;
}
servicingDmaInterrupts = false;
}
void servicePendingGuestCallbacks()
{
if (servicingGuestCallbacks || pendingGuestCallbacks.empty())
return;
std::vector<ScheduledGuestCallback> callbacks;
for (auto it = pendingGuestCallbacks.begin(); it != pendingGuestCallbacks.end();)
{
if (it->first > totalCycles)
break;
callbacks.push_back(it->second);
it = pendingGuestCallbacks.erase(it);
}
if (callbacks.empty())
return;
servicingGuestCallbacks = true;
try
{
for (const ScheduledGuestCallback &callback : callbacks)
{
if (callback.function != 0u)
{
(void)callFunction(callback.function,
callback.argument,
0u,
0u,
0u,
callback.gp,
100000u);
}
}
}
catch (...)
{
servicingGuestCallbacks = false;
throw;
}
servicingGuestCallbacks = false;
}
void runCycles(uint64_t cycles) noexcept
{
try
{
const uint64_t target = totalCycles + cycles;
while (totalCycles < target)
{
servicePendingDmaInterrupts();
servicePendingGuestCallbacks();
timrman.serviceDue(totalCycles, *this);
IopThread *next = kernel.beginNextReady(totalCycles);
if (!next)
{
uint64_t nextWake = kernel.nextWakeCycle(target);
for (const auto &[irq, completionCycle] : pendingDmaInterrupts)
nextWake = std::min(nextWake, completionCycle);
if (!pendingGuestCallbacks.empty())
nextWake = std::min(nextWake, pendingGuestCallbacks.begin()->first);
nextWake = timrman.nextEventCycle(nextWake);
totalCycles = std::max(totalCycles + 1u, std::min(target, nextWake));
continue;
}
const uint64_t before = totalCycles;
runCpu(next->cpu, static_cast<uint32_t>(std::min<uint64_t>(kDefaultSlice, target - totalCycles)));
kernel.endTimeslice(*next, kThreadReturnSentinel);
if (totalCycles == before)
++totalCycles;
}
}
catch (...)
{
// Runtime scheduling must never throw through EeScheduler::accountCycles().
}
}
ModuleLoadResult loadImage(std::string path, std::span<const uint8_t> image, const void *arguments, uint32_t argumentSize)
{
ModuleLoadResult result{true, -1, -1};
const IopImageLoadResult loaded = IopModuleLoader::load(image, memory, moduleCursor);
moduleCursor = loaded.nextModuleCursor;
if (!loaded)
{
if (loaded.error == IopImageLoadError::InvalidElf)
log(LogLevel::Error, "[IOP] rejected invalid/non-MIPS IRX ELF");
else if (loaded.error == IopImageLoadError::ArenaExhausted)
log(LogLevel::Error, "[IOP] module arena exhausted");
return result;
}
if (!loaded.relocationsComplete)
log(LogLevel::Warning, "[IOP] one or more IRX relocations were unsupported");
Module module;
module.id = nextModuleId++;
module.path = std::move(path);
const size_t slash = module.path.find_last_of("/\\:");
module.name = slash == std::string::npos ? module.path : module.path.substr(slash + 1u);
module.base = loaded.base;
module.size = loaded.size;
module.entry = loaded.entry;
module.gp = loaded.gp;
uint32_t args = 0u;
if (arguments && argumentSize)
{
args = allocate(argumentSize + 1u, 16u);
if (args)
{
writeRam(args, arguments, argumentSize);
write8(args + argumentSize, 0u);
}
}
const uint32_t startResult = callFunction(module.entry, argumentSize, args, 0u, 0u, module.gp);
if (args)
freeAllocation(args);
module.resident = startResult == 0u || startResult == 2u;
result.moduleId = module.id;
result.startResult = static_cast<int32_t>(startResult);
modules[module.id] = std::move(module);
std::ostringstream out;
out << "[IOP] loaded IRX id=" << result.moduleId
<< " entry=0x" << std::hex << modules[result.moduleId].entry
<< " base=0x" << modules[result.moduleId].base
<< " start=" << std::dec << result.startResult;
log(LogLevel::Info, out.str());
return result;
}
ModuleLoadResult loadModule(std::string_view path, const void *arguments, uint32_t argumentSize)
{
std::vector<uint8_t> image;
if (!IopModuleLoader::readWholeHostFile(host, path, image))
{
log(LogLevel::Warning, std::string("[IOP] failed to open IRX '") + std::string(path) + "'");
return {true, -1, -1};
}
return loadImage(std::string(path), image, arguments, argumentSize);
}
ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize)
{
std::vector<uint8_t> image;
if (!IopModuleLoader::readElfFromGuest(host, guestAddress, image))
return {true, -1, -1};
std::ostringstream tag;
tag << "buffer@0x" << std::hex << guestAddress;
return loadImage(tag.str(), image, arguments, argumentSize);
}
bool stopModule(int32_t moduleId, int32_t *result)
{
auto it = modules.find(moduleId);
if (it == modules.end())
return false;
// A removable IRX normally exposes a stop entry through module metadata. We do not guess it; terminate owned execution and release the image cleanly.
kernel.terminateThreadsInRange(it->second.base, it->second.size);
rpc.removeServersInRange(it->second.base, it->second.size);
imports.eraseRange(it->second.base, it->second.size);
modules.erase(it);
kernel.cleanupDeadThreads();
if (result)
*result = 0;
return true;
}
IopHost &host;
IopMemory memory;
IopSysmem sysmem;
IopKernel kernel;
IopCdvd cdvd;
IopVblank vblank;
IopRpcBridge rpc;
IopSysclib sysclib;
IopStdio stdio;
IopHeaplib heaplib;
IopIntrman intrman;
IopTimrman timrman;
IopIoman ioman;
IopCpuCore cpuCore;
IopImportRegistry imports;
IopLoadcore loadcore;
std::map<int, Module> modules;
std::map<int, uint64_t> pendingDmaInterrupts;
std::multimap<uint64_t, ScheduledGuestCallback> pendingGuestCallbacks;
uint32_t nextModuleId = 1;
uint32_t moduleCursor = kModuleLoadBase;
uint64_t totalCycles = 0;
uint64_t totalInstructions = 0;
uint64_t eeCycleCarry = 0;
CpuState *activeCpu = nullptr;
std::string lastError;
bool servicingDmaInterrupts = false;
bool servicingGuestCallbacks = false;
uint32_t callDepth = 0u;
GuestCallback secrMcCommandHandler;
GuestCallback secrMcDevIdHandler;
GuestCallback checkKelfPathCallback;
};
IopEmulator::IopEmulator(IopHost &host)
: m_impl(std::make_unique<Impl>(host))
{
}
IopEmulator::~IopEmulator() = default;
void IopEmulator::reset()
{
m_impl->reset();
}
ModuleLoadResult IopEmulator::loadModule(std::string_view path, const void *arguments, uint32_t argumentSize)
{
return m_impl->loadModule(path, arguments, argumentSize);
}
ModuleLoadResult IopEmulator::loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize)
{
return m_impl->loadModuleBuffer(guestAddress, arguments, argumentSize);
}
bool IopEmulator::stopModule(int32_t moduleId, int32_t *result)
{
return m_impl->stopModule(moduleId, result);
}
void IopEmulator::runEeCycles(uint64_t eeCycles) noexcept
{
const uint64_t total = m_impl->eeCycleCarry + eeCycles;
const uint64_t iopCycles = total / 8u;
m_impl->eeCycleCarry = total % 8u;
if (iopCycles)
m_impl->runCycles(iopCycles);
}
RpcResult IopEmulator::handleRpc(const RpcRequest &request)
{
return m_impl->rpc.handleRpc(request, *m_impl);
}
bool IopEmulator::hasRpcServer(uint32_t sid) const noexcept
{
return m_impl->rpc.hasServer(sid);
}
void IopEmulator::onSifTransfer(const SifTransfer &transfer)
{
m_impl->rpc.onSifTransfer(transfer);
}
uint32_t IopEmulator::allocateMemory(uint32_t size, uint32_t alignment)
{
return m_impl->memory.allocate(size, alignment);
}
bool IopEmulator::freeMemory(uint32_t address)
{
return m_impl->memory.freeAllocation(address);
}
bool IopEmulator::readMemory(uint32_t address, void *destination, size_t size) const
{
return isMemoryRange(address, size) &&
m_impl->memory.readRam(address, destination, size);
}
bool IopEmulator::writeMemory(uint32_t address, const void *source, size_t size)
{
return isMemoryRange(address, size) &&
m_impl->memory.writeRam(address, source, size);
}
bool IopEmulator::zeroMemory(uint32_t address, size_t size)
{
return isMemoryRange(address, size) &&
m_impl->memory.zeroRam(address, size);
}
bool IopEmulator::isMemoryRange(uint32_t address, size_t size) const
{
const bool physicalSegment = address < IopMemory::RamSize;
const bool cachedSegment = address >= 0x80000000u && address < 0x80200000u;
const bool uncachedSegment = address >= 0xA0000000u && address < 0xA0200000u;
if (!physicalSegment && !cachedSegment && !uncachedSegment)
return false;
const uint32_t physical = IopMemory::physicalAddress(address);
return physical <= IopMemory::RamSize && size <= IopMemory::RamSize - physical;
}
uint64_t IopEmulator::cycles() const noexcept
{
return m_impl->totalCycles;
}
uint64_t IopEmulator::instructions() const noexcept
{
return m_impl->totalInstructions;
}
uint32_t IopEmulator::loadedModuleCount() const noexcept
{
return static_cast<uint32_t>(m_impl->modules.size());
}
uint32_t IopEmulator::threadCount() const noexcept
{
return static_cast<uint32_t>(m_impl->kernel.threadCount());
}
uint32_t IopEmulator::rpcServerCount() const noexcept
{
return static_cast<uint32_t>(m_impl->rpc.serverCount());
}
}