feat: remove recompiled version of GetRomName

refactor: split IOP emulator logic
feat: added more HLE IOP modules
feat: added ps2_path
This commit is contained in:
Ran-j
2026-09-02 17:19:00 -03:00
parent cc9e075573
commit b9dba607d8
51 changed files with 2690 additions and 685 deletions
+23 -8
View File
@@ -6,16 +6,26 @@ option(PS2X_IOP_ENABLE_PLUGINS "Enable dynamic ps2xIOP plugins" OFF)
option(PS2X_IOP_BUILD_TESTS "Build ps2xIOP emulator smoke tests" OFF)
add_library(ps2_iop STATIC
src/ps2_path.cpp
src/iop_module_manager.cpp
src/iop_subsystem.cpp
src/emulator/iop_emulator.cpp
src/emulator/iop_cdvd.cpp
src/emulator/iop_cpu.cpp
src/emulator/iop_imports.cpp
src/emulator/iop_kernel.cpp
src/emulator/iop_memory.cpp
src/emulator/iop_module_loader.cpp
src/emulator/iop_rpc.cpp
src/emulator/iop_sysclib.cpp
src/emulator/core/iop_cpu.cpp
src/emulator/core/iop_kernel.cpp
src/emulator/core/iop_memory.cpp
src/emulator/services/iop_module_loader.cpp
src/emulator/services/iop_rpc.cpp
src/emulator/imports/iop_cdvd.cpp
src/emulator/imports/iop_heaplib.cpp
src/emulator/imports/iop_imports.cpp
src/emulator/imports/iop_intrman.cpp
src/emulator/imports/iop_ioman.cpp
src/emulator/imports/iop_loadcore.cpp
src/emulator/imports/iop_stdio.cpp
src/emulator/imports/iop_sysclib.cpp
src/emulator/imports/iop_sysmem.cpp
src/emulator/imports/iop_timrman.cpp
src/emulator/imports/iop_vblank.cpp
src/builtin_profiles.cpp
src/plugin_loader.cpp
src/modules/dbcman.cpp
@@ -53,6 +63,11 @@ if(PS2X_IOP_BUILD_TESTS)
add_executable(ps2_iop_emulator_tests tests/iop_emulator_tests.cpp)
target_link_libraries(ps2_iop_emulator_tests PRIVATE ps2_iop)
add_test(NAME ps2_iop_emulator_tests COMMAND ps2_iop_emulator_tests)
add_executable(ps2_iop_import_tests tests/iop_import_tests.cpp)
target_link_libraries(ps2_iop_import_tests PRIVATE ps2_iop)
target_include_directories(ps2_iop_import_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
add_test(NAME ps2_iop_import_tests COMMAND ps2_iop_import_tests)
endif()
install(TARGETS ps2_iop
+40
View File
@@ -62,6 +62,34 @@ namespace ps2x::iop
virtual bool writeGuest(uint32_t address, const void *source, size_t size) = 0;
virtual bool zeroGuest(uint32_t address, size_t size) = 0;
virtual bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const = 0;
// IOP RAM is a distinct address space from the EE guest. TODO remove this later
virtual bool readIopMemory(uint32_t address, void *destination, size_t size) const
{
(void)address;
(void)destination;
(void)size;
return false;
}
virtual bool writeIopMemory(uint32_t address, const void *source, size_t size)
{
(void)address;
(void)source;
(void)size;
return false;
}
virtual bool zeroIopMemory(uint32_t address, size_t size)
{
(void)address;
(void)size;
return false;
}
virtual bool normalizeIopAddress(uint32_t address, uint32_t &normalized) const
{
(void)address;
normalized = 0u;
return false;
}
virtual uint32_t allocateIopHandle(IopHandleKind kind) = 0;
virtual uint32_t allocateGuest(uint32_t size, uint32_t alignment) = 0;
virtual void freeGuest(uint32_t address) = 0;
@@ -90,6 +118,18 @@ namespace ps2x::iop
uint32_t a3,
uint32_t *resultAddress) = 0;
// Deliver an IOP -> EE SIF command packet. The default keeps hosts
// which do not emulate the EE command dispatcher source-compatible.
virtual bool sendSifCommand(uint32_t commandId,
const void *packet,
size_t packetSize)
{
(void)commandId;
(void)packet;
(void)packetSize;
return false;
}
virtual void log(LogLevel level, std::string_view message) = 0;
};
}
+8
View File
@@ -38,6 +38,14 @@ namespace ps2x::iop
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request);
void onSifTransfer(const SifTransfer &transfer);
// Physical IOP RAM access shared by the emulator, SIF DMA, and HLE services. Addresses are IOP addresses.
[[nodiscard]] uint32_t allocateMemory(uint32_t size, uint32_t alignment = 16u);
[[nodiscard]] bool freeMemory(uint32_t address);
[[nodiscard]] bool readMemory(uint32_t address, void *destination, size_t size) const;
[[nodiscard]] bool writeMemory(uint32_t address, const void *source, size_t size);
[[nodiscard]] bool zeroMemory(uint32_t address, size_t size);
[[nodiscard]] bool isMemoryRange(uint32_t address, size_t size) const;
[[nodiscard]] DebugSnapshot debugSnapshot() const;
private:
+1
View File
@@ -139,6 +139,7 @@ namespace ps2x::iop
std::string name;
std::vector<uint32_t> sids;
bool profileSpecific = false;
bool active = true;
std::vector<DebugMetric> metrics;
};
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <string>
#include <string_view>
namespace ps2x::iop
{
enum class Ps2PathDevice
{
Invalid,
Host,
Cdrom,
MemoryCard0,
Rom0,
NativeHost,
};
struct ParsedPs2Path
{
Ps2PathDevice device = Ps2PathDevice::Invalid;
std::string deviceName;
std::string path;
[[nodiscard]] explicit operator bool() const noexcept
{
return device != Ps2PathDevice::Invalid;
}
};
[[nodiscard]] ParsedPs2Path parsePs2Path(std::string_view path);
// Returns a lower-case module/file leaf without an optional .irx suffix.
[[nodiscard]] std::string ps2PathLeafKey(const ParsedPs2Path &path);
[[nodiscard]] std::string ps2PathLeafKey(std::string_view path);
}
@@ -1,7 +1,7 @@
#include "iop_kernel.h"
#include "iop_memory.h"
#include "iop_emulator_const.h"
#include "../iop_emulator_const.h"
#include <algorithm>
@@ -502,6 +502,28 @@ namespace ps2x::iop::detail
}
}
int IopKernel::createInternalEventFlag(uint32_t attr, uint32_t option, uint32_t bits)
{
EventFlag event;
event.id = static_cast<int>(m_nextEventFlagId++);
event.attr = attr;
event.option = option;
event.bits = bits;
const int id = event.id;
m_eventFlags.emplace(id, event);
return id;
}
bool IopKernel::setInternalEventFlag(int id, uint32_t bits)
{
const auto event = m_eventFlags.find(id);
if (event == m_eventFlags.end())
return false;
event->second.bits |= bits;
wakeEventWaiters(event->second);
return true;
}
bool IopKernel::dispatchEventImport(uint16_t ordinal, IopCpuState &cpu)
{
const auto setV0 = [&](int32_t value)
@@ -514,13 +536,9 @@ namespace ps2x::iop::detail
case 4:
{
const uint32_t descriptor = cpu.gpr[4];
EventFlag event;
event.id = static_cast<int>(m_nextEventFlagId++);
event.attr = m_memory.read32(descriptor + 0u);
event.option = m_memory.read32(descriptor + 4u);
event.bits = m_memory.read32(descriptor + 8u);
m_eventFlags.emplace(event.id, event);
setV0(event.id);
setV0(createInternalEventFlag(m_memory.read32(descriptor + 0u),
m_memory.read32(descriptor + 4u),
m_memory.read32(descriptor + 8u)));
return true;
}
case 5:
@@ -546,14 +564,11 @@ namespace ps2x::iop::detail
case 6:
case 7:
{
const auto event = m_eventFlags.find(static_cast<int>(cpu.gpr[4]));
if (event == m_eventFlags.end())
if (!setInternalEventFlag(static_cast<int>(cpu.gpr[4]), cpu.gpr[5]))
{
setV0(-1);
return true;
}
event->second.bits |= cpu.gpr[5];
wakeEventWaiters(event->second);
setV0(0);
return true;
}
@@ -54,6 +54,9 @@ namespace ps2x::iop::detail
[[nodiscard]] bool dispatchSemaphoreImport(uint16_t ordinal, IopCpuState &cpu);
[[nodiscard]] bool dispatchEventImport(uint16_t ordinal, IopCpuState &cpu);
[[nodiscard]] int createInternalEventFlag(uint32_t attr, uint32_t option, uint32_t bits);
[[nodiscard]] bool setInternalEventFlag(int id, uint32_t bits);
void sleepCurrent(IopCpuState &cpu);
void delayCurrentUntil(uint64_t wakeCycle, IopCpuState &cpu);
@@ -84,6 +84,7 @@ namespace ps2x::iop::detail
}
if ((phys & 3u) == 0u && isHardwareAddress(phys))
return readHardware32(phys);
return static_cast<uint32_t>(read8(address)) |
(static_cast<uint32_t>(read8(address + 1u)) << 8u) |
(static_cast<uint32_t>(read8(address + 2u)) << 16u) |
@@ -1,8 +1,10 @@
#include "iop_cdvd.h"
#include "iop_cpu.h"
#include "iop_memory.h"
#include "../core/iop_cpu.h"
#include "../core/iop_kernel.h"
#include "../core/iop_memory.h"
#include "ps2x/iop/iop_host.h"
#include "ps2x/iop/ps2_path.h"
#include <algorithm>
#include <array>
@@ -32,6 +34,11 @@ namespace ps2x::iop::detail
constexpr uint32_t kCdvdInitExit = 5u;
constexpr uint32_t kCdvdCallbackRead = 1u;
constexpr uint32_t kCdvdCallbackSeek = 4u;
constexpr uint32_t kCdvdInterruptReadyBits = 0x29u;
constexpr uint32_t kEventFlagMulti = 2u;
constexpr uint32_t kCdvdStreamTimeout = 5000u;
constexpr uint32_t kCdvdSyncTimeout = 15000u;
constexpr uint32_t kCdvdmanVersion = 0x0226u;
uint32_t alignSectors(uint64_t bytes)
{
@@ -109,6 +116,23 @@ namespace ps2x::iop::detail
return static_cast<uint32_t>(std::max<uint64_t>(kSectorSize, alignSectors(cursor) * kSectorSize));
}
std::string normalizedIsoComponent(std::string_view value)
{
std::string result(value);
const size_t semicolon = result.rfind(';');
if (semicolon != std::string::npos && semicolon + 1u < result.size() &&
std::all_of(result.begin() + static_cast<std::ptrdiff_t>(semicolon + 1u), result.end(),
[](unsigned char ch)
{ return std::isdigit(ch) != 0; }))
{
result.resize(semicolon);
}
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char ch)
{ return static_cast<char>(std::toupper(ch)); });
return result;
}
size_t writeDirectoryRecord(uint8_t *destination,
uint32_t lsn,
uint32_t size,
@@ -151,8 +175,8 @@ namespace ps2x::iop::detail
std::vector<size_t> children;
};
Impl(IopHost &hostRef, IopMemory &memoryRef)
: host(hostRef), memory(memoryRef)
Impl(IopHost &hostRef, IopMemory &memoryRef, IopKernel &kernelRef)
: host(hostRef), memory(memoryRef), kernel(kernelRef)
{
}
@@ -169,6 +193,9 @@ namespace ps2x::iop::detail
mediaMode = 0u;
currentLsn = 0u;
lastError = kCdvdErrorNone;
streamFlag = 0u;
lastReadTimeout = 0u;
interruptEventFlagId = 0;
virtualIsoBuilt = false;
virtualIsoValid = false;
completionCallback.reset();
@@ -203,6 +230,7 @@ namespace ps2x::iop::detail
case 6: // sceCdRead
if (readSectors(a0, a1, a2))
{
signalCommandComplete();
if (callback.address != 0u)
{
completionCallback = CompletionCallback{
@@ -220,6 +248,7 @@ namespace ps2x::iop::detail
case 7: // sceCdSeek
currentLsn = a0;
lastError = kCdvdErrorNone;
signalCommandComplete();
if (callback.address != 0u)
{
completionCallback = CompletionCallback{
@@ -235,6 +264,10 @@ namespace ps2x::iop::detail
cpu.gpr[2] = lastError;
return true;
case 10: // sceCdSearchFile
cpu.gpr[2] = searchFile(a0, a1) ? 1u : 0u;
return true;
case 11: // sceCdSync
cpu.gpr[2] = 0u;
return true;
@@ -259,6 +292,52 @@ namespace ps2x::iop::detail
return true;
}
case 50: // sceCdSC
{
const int32_t code = static_cast<int32_t>(a0);
switch (code)
{
case -23: // Translate a logical sector for a dual-layer disc.
// The host image exposes one continuous LSN space, so no layer offset is required.
cpu.gpr[2] = a1 != 0u ? memory.read32(a1) : 0u;
return true;
case -18:
lastReadTimeout = a1 != 0u ? memory.read32(a1) : 0u;
cpu.gpr[2] = 0u;
return true;
case -17:
cpu.gpr[2] = kCdvdStreamTimeout;
return true;
case -15:
cpu.gpr[2] = kCdvdSyncTimeout;
return true;
case -11:
cpu.gpr[2] = static_cast<uint32_t>(ensureInterruptEventFlag());
return true;
case -9:
cpu.gpr[2] = kCdvdmanVersion;
return true;
case -2:
lastError = a1 != 0u ? memory.read8(a1) : kCdvdErrorNone;
cpu.gpr[2] = lastError;
return true;
case -1:
case 0:
case 1:
case 2:
if (a1 != 0u)
memory.write32(a1, lastError & 0xFFu);
if (code != -1)
streamFlag = static_cast<uint32_t>(code);
cpu.gpr[2] = streamFlag;
return true;
default:
// sceCdSC is intentionally extensible; unsupported controls are no-ops in cdvdman.
cpu.gpr[2] = 0u;
return true;
}
}
case 75: // sceCdMmode
mediaMode = a0;
cpu.gpr[2] = 1u;
@@ -277,6 +356,22 @@ namespace ps2x::iop::detail
}
private:
int ensureInterruptEventFlag()
{
if (interruptEventFlagId == 0)
{
interruptEventFlagId = kernel.createInternalEventFlag(
kEventFlagMulti, 0u, kCdvdInterruptReadyBits);
}
return interruptEventFlagId;
}
void signalCommandComplete()
{
if (interruptEventFlagId != 0)
(void)kernel.setInternalEventFlag(interruptEventFlagId, kCdvdInterruptReadyBits);
}
void closeFiles()
{
if (imageHandle != 0u)
@@ -463,6 +558,62 @@ namespace ps2x::iop::detail
return true;
}
IsoNode *findVirtualIsoNode(std::string_view guestPath)
{
if (!buildVirtualIso())
return nullptr;
const ParsedPs2Path parsed = parsePs2Path(guestPath);
if (!parsed || parsed.device != Ps2PathDevice::Cdrom)
return nullptr;
size_t current = 0u;
size_t begin = 0u;
while (begin <= parsed.path.size())
{
const size_t end = parsed.path.find('/', begin);
const size_t length = (end == std::string::npos) ? parsed.path.size() - begin : end - begin;
const std::string_view component(parsed.path.data() + begin, length);
begin = (end == std::string::npos) ? parsed.path.size() + 1u : end + 1u;
if (component.empty() || component == ".")
continue;
if (component == "..")
return nullptr;
const std::string wanted = normalizedIsoComponent(component);
const auto child = std::find_if(nodes[current].children.begin(), nodes[current].children.end(),
[&](size_t childIndex)
{
return normalizedIsoComponent(nodes[childIndex].identifier) == wanted;
});
if (child == nodes[current].children.end())
return nullptr;
current = *child;
}
return &nodes[current];
}
bool searchFile(uint32_t resultAddress, uint32_t nameAddress)
{
if (resultAddress == 0u || nameAddress == 0u)
return false;
const std::string guestPath = memory.readString(nameAddress, 1024u);
IsoNode *node = findVirtualIsoNode(guestPath);
if (!node)
return false;
// sceCdlFILE: lsn, size, name[16], date/flags[8].
std::array<uint8_t, 32u> result{};
writeLe32(result.data(), node->lsn);
writeLe32(result.data() + 4u, node->size);
const std::string leaf = normalizedIsoComponent(node->identifier);
std::memcpy(result.data() + 8u, leaf.data(), std::min<size_t>(16u, leaf.size()));
result[24u] = node->directory ? 2u : 0u;
return memory.writeRam(resultAddress, result.data(), result.size());
}
IsoNode *fileForSector(uint32_t lsn)
{
for (IsoNode &node : nodes)
@@ -558,12 +709,16 @@ namespace ps2x::iop::detail
IopHost &host;
IopMemory &memory;
IopKernel &kernel;
Callback callback;
std::optional<CompletionCallback> completionCallback;
bool initialized = false;
uint32_t mediaMode = 0u;
uint32_t currentLsn = 0u;
uint32_t lastError = kCdvdErrorNone;
uint32_t streamFlag = 0u;
uint32_t lastReadTimeout = 0u;
int interruptEventFlagId = 0;
uint64_t imageHandle = 0u;
bool virtualIsoBuilt = false;
bool virtualIsoValid = false;
@@ -572,8 +727,8 @@ namespace ps2x::iop::detail
std::unordered_map<uint32_t, std::array<uint8_t, kSectorSize>> metadataSectors;
};
IopCdvd::IopCdvd(IopHost &host, IopMemory &memory)
: m_impl(std::make_unique<Impl>(host, memory))
IopCdvd::IopCdvd(IopHost &host, IopMemory &memory, IopKernel &kernel)
: m_impl(std::make_unique<Impl>(host, memory, kernel))
{
}
@@ -12,6 +12,7 @@ namespace ps2x::iop
namespace ps2x::iop::detail
{
struct IopCpuState;
class IopKernel;
class IopMemory;
class IopCdvd
@@ -24,7 +25,7 @@ namespace ps2x::iop::detail
uint32_t reason = 0u;
};
IopCdvd(IopHost &host, IopMemory &memory);
IopCdvd(IopHost &host, IopMemory &memory, IopKernel &kernel);
~IopCdvd();
IopCdvd(const IopCdvd &) = delete;
@@ -0,0 +1,54 @@
#include "iop_heaplib.h"
#include "../core/iop_cpu.h"
#include "../core/iop_memory.h"
namespace ps2x::iop::detail
{
IopHeaplib::IopHeaplib(IopMemory &memory) noexcept
: m_memory(memory)
{
}
bool IopHeaplib::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
{
const uint32_t a0 = cpu.gpr[4];
const uint32_t a1 = cpu.gpr[5];
const auto setV0 = [&](uint32_t value)
{
cpu.gpr[2] = value;
};
switch (ordinal)
{
case 4: // CreateHeap
setV0(m_memory.allocate(16u, 16u));
return true;
case 5: // DeleteHeap
if (a0 != 0u)
(void)m_memory.freeAllocation(a0);
setV0(0u);
return true;
case 6:
setV0(m_memory.allocate(a1, 16u));
return true;
case 7:
setV0(m_memory.freeAllocation(a1) ? 0u : 0xFFFFFFFFu);
return true;
case 8:
setV0(m_memory.maxFreeMemory());
return true;
case 11:
setV0(0u);
return true;
case 15:
if (const auto block = m_memory.allocationContaining(a0))
setV0(block->size);
else
setV0(0xFFFFFFFFu);
return true;
default:
return false;
}
}
}
@@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
namespace ps2x::iop::detail
{
struct IopCpuState;
class IopMemory;
class IopHeaplib
{
public:
explicit IopHeaplib(IopMemory &memory) noexcept;
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
private:
IopMemory &m_memory;
};
}
@@ -1,6 +1,6 @@
#include "iop_imports.h"
#include "iop_memory.h"
#include "../core/iop_memory.h"
#include <algorithm>
#include <cctype>
@@ -147,6 +147,26 @@ namespace ps2x::iop::detail
return found->second.functions[ordinal];
}
int32_t IopImportRegistry::setRebootTimeLibraryHandlingMode(uint32_t address, uint32_t mode)
{
constexpr int32_t kLibraryNotFound = -213;
constexpr int32_t kIllegalLibrary = -214;
if (address == 0u)
return kIllegalLibrary;
const uint32_t physical = IopMemory::physicalAddress(address);
if (physical + 12u > IopMemory::RamSize)
return kLibraryNotFound;
const bool registered = m_libraries.find(physical) != m_libraries.end();
if (!registered && m_memory.read32(physical) != kExportMagic)
return kLibraryNotFound;
const uint16_t oldMode = m_memory.read16(physical + 10u);
m_memory.write16(physical + 10u, static_cast<uint16_t>((oldMode & ~6u) | (mode & 6u)));
return 0;
}
void IopImportRegistry::eraseRange(uint32_t base, uint32_t size)
{
for (auto library = m_libraries.begin(); library != m_libraries.end();)
@@ -29,6 +29,7 @@ namespace ps2x::iop::detail
[[nodiscard]] bool releaseExportTable(uint32_t address);
[[nodiscard]] uint32_t findTable(std::string_view library) const;
[[nodiscard]] uint32_t resolve(std::string_view library, uint16_t ordinal) const;
[[nodiscard]] int32_t setRebootTimeLibraryHandlingMode(uint32_t address, uint32_t mode);
void eraseRange(uint32_t base, uint32_t size);
private:
@@ -0,0 +1,113 @@
#include "iop_intrman.h"
#include "../core/iop_cpu.h"
#include "../core/iop_memory.h"
#include "../services/iop_rpc.h"
namespace ps2x::iop::detail
{
IopIntrman::IopIntrman(IopMemory &memory) noexcept
: m_memory(memory)
{
}
void IopIntrman::reset()
{
m_handlers.clear();
m_enabled.clear();
}
bool IopIntrman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor)
{
const uint32_t a0 = cpu.gpr[4];
const uint32_t a1 = cpu.gpr[5];
const uint32_t a2 = cpu.gpr[6];
const uint32_t a3 = cpu.gpr[7];
const auto setV0 = [&](uint32_t value)
{
cpu.gpr[2] = value;
};
switch (ordinal)
{
case 3:
setV0(0u);
return true;
case 4: // RegisterIntrHandler
m_handlers[static_cast<int>(a0)] = {a2, a3, cpu.gpr[28]};
setV0(0u);
return true;
case 5: // ReleaseIntrHandler
m_handlers.erase(static_cast<int>(a0));
setV0(0u);
return true;
case 6: // EnableIntr
m_enabled[static_cast<int>(a0)] = true;
if (a0 < 32u)
m_memory.setInterruptMask(m_memory.interruptMask() | (1u << a0));
setV0(0u);
return true;
case 7: // DisableIntr
if (a1 != 0u)
m_memory.write32(a1, a0);
if (a0 < 32u)
m_memory.setInterruptMask(m_memory.interruptMask() & ~(1u << a0));
m_enabled[static_cast<int>(a0)] = false;
setV0(0u);
return true;
case 8: // CpuDisableIntr
m_memory.setInterruptControl(0u);
setV0(0u);
return true;
case 9: // CpuEnableIntr
m_memory.setInterruptControl(1u);
setV0(0u);
return true;
case 14:
setV0(a0 != 0u
? executor.executeGuestFunctionWithBudget(a0, a1, a2, a3, 0u, cpu.gpr[28], 100000u)
: 0u);
return true;
case 15:
case 16:
case 23:
case 24:
case 25:
case 28:
case 30:
setV0(0u);
return true;
case 17:
if (a0 != 0u)
m_memory.write32(a0, m_memory.interruptControl());
m_memory.setInterruptControl(0u);
setV0(0u);
return true;
case 18:
m_memory.setInterruptControl(a0 != 0u ? 1u : 0u);
setV0(0u);
return true;
default:
return false;
}
}
bool IopIntrman::dispatchInterrupt(int irq, IopGuestExecutor &executor) const
{
const auto enabled = m_enabled.find(irq);
if (enabled == m_enabled.end() || !enabled->second)
return false;
const auto handler = m_handlers.find(irq);
if (handler == m_handlers.end() || handler->second.function == 0u)
return false;
(void)executor.executeGuestFunctionWithBudget(handler->second.function,
handler->second.argument,
0u,
0u,
0u,
handler->second.gp,
100000u);
return true;
}
}
@@ -0,0 +1,33 @@
#pragma once
#include <cstdint>
#include <map>
namespace ps2x::iop::detail
{
struct IopCpuState;
class IopGuestExecutor;
class IopMemory;
class IopIntrman
{
public:
explicit IopIntrman(IopMemory &memory) noexcept;
void reset();
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor);
[[nodiscard]] bool dispatchInterrupt(int irq, IopGuestExecutor &executor) const;
private:
struct Handler
{
uint32_t function = 0u;
uint32_t argument = 0u;
uint32_t gp = 0u;
};
IopMemory &m_memory;
std::map<int, Handler> m_handlers;
std::map<int, bool> m_enabled;
};
}
@@ -0,0 +1,91 @@
#include "iop_ioman.h"
#include "../core/iop_cpu.h"
#include "../core/iop_memory.h"
#include "../services/iop_rpc.h"
#include <algorithm>
namespace ps2x::iop::detail
{
IopIoman::IopIoman(IopMemory &memory) noexcept
: m_memory(memory)
{
}
void IopIoman::reset()
{
m_devices.clear();
}
bool IopIoman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor)
{
constexpr size_t kMaxDevices = 16u;
const uint32_t a0 = cpu.gpr[4];
const auto setV0 = [&](uint32_t value)
{
cpu.gpr[2] = value;
};
switch (ordinal)
{
case 20: // AddDrv
{
if (a0 == 0u || m_devices.size() >= kMaxDevices)
{
setV0(0xFFFFFFFFu);
return true;
}
const uint32_t nameAddress = m_memory.read32(a0);
const uint32_t operations = m_memory.read32(a0 + 16u);
const std::string name = m_memory.readString(nameAddress, 64u);
if (nameAddress == 0u || operations == 0u || name.empty())
{
setV0(0xFFFFFFFFu);
return true;
}
m_devices.push_back({a0, cpu.gpr[28], name});
const uint32_t init = m_memory.read32(operations);
if (init != 0u)
{
const int32_t result = static_cast<int32_t>(
executor.executeGuestFunction(init, a0, 0u, 0u, 0u, cpu.gpr[28]));
if (result < 0)
{
m_devices.pop_back();
setV0(0xFFFFFFFFu);
return true;
}
}
setV0(0u);
return true;
}
case 21: // DelDrv
{
const std::string name = m_memory.readString(a0, 64u);
const auto device = std::find_if(
m_devices.begin(), m_devices.end(),
[&](const Device &candidate)
{ return candidate.name == name; });
if (device == m_devices.end())
{
setV0(0xFFFFFFFFu);
return true;
}
const uint32_t operations = m_memory.read32(device->address + 16u);
const uint32_t deinit = operations != 0u ? m_memory.read32(operations + 4u) : 0u;
if (deinit != 0u)
(void)executor.executeGuestFunction(deinit, device->address, 0u, 0u, 0u, device->gp);
m_devices.erase(device);
setV0(0u);
return true;
}
default:
return false;
}
}
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace ps2x::iop::detail
{
struct IopCpuState;
class IopGuestExecutor;
class IopMemory;
class IopIoman
{
public:
explicit IopIoman(IopMemory &memory) noexcept;
void reset();
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor);
private:
struct Device
{
uint32_t address = 0u;
uint32_t gp = 0u;
std::string name;
};
IopMemory &m_memory;
std::vector<Device> m_devices;
};
}
@@ -0,0 +1,56 @@
#include "iop_loadcore.h"
#include "../core/iop_cpu.h"
#include "iop_imports.h"
#include "../core/iop_memory.h"
namespace ps2x::iop::detail
{
IopLoadcore::IopLoadcore(IopMemory &memory, IopImportRegistry &imports) noexcept
: m_memory(memory), m_imports(imports)
{
}
bool IopLoadcore::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
{
const uint32_t a0 = cpu.gpr[4];
const auto setV0 = [&](uint32_t value)
{
cpu.gpr[2] = value;
};
switch (ordinal)
{
case 3:
case 4:
case 5:
case 8:
case 9:
case 12:
case 13:
case 14:
case 15:
case 16:
case 17:
case 20:
case 21:
setV0(0u);
return true;
case 6:
case 10:
setV0(m_imports.registerExportTable(a0) ? 0u : 0xFFFFFFFFu);
return true;
case 7:
setV0(m_imports.releaseExportTable(a0) ? 0u : 0xFFFFFFFFu);
return true;
case 11:
setV0(m_imports.findTable(m_memory.readString(a0 + 12u, 8u)));
return true;
case 27: // SetRebootTimeLibraryHandlingMode
setV0(static_cast<uint32_t>(m_imports.setRebootTimeLibraryHandlingMode(a0, cpu.gpr[5])));
return true;
default:
return false;
}
}
}
@@ -0,0 +1,22 @@
#pragma once
#include <cstdint>
namespace ps2x::iop::detail
{
struct IopCpuState;
class IopImportRegistry;
class IopMemory;
class IopLoadcore
{
public:
IopLoadcore(IopMemory &memory, IopImportRegistry &imports) noexcept;
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
private:
IopMemory &m_memory;
IopImportRegistry &m_imports;
};
}
@@ -0,0 +1,67 @@
#include "iop_stdio.h"
#include "../core/iop_cpu.h"
#include "../core/iop_memory.h"
#include "ps2x/iop/iop_host.h"
#include <string>
namespace ps2x::iop::detail
{
IopStdio::IopStdio(IopHost &host, IopMemory &memory) noexcept
: m_host(host), m_memory(memory)
{
}
bool IopStdio::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
{
const uint32_t a0 = cpu.gpr[4];
const uint32_t a1 = cpu.gpr[5];
const auto setV0 = [&](uint32_t value)
{
cpu.gpr[2] = value;
};
const auto logString = [&](std::string_view prefix, uint32_t address, uint32_t resultBias = 0u)
{
const std::string text = m_memory.readString(address, 2048u);
m_host.log(LogLevel::Info, std::string(prefix) + text);
setV0(static_cast<uint32_t>(text.size()) + resultBias);
};
switch (ordinal)
{
case 4: // printf
logString("[IOP printf] ", a0);
return true;
case 5: // getchar
case 10:
setV0(0xFFFFFFFFu);
return true;
case 6: // putchar
m_host.log(LogLevel::Info, std::string("[IOP putchar] ") + static_cast<char>(a0 & 0xFFu));
setV0(a0 & 0xFFu);
return true;
case 7: // puts
logString("[IOP puts] ", a0, 1u);
return true;
case 8: // gets
case 13:
setV0(0u);
return true;
case 9: // fdprintf
logString("[IOP fdprintf] ", a1);
return true;
case 11:
setV0(a0 & 0xFFu);
return true;
case 12: // fdputs
logString("[IOP fdputs] ", a0);
return true;
case 14: // vfdprintf
logString("[IOP vfdprintf] ", a1);
return true;
default:
return false;
}
}
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
namespace ps2x::iop
{
class IopHost;
}
namespace ps2x::iop::detail
{
struct IopCpuState;
class IopMemory;
class IopStdio
{
public:
IopStdio(IopHost &host, IopMemory &memory) noexcept;
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
private:
IopHost &m_host;
IopMemory &m_memory;
};
}
@@ -1,7 +1,7 @@
#include "iop_sysclib.h"
#include "iop_cpu.h"
#include "iop_memory.h"
#include "../core/iop_cpu.h"
#include "../core/iop_memory.h"
#include <cctype>
#include <cstdlib>
@@ -64,7 +64,7 @@ namespace ps2x::iop::detail
case 4: // setjmp - enough for callers which only test the initial return.
setV0(0);
return true;
case 5: // longjmp cannot be safely synthesized without the BIOS jmp_buf ABI.
case 5: // longjmp, TODO bc w can do it without the BIOS jmp_buf ABI.
setV0(a1 == 0u ? 1u : a1);
return true;
case 6:
@@ -0,0 +1,72 @@
#include "iop_sysmem.h"
#include "../core/iop_cpu.h"
#include "../core/iop_memory.h"
#include "ps2x/iop/iop_host.h"
#include <string>
namespace ps2x::iop::detail
{
IopSysmem::IopSysmem(IopHost &host, IopMemory &memory) noexcept
: m_host(host), m_memory(memory)
{
}
bool IopSysmem::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
{
const uint32_t a0 = cpu.gpr[4];
const uint32_t a1 = cpu.gpr[5];
const uint32_t a2 = cpu.gpr[6];
const auto setV0 = [&](uint32_t value)
{
cpu.gpr[2] = value;
};
switch (ordinal)
{
case 4: // AllocSysMemory
{
const uint32_t address = a0 == 2u
? m_memory.allocate(a1, 16u, a2)
: m_memory.allocate(a1, 16u);
setV0(address);
return true;
}
case 5: // FreeSysMemory
setV0(m_memory.freeAllocation(a0) ? 0u : 0xFFFFFFFFu);
return true;
case 6: // QueryMemSize
setV0(IopMemory::RamSize);
return true;
case 7: // QueryMaxFreeMemSize
case 8: // QueryTotalFreeMemSize
setV0(m_memory.maxFreeMemory());
return true;
case 9: // QueryBlockTopAddress
if (const auto block = m_memory.allocationContaining(a0))
setV0(block->address);
else
setV0(0u);
return true;
case 10: // QueryBlockSize
if (const auto block = m_memory.allocationContaining(a0))
setV0(block->size);
else
setV0(0xFFFFFFFFu);
return true;
case 14: // Kprintf
{
const std::string format = m_memory.readString(a0, 512u);
m_host.log(LogLevel::Info, std::string("[IOP Kprintf] ") + format);
setV0(static_cast<uint32_t>(format.size()));
return true;
}
case 15:
setV0(0u);
return true;
default:
return false;
}
}
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
namespace ps2x::iop
{
class IopHost;
}
namespace ps2x::iop::detail
{
struct IopCpuState;
class IopMemory;
class IopSysmem
{
public:
IopSysmem(IopHost &host, IopMemory &memory) noexcept;
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
private:
IopHost &m_host;
IopMemory &m_memory;
};
}
@@ -0,0 +1,476 @@
#include "iop_timrman.h"
#include "../core/iop_cpu.h"
#include "../services/iop_rpc.h"
#include <algorithm>
#include <array>
#include <limits>
namespace ps2x::iop::detail
{
namespace
{
constexpr int32_t kNoTimer = -150;
constexpr int32_t kIllegalTimerId = -151;
constexpr int32_t kIllegalSource = -152;
constexpr int32_t kIllegalPrescale = -153;
constexpr int32_t kTimerBusy = -154;
constexpr int32_t kTimerNotConfigured = -155;
constexpr int32_t kTimerNotRunning = -156;
constexpr int32_t kIllegalMode = -405;
constexpr uint64_t kIopClockHz = 36'864'000ull;
constexpr uint64_t kPixelClockHz = 13'500'000ull;
constexpr uint64_t kHlineClockHz = 15'734ull;
constexpr std::array<size_t, 6> kAllocationOrder{2u, 5u, 4u, 3u, 0u, 1u};
constexpr std::array<uint32_t, 6> kAddresses{
0xBF801100u,
0xBF801110u,
0xBF801120u,
0xBF801480u,
0xBF801490u,
0xBF8014A0u,
};
constexpr std::array<uint8_t, 6> kSources{0x0Bu, 0x0Du, 0x01u, 0x05u, 0x01u, 0x01u};
constexpr std::array<uint8_t, 6> kWidths{16u, 16u, 16u, 32u, 32u, 32u};
constexpr std::array<uint16_t, 6> kMaxPrescales{1u, 1u, 8u, 1u, 256u, 256u};
constexpr std::array<uint8_t, 6> kIrqs{4u, 5u, 6u, 14u, 15u, 16u};
uint32_t errorValue(int32_t error) noexcept
{
return static_cast<uint32_t>(error);
}
}
void IopTimrman::reset() noexcept
{
for (size_t i = 0u; i < m_timers.size(); ++i)
{
m_timers[i] = {};
m_timers[i].address = kAddresses[i];
m_timers[i].sources = kSources[i];
m_timers[i].width = kWidths[i];
m_timers[i].maxPrescale = kMaxPrescales[i];
m_timers[i].irq = kIrqs[i];
}
m_holdMode = 0u;
m_servicing = false;
}
uint32_t IopTimrman::timerId(size_t index) noexcept
{
return (static_cast<uint32_t>(index + 1u) << 28u) | (kAddresses[index] >> 4u);
}
IopTimrman::Timer *IopTimrman::timerFromId(uint32_t id) noexcept
{
const uint32_t encoded = id >> 28u;
if (encoded == 0u || encoded > m_timers.size())
return nullptr;
Timer &timer = m_timers[encoded - 1u];
return timer.users != 0u && (id & 0x0FFFFFFFu) == (timer.address >> 4u)
? &timer
: nullptr;
}
const IopTimrman::Timer *IopTimrman::timerFromId(uint32_t id) const noexcept
{
return const_cast<IopTimrman *>(this)->timerFromId(id);
}
uint64_t IopTimrman::ticksToCycles(const Timer &timer, uint64_t ticks) noexcept
{
const uint64_t prescale = std::max<uint64_t>(timer.prescale, 1u);
const uint64_t sourceHz = timer.source == 2u
? kPixelClockHz
: (timer.source == 4u ? kHlineClockHz : kIopClockHz);
if (ticks == 0u)
ticks = timer.width == 16u ? (1ull << 16u) : (1ull << 32u);
const unsigned long long scaled = ticks * prescale;
if (sourceHz == kIopClockHz)
return std::max<uint64_t>(scaled, 1u);
const uint64_t whole = (scaled / sourceHz) * kIopClockHz;
const uint64_t remainder = scaled % sourceHz;
return std::max<uint64_t>(1u, whole + (remainder * kIopClockHz + sourceHz - 1u) / sourceHz);
}
uint64_t IopTimrman::elapsedTicks(const Timer &timer, uint64_t currentCycle) noexcept
{
if (!timer.running || currentCycle <= timer.counterBaseCycle)
return 0u;
const uint64_t elapsed = currentCycle - timer.counterBaseCycle;
const uint64_t sourceHz = timer.source == 2u
? kPixelClockHz
: (timer.source == 4u ? kHlineClockHz : kIopClockHz);
return (elapsed * sourceHz) / (kIopClockHz * std::max<uint64_t>(timer.prescale, 1u));
}
uint32_t IopTimrman::counterValue(const Timer &timer, uint64_t currentCycle) noexcept
{
const uint64_t value = static_cast<uint64_t>(timer.counterBase) + elapsedTicks(timer, currentCycle);
return timer.width == 16u ? static_cast<uint32_t>(value & 0xFFFFu) : static_cast<uint32_t>(value);
}
void IopTimrman::schedule(Timer &timer, uint64_t currentCycle) noexcept
{
timer.compareCycle = UINT64_MAX;
timer.overflowCycle = UINT64_MAX;
if (!timer.running)
return;
const uint64_t current = counterValue(timer, currentCycle);
timer.counterBase = static_cast<uint32_t>(current);
timer.counterBaseCycle = currentCycle;
if (timer.compareCallback.function != 0u)
{
const uint64_t modulus = timer.width == 16u ? (1ull << 16u) : (1ull << 32u);
const uint64_t compare = timer.width == 16u ? (timer.compare & 0xFFFFu) : timer.compare;
uint64_t delta = (compare + modulus - current) % modulus;
if (delta == 0u)
delta = modulus;
timer.compareCycle = currentCycle + ticksToCycles(timer, delta);
}
if (timer.overflowCallback.function != 0u)
{
const uint64_t modulus = timer.width == 16u ? (1ull << 16u) : (1ull << 32u);
uint64_t delta = modulus - current;
if (delta == 0u)
delta = modulus;
timer.overflowCycle = currentCycle + ticksToCycles(timer, delta);
}
}
void IopTimrman::stop(Timer &timer, uint64_t currentCycle) noexcept
{
timer.counterBase = counterValue(timer, currentCycle);
timer.counterBaseCycle = currentCycle;
timer.running = false;
timer.liveMode = 0u;
timer.compareCycle = UINT64_MAX;
timer.overflowCycle = UINT64_MAX;
}
bool IopTimrman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle)
{
const uint32_t a0 = cpu.gpr[4];
const uint32_t a1 = cpu.gpr[5];
const uint32_t a2 = cpu.gpr[6];
const uint32_t a3 = cpu.gpr[7];
const auto setV0 = [&](uint32_t value) { cpu.gpr[2] = value; };
switch (ordinal)
{
case 3: // GetTimersTable
setV0(0u);
return true;
case 4: // AllocHardTimer
for (const size_t index : kAllocationOrder)
{
Timer &timer = m_timers[index];
if (timer.users != 0u || (timer.sources & a0) == 0u || timer.width != a1 || timer.maxPrescale < a2)
continue;
timer.users = 1u;
timer.source = a0;
timer.prescale = std::max(a2, 1u);
timer.counterBaseCycle = currentCycle;
setV0(timerId(index));
return true;
}
setV0(errorValue(kNoTimer));
return true;
case 5: // ReferHardTimer
for (size_t index = 0u; index < m_timers.size(); ++index)
{
Timer &timer = m_timers[index];
if (timer.users == 0u || timer.liveMode == 0u || (timer.sources & a0) == 0u ||
timer.width != a1 || (timer.liveMode & a3) != a2)
continue;
++timer.users;
setV0(timerId(index));
return true;
}
setV0(errorValue(kNoTimer));
return true;
case 6: // FreeHardTimer
{
Timer *timer = timerFromId(a0);
if (!timer)
{
setV0(errorValue(kIllegalTimerId));
return true;
}
if (--timer->users == 0u)
{
const uint32_t address = timer->address;
const uint8_t sources = timer->sources;
const uint8_t width = timer->width;
const uint16_t maxPrescale = timer->maxPrescale;
const uint8_t irq = timer->irq;
*timer = {};
timer->address = address;
timer->sources = sources;
timer->width = width;
timer->maxPrescale = maxPrescale;
timer->irq = irq;
}
setV0(0u);
return true;
}
case 7: // SetTimerMode
{
Timer *timer = timerFromId(a0);
if (!timer)
{
setV0(errorValue(kIllegalTimerId));
return true;
}
if (a1 == 0u)
stop(*timer, currentCycle);
else
{
timer->liveMode = a1;
timer->running = true;
timer->counterBaseCycle = currentCycle;
schedule(*timer, currentCycle);
}
setV0(0u);
return true;
}
case 8: // GetTimerStatus
case 17: // GetTimerMode
{
const Timer *timer = timerFromId(a0);
setV0(timer ? timer->liveMode : errorValue(kIllegalTimerId));
return true;
}
case 9: // SetTimerCounter
{
Timer *timer = timerFromId(a0);
if (!timer)
{
setV0(errorValue(kIllegalTimerId));
return true;
}
timer->counterBase = timer->width == 16u ? (a1 & 0xFFFFu) : a1;
timer->counterBaseCycle = currentCycle;
schedule(*timer, currentCycle);
setV0(0u);
return true;
}
case 10: // GetTimerCounter
{
const Timer *timer = timerFromId(a0);
setV0(timer ? counterValue(*timer, currentCycle) : errorValue(kIllegalTimerId));
return true;
}
case 11: // SetTimerCompare
{
Timer *timer = timerFromId(a0);
if (!timer)
{
setV0(errorValue(kIllegalTimerId));
return true;
}
timer->compare = timer->width == 16u ? (a1 & 0xFFFFu) : a1;
schedule(*timer, currentCycle);
setV0(0u);
return true;
}
case 12: // GetTimerCompare
{
const Timer *timer = timerFromId(a0);
setV0(timer ? timer->compare : errorValue(kIllegalTimerId));
return true;
}
case 13: // SetHoldMode
m_holdMode = (m_holdMode & ~(0xFu << ((a0 & 7u) * 4u))) | ((a1 & 0xFu) << ((a0 & 7u) * 4u));
setV0(0u);
return true;
case 14: // GetHoldMode
setV0((m_holdMode >> ((a0 & 7u) * 4u)) & 0xFu);
return true;
case 15: // GetHoldReg
setV0(0u);
return true;
case 16: // GetHardTimerIntrCode
{
const Timer *timer = timerFromId(a0);
setV0(timer ? timer->irq : errorValue(kIllegalTimerId));
return true;
}
case 18: // GetTimerReadFunc
// Returning a host-side register reader as a guest function is not meaningful.
setV0(0u);
return true;
case 20: // SetTimerHandler
case 21: // SetOverflowHandler
{
Timer *timer = timerFromId(a0);
if (!timer)
{
setV0(errorValue(kIllegalTimerId));
return true;
}
if (timer->running)
{
setV0(errorValue(kTimerNotRunning));
return true;
}
if (ordinal == 20u)
{
timer->compare = timer->width == 16u ? (a1 & 0xFFFFu) : a1;
timer->compareCallback = {a2, a3, cpu.gpr[28]};
}
else
{
timer->overflowCallback = {a1, a2, cpu.gpr[28]};
}
setV0(0u);
return true;
}
case 22: // SetupHardTimer
{
Timer *timer = timerFromId(a0);
if (!timer)
{
setV0(errorValue(kIllegalTimerId));
return true;
}
if (timer->running)
{
setV0(errorValue(kTimerBusy));
return true;
}
if ((a2 != 0u && a2 != 1u && a2 != 3u && a2 != 5u && a2 != 7u))
{
setV0(errorValue(kIllegalMode));
return true;
}
if ((timer->sources & a1) == 0u)
{
setV0(errorValue(kIllegalSource));
return true;
}
if (a3 == 0u || a3 > timer->maxPrescale)
{
setV0(errorValue(kIllegalPrescale));
return true;
}
timer->source = a1;
timer->setupMode = a2;
timer->prescale = a3;
timer->configured = true;
setV0(0u);
return true;
}
case 23: // StartHardTimer
{
Timer *timer = timerFromId(a0);
if (!timer)
{
setV0(errorValue(kIllegalTimerId));
return true;
}
if (timer->running)
{
setV0(errorValue(kTimerBusy));
return true;
}
if (!timer->configured)
{
setV0(errorValue(kTimerNotConfigured));
return true;
}
timer->counterBase = 0u;
timer->counterBaseCycle = currentCycle;
timer->liveMode = 0x80000000u | timer->setupMode;
timer->running = true;
schedule(*timer, currentCycle);
setV0(0u);
return true;
}
case 24: // StopHardTimer
{
Timer *timer = timerFromId(a0);
if (!timer)
{
setV0(errorValue(kIllegalTimerId));
return true;
}
if (!timer->running)
{
setV0(errorValue(kTimerNotRunning));
return true;
}
stop(*timer, currentCycle);
setV0(0u);
return true;
}
default:
return false;
}
}
void IopTimrman::serviceDue(uint64_t currentCycle, IopGuestExecutor &executor)
{
if (m_servicing)
return;
m_servicing = true;
struct ServiceGuard
{
bool &flag;
~ServiceGuard() { flag = false; }
} guard{m_servicing};
for (Timer &timer : m_timers)
{
if (!timer.running)
continue;
const bool compareDue = timer.compareCycle <= currentCycle;
const bool overflowDue = timer.overflowCycle <= currentCycle;
if (!compareDue && !overflowDue)
continue;
const Callback callback = compareDue ? timer.compareCallback : timer.overflowCallback;
timer.compareCycle = UINT64_MAX;
timer.overflowCycle = UINT64_MAX;
const uint32_t result = callback.function != 0u
? executor.executeGuestFunctionWithBudget(callback.function,
callback.common,
0u,
0u,
0u,
callback.gp,
100000u)
: 0u;
if (!timer.running)
continue;
if (result == 0u)
{
stop(timer, currentCycle);
continue;
}
if (compareDue)
timer.compare = timer.width == 16u ? (result & 0xFFFFu) : result;
timer.counterBase = 0u;
timer.counterBaseCycle = currentCycle;
schedule(timer, currentCycle);
}
}
uint64_t IopTimrman::nextEventCycle(uint64_t fallback) const noexcept
{
uint64_t next = fallback;
for (const Timer &timer : m_timers)
{
next = std::min(next, timer.compareCycle);
next = std::min(next, timer.overflowCycle);
}
return next;
}
}
@@ -0,0 +1,67 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
namespace ps2x::iop::detail
{
struct IopCpuState;
class IopGuestExecutor;
class IopTimrman
{
public:
void reset() noexcept;
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle);
void serviceDue(uint64_t currentCycle, IopGuestExecutor &executor);
[[nodiscard]] uint64_t nextEventCycle(uint64_t fallback) const noexcept;
private:
struct Callback
{
uint32_t function = 0u;
uint32_t common = 0u;
uint32_t gp = 0u;
};
struct Timer
{
uint32_t address = 0u;
uint8_t sources = 0u;
uint8_t width = 0u;
uint16_t maxPrescale = 0u;
uint8_t irq = 0u;
uint8_t users = 0u;
uint32_t source = 1u;
uint32_t prescale = 1u;
uint32_t setupMode = 0u;
uint32_t liveMode = 0u;
uint32_t counterBase = 0u;
uint32_t compare = 0u;
uint64_t counterBaseCycle = 0u;
uint64_t compareCycle = UINT64_MAX;
uint64_t overflowCycle = UINT64_MAX;
bool configured = false;
bool running = false;
Callback compareCallback;
Callback overflowCallback;
};
[[nodiscard]] Timer *timerFromId(uint32_t timerId) noexcept;
[[nodiscard]] const Timer *timerFromId(uint32_t timerId) const noexcept;
[[nodiscard]] static uint32_t timerId(size_t index) noexcept;
[[nodiscard]] static uint64_t ticksToCycles(const Timer &timer, uint64_t ticks) noexcept;
[[nodiscard]] static uint64_t elapsedTicks(const Timer &timer, uint64_t currentCycle) noexcept;
[[nodiscard]] static uint32_t counterValue(const Timer &timer, uint64_t currentCycle) noexcept;
static void schedule(Timer &timer, uint64_t currentCycle) noexcept;
static void stop(Timer &timer, uint64_t currentCycle) noexcept;
std::array<Timer, 6> m_timers{};
uint32_t m_holdMode = 0u;
bool m_servicing = false;
};
}
@@ -0,0 +1,42 @@
#include "iop_vblank.h"
#include "../core/iop_cpu.h"
#include "../iop_emulator_const.h"
#include "../core/iop_kernel.h"
namespace ps2x::iop::detail
{
IopVblank::IopVblank(IopKernel &kernel) noexcept
: m_kernel(kernel)
{
}
bool IopVblank::dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle)
{
switch (ordinal)
{
case 4: // WaitVblankStart
case 5: // WaitVblankEnd
case 6: // WaitVblank
case 7: // WaitNonVblank
{
const bool waitForEnd = ordinal == 5u || ordinal == 7u;
const uint64_t phase = waitForEnd ? kVblankEndPhaseCycles : 0u;
const uint64_t fieldStart = currentCycle - (currentCycle % kVblankPeriodCycles);
uint64_t wakeCycle = fieldStart + phase;
if (wakeCycle <= currentCycle)
wakeCycle += kVblankPeriodCycles;
m_kernel.delayCurrentUntil(wakeCycle, cpu);
cpu.gpr[2] = 0u;
return true;
}
case 8: // RegisterVblankHandler
case 9: // ReleaseVblankHandler
// Callback delivery is not required by the scheduler wait ABI yet.
cpu.gpr[2] = 0u;
return true;
default:
return false;
}
}
}
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
namespace ps2x::iop::detail
{
struct IopCpuState;
class IopKernel;
class IopVblank
{
public:
explicit IopVblank(IopKernel &kernel) noexcept;
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle);
private:
IopKernel &m_kernel;
};
}
+109 -446
View File
@@ -1,12 +1,20 @@
#include "iop_emulator.h"
#include "iop_cdvd.h"
#include "iop_cpu.h"
#include "iop_imports.h"
#include "iop_kernel.h"
#include "iop_memory.h"
#include "iop_module_loader.h"
#include "iop_rpc.h"
#include "iop_sysclib.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>
@@ -15,7 +23,6 @@
#include <optional>
#include <span>
#include <sstream>
#include <unordered_map>
#include <utility>
namespace ps2x::iop::detail
@@ -73,13 +80,6 @@ namespace ps2x::iop::detail
bool resident = false;
};
struct InterruptHandler
{
uint32_t function = 0;
uint32_t argument = 0;
uint32_t gp = 0;
};
struct GuestCallback
{
uint32_t function = 0;
@@ -93,21 +93,22 @@ namespace ps2x::iop::detail
uint32_t argument = 0u;
};
struct IomanDevice
{
uint32_t address = 0;
uint32_t gp = 0;
std::string name;
};
explicit Impl(IopHost &hostRef)
: host(hostRef),
cdvd(host, memory),
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)
imports(memory),
loadcore(memory, imports)
{
reset();
}
@@ -120,9 +121,9 @@ namespace ps2x::iop::detail
imports.reset();
rpc.reset();
cdvd.reset();
interruptHandlers.clear();
iomanDevices.clear();
interruptEnabled.clear();
intrman.reset();
timrman.reset();
ioman.reset();
pendingDmaInterrupts.clear();
pendingGuestCallbacks.clear();
nextModuleId = 1;
@@ -209,16 +210,6 @@ namespace ps2x::iop::detail
return memory.freeAllocation(address);
}
uint32_t maxFreeMemory() const
{
return memory.maxFreeMemory();
}
std::string readString(uint32_t address, size_t limit = 1024) const
{
return memory.readString(address, limit);
}
void log(LogLevel level, std::string_view text)
{
host.log(level, text);
@@ -249,77 +240,15 @@ namespace ps2x::iop::detail
ImportDisposition dispatchImport(const IopImportCall &call, CpuState &cpu)
{
const uint32_t a0 = cpu.gpr[4];
const uint32_t a1 = cpu.gpr[5];
const uint32_t a2 = cpu.gpr[6];
const uint32_t a3 = cpu.gpr[7];
auto setV0 = [&](uint32_t value)
{
cpu.gpr[2] = value;
};
// TODO this will become a dispatch table once we have more imports implemented, but for now this is fine.
if (iequals(call.library, "sysmem"))
{
switch (call.ordinal)
{
case 4:
{
const uint32_t mode = a0;
const uint32_t size = a1;
const uint32_t ptr = a2;
const uint32_t address = mode == 2u ? allocate(size, 16u, ptr) : allocate(size, 16u);
setV0(address);
return ImportDisposition::Handled;
}
case 5:
setV0(freeAllocation(a0) ? 0u : 0xFFFFFFFFu);
return ImportDisposition::Handled;
case 6:
setV0(kRamSize);
return ImportDisposition::Handled;
case 7:
setV0(maxFreeMemory());
return ImportDisposition::Handled;
case 8:
setV0(maxFreeMemory());
return ImportDisposition::Handled;
case 9:
{
if (const auto block = memory.allocationContaining(a0))
{
setV0(block->address);
return ImportDisposition::Handled;
}
setV0(0u);
return ImportDisposition::Handled;
}
case 10:
{
if (const auto block = memory.allocationContaining(a0))
{
setV0(block->size);
return ImportDisposition::Handled;
}
setV0(0xFFFFFFFFu);
return ImportDisposition::Handled;
}
case 14:
{
const std::string format = readString(a0, 512);
log(LogLevel::Info, std::string("[IOP Kprintf] ") + format);
setV0(static_cast<uint32_t>(format.size()));
return ImportDisposition::Handled;
}
case 15:
setV0(0);
return ImportDisposition::Handled;
default:
break;
}
}
if (iequals(call.library, "sysmem") && sysmem.dispatchImport(call.ordinal, cpu))
return ImportDisposition::Handled;
if (iequals(call.library, "cdvdman") &&
cdvd.dispatchImport(call.ordinal, cpu))
if (iequals(call.library, "cdvdman") && cdvd.dispatchImport(call.ordinal, cpu))
{
if (const auto callback = cdvd.takeCompletionCallback())
{
@@ -334,50 +263,8 @@ namespace ps2x::iop::detail
return ImportDisposition::Handled;
}
if (iequals(call.library, "loadcore"))
{
switch (call.ordinal)
{
case 3:
setV0(0);
return ImportDisposition::Handled;
case 4:
case 5:
setV0(0);
return ImportDisposition::Handled;
case 6:
case 10:
setV0(imports.registerExportTable(a0) ? 0u : 0xFFFFFFFFu);
return ImportDisposition::Handled;
case 7:
setV0(imports.releaseExportTable(a0) ? 0u : 0xFFFFFFFFu);
return ImportDisposition::Handled;
case 8:
case 9:
setV0(0);
return ImportDisposition::Handled;
case 11:
{
const std::string name = readString(a0 + 12u, 8u);
setV0(imports.findTable(name));
return ImportDisposition::Handled;
}
case 12:
setV0(0);
return ImportDisposition::Handled;
case 13:
case 14:
case 15:
case 16:
case 17:
case 20:
case 21:
setV0(0);
return ImportDisposition::Handled;
default:
break;
}
}
if (iequals(call.library, "loadcore") && loadcore.dispatchImport(call.ordinal, cpu))
return ImportDisposition::Handled;
if (iequals(call.library, "thbase") || iequals(call.library, "threadman"))
{
@@ -403,83 +290,8 @@ namespace ps2x::iop::detail
? ImportDisposition::Handled
: ImportDisposition::Missing;
}
if (iequals(call.library, "intrman"))
{
switch (call.ordinal)
{
case 3:
setV0(0);
return ImportDisposition::Handled;
case 4: // RegisterIntrHandler
interruptHandlers[static_cast<int>(a0)] = {a2, a3, cpu.gpr[28]};
setV0(0);
return ImportDisposition::Handled;
case 5:
interruptHandlers.erase(static_cast<int>(a0));
setV0(0);
return ImportDisposition::Handled;
case 6:
interruptEnabled[static_cast<int>(a0)] = true;
if (a0 < 32u)
memory.setInterruptMask(memory.interruptMask() | (1u << a0));
setV0(0);
return ImportDisposition::Handled;
case 7:
if (a1)
write32(a1, a0);
if (a0 < 32u)
memory.setInterruptMask(memory.interruptMask() & ~(1u << a0));
interruptEnabled[static_cast<int>(a0)] = false;
setV0(0);
return ImportDisposition::Handled;
case 8:
memory.setInterruptControl(0u);
setV0(0);
return ImportDisposition::Handled;
case 9:
memory.setInterruptControl(1u);
setV0(0);
return ImportDisposition::Handled;
case 14:
if (a0)
{
const uint32_t ret = callFunction(a0, a1, a2, a3, cpu.gpr[28], 100000u);
setV0(ret);
}
else
setV0(0);
return ImportDisposition::Handled;
case 15:
case 16:
setV0(0);
return ImportDisposition::Handled;
case 17:
if (a0)
write32(a0, memory.interruptControl());
memory.setInterruptControl(0u);
setV0(0);
return ImportDisposition::Handled;
case 18:
memory.setInterruptControl(a0 ? 1u : 0u);
setV0(0);
return ImportDisposition::Handled;
case 23:
setV0(0);
return ImportDisposition::Handled;
case 24:
setV0(0);
return ImportDisposition::Handled;
case 25:
setV0(0);
return ImportDisposition::Handled;
case 28:
case 30:
setV0(0);
return ImportDisposition::Handled;
default:
break;
}
}
if (iequals(call.library, "intrman") && intrman.dispatchImport(call.ordinal, cpu, *this))
return ImportDisposition::Handled;
if (iequals(call.library, "secrman"))
{
switch (call.ordinal)
@@ -498,228 +310,37 @@ namespace ps2x::iop::detail
}
if (iequals(call.library, "modload") && call.ordinal == 13u)
{
// SetCheckKelfPathCallback. MODLOAD owns this callback in the
// real IOP; retain both the address and the registering
// module's GP so a later KELF load can invoke it faithfully.
checkKelfPathCallback = {a0, cpu.gpr[28]};
setV0(0);
return ImportDisposition::Handled;
}
if (iequals(call.library, "ioman"))
{
switch (call.ordinal)
{
case 20: // AddDrv
{
constexpr size_t kMaxIomanDevices = 16u;
if (a0 == 0u || iomanDevices.size() >= kMaxIomanDevices)
{
setV0(0xFFFFFFFFu);
return ImportDisposition::Handled;
}
const uint32_t nameAddress = read32(a0 + 0u);
const uint32_t operations = read32(a0 + 16u);
const std::string name = readString(nameAddress, 64u);
if (nameAddress == 0u || operations == 0u || name.empty())
{
setV0(0xFFFFFFFFu);
return ImportDisposition::Handled;
}
// The original IOMAN exposes the device before invoking
// init, then removes it again if initialization fails.
iomanDevices.push_back({a0, cpu.gpr[28], name});
const uint32_t init = read32(operations + 0u);
if (init != 0u)
{
const int32_t initResult = static_cast<int32_t>(
callFunction(init, a0, 0u, 0u, 0u, cpu.gpr[28]));
if (initResult < 0)
{
iomanDevices.pop_back();
setV0(0xFFFFFFFFu);
return ImportDisposition::Handled;
}
}
setV0(0u);
return ImportDisposition::Handled;
}
case 21: // DelDrv
{
const std::string name = readString(a0, 64u);
const auto device = std::find_if(
iomanDevices.begin(), iomanDevices.end(),
[&](const IomanDevice &candidate)
{ return candidate.name == name; });
if (device == iomanDevices.end())
{
setV0(0xFFFFFFFFu);
return ImportDisposition::Handled;
}
const uint32_t operations = read32(device->address + 16u);
const uint32_t deinit = operations != 0u ? read32(operations + 4u) : 0u;
if (deinit != 0u)
(void)callFunction(deinit, device->address, 0u, 0u, 0u, device->gp);
iomanDevices.erase(device);
setV0(0u);
return ImportDisposition::Handled;
}
default:
break;
}
}
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"))
{
switch (call.ordinal)
{
case 4: // WaitVblankStart
case 5: // WaitVblankEnd
case 6: // WaitVblank
case 7: // WaitNonVblank
{
const bool waitForEnd = call.ordinal == 5u || call.ordinal == 7u;
const uint64_t phase = waitForEnd ? kVblankEndPhaseCycles : 0u;
const uint64_t fieldStart = totalCycles - (totalCycles % kVblankPeriodCycles);
uint64_t wakeCycle = fieldStart + phase;
if (wakeCycle <= totalCycles)
wakeCycle += kVblankPeriodCycles;
kernel.delayCurrentUntil(wakeCycle, cpu);
}
setV0(0);
return ImportDisposition::Handled;
case 8: // RegisterVblankHandler
case 9: // ReleaseVblankHandler
// Callback delivery is not required by the scheduler wait
// ABI yet.
setV0(0);
return ImportDisposition::Handled;
default:
break;
}
}
if (iequals(call.library, "timrman") || iequals(call.library, "dmacman"))
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"))
{
switch (call.ordinal)
{
case 4: // printf
{
const std::string text = readString(a0, 2048);
log(LogLevel::Info, std::string("[IOP printf] ") + text);
setV0(static_cast<uint32_t>(text.size()));
return ImportDisposition::Handled;
}
case 5:
setV0(0xFFFFFFFFu);
return ImportDisposition::Handled; // getchar
case 6:
{
const char ch = static_cast<char>(a0 & 0xFFu);
log(LogLevel::Info, std::string("[IOP putchar] ") + ch);
setV0(a0 & 0xFFu);
return ImportDisposition::Handled;
}
case 7:
{
const std::string text = readString(a0, 2048);
log(LogLevel::Info, std::string("[IOP puts] ") + text);
setV0(static_cast<uint32_t>(text.size() + 1u));
return ImportDisposition::Handled;
}
case 8:
setV0(0);
return ImportDisposition::Handled; // gets
case 9:
{
const std::string text = readString(a1, 2048);
log(LogLevel::Info, std::string("[IOP fdprintf] ") + text);
setV0(static_cast<uint32_t>(text.size()));
return ImportDisposition::Handled;
}
case 10:
setV0(0xFFFFFFFFu);
return ImportDisposition::Handled;
case 11:
setV0(a0 & 0xFFu);
return ImportDisposition::Handled;
case 12:
{
const std::string text = readString(a0, 2048);
log(LogLevel::Info, std::string("[IOP fdputs] ") + text);
setV0(static_cast<uint32_t>(text.size()));
return ImportDisposition::Handled;
}
case 13:
setV0(0);
return ImportDisposition::Handled;
case 14:
{
const std::string text = readString(a1, 2048);
log(LogLevel::Info, std::string("[IOP vfdprintf] ") + text);
setV0(static_cast<uint32_t>(text.size()));
return ImportDisposition::Handled;
}
default:
break;
}
}
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"))
{
switch (call.ordinal)
{
case 4: // CreateHeap - heap identity is opaque to callers.
setV0(allocate(16u, 16u));
return ImportDisposition::Handled;
case 5: // DeleteHeap
if (a0)
freeAllocation(a0);
setV0(0);
return ImportDisposition::Handled;
case 6:
setV0(allocate(a1, 16u));
return ImportDisposition::Handled;
case 7:
setV0(freeAllocation(a1) ? 0u : 0xFFFFFFFFu);
return ImportDisposition::Handled;
case 8:
setV0(maxFreeMemory());
return ImportDisposition::Handled;
case 11:
setV0(0);
return ImportDisposition::Handled;
case 15:
{
if (const auto block = memory.allocationContaining(a0))
{
setV0(block->size);
return ImportDisposition::Handled;
}
setV0(0xFFFFFFFFu);
return ImportDisposition::Handled;
}
default:
break;
}
}
if (iequals(call.library, "heaplib") && heaplib.dispatchImport(call.ordinal, cpu))
return ImportDisposition::Handled;
const uint32_t target = imports.resolve(call.library, call.ordinal);
if (target != 0u)
@@ -847,6 +468,17 @@ namespace ps2x::iop::detail
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()
{
@@ -869,22 +501,7 @@ namespace ps2x::iop::detail
try
{
for (const int irq : completed)
{
const auto enabled = interruptEnabled.find(irq);
if (enabled == interruptEnabled.end() || !enabled->second)
continue;
const auto handler = interruptHandlers.find(irq);
if (handler == interruptHandlers.end() || handler->second.function == 0u)
continue;
(void)callFunction(handler->second.function,
handler->second.argument,
0u,
0u,
0u,
handler->second.gp,
100000u);
}
(void)intrman.dispatchInterrupt(irq, *this);
}
catch (...)
{
@@ -944,6 +561,7 @@ namespace ps2x::iop::detail
{
servicePendingDmaInterrupts();
servicePendingGuestCallbacks();
timrman.serviceDue(totalCycles, *this);
IopThread *next = kernel.beginNextReady(totalCycles);
if (!next)
{
@@ -952,6 +570,7 @@ namespace ps2x::iop::detail
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;
}
@@ -1060,16 +679,21 @@ namespace ps2x::iop::detail
IopHost &host;
IopMemory memory;
IopCdvd cdvd;
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::unordered_map<int, InterruptHandler> interruptHandlers;
std::vector<IomanDevice> iomanDevices;
std::map<int, bool> interruptEnabled;
std::map<int, uint64_t> pendingDmaInterrupts;
std::multimap<uint64_t, ScheduledGuestCallback> pendingGuestCallbacks;
uint32_t nextModuleId = 1;
@@ -1138,6 +762,45 @@ namespace ps2x::iop::detail
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;
+7
View File
@@ -29,6 +29,13 @@ namespace ps2x::iop::detail
[[nodiscard]] bool hasRpcServer(uint32_t sid) const noexcept;
void onSifTransfer(const SifTransfer &transfer);
[[nodiscard]] uint32_t allocateMemory(uint32_t size, uint32_t alignment = 16u);
[[nodiscard]] bool freeMemory(uint32_t address);
[[nodiscard]] bool readMemory(uint32_t address, void *destination, size_t size) const;
[[nodiscard]] bool writeMemory(uint32_t address, const void *source, size_t size);
[[nodiscard]] bool zeroMemory(uint32_t address, size_t size);
[[nodiscard]] bool isMemoryRange(uint32_t address, size_t size) const;
[[nodiscard]] uint64_t cycles() const noexcept;
[[nodiscard]] uint64_t instructions() const noexcept;
[[nodiscard]] uint32_t loadedModuleCount() const noexcept;
@@ -1,6 +1,6 @@
#include "iop_module_loader.h"
#include "iop_memory.h"
#include "../core/iop_memory.h"
#include "ps2x/iop/iop_subsystem.h"
#include <algorithm>
@@ -170,9 +170,7 @@ namespace ps2x::iop::detail
if (relsec.link < sections.size())
{
const Elf32Shdr &symsec = sections[relsec.link];
if (symsec.type == SHT_SYMTAB &&
symsec.entsize >= sizeof(Elf32Sym) &&
checkedRange(image.size(), symsec.offset, symsec.size))
if (symsec.type == SHT_SYMTAB && symsec.entsize >= sizeof(Elf32Sym) && checkedRange(image.size(), symsec.offset, symsec.size))
{
const size_t count = symsec.size / symsec.entsize;
symbolStorage.resize(count);
@@ -198,9 +196,7 @@ namespace ps2x::iop::detail
if (relsec.type == SHT_RELA)
{
Elf32Rela relocation{};
std::memcpy(&relocation,
image.data() + relsec.offset + offset,
sizeof(relocation));
std::memcpy(&relocation, image.data() + relsec.offset + offset, sizeof(relocation));
relocationOffset = relocation.offset;
relocationInfo = relocation.info;
explicitAddend = relocation.addend;
@@ -224,8 +220,7 @@ namespace ps2x::iop::detail
symbolValue = symbol.value;
if (symbol.shndx != 0u)
{
symbolValue = static_cast<uint32_t>(
static_cast<int64_t>(symbolValue) + delta);
symbolValue = static_cast<uint32_t>(static_cast<int64_t>(symbolValue) + delta);
}
}
}
@@ -258,16 +253,12 @@ namespace ps2x::iop::detail
break;
case R_MIPS_32:
case R_MIPS_REL32:
memory.write32(place,
static_cast<uint32_t>(
static_cast<int64_t>(addend) + symbolValue));
memory.write32(place, static_cast<uint32_t>(static_cast<int64_t>(addend) + symbolValue));
break;
case R_MIPS_26:
{
const uint32_t target = ((word & 0x03FFFFFFu) << 2u) + symbolValue;
memory.write32(place,
(word & 0xFC000000u) |
((target >> 2u) & 0x03FFFFFFu));
memory.write32(place, (word & 0xFC000000u) | ((target >> 2u) & 0x03FFFFFFu));
break;
}
case R_MIPS_HI16:
@@ -501,9 +492,7 @@ namespace ps2x::iop::detail
else
{
if (!checkedRange(image.size(), section.offset, section.size) ||
!memory.writeRam(destination,
image.data() + section.offset,
section.size))
!memory.writeRam(destination, image.data() + section.offset, section.size))
return result;
}
}
@@ -1,12 +1,13 @@
#include "iop_rpc.h"
#include "iop_cpu.h"
#include "iop_kernel.h"
#include "iop_memory.h"
#include "../core/iop_cpu.h"
#include "../core/iop_kernel.h"
#include "../core/iop_memory.h"
#include "ps2x/iop/iop_host.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <limits>
#include <vector>
@@ -119,8 +120,6 @@ namespace ps2x::iop::detail
return true;
}
case 8: // sceSifDmaStat
// Transfers are applied synchronously above. The IOP API reports
// a negative value once the transaction is no longer active.
setV0(0xFFFFFFFFu);
return true;
case 29: // sceSifCheckInit
@@ -148,13 +147,67 @@ namespace ps2x::iop::detail
case 9:
case 10:
case 11:
case 12:
case 13:
case 14: // InitRpc
case 15:
case 16:
setV0(0);
return true;
case 12: // sceSifSendCmd
case 13: // isceSifSendCmd
{
constexpr uint32_t kHeaderSize = 16u;
constexpr uint32_t kMaxPacketSize = 112u;
const uint32_t commandId = cpu.gpr[4];
const uint32_t packetAddress = cpu.gpr[5];
const uint32_t packetSize = cpu.gpr[6];
const uint32_t extraSource = cpu.gpr[7];
const uint32_t stackPointer = cpu.gpr[29];
const uint32_t extraDestination = m_memory.read32(stackPointer + 16u);
const int32_t signedExtraSize = static_cast<int32_t>(m_memory.read32(stackPointer + 20u));
if (packetAddress == 0u || packetSize < kHeaderSize || packetSize > kMaxPacketSize ||
!m_memory.ownsRamRange(packetAddress, packetSize))
{
setV0(0u);
return true;
}
std::array<uint8_t, kMaxPacketSize> packet{};
if (!m_memory.readRam(packetAddress, packet.data(), packetSize))
{
setV0(0u);
return true;
}
uint32_t extraSize = 0u;
if (signedExtraSize > 0)
{
extraSize = static_cast<uint32_t>(signedExtraSize);
if (extraSource == 0u || extraDestination == 0u ||
!m_memory.ownsRamRange(extraSource, extraSize) ||
!m_host.writeGuest(extraDestination, m_memory.ram().data() + IopMemory::physicalAddress(extraSource), extraSize))
{
setV0(0u);
return true;
}
}
const uint32_t sizeWord = packetSize | (extraSize << 8u);
std::memcpy(packet.data() + 0u, &sizeWord, sizeof(sizeWord));
std::memcpy(packet.data() + 4u, &extraDestination, sizeof(extraDestination));
std::memcpy(packet.data() + 8u, &commandId, sizeof(commandId));
if (!m_host.sendSifCommand(commandId, packet.data(), packetSize))
{
// A command without an EE handler is still a completed DMA on real hardware. Only malformed packets fail above.
}
const uint32_t dmaId = m_nextDmaId++;
if (m_nextDmaId == 0u || m_nextDmaId > static_cast<uint32_t>(std::numeric_limits<int32_t>::max()))
m_nextDmaId = 1u;
setV0(dmaId);
return true;
}
case 17: // sceSifRegisterRpc
{
RpcServer server;
@@ -255,8 +308,7 @@ namespace ps2x::iop::detail
const uint32_t copySize = std::min<uint32_t>(request.receive.size, IopMemory::RamSize - physical);
(void)m_host.writeGuest(request.receive.address, m_memory.ram().data() + physical, copySize);
if (copySize < request.receive.size)
(void)m_host.zeroGuest(request.receive.address + copySize,
request.receive.size - copySize);
(void)m_host.zeroGuest(request.receive.address + copySize, request.receive.size - copySize);
}
}
@@ -270,39 +322,11 @@ namespace ps2x::iop::detail
void IopRpcBridge::onSifTransfer(const SifTransfer &transfer)
{
if (transfer.size == 0u)
return;
if (transfer.kind == SifTransferKind::SetDma)
{
// sceSifSetDma transfers EE memory to IOP RAM. The runtime performs its
// legacy EE-side mirror first, then gives the physical emulator the same
// payload so the IOP observes it at the requested destination.
if (transfer.phase != SifTransferPhase::AfterCopy)
return;
const uint32_t destination = IopMemory::physicalAddress(transfer.destinationAddress);
if (destination >= IopMemory::RamSize)
return;
const size_t copySize = std::min<size_t>(transfer.size, IopMemory::RamSize - destination);
std::vector<uint8_t> data(copySize);
if (m_host.readGuest(transfer.sourceAddress, data.data(), data.size()))
(void)m_memory.writeRam(destination, data.data(), data.size());
return;
}
if (transfer.kind == SifTransferKind::GetOtherData && transfer.phase == SifTransferPhase::BeforeCopy)
{
// sceSifGetOtherData is the reverse direction: its source is an IOP
// address and its destination is in EE memory. Stage the physical IOP
// bytes at the source's EE mirror before the runtime performs the copy.
const uint32_t source = IopMemory::physicalAddress(transfer.sourceAddress);
if (source >= IopMemory::RamSize)
return;
const size_t copySize = std::min<size_t>(transfer.size, IopMemory::RamSize - source);
if (!m_memory.ownsRamRange(source, copySize))
return;
(void)m_host.writeGuest(transfer.sourceAddress, m_memory.ram().data() + source, copySize);
}
// The EE SIF transport owns the actual directional memory movement.
// Services still receive both phases through IopSubsystem, but mirroring
// IOP bytes through an equal-numbered EE address would alias two distinct
// PS2 address spaces and can overwrite live game data.
(void)transfer;
}
void IopRpcBridge::removeServersInRange(uint32_t base, uint32_t size)
@@ -28,6 +28,16 @@ namespace ps2x::iop::detail
uint32_t a2,
uint32_t a3,
uint32_t gp) = 0;
[[nodiscard]] virtual uint32_t executeGuestFunctionWithBudget(uint32_t address,
uint32_t a0,
uint32_t a1,
uint32_t a2,
uint32_t a3,
uint32_t gp,
uint32_t instructionBudget)
{
return executeGuestFunction(address, a0, a1, a2, a3, gp);
}
};
class IopRpcBridge
+177
View File
@@ -0,0 +1,177 @@
#include "iop_module_manager.h"
#include "ps2x/iop/ps2_path.h"
#include <algorithm>
namespace ps2x::iop::detail
{
IopModuleManager::IopModuleManager()
{
// ROM modules that the no-BIOS HLE environment can legitimately provide.
// Entries with RPC services become routable only after load.
constexpr std::string_view modules[] = {
"sysmem",
"loadcore",
"intrman",
"sifman",
"sifcmd",
"sifinit",
"ioman",
"iomanx",
"modload",
"stdio",
"sysclib",
"thbase",
"thevent",
"thsemap",
"thmsgbx",
"timrman",
"vblank",
"secrman",
"sio2man",
"sio2d",
"padman",
"mcman",
"mcserv",
"libsd",
"cdvdman",
"cdvdfsv",
"dev9",
"usbd",
"usbhdfsd",
"udnl",
"fileio",
"poweroff",
"netman",
"ps2ip",
"dbcman",
"dbcm",
};
for (const std::string_view module : modules)
m_builtinKeys.emplace(module);
}
void IopModuleManager::reset()
{
m_records.clear();
m_hleIdsByKey.clear();
m_loadedKeyReferences.clear();
m_nextHleId = 0x40000000;
}
void IopModuleManager::setServiceModuleKeys(std::vector<std::string> keys)
{
m_serviceKeys.clear();
for (std::string &key : keys)
{
const std::string normalized = ps2PathLeafKey(key);
if (!normalized.empty())
m_serviceKeys.emplace(normalized);
}
}
ModuleLoadResult IopModuleManager::loadHle(std::string_view path)
{
ModuleLoadResult result{true, -1, -1};
const std::string key = ps2PathLeafKey(path);
if (key.empty() || (!m_builtinKeys.contains(key) && !m_serviceKeys.contains(key)))
return result;
const auto existing = m_hleIdsByKey.find(key);
if (existing != m_hleIdsByKey.end())
{
Record &record = m_records[existing->second];
++record.references;
addLoadedKey(key);
result.moduleId = existing->second;
result.startResult = 0;
return result;
}
if (m_nextHleId <= 0)
return result;
const int32_t id = m_nextHleId++;
m_records.emplace(id, Record{key, 1u, false});
m_hleIdsByKey.emplace(key, id);
addLoadedKey(key);
result.moduleId = id;
result.startResult = 0;
return result;
}
void IopModuleManager::observePhysicalLoad(int32_t moduleId, std::string_view path)
{
if (moduleId <= 0)
return;
const std::string key = ps2PathLeafKey(path);
if (key.empty())
return;
m_records[moduleId] = Record{key, 1u, true};
addLoadedKey(key);
}
bool IopModuleManager::stopHle(int32_t moduleId, int32_t *result)
{
const auto found = m_records.find(moduleId);
if (found == m_records.end() || found->second.physical)
return false;
Record &record = found->second;
removeLoadedKey(record.key);
if (record.references > 1u)
{
--record.references;
}
else
{
m_hleIdsByKey.erase(record.key);
m_records.erase(found);
}
if (result)
*result = 0;
return true;
}
void IopModuleManager::observePhysicalStop(int32_t moduleId)
{
const auto found = m_records.find(moduleId);
if (found == m_records.end() || !found->second.physical)
return;
removeLoadedKey(found->second.key);
m_records.erase(found);
}
bool IopModuleManager::isLoaded(std::span<const std::string_view> aliases) const
{
if (aliases.empty())
return true;
return std::any_of(aliases.begin(), aliases.end(), [&](std::string_view alias)
{
const std::string key = ps2PathLeafKey(alias);
const auto found = m_loadedKeyReferences.find(key);
return found != m_loadedKeyReferences.end() && found->second != 0u; });
}
bool IopModuleManager::recognizes(std::string_view path) const
{
const std::string key = ps2PathLeafKey(path);
return m_builtinKeys.contains(key) || m_serviceKeys.contains(key);
}
void IopModuleManager::addLoadedKey(std::string_view key)
{
++m_loadedKeyReferences[std::string(key)];
}
void IopModuleManager::removeLoadedKey(std::string_view key)
{
const auto found = m_loadedKeyReferences.find(std::string(key));
if (found == m_loadedKeyReferences.end())
return;
if (found->second > 1u)
--found->second;
else
m_loadedKeyReferences.erase(found);
}
}
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include "ps2x/iop/iop_types.h"
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace ps2x::iop::detail
{
class IopModuleManager
{
public:
IopModuleManager();
void reset();
void setServiceModuleKeys(std::vector<std::string> keys);
[[nodiscard]] ModuleLoadResult loadHle(std::string_view path);
void observePhysicalLoad(int32_t moduleId, std::string_view path);
[[nodiscard]] bool stopHle(int32_t moduleId, int32_t *result);
void observePhysicalStop(int32_t moduleId);
[[nodiscard]] bool isLoaded(std::span<const std::string_view> aliases) const;
[[nodiscard]] bool recognizes(std::string_view path) const;
private:
struct Record
{
std::string key;
uint32_t references = 0u;
bool physical = false;
};
void addLoadedKey(std::string_view key);
void removeLoadedKey(std::string_view key);
std::unordered_set<std::string> m_builtinKeys;
std::unordered_set<std::string> m_serviceKeys;
std::unordered_map<int32_t, Record> m_records;
std::unordered_map<std::string, int32_t> m_hleIdsByKey;
std::unordered_map<std::string, uint32_t> m_loadedKeyReferences;
int32_t m_nextHleId = 0x40000000;
};
}
+7
View File
@@ -18,6 +18,13 @@ namespace ps2x::iop::detail
[[nodiscard]] virtual std::string_view name() const = 0;
[[nodiscard]] virtual std::span<const uint32_t> sids() const = 0;
// A service with aliases is dormant until one of these IOP modules is
// actually loaded. Profile services can omit aliases when the profile
// itself is the explicit compatibility contract.
[[nodiscard]] virtual std::span<const std::string_view> moduleAliases() const
{
return {};
}
virtual void reset() = 0;
[[nodiscard]] virtual RpcAbi selectRpcAbi(const RpcAbiRequest &request) const
+105 -8
View File
@@ -1,8 +1,10 @@
#include "ps2x/iop/iop_subsystem.h"
#include "iop_service.h"
#include "iop_module_manager.h"
#include "emulator/iop_emulator.h"
#include "plugin_loader.h"
#include "ps2x/iop/ps2_path.h"
#include <algorithm>
#include <cctype>
@@ -69,11 +71,39 @@ namespace ps2x::iop
{
public:
explicit Impl(IopHost &hostRef)
: host(hostRef), pluginCatalog(hostRef), coreServices(detail::createCoreServices(hostRef)), profiles(detail::createBuiltinProfiles()), emulator(hostRef)
: host(hostRef),
pluginCatalog(hostRef),
coreServices(detail::createCoreServices(hostRef)),
profiles(detail::createBuiltinProfiles()),
emulator(hostRef)
{
refreshServiceModuleKeys();
rebuildRoutes();
}
bool serviceActive(const detail::IopService &service) const
{
return moduleManager.isLoaded(service.moduleAliases());
}
void refreshServiceModuleKeys()
{
std::vector<std::string> keys;
auto collect = [&](const detail::ServiceList &services)
{
for (const auto &service : services)
{
if (!service)
continue;
for (std::string_view alias : service->moduleAliases())
keys.emplace_back(alias);
}
};
collect(coreServices);
collect(profileServices);
moduleManager.setServiceModuleKeys(std::move(keys));
}
void rebuildRoutes()
{
routes.clear();
@@ -82,7 +112,7 @@ namespace ps2x::iop
std::unordered_map<uint32_t, detail::IopService *> layer;
for (const auto &service : services)
{
if (!service)
if (!service || !serviceActive(*service))
{
continue;
}
@@ -119,6 +149,7 @@ namespace ps2x::iop
std::string activeProvider;
std::string lastError;
bool routesValid = true;
detail::IopModuleManager moduleManager;
detail::IopEmulator emulator;
};
@@ -180,6 +211,7 @@ namespace ps2x::iop
{
*error = m_impl->lastError;
}
m_impl->refreshServiceModuleKeys();
m_impl->rebuildRoutes();
return false;
}
@@ -199,6 +231,7 @@ namespace ps2x::iop
{
*error = m_impl->lastError;
}
m_impl->refreshServiceModuleKeys();
m_impl->rebuildRoutes();
return false;
}
@@ -209,11 +242,13 @@ namespace ps2x::iop
{
*error = m_impl->lastError;
}
m_impl->refreshServiceModuleKeys();
m_impl->rebuildRoutes();
return false;
}
}
m_impl->refreshServiceModuleKeys();
m_impl->rebuildRoutes();
if (!m_impl->routesValid)
{
@@ -221,6 +256,7 @@ namespace ps2x::iop
m_impl->profileServices.clear();
m_impl->activeProfile.clear();
m_impl->activeProvider.clear();
m_impl->refreshServiceModuleKeys();
m_impl->rebuildRoutes();
m_impl->lastError = routeError;
if (error)
@@ -236,6 +272,7 @@ namespace ps2x::iop
void IopSubsystem::reset()
{
m_impl->moduleManager.reset();
for (auto &service : m_impl->coreServices)
{
if (service)
@@ -251,11 +288,31 @@ namespace ps2x::iop
}
}
m_impl->emulator.reset();
m_impl->refreshServiceModuleKeys();
m_impl->rebuildRoutes();
}
ModuleLoadResult IopSubsystem::loadModule(std::string_view path, const void *arguments, uint32_t argumentSize)
{
return m_impl->emulator.loadModule(path, arguments, argumentSize);
const ParsedPs2Path parsed = parsePs2Path(path);
if (!parsed)
return {true, -1, -1};
if (parsed.device != Ps2PathDevice::Rom0)
{
ModuleLoadResult physical = m_impl->emulator.loadModule(path, arguments, argumentSize);
if (physical.moduleId > 0)
{
m_impl->moduleManager.observePhysicalLoad(physical.moduleId, path);
m_impl->rebuildRoutes();
return physical;
}
}
ModuleLoadResult hle = m_impl->moduleManager.loadHle(path);
if (hle.moduleId > 0)
m_impl->rebuildRoutes();
return hle;
}
ModuleLoadResult IopSubsystem::loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize)
@@ -265,7 +322,16 @@ namespace ps2x::iop
bool IopSubsystem::stopModule(int32_t moduleId, int32_t *result)
{
return m_impl->emulator.stopModule(moduleId, result);
if (m_impl->moduleManager.stopHle(moduleId, result))
{
m_impl->rebuildRoutes();
return true;
}
if (!m_impl->emulator.stopModule(moduleId, result))
return false;
m_impl->moduleManager.observePhysicalStop(moduleId);
m_impl->rebuildRoutes();
return true;
}
void IopSubsystem::runEeCycles(uint64_t eeCycles) noexcept
@@ -277,7 +343,7 @@ namespace ps2x::iop
{
for (const auto &service : m_impl->profileServices)
{
if (service)
if (service && m_impl->serviceActive(*service))
{
const RpcAbi selected = service->selectRpcAbi(request);
if (selected != RpcAbi::RuntimeDefault)
@@ -288,7 +354,7 @@ namespace ps2x::iop
}
for (const auto &service : m_impl->coreServices)
{
if (service)
if (service && m_impl->serviceActive(*service))
{
const RpcAbi selected = service->selectRpcAbi(request);
if (selected != RpcAbi::RuntimeDefault)
@@ -341,14 +407,14 @@ namespace ps2x::iop
{
for (auto &service : m_impl->coreServices)
{
if (service)
if (service && m_impl->serviceActive(*service))
{
service->onSifTransfer(transfer);
}
}
for (auto &service : m_impl->profileServices)
{
if (service)
if (service && m_impl->serviceActive(*service))
{
service->onSifTransfer(transfer);
}
@@ -356,6 +422,36 @@ namespace ps2x::iop
m_impl->emulator.onSifTransfer(transfer);
}
uint32_t IopSubsystem::allocateMemory(uint32_t size, uint32_t alignment)
{
return m_impl->emulator.allocateMemory(size, alignment);
}
bool IopSubsystem::freeMemory(uint32_t address)
{
return m_impl->emulator.freeMemory(address);
}
bool IopSubsystem::readMemory(uint32_t address, void *destination, size_t size) const
{
return m_impl->emulator.readMemory(address, destination, size);
}
bool IopSubsystem::writeMemory(uint32_t address, const void *source, size_t size)
{
return m_impl->emulator.writeMemory(address, source, size);
}
bool IopSubsystem::zeroMemory(uint32_t address, size_t size)
{
return m_impl->emulator.zeroMemory(address, size);
}
bool IopSubsystem::isMemoryRange(uint32_t address, size_t size) const
{
return m_impl->emulator.isMemoryRange(address, size);
}
DebugSnapshot IopSubsystem::debugSnapshot() const
{
DebugSnapshot snapshot;
@@ -384,6 +480,7 @@ namespace ps2x::iop
row.name = service->name();
row.sids.assign(service->sids().begin(), service->sids().end());
row.profileSpecific = profileSpecific;
row.active = m_impl->serviceActive(*service);
service->appendDebugMetrics(row.metrics);
snapshot.services.push_back(std::move(row));
}
+9 -2
View File
@@ -187,7 +187,7 @@ namespace ps2x::iop::detail
uint32_t normalizedSource = 0u;
uint32_t normalizedDestination = 0u;
if (!m_host.normalizeGuestAddress(transfer.sourceAddress, normalizedSource) ||
!m_host.normalizeGuestAddress(transfer.destinationAddress, normalizedDestination))
!m_host.normalizeIopAddress(transfer.destinationAddress, normalizedDestination))
{
return;
}
@@ -333,6 +333,13 @@ namespace ps2x::iop::detail
return normalized;
}
uint32_t normalizeIopAddress(uint32_t address) const
{
uint32_t normalized = 0u;
(void)m_host.normalizeIopAddress(address, normalized);
return normalized;
}
RpcResult handleCreateTransport(const RpcRequest &request, RpcResult result)
{
uint32_t dtxId = 0u;
@@ -351,7 +358,7 @@ namespace ps2x::iop::detail
}
const uint32_t normalizedEeWorkAddress = normalizeAddress(eeWorkAddress);
const uint32_t normalizedIopWorkAddress = normalizeAddress(iopWorkAddress);
const uint32_t normalizedIopWorkAddress = normalizeIopAddress(iopWorkAddress);
uint32_t remoteHandle = 0u;
{
+6
View File
@@ -33,6 +33,11 @@ namespace ps2x::iop::detail
return kSids;
}
[[nodiscard]] std::span<const std::string_view> moduleAliases() const override
{
return kModuleAliases;
}
void reset() override
{
std::lock_guard<std::mutex> lock(m_mutex);
@@ -99,6 +104,7 @@ namespace ps2x::iop::detail
private:
inline static constexpr std::array<uint32_t, 1> kSids{kDbcManSid};
inline static constexpr std::array<std::string_view, 3> kModuleAliases{"dbcman", "dbcm", "dbcmserv"};
IopHost &m_host;
mutable std::mutex m_mutex;
+6
View File
@@ -27,6 +27,11 @@ namespace ps2x::iop::detail
return kSids;
}
[[nodiscard]] std::span<const std::string_view> moduleAliases() const override
{
return kModuleAliases;
}
void reset() override
{
}
@@ -51,6 +56,7 @@ namespace ps2x::iop::detail
private:
inline static constexpr std::array<uint32_t, 1> kSids{kLibSdSid};
inline static constexpr std::array<std::string_view, 1> kModuleAliases{"libsd"};
IopHost &m_host;
};
+71 -44
View File
@@ -82,43 +82,75 @@ namespace ps2x::iop::detail
flavor = Flavor::NewXmcserv;
switch (function)
{
case 0xFEu: return Operation::Init;
case 0x01u: return Operation::GetInfo;
case 0x02u: return Operation::Open;
case 0x03u: return Operation::Close;
case 0x04u: return Operation::Seek;
case 0x05u: return Operation::Read;
case 0x06u: return Operation::Write;
case 0x0Au: return Operation::Flush;
case 0x0Cu: return Operation::Chdir;
case 0x0Du: return Operation::GetDir;
case 0x0Eu: return Operation::SetInfo;
case 0x0Fu: return Operation::Delete;
case 0x10u: return Operation::Format;
case 0x11u: return Operation::Unformat;
case 0x12u: return Operation::GetEnt;
case 0x14u: return Operation::ChangePriority;
default: break;
case 0xFEu:
return Operation::Init;
case 0x01u:
return Operation::GetInfo;
case 0x02u:
return Operation::Open;
case 0x03u:
return Operation::Close;
case 0x04u:
return Operation::Seek;
case 0x05u:
return Operation::Read;
case 0x06u:
return Operation::Write;
case 0x0Au:
return Operation::Flush;
case 0x0Cu:
return Operation::Chdir;
case 0x0Du:
return Operation::GetDir;
case 0x0Eu:
return Operation::SetInfo;
case 0x0Fu:
return Operation::Delete;
case 0x10u:
return Operation::Format;
case 0x11u:
return Operation::Unformat;
case 0x12u:
return Operation::GetEnt;
case 0x14u:
return Operation::ChangePriority;
default:
break;
}
flavor = Flavor::OldMcserv;
switch (function)
{
case 0x70u: return Operation::Init;
case 0x71u: return Operation::Open;
case 0x72u: return Operation::Close;
case 0x73u: return Operation::Read;
case 0x74u: return Operation::Write;
case 0x75u: return Operation::Seek;
case 0x76u: return Operation::GetDir;
case 0x77u: return Operation::Format;
case 0x78u: return Operation::GetInfo;
case 0x79u: return Operation::Delete;
case 0x7Au: return Operation::Flush;
case 0x7Bu: return Operation::Chdir;
case 0x7Cu: return Operation::SetInfo;
case 0x80u: return Operation::Unformat;
default: return Operation::Unknown;
case 0x70u:
return Operation::Init;
case 0x71u:
return Operation::Open;
case 0x72u:
return Operation::Close;
case 0x73u:
return Operation::Read;
case 0x74u:
return Operation::Write;
case 0x75u:
return Operation::Seek;
case 0x76u:
return Operation::GetDir;
case 0x77u:
return Operation::Format;
case 0x78u:
return Operation::GetInfo;
case 0x79u:
return Operation::Delete;
case 0x7Au:
return Operation::Flush;
case 0x7Bu:
return Operation::Chdir;
case 0x7Cu:
return Operation::SetInfo;
case 0x80u:
return Operation::Unformat;
default:
return Operation::Unknown;
}
}
@@ -136,6 +168,7 @@ namespace ps2x::iop::detail
[[nodiscard]] std::string_view name() const override { return "MCSERV"; }
[[nodiscard]] std::span<const uint32_t> sids() const override { return m_sids; }
[[nodiscard]] std::span<const std::string_view> moduleAliases() const override { return m_moduleAliases; }
void reset() override
{
@@ -245,8 +278,7 @@ namespace ps2x::iop::detail
(void)m_host.writeGuest(receive.address, &result, sizeof(result));
if (receive.size > sizeof(result))
{
(void)m_host.zeroGuest(receive.address + sizeof(result),
receive.size - sizeof(result));
(void)m_host.zeroGuest(receive.address + sizeof(result), receive.size - sizeof(result));
}
}
@@ -270,8 +302,7 @@ namespace ps2x::iop::detail
uint32_t sendAddress,
const NameParameter &parameter)
{
const uint32_t nameAddress =
sendAddress + static_cast<uint32_t>(offsetof(NameParameter, name));
const uint32_t nameAddress = sendAddress + static_cast<uint32_t>(offsetof(NameParameter, name));
const uint32_t port = static_cast<uint32_t>(parameter.port);
const uint32_t slot = static_cast<uint32_t>(parameter.slot);
switch (operation)
@@ -281,12 +312,9 @@ namespace ps2x::iop::detail
{
return call(MemoryCardOperation::Mkdir, port, slot, nameAddress);
}
return call(MemoryCardOperation::Open,
port, slot, nameAddress,
static_cast<uint32_t>(parameter.flags));
return call(MemoryCardOperation::Open, port, slot, nameAddress, static_cast<uint32_t>(parameter.flags));
case Operation::Chdir:
return call(MemoryCardOperation::Chdir,
port, slot, nameAddress, parameter.pointer);
return call(MemoryCardOperation::Chdir, port, slot, nameAddress, parameter.pointer);
case Operation::SetInfo:
return call(MemoryCardOperation::SetFileInfo, port, slot, nameAddress);
case Operation::Delete:
@@ -302,9 +330,7 @@ namespace ps2x::iop::detail
}
}
int32_t handleDescriptorOperation(Operation operation,
Flavor flavor,
const DescriptorParameter &parameter)
int32_t handleDescriptorOperation(Operation operation, Flavor flavor, const DescriptorParameter &parameter)
{
switch (operation)
{
@@ -392,6 +418,7 @@ namespace ps2x::iop::detail
mutable std::mutex m_mutex;
uint32_t m_unknownRpcLogCount = 0u;
const std::array<uint32_t, 2> m_sids = {kMcservSid, kMcservDev9Sid};
const std::array<std::string_view, 2> m_moduleAliases = {"mcserv", "xmcserv"};
};
}
+56 -93
View File
@@ -52,6 +52,19 @@ namespace ps2x::iop::detail
return host.writeGuest(address, &value, sizeof(value));
}
template <typename T>
bool readIopPod(const IopHost &host, uint32_t address, T &value)
{
value = {};
return host.readIopMemory(address, &value, sizeof(value));
}
template <typename T>
bool writeIopPod(IopHost &host, uint32_t address, const T &value)
{
return host.writeIopMemory(address, &value, sizeof(value));
}
template <typename T, size_t Size>
bool hasAnyNonZero(const std::array<T, Size> &values)
{
@@ -174,9 +187,7 @@ namespace ps2x::iop::detail
handleCommandBuffer(request.send);
result.handled = true;
}
else if (request.sid == kStateSid &&
(request.function == kGetStatusAddressFunction ||
request.function == kGetAddressTableFunction))
else if (request.sid == kStateSid && (request.function == kGetStatusAddressFunction || request.function == kGetAddressTableFunction))
{
uint32_t responseAddress = 0u;
{
@@ -195,8 +206,7 @@ namespace ps2x::iop::detail
(void)writeGuestPod(m_host, request.receive.address, responseAddress);
if (request.receive.size > sizeof(uint32_t))
{
(void)m_host.zeroGuest(request.receive.address + sizeof(uint32_t),
request.receive.size - sizeof(uint32_t));
(void)m_host.zeroGuest(request.receive.address + sizeof(uint32_t), request.receive.size - sizeof(uint32_t));
}
result.resultAddress = request.receive.address;
}
@@ -210,7 +220,8 @@ namespace ps2x::iop::detail
const auto rule = std::find_if(
m_bindings.completionRules.begin(),
m_bindings.completionRules.end(),
[&](const TsnddrvCompletionRule &candidate) {
[&](const TsnddrvCompletionRule &candidate)
{
return candidate.eeFunction == request.endFunction;
});
if (rule != m_bindings.completionRules.end())
@@ -223,9 +234,7 @@ namespace ps2x::iop::detail
if (rule->clearBusy)
{
constexpr uint32_t idle = 0u;
(void)writeGuestPod(m_host,
m_bindings.busyFlagAddress,
idle);
(void)writeGuestPod(m_host, m_bindings.busyFlagAddress, idle);
}
}
}
@@ -280,15 +289,10 @@ namespace ps2x::iop::detail
{
const TsnddrvGuestArena &arena = m_bindings.arena;
const uint32_t statusAddress = alignUp(arena.base, arena.statusAlignment);
const uint32_t addressTableAddress =
alignUp(statusAddress + kStatusSize, arena.tableAlignment);
const uint32_t hdBaseAddress =
alignUp(addressTableAddress + (kAddressTableEntries * sizeof(uint32_t)),
arena.storageAlignment);
const uint32_t sqBaseAddress =
alignUp(hdBaseAddress + arena.hdBytes, arena.storageAlignment);
const uint32_t dataBaseAddress =
alignUp(sqBaseAddress + arena.sqBytes, arena.storageAlignment);
const uint32_t addressTableAddress = alignUp(statusAddress + kStatusSize, arena.tableAlignment);
const uint32_t hdBaseAddress = alignUp(addressTableAddress + (kAddressTableEntries * sizeof(uint32_t)), arena.storageAlignment);
const uint32_t sqBaseAddress = alignUp(hdBaseAddress + arena.hdBytes, arena.storageAlignment);
const uint32_t dataBaseAddress = alignUp(sqBaseAddress + arena.sqBytes, arena.storageAlignment);
const uint32_t storageEnd = dataBaseAddress + arena.dataBytes;
if (storageEnd > arena.limit)
{
@@ -313,23 +317,17 @@ namespace ps2x::iop::detail
if (!m_state.initialized)
{
if (!m_host.zeroGuest(m_state.statusAddress, kStatusSize) ||
!m_host.zeroGuest(m_state.addressTableAddress,
kAddressTableEntries * sizeof(uint32_t)) ||
!m_host.zeroGuest(m_state.storageBaseAddress, m_state.storageSize))
if (!m_host.zeroIopMemory(m_state.statusAddress, kStatusSize) ||
!m_host.zeroIopMemory(m_state.addressTableAddress, kAddressTableEntries * sizeof(uint32_t)) ||
!m_host.zeroIopMemory(m_state.storageBaseAddress, m_state.storageSize))
{
return false;
}
if (!writeGuestPod(m_host,
m_state.addressTableAddress + (0u * sizeof(uint32_t)),
m_state.hdBaseAddress) ||
!writeGuestPod(m_host,
m_state.addressTableAddress + (1u * sizeof(uint32_t)),
m_state.sqBaseAddress) ||
!writeGuestPod(m_host,
m_state.addressTableAddress + (2u * sizeof(uint32_t)),
m_state.dataBaseAddress))
if (
!writeIopPod(m_host, m_state.addressTableAddress + (0u * sizeof(uint32_t)), m_state.hdBaseAddress) ||
!writeIopPod(m_host, m_state.addressTableAddress + (1u * sizeof(uint32_t)), m_state.sqBaseAddress) ||
!writeIopPod(m_host, m_state.addressTableAddress + (2u * sizeof(uint32_t)), m_state.dataBaseAddress))
{
return false;
}
@@ -339,9 +337,7 @@ namespace ps2x::iop::detail
return true;
}
int16_t checkValue(bool seTable,
uint32_t index,
uint32_t count) const
int16_t checkValue(bool seTable, uint32_t index, uint32_t count) const
{
if (index >= count)
{
@@ -352,10 +348,7 @@ namespace ps2x::iop::detail
{
const uint32_t base = seTable ? candidate.seAddress : candidate.midiAddress;
int16_t value = 0;
if (readGuestPod(m_host,
base + (index * sizeof(int16_t)),
value) &&
value != 0)
if (readGuestPod(m_host, base + (index * sizeof(int16_t)), value) && value != 0)
{
return value;
}
@@ -370,19 +363,13 @@ namespace ps2x::iop::detail
{
std::array<int16_t, 5> seValues{};
std::array<int16_t, 4> midiValues{};
const bool seReadable = m_host.readGuest(candidate.seAddress,
seValues.data(),
sizeof(seValues));
const bool midiReadable = m_host.readGuest(candidate.midiAddress,
midiValues.data(),
sizeof(midiValues));
const bool seReadable = m_host.readGuest(candidate.seAddress, seValues.data(), sizeof(seValues));
const bool midiReadable = m_host.readGuest(candidate.midiAddress, midiValues.data(), sizeof(midiValues));
if (seReadable && midiReadable && !firstReadable)
{
firstReadable = &candidate;
}
const bool looksLive =
(seReadable && hasAnyNonZero(seValues)) ||
(midiReadable && hasAnyNonZero(midiValues));
const bool looksLive = (seReadable && hasAnyNonZero(seValues)) || (midiReadable && hasAnyNonZero(midiValues));
if (seReadable && midiReadable && looksLive)
{
seBase = candidate.seAddress;
@@ -409,35 +396,23 @@ namespace ps2x::iop::detail
return;
}
auto backfillSlots = [&](uint32_t statusOffset,
uint32_t compatBase,
uint32_t slotCount)
auto backfillSlots = [&](uint32_t statusOffset, uint32_t compatBase, uint32_t slotCount)
{
for (uint32_t slot = 0u; slot < slotCount; ++slot)
{
int16_t liveValue = 0;
if (!readGuestPod(m_host,
m_state.statusAddress + statusOffset +
(slot * sizeof(int16_t)),
liveValue) ||
liveValue != 0)
if (!readIopPod(m_host, m_state.statusAddress + statusOffset + (slot * sizeof(int16_t)), liveValue) || liveValue != 0)
{
continue;
}
int16_t compatValue = 0;
if (!readGuestPod(m_host,
compatBase + (slot * sizeof(int16_t)),
compatValue) ||
compatValue == 0)
if (!readGuestPod(m_host, compatBase + (slot * sizeof(int16_t)), compatValue) || compatValue == 0)
{
continue;
}
(void)writeGuestPod(m_host,
m_state.statusAddress + statusOffset +
(slot * sizeof(int16_t)),
compatValue);
(void)writeIopPod(m_host, m_state.statusAddress + statusOffset + (slot * sizeof(int16_t)), compatValue);
}
};
@@ -458,28 +433,23 @@ namespace ps2x::iop::detail
{
const uint32_t port = command[1] & 0x0Fu;
uint16_t midiInfo = 0u;
(void)readGuestPod(m_host,
m_state.statusAddress + kMidiInfoOffset,
midiInfo);
(void)readIopPod(m_host,
m_state.statusAddress + kMidiInfoOffset,
midiInfo);
midiInfo = static_cast<uint16_t>(midiInfo |
static_cast<uint16_t>(1u << port));
(void)writeGuestPod(m_host,
m_state.statusAddress + kMidiInfoOffset,
midiInfo);
(void)writeIopPod(m_host,
m_state.statusAddress + kMidiInfoOffset,
midiInfo);
break;
}
case 0x21u: // SdrBgmStop
{
const uint32_t port = command[1] & 0x0Fu;
uint16_t midiInfo = 0u;
(void)readGuestPod(m_host,
m_state.statusAddress + kMidiInfoOffset,
midiInfo);
midiInfo = static_cast<uint16_t>(midiInfo &
~static_cast<uint16_t>(1u << port));
(void)writeGuestPod(m_host,
m_state.statusAddress + kMidiInfoOffset,
midiInfo);
(void)readIopPod(m_host, m_state.statusAddress + kMidiInfoOffset, midiInfo);
midiInfo = static_cast<uint16_t>(midiInfo & ~static_cast<uint16_t>(1u << port));
(void)writeIopPod(m_host, m_state.statusAddress + kMidiInfoOffset, midiInfo);
break;
}
case 0x28u: // SdrHDDataSet
@@ -490,10 +460,7 @@ namespace ps2x::iop::detail
break;
}
const int16_t checksum = checkValue(false, port, 4u);
(void)writeGuestPod(m_host,
m_state.statusAddress + kMidiSumOffset +
(port * sizeof(int16_t)),
checksum);
(void)writeIopPod(m_host, m_state.statusAddress + kMidiSumOffset + (port * sizeof(int16_t)), checksum);
break;
}
case 0x29u: // SdrHDDataSet2
@@ -504,15 +471,11 @@ namespace ps2x::iop::detail
break;
}
const int16_t checksum = checkValue(true, port, 5u);
(void)writeGuestPod(m_host,
m_state.statusAddress + kSeSumOffset +
(port * sizeof(int16_t)),
checksum);
(void)writeIopPod(m_host, m_state.statusAddress + kSeSumOffset + (port * sizeof(int16_t)), checksum);
break;
}
case 0x10u: // SdrSeAllStop
(void)m_host.zeroGuest(m_state.statusAddress + kSeInfoOffset,
6u * sizeof(uint16_t));
(void)m_host.zeroIopMemory(m_state.statusAddress + kSeInfoOffset, 6u * sizeof(uint16_t));
break;
default:
break;
@@ -569,12 +532,13 @@ namespace ps2x::iop::detail
std::unique_ptr<IopService> createTsnddrvService(IopHost &host,
TsnddrvBindings bindings)
{
const auto isPowerOfTwo = [](uint32_t value) {
const auto isPowerOfTwo = [](uint32_t value)
{
return value != 0u && (value & (value - 1u)) == 0u;
};
const auto alignUp64 = [](uint64_t value, uint32_t alignment) {
return (value + (alignment - 1u)) &
~static_cast<uint64_t>(alignment - 1u);
const auto alignUp64 = [](uint64_t value, uint32_t alignment)
{
return (value + (alignment - 1u)) & ~static_cast<uint64_t>(alignment - 1u);
};
const TsnddrvGuestArena &arena = bindings.arena;
@@ -590,8 +554,7 @@ namespace ps2x::iop::detail
}
uint64_t end = alignUp64(arena.base, arena.statusAlignment) + kStatusSize;
end = alignUp64(end, arena.tableAlignment) +
(kAddressTableEntries * sizeof(uint32_t));
end = alignUp64(end, arena.tableAlignment) + (kAddressTableEntries * sizeof(uint32_t));
end = alignUp64(end, arena.storageAlignment) + arena.hdBytes;
end = alignUp64(end, arena.storageAlignment) + arena.sqBytes;
end = alignUp64(end, arena.storageAlignment) + arena.dataBytes;
+123
View File
@@ -0,0 +1,123 @@
#include "ps2x/iop/ps2_path.h"
#include <algorithm>
#include <cctype>
namespace ps2x::iop
{
namespace
{
std::string lowerAscii(std::string_view value)
{
std::string result(value);
std::transform(result.begin(), result.end(), result.begin(), [](unsigned char ch)
{ return static_cast<char>(std::tolower(ch)); });
return result;
}
void normalizeSuffix(std::string &suffix)
{
std::replace(suffix.begin(), suffix.end(), '\\', '/');
while (!suffix.empty() && suffix.front() == '/')
suffix.erase(suffix.begin());
const size_t semicolon = suffix.rfind(';');
if (semicolon == std::string::npos || semicolon + 1u == suffix.size())
return;
const bool numeric = std::all_of(suffix.begin() + static_cast<std::ptrdiff_t>(semicolon + 1u),
suffix.end(),
[](unsigned char ch)
{ return std::isdigit(ch) != 0; });
if (numeric)
suffix.erase(semicolon);
}
}
ParsedPs2Path parsePs2Path(std::string_view value)
{
ParsedPs2Path result;
if (value.empty())
return result;
const std::string lower = lowerAscii(value);
size_t prefixLength = 0u;
if (lower.rfind("host0:", 0u) == 0u)
{
result.device = Ps2PathDevice::Host;
result.deviceName = "host0";
prefixLength = 6u;
}
else if (lower.rfind("host:", 0u) == 0u)
{
result.device = Ps2PathDevice::Host;
result.deviceName = "host";
prefixLength = 5u;
}
else if (lower.rfind("cdrom0:", 0u) == 0u)
{
result.device = Ps2PathDevice::Cdrom;
result.deviceName = "cdrom0";
prefixLength = 7u;
}
else if (lower.rfind("cdrom:", 0u) == 0u)
{
result.device = Ps2PathDevice::Cdrom;
result.deviceName = "cdrom";
prefixLength = 6u;
}
else if (lower.rfind("mc0:", 0u) == 0u)
{
result.device = Ps2PathDevice::MemoryCard0;
result.deviceName = "mc0";
prefixLength = 4u;
}
else if (lower.rfind("rom0:", 0u) == 0u)
{
result.device = Ps2PathDevice::Rom0;
result.deviceName = "rom0";
prefixLength = 5u;
}
else if (value.size() > 2u && std::isalpha(static_cast<unsigned char>(value[0])) &&
value[1] == ':' && (value[2] == '/' || value[2] == '\\'))
{
result.device = Ps2PathDevice::NativeHost;
result.deviceName = "native";
}
else if (value.find(':') != std::string_view::npos)
{
// TODO maybe log an error here, but don't fail the parse. This is a non-standard device name.
return result;
}
else
{
result.device = Ps2PathDevice::Cdrom;
result.deviceName = "cdrom0";
}
result.path.assign(value.substr(prefixLength));
if (result.device != Ps2PathDevice::NativeHost)
normalizeSuffix(result.path);
return result;
}
std::string ps2PathLeafKey(const ParsedPs2Path &parsed)
{
if (!parsed)
return {};
std::string path = parsed.path;
std::replace(path.begin(), path.end(), '\\', '/');
const size_t slash = path.find_last_of('/');
if (slash != std::string::npos)
path.erase(0u, slash + 1u);
path = lowerAscii(path);
if (path.size() > 4u && path.ends_with(".irx"))
path.resize(path.size() - 4u);
return path;
}
std::string ps2PathLeafKey(std::string_view path)
{
return ps2PathLeafKey(parsePs2Path(path));
}
}
+338
View File
@@ -0,0 +1,338 @@
#include "emulator/core/iop_cpu.h"
#include "emulator/core/iop_kernel.h"
#include "emulator/core/iop_memory.h"
#include "emulator/imports/iop_cdvd.h"
#include "emulator/imports/iop_imports.h"
#include "emulator/imports/iop_loadcore.h"
#include "emulator/imports/iop_timrman.h"
#include "emulator/services/iop_rpc.h"
#include "ps2x/iop/iop_host.h"
#include <cstdint>
#include <chrono>
#include <cstring>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <string>
#include <string_view>
namespace
{
using namespace ps2x::iop;
using namespace ps2x::iop::detail;
constexpr uint32_t kExportMagic = 0x41C00000u;
constexpr int32_t kLibraryNotFound = -213;
constexpr int32_t kIllegalLibrary = -214;
class NullHost : public IopHost
{
public:
bool readGuest(uint32_t, void *, size_t) const override { return false; }
bool writeGuest(uint32_t, const void *, size_t) override { return false; }
bool zeroGuest(uint32_t, size_t) override { return false; }
bool normalizeGuestAddress(uint32_t, uint32_t &) const override { return false; }
uint32_t allocateIopHandle(IopHandleKind) override { return 1u; }
uint32_t allocateGuest(uint32_t, uint32_t) override { return 0u; }
void freeGuest(uint32_t) override {}
void audioCommand(uint32_t, uint32_t, GuestBuffer, GuestBuffer) override {}
std::string hostPath(HostPathKind) const override { return {}; }
std::string translateGuestPath(std::string_view path) const override { return std::string(path); }
uint64_t openHostFile(std::string_view) override { return 0u; }
bool hostFileSize(uint64_t, uint64_t &) const override { return false; }
bool readHostFile(uint64_t, uint64_t, void *, size_t, size_t &) override { return false; }
void closeHostFile(uint64_t) override {}
int32_t memoryCard(const MemoryCardRequest &) override { return 0; }
bool hasGuestFunction(uint32_t) const override { return false; }
bool invokeGuestFunction(uint64_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t *) override { return false; }
void log(LogLevel, std::string_view) override {}
};
class CdRootHost final : public NullHost
{
public:
explicit CdRootHost(std::filesystem::path rootPath)
: root(std::move(rootPath))
{
}
std::string hostPath(HostPathKind kind) const override
{
return kind == HostPathKind::CdRoot ? root.string() : std::string{};
}
private:
std::filesystem::path root;
};
class RecordingExecutor final : public IopGuestExecutor
{
public:
uint32_t executeGuestFunction(uint32_t address,
uint32_t a0,
uint32_t,
uint32_t,
uint32_t,
uint32_t gp) override
{
++calls;
lastAddress = address;
lastArgument = a0;
lastGp = gp;
return callbackResult;
}
uint32_t callbackResult = 0u;
uint32_t calls = 0u;
uint32_t lastAddress = 0u;
uint32_t lastArgument = 0u;
uint32_t lastGp = 0u;
};
bool expect(bool condition, std::string_view message)
{
if (condition)
return true;
std::cerr << "FAIL: " << message << '\n';
return false;
}
bool testLoadcoreRebootLibraryMode()
{
IopMemory memory;
IopImportRegistry imports(memory);
IopLoadcore loadcore(memory, imports);
IopCpuState cpu{};
cpu.gpr[4] = 0u;
cpu.gpr[5] = 2u;
if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 was not handled") ||
!expect(static_cast<int32_t>(cpu.gpr[2]) == kIllegalLibrary,
"loadcore:27 did not reject a null export table"))
return false;
constexpr uint32_t table = 0x1000u;
memory.write32(table, kExportMagic);
memory.write16(table + 8u, 0x0101u);
memory.write16(table + 10u, 0x1234u);
const char name[8] = {'t', 'e', 's', 't', 'l', 'i', 'b', '\0'};
(void)memory.writeRam(table + 12u, name, sizeof(name));
memory.write32(table + 20u, 0u);
cpu = {};
cpu.gpr[4] = table;
cpu.gpr[5] = 2u;
if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 rejected a valid export table") ||
!expect(cpu.gpr[2] == 0u, "loadcore:27 returned an error for a valid export table") ||
!expect(memory.read16(table + 10u) == 0x1232u,
"loadcore:27 did not replace only export mode bits 1 and 2"))
return false;
constexpr uint32_t invalidTable = 0x1100u;
memory.write32(invalidTable, 0xDEADBEEFu);
cpu = {};
cpu.gpr[4] = invalidTable;
cpu.gpr[5] = 6u;
if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 did not consume an invalid-table call") ||
!expect(static_cast<int32_t>(cpu.gpr[2]) == kLibraryNotFound,
"loadcore:27 returned the wrong invalid-table error"))
return false;
if (!expect(imports.registerExportTable(table), "test export table did not register"))
return false;
memory.write32(table, 0u);
cpu = {};
cpu.gpr[4] = table;
cpu.gpr[5] = 6u;
return expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 rejected a registered table") &&
expect(cpu.gpr[2] == 0u, "loadcore:27 returned an error for a registered table") &&
expect(memory.read16(table + 10u) == 0x1236u,
"loadcore:27 did not update a registered table's mode");
}
bool pollEvent(IopKernel &kernel, int eventId, uint32_t bits, uint32_t resultAddress, int32_t expected)
{
IopCpuState cpu{};
cpu.gpr[4] = static_cast<uint32_t>(eventId);
cpu.gpr[5] = bits;
cpu.gpr[6] = 0u; // WEF_AND
cpu.gpr[7] = resultAddress;
return expect(kernel.dispatchEventImport(11u, cpu), "PollEventFlag was not handled") &&
expect(static_cast<int32_t>(cpu.gpr[2]) == expected, "PollEventFlag returned an unexpected result");
}
bool testCdvdSpecialControl()
{
NullHost host;
IopMemory memory;
IopKernel kernel(memory);
kernel.reset();
IopCdvd cdvd(host, memory, kernel);
cdvd.reset();
constexpr uint32_t param = 0x2000u;
constexpr uint32_t eventResult = 0x2010u;
IopCpuState cpu{};
cpu.gpr[4] = static_cast<uint32_t>(-11); // sceCdSC: return cdvdman interrupt event flag
cpu.gpr[5] = param;
if (!expect(cdvd.dispatchImport(50u, cpu), "cdvdman:50 was not handled") ||
!expect(static_cast<int32_t>(cpu.gpr[2]) > 0, "sceCdSC(-11) did not return a valid event flag"))
return false;
const int eventId = static_cast<int>(cpu.gpr[2]);
if (!pollEvent(kernel, eventId, 0x29u, eventResult, 0) ||
!expect(memory.read32(eventResult) == 0x29u, "cdvdman event flag did not start with bits 0x29"))
return false;
IopCpuState clear{};
clear.gpr[4] = static_cast<uint32_t>(eventId);
clear.gpr[5] = ~0x29u;
if (!expect(kernel.dispatchEventImport(8u, clear), "ClearEventFlag was not handled") ||
!pollEvent(kernel, eventId, 0x29u, eventResult, -418))
return false;
cpu = {};
cpu.gpr[4] = 0x12345u;
if (!expect(cdvd.dispatchImport(7u, cpu), "sceCdSeek was not handled") ||
!pollEvent(kernel, eventId, 0x29u, eventResult, 0))
return false;
memory.write8(param, 0x30u);
cpu = {};
cpu.gpr[4] = static_cast<uint32_t>(-2);
cpu.gpr[5] = param;
if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(-2) was not handled") ||
!expect(cpu.gpr[2] == 0x30u, "sceCdSC(-2) did not store the low-byte error"))
return false;
memory.write32(param, 0u);
cpu = {};
cpu.gpr[4] = static_cast<uint32_t>(-1);
cpu.gpr[5] = param;
if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(-1) was not handled") ||
!expect(cpu.gpr[2] == 0u, "sceCdSC(-1) returned the wrong initial stream state") ||
!expect(memory.read32(param) == 0x30u, "sceCdSC(-1) did not publish the last error"))
return false;
cpu = {};
cpu.gpr[4] = 2u;
cpu.gpr[5] = param;
if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(2) was not handled") ||
!expect(cpu.gpr[2] == 2u, "sceCdSC(2) did not update the stream state"))
return false;
cpu = {};
cpu.gpr[4] = static_cast<uint32_t>(-1);
cpu.gpr[5] = param;
return expect(cdvd.dispatchImport(50u, cpu), "second sceCdSC(-1) was not handled") &&
expect(cpu.gpr[2] == 2u, "sceCdSC(-1) did not preserve the stream state");
}
bool testCdvdSearchFile()
{
const auto suffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
const std::filesystem::path root =
std::filesystem::temp_directory_path() / ("ps2x-iop-cdvd-search-" + suffix);
const std::filesystem::path movieDirectory = root / "MOVIE";
const std::filesystem::path moviePath = movieDirectory / "OPENING.PSS";
std::error_code error;
std::filesystem::create_directories(movieDirectory, error);
if (!expect(!error, "could not create the temporary CD root"))
return false;
{
std::ofstream movie(moviePath, std::ios::binary);
movie.write("PSS!", 4);
}
CdRootHost host(root);
IopMemory memory;
IopKernel kernel(memory);
kernel.reset();
IopCdvd cdvd(host, memory, kernel);
cdvd.reset();
constexpr uint32_t resultAddress = 0x2400u;
constexpr uint32_t pathAddress = 0x2480u;
const char path[] = "cdrom0:\\movie\\opening.pss;1";
(void)memory.writeRam(pathAddress, path, sizeof(path));
IopCpuState cpu{};
cpu.gpr[4] = resultAddress;
cpu.gpr[5] = pathAddress;
const bool handled = cdvd.dispatchImport(10u, cpu);
const bool passed =
expect(handled, "cdvdman:10 was not handled") &&
expect(cpu.gpr[2] == 1u, "sceCdSearchFile did not find a case-insensitive ISO path") &&
expect(memory.read32(resultAddress) >= 20u, "sceCdSearchFile returned an invalid LSN") &&
expect(memory.read32(resultAddress + 4u) == 4u, "sceCdSearchFile returned the wrong size") &&
expect(memory.readString(resultAddress + 8u, 16u) == "OPENING.PSS",
"sceCdSearchFile returned the wrong file name");
std::filesystem::remove_all(root, error);
return passed;
}
bool testTimrmanPeriodicCallback()
{
IopTimrman timrman;
timrman.reset();
IopCpuState cpu{};
cpu.gpr[4] = 1u; // SYSCLK
cpu.gpr[5] = 32u;
cpu.gpr[6] = 1u;
if (!expect(timrman.dispatchImport(4u, cpu, 100u), "AllocHardTimer was not handled") ||
!expect(static_cast<int32_t>(cpu.gpr[2]) > 0, "AllocHardTimer did not allocate a 32-bit timer"))
return false;
const uint32_t timerId = cpu.gpr[2];
cpu = {};
cpu.gpr[4] = timerId;
cpu.gpr[5] = 100u;
cpu.gpr[6] = 0x12340u;
cpu.gpr[7] = 0x45670u;
cpu.gpr[28] = 0x89AB0u;
if (!expect(timrman.dispatchImport(20u, cpu, 100u), "SetTimerHandler was not handled") ||
!expect(cpu.gpr[2] == 0u, "SetTimerHandler failed"))
return false;
cpu = {};
cpu.gpr[4] = timerId;
cpu.gpr[5] = 1u;
cpu.gpr[6] = 0u;
cpu.gpr[7] = 1u;
if (!expect(timrman.dispatchImport(22u, cpu, 100u), "SetupHardTimer was not handled") ||
!expect(cpu.gpr[2] == 0u, "SetupHardTimer failed"))
return false;
cpu = {};
cpu.gpr[4] = timerId;
if (!expect(timrman.dispatchImport(23u, cpu, 100u), "StartHardTimer was not handled") ||
!expect(cpu.gpr[2] == 0u, "StartHardTimer failed") ||
!expect(timrman.nextEventCycle(1000u) == 200u, "timer compare was scheduled at the wrong cycle"))
return false;
RecordingExecutor executor;
executor.callbackResult = 100u;
timrman.serviceDue(199u, executor);
if (!expect(executor.calls == 0u, "timer callback ran too early"))
return false;
timrman.serviceDue(200u, executor);
return expect(executor.calls == 1u, "timer callback did not run") &&
expect(executor.lastAddress == 0x12340u, "timer called the wrong handler") &&
expect(executor.lastArgument == 0x45670u, "timer passed the wrong common argument") &&
expect(executor.lastGp == 0x89AB0u, "timer callback lost the registering module GP") &&
expect(timrman.nextEventCycle(1000u) == 300u, "timer callback return did not rearm compare");
}
}
int main()
{
if (!testLoadcoreRebootLibraryMode() || !testCdvdSpecialControl() || !testCdvdSearchFile() ||
!testTimrmanPeriodicCallback())
return 1;
std::cout << "ps2xIOP import tests passed\n";
return 0;
}
@@ -115,7 +115,7 @@ public class ExportPS2Functions extends GhidraScript {
"fioWrite", "fioLseek", "fioMkdir", "fioChdir", "fioRmdir", "fioGetstat",
"fioRemove", "SetGsCrt", "GsSetCrt", "GsGetIMR", "iGsGetIMR", "GsPutIMR",
"iGsPutIMR", "SetVSyncFlag", "SetSyscall", "GsSetVideoMode", "GetOsdConfigParam", "SetOsdConfigParam",
"EnableCache", "DisableCache", "GetRomName", "SifLoadElfPart", "sceSifLoadElf", "sceSifLoadElfPart",
"EnableCache", "DisableCache", "SifLoadElfPart", "sceSifLoadElf", "sceSifLoadElfPart",
"sceSifLoadModule", "sceSifLoadModuleBuffer", "SetupThread", "EndOfHeap", "GetMemorySize", "Deci2Call",
"QueryBootMode", "GetThreadTLS", "Copy", "GetEntryAddress", "RegisterExitHandler", "ret0", "ret1", "reta0",
"calloc_r", "free_r", "realloc_r", "memalign_r", "malloc_r", "malloc_extend_top", "malloc_trim_r", "mbtowc_r", "printf_r",