Files
PS2Recomp/ps2xRuntime/src/lib/ps2_runtime.cpp
T
Ranieri 669114f3f6 Feature/runtime review codegen fixes (#87)
* feat: small fixes on code gen

* feat: added code gen test

* feat: rename IOP

* fix: fix special case on JR
feat: added code generator test

* feat: ps2 logs now need special macros

* feat: a lot of regressions test
feat: use test to fix bugs on runtime
fix: fix incorrect instructions on code generator
feat: added missing decode on r5900 decoder
feat: added scissor on rasterizer

* feat: better ghidra plugin analyzer
fix: fix real bug on function finding on elf analyzer

* feat: some logs on GS
feat: added more syscalls stubs
feat: added more ps2 stubs

* feat: added missing stub
2026-02-27 03:44:59 -03:00

1887 lines
60 KiB
C++

#include "ps2_runtime.h"
#include "ps2_syscalls.h"
#include "ps2_stubs.h"
#include "game_overrides.h"
#include "ps2_runtime_macros.h"
#include <iostream>
#include <fstream>
#include <algorithm>
#include <array>
#include <cctype>
#include <cstring>
#include <limits>
#include <chrono>
#include <atomic>
#include <thread>
#include <unordered_map>
#include <sstream>
#include "raylib.h"
#include "ps2_gs_gpu.h"
#include <ThreadNaming.h>
#define ELF_MAGIC 0x464C457F // "\x7FELF" in little endian
#define ET_EXEC 2 // Executable file
#define EM_MIPS 8 // MIPS architecture
#define PT_LOAD 1 // Loadable segment
static constexpr int FB_WIDTH = 640;
static constexpr int FB_HEIGHT = 448;
static constexpr uint32_t DEFAULT_FB_SIZE = FB_WIDTH * FB_HEIGHT * 4;
static constexpr uint32_t DEFAULT_FB_ADDR = (PS2_RAM_SIZE - DEFAULT_FB_SIZE - 0x10000u);
struct ElfHeader
{
uint32_t magic;
uint8_t elf_class;
uint8_t endianness;
uint8_t version;
uint8_t os_abi;
uint8_t abi_version;
uint8_t padding[7];
uint16_t type;
uint16_t machine;
uint32_t version2;
uint32_t entry;
uint32_t phoff;
uint32_t shoff;
uint32_t flags;
uint16_t ehsize;
uint16_t phentsize;
uint16_t phnum;
uint16_t shentsize;
uint16_t shnum;
uint16_t shstrndx;
};
struct ProgramHeader
{
uint32_t type;
uint32_t offset;
uint32_t vaddr;
uint32_t paddr;
uint32_t filesz;
uint32_t memsz;
uint32_t flags;
uint32_t align;
};
namespace
{
constexpr uint32_t kGuestHeapDefaultBase = 0x00100000u;
constexpr uint32_t kGuestHeapDefaultAlignment = 16u;
constexpr uint32_t kGuestHeapSafetyPad = 0x1000u;
constexpr uint32_t kGuestHeapHardLimit = 0x01F00000u;
constexpr uint32_t COP0_CAUSE_EXCCODE_MASK = 0x0000007Cu;
constexpr uint32_t COP0_CAUSE_BD = 0x80000000u;
constexpr uint32_t COP0_STATUS_EXL = 0x00000002u;
constexpr uint32_t COP0_STATUS_BEV = 0x00400000u;
constexpr uint32_t EXCEPTION_VECTOR_GENERAL = 0x80000080u;
constexpr uint32_t EXCEPTION_VECTOR_TLB_REFILL = 0x80000000u;
constexpr uint32_t EXCEPTION_VECTOR_BOOT = 0xBFC00200u;
struct DispatchHistory
{
std::array<uint32_t, 64> pcs{};
uint32_t next = 0u;
bool wrapped = false;
};
thread_local DispatchHistory g_dispatchHistory;
void pushDispatchPc(uint32_t pc)
{
DispatchHistory &h = g_dispatchHistory;
h.pcs[h.next] = pc;
h.next = (h.next + 1u) % static_cast<uint32_t>(h.pcs.size());
if (h.next == 0u)
{
h.wrapped = true;
}
}
std::string formatDispatchHistory()
{
const DispatchHistory &h = g_dispatchHistory;
const uint32_t count = h.wrapped ? static_cast<uint32_t>(h.pcs.size()) : h.next;
if (count == 0u)
{
return "(empty)";
}
std::ostringstream oss;
bool first = true;
for (uint32_t i = 0u; i < count; ++i)
{
const uint32_t idx = (h.next + h.pcs.size() - count + i) % static_cast<uint32_t>(h.pcs.size());
if (!first)
{
oss << " -> ";
}
first = false;
oss << "0x" << std::hex << h.pcs[idx];
}
return oss.str();
}
uint32_t selectDispatchRecoveryPc(const PS2Runtime *runtime)
{
const DispatchHistory &h = g_dispatchHistory;
const uint32_t count = h.wrapped ? static_cast<uint32_t>(h.pcs.size()) : h.next;
if (count == 0u)
{
return 0u;
}
uint32_t firstHigh = 0u;
for (uint32_t step = 1u; step <= count; ++step)
{
const uint32_t idx = (h.next + h.pcs.size() - step) % static_cast<uint32_t>(h.pcs.size());
const uint32_t pc = h.pcs[idx];
if (pc < 0x00100000u)
{
continue;
}
if (runtime && !runtime->hasFunction(pc))
{
continue;
}
if (firstHigh == 0u)
{
firstHigh = pc;
continue;
}
return pc;
}
return firstHigh;
}
uint32_t selectExceptionVector(const R5900Context *ctx, bool tlbRefill)
{
if (ctx->cop0_status & COP0_STATUS_BEV)
{
return EXCEPTION_VECTOR_BOOT;
}
return tlbRefill ? EXCEPTION_VECTOR_TLB_REFILL : EXCEPTION_VECTOR_GENERAL;
}
void raiseCop0Exception(R5900Context *ctx, uint32_t exceptionCode, bool tlbRefill = false)
{
if (ctx->in_delay_slot)
{
ctx->cop0_epc = ctx->branch_pc;
ctx->cop0_cause = (ctx->cop0_cause & ~COP0_CAUSE_EXCCODE_MASK) |
((exceptionCode << 2) & COP0_CAUSE_EXCCODE_MASK) |
COP0_CAUSE_BD;
}
else
{
ctx->cop0_epc = ctx->pc;
ctx->cop0_cause = (ctx->cop0_cause & ~(COP0_CAUSE_EXCCODE_MASK | COP0_CAUSE_BD)) |
((exceptionCode << 2) & COP0_CAUSE_EXCCODE_MASK);
}
ctx->cop0_status |= COP0_STATUS_EXL;
ctx->pc = selectExceptionVector(ctx, tlbRefill);
ctx->in_delay_slot = false;
}
std::filesystem::path normalizeAbsolutePath(const std::filesystem::path &path)
{
if (path.empty())
{
return {};
}
std::error_code ec;
const std::filesystem::path absolute = std::filesystem::absolute(path, ec);
if (ec)
{
return path.lexically_normal();
}
return absolute.lexically_normal();
}
PS2Runtime::IoPaths &runtimeIoPaths()
{
static PS2Runtime::IoPaths paths = []()
{
PS2Runtime::IoPaths defaults;
std::error_code ec;
const std::filesystem::path cwd = std::filesystem::current_path(ec);
defaults.elfDirectory = ec ? std::filesystem::path(".") : cwd.lexically_normal();
defaults.hostRoot = defaults.elfDirectory;
defaults.cdRoot = defaults.elfDirectory;
defaults.mcRoot = defaults.elfDirectory / "mc0";
return defaults;
}();
return paths;
}
uint32_t readGuestU32Wrapped(const uint8_t *rdram, uint32_t addr)
{
if (!rdram)
{
return 0;
}
uint32_t value = 0;
value |= static_cast<uint32_t>(rdram[(addr + 0u) & PS2_RAM_MASK]) << 0;
value |= static_cast<uint32_t>(rdram[(addr + 1u) & PS2_RAM_MASK]) << 8;
value |= static_cast<uint32_t>(rdram[(addr + 2u) & PS2_RAM_MASK]) << 16;
value |= static_cast<uint32_t>(rdram[(addr + 3u) & PS2_RAM_MASK]) << 24;
return value;
}
uint64_t readGuestU64Wrapped(const uint8_t *rdram, uint32_t addr)
{
const uint64_t lo = readGuestU32Wrapped(rdram, addr);
const uint64_t hi = readGuestU32Wrapped(rdram, addr + 4u);
return lo | (hi << 32);
}
uint32_t selectStackRecoveryPc(const uint8_t *rdram, const R5900Context *ctx, const PS2Runtime *runtime)
{
if (!rdram || !ctx || !runtime)
{
return 0u;
}
const uint32_t sp = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[29], 0));
constexpr uint32_t kScanBytes = 0x200u;
for (uint32_t offset = 0u; offset < kScanBytes; offset += 8u)
{
const uint32_t slotAddr = sp + offset;
const uint32_t ra32 = static_cast<uint32_t>(readGuestU64Wrapped(rdram, slotAddr));
if (ra32 < 0x00100000u)
{
continue;
}
if (!runtime->hasFunction(ra32))
{
continue;
}
return ra32;
}
for (uint32_t offset = 0u; offset < kScanBytes; offset += 4u)
{
const uint32_t slotAddr = sp + offset;
const uint32_t ra32 = readGuestU32Wrapped(rdram, slotAddr);
if (ra32 < 0x00100000u)
{
continue;
}
if (!runtime->hasFunction(ra32))
{
continue;
}
return ra32;
}
return 0u;
}
std::string readGuestPrintableString(const uint8_t *rdram, uint32_t addr, size_t maxLen)
{
std::string out;
if (!rdram || maxLen == 0)
{
return out;
}
out.reserve(std::min<size_t>(maxLen, 64));
for (size_t i = 0; i < maxLen; ++i)
{
const char ch = static_cast<char>(rdram[(addr + static_cast<uint32_t>(i)) & PS2_RAM_MASK]);
if (ch == '\0')
{
break;
}
if (ch >= 0x20 && ch < 0x7F)
{
out.push_back(ch);
}
else
{
out.push_back('.');
}
}
return out;
}
}
static void UploadFrame(Texture2D &tex, PS2Runtime *rt)
{
// For now lets keep the display snapshot in sync with rasterized VRAM so the host frame
rt->gs().refreshDisplaySnapshot();
const GSRegisters &gs = rt->memory().gs();
uint32_t dispfb = static_cast<uint32_t>(gs.dispfb1 & 0xFFFFFFFFULL);
uint32_t fbp = dispfb & 0x1FF;
uint32_t fbw = (dispfb >> 9) & 0x3F;
uint32_t psm = (dispfb >> 15) & 0x1F;
uint64_t display64 = gs.display1;
uint32_t dw = static_cast<uint32_t>((display64 >> 32) & 0xFFF);
uint32_t dh = static_cast<uint32_t>((display64 >> 44) & 0x7FF);
uint32_t width = (dw + 1);
uint32_t height = (dh + 1);
if (width < 64 || height < 64)
{
width = FB_WIDTH;
height = FB_HEIGHT;
}
if (width > FB_WIDTH)
width = FB_WIDTH;
if (height > FB_HEIGHT)
height = FB_HEIGHT;
uint32_t baseBytes = fbp * 8192u;
const uint32_t bytesPerPixel = (psm == 2u || psm == 0x0Au) ? 2u : 4u;
uint32_t strideBytes = (fbw ? fbw : (FB_WIDTH / 64)) * 64 * bytesPerPixel;
std::vector<uint8_t> scratch(FB_WIDTH * FB_HEIGHT * 4, 0);
uint8_t *rdram = rt->memory().getRDRAM();
uint8_t *gsvram = rt->memory().getGSVRAM();
uint32_t snapSize = 0;
const uint8_t *snapVram = rt->gs().lockDisplaySnapshot(snapSize);
const uint8_t *vramSrc = (snapVram && snapSize > 0) ? snapVram : gsvram;
if (snapVram)
{
baseBytes = rt->gs().getLastDisplayBaseBytes();
}
if (psm == 0u)
{
for (uint32_t y = 0; y < height; ++y)
{
uint32_t srcOff = baseBytes + y * strideBytes;
uint32_t dstOff = y * FB_WIDTH * 4;
uint32_t copyW = width * 4;
uint32_t srcIdx = srcOff;
if (srcIdx + copyW <= PS2_GS_VRAM_SIZE && vramSrc)
std::memcpy(&scratch[dstOff], vramSrc + srcIdx, copyW);
else
{
uint32_t rdramIdx = srcOff & PS2_RAM_MASK;
if (rdramIdx + copyW > PS2_RAM_SIZE)
copyW = PS2_RAM_SIZE - rdramIdx;
std::memcpy(&scratch[dstOff], rdram + rdramIdx, copyW);
}
uint8_t *row = scratch.data() + dstOff;
for (uint32_t x = 0; x < width; ++x)
row[x * 4 + 3] = 255u;
}
}
else if (psm == 2u)
{
const uint32_t srcLineBytes = width * 2u;
for (uint32_t y = 0; y < height; ++y)
{
uint32_t srcOff = baseBytes + y * strideBytes;
uint32_t dstOff = y * FB_WIDTH * 4;
const uint8_t *src = nullptr;
if (srcOff + srcLineBytes <= PS2_GS_VRAM_SIZE && vramSrc)
src = vramSrc + srcOff;
else if ((srcOff & PS2_RAM_MASK) + srcLineBytes <= PS2_RAM_SIZE)
src = rdram + (srcOff & PS2_RAM_MASK);
if (!src)
continue;
uint8_t *dst = scratch.data() + dstOff;
for (uint32_t x = 0; x < width; ++x)
{
uint16_t p = *reinterpret_cast<const uint16_t *>(src + x * 2);
uint32_t r = (p >> 10) & 31u;
uint32_t g = (p >> 5) & 31u;
uint32_t b = p & 31u;
dst[x * 4 + 0] = static_cast<uint8_t>((r << 3) | (r >> 2));
dst[x * 4 + 1] = static_cast<uint8_t>((g << 3) | (g >> 2));
dst[x * 4 + 2] = static_cast<uint8_t>((b << 3) | (b >> 2));
dst[x * 4 + 3] = 255u;
}
}
}
else
{
rt->gs().unlockDisplaySnapshot();
Image blank = GenImageColor(FB_WIDTH, FB_HEIGHT, MAGENTA);
UpdateTexture(tex, blank.data);
UnloadImage(blank);
return;
}
rt->gs().unlockDisplaySnapshot();
UpdateTexture(tex, scratch.data());
}
PS2Runtime::PS2Runtime()
{
std::memset(&m_cpuContext, 0, sizeof(m_cpuContext));
// R0 is always zero in MIPS
m_cpuContext.r[0] = _mm_set1_epi32(0);
// Stack pointer (SP) and global pointer (GP) will be set by the loaded ELF
m_functionTable.clear();
m_loadedModules.clear();
m_guestHeapBlocks.clear();
m_guestHeapBase = kGuestHeapDefaultBase;
m_guestHeapEnd = kGuestHeapDefaultBase;
m_guestHeapLimit = std::min(kGuestHeapHardLimit, PS2_RAM_SIZE);
m_guestHeapSuggestedBase = kGuestHeapDefaultBase;
m_guestHeapConfigured = false;
}
PS2Runtime::~PS2Runtime()
{
requestStop();
if (IsWindowReady())
{
CloseWindow();
}
m_loadedModules.clear();
m_functionTable.clear();
}
bool PS2Runtime::initialize(const char *title)
{
if (!m_memory.initialize())
{
std::cerr << "Failed to initialize PS2 memory" << std::endl;
return false;
}
m_gs.init(m_memory.getGSVRAM(), static_cast<uint32_t>(PS2_GS_VRAM_SIZE), &m_memory.gs());
m_gs.reset();
m_gifArbiter.setProcessPacketFn([this](const uint8_t *data, uint32_t size) { m_gs.processGIFPacket(data, size); });
m_memory.setGifArbiter(&m_gifArbiter);
m_memory.setVu1MscalCallback([this](uint32_t startPC, uint32_t itop) {
m_vu1.execute(m_memory.getVU1Code(), PS2_VU1_CODE_SIZE,
m_memory.getVU1Data(), PS2_VU1_DATA_SIZE,
m_gs, &m_memory, startPC, itop, 65536);
});
m_iop.init(m_memory.getRDRAM());
m_iop.reset();
SetConfigFlags(FLAG_WINDOW_RESIZABLE);
InitWindow(FB_WIDTH, FB_HEIGHT, title);
InitAudioDevice();
m_audioBackend.setAudioReady(IsAudioDeviceReady());
SetTargetFPS(60);
m_vu1.reset();
return true;
}
bool PS2Runtime::loadELF(const std::string &elfPath)
{
configureIoPathsFromElf(elfPath);
std::ifstream file(elfPath, std::ios::binary);
if (!file)
{
std::cerr << "Failed to open ELF file: " << elfPath << std::endl;
return false;
}
file.seekg(0, std::ios::end);
const std::streamoff fileSize = file.tellg();
if (fileSize < static_cast<std::streamoff>(sizeof(ElfHeader)))
{
std::cerr << "ELF file is too small: " << elfPath << std::endl;
return false;
}
file.seekg(0, std::ios::beg);
ElfHeader header{};
if (!file.read(reinterpret_cast<char *>(&header), sizeof(header)))
{
std::cerr << "Failed to read ELF header from: " << elfPath << std::endl;
return false;
}
if (header.magic != ELF_MAGIC)
{
std::cerr << "Invalid ELF magic number" << std::endl;
return false;
}
if (header.elf_class != 1u || header.endianness != 1u)
{
std::cerr << "Unsupported ELF format (expected 32-bit little-endian)." << std::endl;
return false;
}
if (header.machine != EM_MIPS || header.type != ET_EXEC)
{
std::cerr << "Not a MIPS executable ELF file" << std::endl;
return false;
}
if (header.phnum != 0u && header.phentsize < sizeof(ProgramHeader))
{
std::cerr << "Unsupported ELF program-header entry size: " << header.phentsize << std::endl;
return false;
}
const uint64_t programHeaderTableEnd =
static_cast<uint64_t>(header.phoff) +
static_cast<uint64_t>(header.phnum) * static_cast<uint64_t>(header.phentsize);
if (programHeaderTableEnd > static_cast<uint64_t>(fileSize))
{
std::cerr << "ELF program-header table is out of range." << std::endl;
return false;
}
m_cpuContext.pc = header.entry;
m_debugPc.store(m_cpuContext.pc, std::memory_order_relaxed);
uint32_t maxLoadedRdramEnd = kGuestHeapDefaultBase;
uint32_t moduleBase = std::numeric_limits<uint32_t>::max();
uint32_t moduleEnd = 0u;
bool loadedAnySegment = false;
for (uint16_t i = 0; i < header.phnum; i++)
{
const uint64_t phOffset =
static_cast<uint64_t>(header.phoff) +
static_cast<uint64_t>(i) * static_cast<uint64_t>(header.phentsize);
if (phOffset + sizeof(ProgramHeader) > static_cast<uint64_t>(fileSize))
{
std::cerr << "ELF program header " << i << " is out of range." << std::endl;
return false;
}
ProgramHeader ph{};
file.seekg(static_cast<std::streamoff>(phOffset), std::ios::beg);
if (!file.read(reinterpret_cast<char *>(&ph), sizeof(ph)))
{
std::cerr << "Failed to read ELF program header " << i << std::endl;
return false;
}
if (ph.type != PT_LOAD || ph.memsz == 0u)
{
continue;
}
if (ph.filesz > ph.memsz)
{
std::cerr << "ELF segment " << i << " has filesz > memsz." << std::endl;
return false;
}
const uint64_t segmentFileEnd = static_cast<uint64_t>(ph.offset) + static_cast<uint64_t>(ph.filesz);
if (segmentFileEnd > static_cast<uint64_t>(fileSize))
{
std::cerr << "ELF segment " << i << " exceeds file bounds." << std::endl;
return false;
}
const bool scratch =
ph.vaddr >= PS2_SCRATCHPAD_BASE &&
ph.vaddr < (PS2_SCRATCHPAD_BASE + PS2_SCRATCHPAD_SIZE);
uint32_t physAddr = 0u;
try
{
physAddr = m_memory.translateAddress(ph.vaddr);
}
catch (const std::exception &e)
{
std::cerr << "Failed to translate ELF segment " << i
<< " virtual address 0x" << std::hex << ph.vaddr
<< std::dec << ": " << e.what() << std::endl;
return false;
}
const uint64_t regionSize = scratch ? static_cast<uint64_t>(PS2_SCRATCHPAD_SIZE)
: static_cast<uint64_t>(PS2_RAM_SIZE);
const uint64_t segmentMemEnd = static_cast<uint64_t>(physAddr) + static_cast<uint64_t>(ph.memsz);
if (segmentMemEnd > regionSize)
{
std::cerr << "ELF segment " << i << " exceeds "
<< (scratch ? "scratchpad" : "RDRAM")
<< " bounds (vaddr=0x" << std::hex << ph.vaddr
<< " memsz=0x" << ph.memsz << std::dec << ")." << std::endl;
return false;
}
uint8_t *destBase = scratch ? m_memory.getScratchpad() : m_memory.getRDRAM();
if (!destBase)
{
std::cerr << "ELF segment " << i << " has no destination memory backing." << std::endl;
return false;
}
uint8_t *dest = destBase + physAddr;
if (ph.filesz > 0u)
{
file.seekg(static_cast<std::streamoff>(ph.offset), std::ios::beg);
if (!file.read(reinterpret_cast<char *>(dest), ph.filesz))
{
std::cerr << "Failed to read ELF segment " << i << " payload." << std::endl;
return false;
}
}
if (ph.memsz > ph.filesz)
{
std::memset(dest + ph.filesz, 0, ph.memsz - ph.filesz);
}
std::cout << "Loading segment: 0x" << std::hex << ph.vaddr
<< " - 0x" << (static_cast<uint64_t>(ph.vaddr) + static_cast<uint64_t>(ph.memsz))
<< " (filesz: 0x" << ph.filesz
<< ", memsz: 0x" << ph.memsz << ")"
<< std::dec << std::endl;
if (!scratch)
{
maxLoadedRdramEnd = std::max(maxLoadedRdramEnd, static_cast<uint32_t>(segmentMemEnd));
}
if (ph.flags & 0x1u) // PF_X
{
const uint64_t execEnd = static_cast<uint64_t>(ph.vaddr) + static_cast<uint64_t>(ph.memsz);
if (execEnd <= std::numeric_limits<uint32_t>::max())
{
m_memory.registerCodeRegion(ph.vaddr, static_cast<uint32_t>(execEnd));
}
}
loadedAnySegment = true;
moduleBase = std::min(moduleBase, ph.vaddr);
const uint64_t segmentVirtualEnd = static_cast<uint64_t>(ph.vaddr) + static_cast<uint64_t>(ph.memsz);
const uint32_t clampedVirtualEnd =
(segmentVirtualEnd > std::numeric_limits<uint32_t>::max())
? std::numeric_limits<uint32_t>::max()
: static_cast<uint32_t>(segmentVirtualEnd);
moduleEnd = std::max(moduleEnd, clampedVirtualEnd);
}
if (!loadedAnySegment)
{
std::cerr << "ELF contains no loadable PT_LOAD segments." << std::endl;
return false;
}
if (maxLoadedRdramEnd > PS2_RAM_SIZE)
{
maxLoadedRdramEnd = PS2_RAM_SIZE;
}
const uint32_t paddedEnd = (maxLoadedRdramEnd > (PS2_RAM_SIZE - kGuestHeapSafetyPad))
? PS2_RAM_SIZE
: (maxLoadedRdramEnd + kGuestHeapSafetyPad);
const uint32_t suggestedHeapBase = alignGuestHeapValue(paddedEnd, kGuestHeapDefaultAlignment);
{
std::lock_guard<std::mutex> lock(m_guestHeapMutex);
if (!m_guestHeapConfigured)
{
const uint32_t hardLimit = std::min(kGuestHeapHardLimit, PS2_RAM_SIZE);
m_guestHeapSuggestedBase = std::min(suggestedHeapBase, hardLimit);
m_guestHeapBase = m_guestHeapSuggestedBase;
m_guestHeapEnd = m_guestHeapSuggestedBase;
m_guestHeapLimit = hardLimit;
}
}
LoadedModule module;
module.name = elfPath.substr(elfPath.find_last_of("/\\") + 1);
module.baseAddress = (moduleBase == std::numeric_limits<uint32_t>::max()) ? 0x00100000u : moduleBase;
module.size = (moduleEnd > module.baseAddress) ? static_cast<size_t>(moduleEnd - module.baseAddress) : 0u;
module.active = true;
m_loadedModules.push_back(module);
ps2_game_overrides::applyMatching(*this, elfPath, m_cpuContext.pc);
std::cout << "ELF file loaded successfully. Entry point: 0x" << std::hex << m_cpuContext.pc << std::dec << std::endl;
return true;
}
const PS2Runtime::IoPaths &PS2Runtime::getIoPaths()
{
return runtimeIoPaths();
}
void PS2Runtime::setIoPaths(const IoPaths &paths)
{
IoPaths normalized = paths;
normalized.elfPath = normalizeAbsolutePath(normalized.elfPath);
normalized.elfDirectory = normalizeAbsolutePath(normalized.elfDirectory);
normalized.hostRoot = normalizeAbsolutePath(normalized.hostRoot);
normalized.cdRoot = normalizeAbsolutePath(normalized.cdRoot);
normalized.mcRoot = normalizeAbsolutePath(normalized.mcRoot);
normalized.cdImage = normalizeAbsolutePath(normalized.cdImage);
if (normalized.elfDirectory.empty() && !normalized.elfPath.empty())
{
normalized.elfDirectory = normalized.elfPath.parent_path();
}
if (normalized.hostRoot.empty())
{
normalized.hostRoot = normalized.elfDirectory;
}
if (normalized.cdRoot.empty())
{
normalized.cdRoot = normalized.elfDirectory;
}
if (normalized.mcRoot.empty())
{
normalized.mcRoot = normalized.elfDirectory / "mc0";
}
runtimeIoPaths() = normalized;
}
void PS2Runtime::configureIoPathsFromElf(const std::string &elfPath)
{
IoPaths paths = runtimeIoPaths();
paths.elfPath = normalizeAbsolutePath(std::filesystem::path(elfPath));
if (!paths.elfPath.empty())
{
paths.elfDirectory = paths.elfPath.parent_path();
}
if (!paths.elfDirectory.empty())
{
paths.hostRoot = paths.elfDirectory;
paths.cdRoot = paths.elfDirectory;
paths.mcRoot = paths.elfDirectory / "mc0";
}
setIoPaths(paths);
}
void PS2Runtime::registerFunction(uint32_t address, RecompiledFunction func)
{
m_functionTable[address] = func;
}
bool PS2Runtime::hasFunction(uint32_t address) const
{
return m_functionTable.find(address) != m_functionTable.end();
}
PS2Runtime::RecompiledFunction PS2Runtime::lookupFunction(uint32_t address)
{
pushDispatchPc(address);
auto it = m_functionTable.find(address);
if (it != m_functionTable.end())
{
return it->second;
}
// Some games dispatch to internal basic-block addresses that belong to a
// larger recompiled function. Map known hot-path aliases to their parent
// function entry so execution can resume from the current ctx->pc.
if (address == 0x2913E4u)
{
auto parent = m_functionTable.find(0x2913B0u);
if (parent != m_functionTable.end())
{
return parent->second;
}
}
std::cerr << "Warning: Function at address 0x" << std::hex << address << std::dec << " not found" << std::endl;
static RecompiledFunction defaultFunction = [](uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
const uint32_t ra = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[31], 0)) : 0u;
const uint32_t sp = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[29], 0)) : 0u;
const uint32_t gp = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[28], 0)) : 0u;
const uint32_t a0 = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[4], 0)) : 0u;
const uint32_t a1 = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[5], 0)) : 0u;
const uint32_t v0 = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[2], 0)) : 0u;
const uint32_t v1 = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[3], 0)) : 0u;
if (ctx && runtime)
{
thread_local uint32_t s_recoverCount = 0u;
thread_local bool s_loggedContext = false;
const uint32_t pc = ctx->pc;
const bool hasPcFunction = runtime->hasFunction(pc);
if (!hasPcFunction && s_recoverCount < 8192u)
{
if (!s_loggedContext)
{
std::ostringstream stackDump;
if (rdram)
{
stackDump << " [stack]";
for (uint32_t off = 0u; off < 0x40u; off += 4u)
{
const uint32_t slot = readGuestU32Wrapped(rdram, sp + off);
stackDump << " +" << std::hex << off << "=0x" << slot;
}
}
std::cerr << "[dispatch:first-bad-pc] bad=0x" << std::hex << pc
<< " ra=0x" << ra
<< " sp=0x" << sp
<< " gp=0x" << gp
<< " v0=0x" << v0
<< " v1=0x" << v1
<< " a0=0x" << a0
<< " a1=0x" << a1
<< " trace=" << formatDispatchHistory()
<< stackDump.str()
<< std::dec << std::endl;
s_loggedContext = true;
}
uint32_t recoveryPc = 0u;
if (ra != 0u && runtime->hasFunction(ra))
{
recoveryPc = ra;
}
if (recoveryPc == 0u)
{
recoveryPc = selectStackRecoveryPc(rdram, ctx, runtime);
}
if (recoveryPc == 0u)
{
recoveryPc = selectDispatchRecoveryPc(runtime);
}
if (recoveryPc != 0u && recoveryPc != pc)
{
if (s_recoverCount < 256u)
{
std::cerr << "[dispatch:recover-pc] bad=0x" << std::hex << pc
<< " ra=0x" << ra
<< " fallback=0x" << recoveryPc
<< " sp=0x" << sp
<< std::dec << std::endl;
}
++s_recoverCount;
ctx->pc = recoveryPc;
return;
}
}
if (hasPcFunction)
{
s_recoverCount = 0u;
s_loggedContext = false;
}
else if (pc < 0x00100000u && ra == pc && s_recoverCount < 4096u)
{
uint32_t recoveryPc = selectStackRecoveryPc(rdram, ctx, runtime);
if (recoveryPc == 0u)
{
recoveryPc = selectDispatchRecoveryPc(runtime);
}
if (recoveryPc != 0u && recoveryPc != pc)
{
if (s_recoverCount < 128u)
{
std::cerr << "[dispatch:recover-low-pc] bad=0x" << std::hex << pc
<< " ra=0x" << ra
<< " fallback=0x" << recoveryPc
<< " sp=0x" << sp
<< std::dec << std::endl;
}
++s_recoverCount;
ctx->pc = recoveryPc;
return;
}
}
}
std::ostringstream oss;
oss << "Error: Called unimplemented function at address 0x" << std::hex << (ctx ? ctx->pc : 0u)
<< " ra=0x" << ra
<< " sp=0x" << sp
<< " gp=0x" << gp
<< " a0=0x" << a0
<< " hostTid=" << std::this_thread::get_id()
<< " pcTrace=" << formatDispatchHistory()
<< std::dec;
static std::mutex s_defaultFnLogMutex;
{
std::lock_guard<std::mutex> lock(s_defaultFnLogMutex);
std::cerr << oss.str() << std::endl;
}
runtime->requestStop();
};
return defaultFunction;
}
void PS2Runtime::SignalException(R5900Context *ctx, PS2Exception exception)
{
if (exception == EXCEPTION_INTEGER_OVERFLOW)
{
HandleIntegerOverflow(ctx);
return;
}
raiseCop0Exception(ctx, static_cast<uint32_t>(exception),
exception == EXCEPTION_TLB_REFILL);
}
void PS2Runtime::executeVU0Microprogram(uint8_t *rdram, R5900Context *ctx, uint32_t address)
{
static std::unordered_map<uint32_t, int> seen;
int &count = seen[address];
if (count < 3)
{
std::cout << "[VU0] microprogram @0x" << std::hex << address
<< " pc=0x" << ctx->pc
<< " ra=0x" << static_cast<uint32_t>(_mm_extract_epi32(ctx->r[31], 0))
<< std::dec << std::endl;
}
++count;
// Seed status so dependent code sees success.
ctx->vu0_clip_flags = 0;
ctx->vu0_clip_flags2 = 0;
ctx->vu0_mac_flags = 0;
ctx->vu0_status = 0;
ctx->vu0_q = 1.0f;
}
void PS2Runtime::vu0StartMicroProgram(uint8_t *rdram, R5900Context *ctx, uint32_t address)
{
// VCALLMS and VCALLMSR both route here.
executeVU0Microprogram(rdram, ctx, address);
}
void PS2Runtime::handleSyscall(uint8_t *rdram, R5900Context *ctx)
{
handleSyscall(rdram, ctx, 0);
}
void PS2Runtime::handleSyscall(uint8_t *rdram, R5900Context *ctx, uint32_t encodedSyscallId)
{
if (ctx->in_delay_slot)
{
throw std::runtime_error("Attempted to execute a syscall inside a branch delay slot! "
"This breaks the atomic basic block model and is structurally unsupported by the emulator.");
}
// Try immediate first
if (encodedSyscallId != 0 && ps2_syscalls::dispatchNumericSyscall(encodedSyscallId, rdram, ctx, this))
{
return;
}
// Try $v1 (standard)
const uint32_t syscallFromV1 = getRegU32(ctx, 3); // $v1
if (ps2_syscalls::dispatchNumericSyscall(syscallFromV1, rdram, ctx, this))
{
return;
}
// Try $v0 (negative syscalls)
const uint32_t syscallFromV0 = getRegU32(ctx, 2); // $v0 (some ABIs)
if (syscallFromV0 != syscallFromV1 &&
ps2_syscalls::dispatchNumericSyscall(syscallFromV0, rdram, ctx, this))
{
return;
}
// God help you
ps2_syscalls::TODO(rdram, ctx, this, encodedSyscallId);
}
void PS2Runtime::handleBreak(uint8_t *rdram, R5900Context *ctx)
{
raiseCop0Exception(ctx, EXCEPTION_BREAKPOINT);
}
void PS2Runtime::handleTrap(uint8_t *rdram, R5900Context *ctx)
{
raiseCop0Exception(ctx, EXCEPTION_TRAP);
}
void PS2Runtime::handleTLBR(uint8_t *rdram, R5900Context *ctx)
{
uint32_t vpn = 0;
uint32_t pfn = 0;
uint32_t mask = 0;
bool valid = false;
const uint32_t index = ctx->cop0_index & 0x3Fu;
if (!m_memory.tlbRead(index, vpn, pfn, mask, valid))
{
raiseCop0Exception(ctx, EXCEPTION_RESERVED_INSTRUCTION);
return;
}
// Preserve low ASID bits in EntryHi.
ctx->cop0_entryhi = (ctx->cop0_entryhi & 0x00000FFFu) | (vpn & 0xFFFFF000u);
ctx->cop0_entrylo0 = (ctx->cop0_entrylo0 & ~0x03FFFFC2u) |
((pfn & 0x000FFFFFu) << 6) |
(valid ? 0x2u : 0u);
ctx->cop0_pagemask = mask & 0x01FFE000u;
}
void PS2Runtime::handleTLBWI(uint8_t *rdram, R5900Context *ctx)
{
const uint32_t index = ctx->cop0_index & 0x3Fu;
const uint32_t vpn = ctx->cop0_entryhi & 0xFFFFF000u;
const uint32_t pfn = (ctx->cop0_entrylo0 >> 6) & 0x000FFFFFu;
const uint32_t mask = ctx->cop0_pagemask & 0x01FFE000u;
const bool valid = (ctx->cop0_entrylo0 & 0x2u) != 0u;
if (!m_memory.tlbWrite(index, vpn, pfn, mask, valid))
{
raiseCop0Exception(ctx, EXCEPTION_RESERVED_INSTRUCTION);
}
}
void PS2Runtime::handleTLBWR(uint8_t *rdram, R5900Context *ctx)
{
const uint32_t entryCount = static_cast<uint32_t>(m_memory.tlbEntryCount());
if (entryCount == 0)
{
raiseCop0Exception(ctx, EXCEPTION_RESERVED_INSTRUCTION);
return;
}
const uint32_t wired = std::min(ctx->cop0_wired, entryCount - 1);
uint32_t random = ctx->cop0_random % entryCount;
if (random < wired)
{
random = wired;
}
const uint32_t vpn = ctx->cop0_entryhi & 0xFFFFF000u;
const uint32_t pfn = (ctx->cop0_entrylo0 >> 6) & 0x000FFFFFu;
const uint32_t mask = ctx->cop0_pagemask & 0x01FFE000u;
const bool valid = (ctx->cop0_entrylo0 & 0x2u) != 0u;
if (!m_memory.tlbWrite(random, vpn, pfn, mask, valid))
{
raiseCop0Exception(ctx, EXCEPTION_RESERVED_INSTRUCTION);
return;
}
// Keep COP0 bookkeeping in sync with the selected slot.
ctx->cop0_index = (ctx->cop0_index & ~0x3Fu) | (random & 0x3Fu);
ctx->cop0_random = (random <= wired) ? (entryCount - 1) : (random - 1);
}
void PS2Runtime::handleTLBP(uint8_t *rdram, R5900Context *ctx)
{
const int32_t index = m_memory.tlbProbe(ctx->cop0_entryhi & 0xFFFFF000u);
if (index >= 0)
{
ctx->cop0_index = (ctx->cop0_index & ~0x8000003Fu) |
(static_cast<uint32_t>(index) & 0x3Fu);
}
else
{
// MIPS sets probe failure bit (P) in Index[31].
ctx->cop0_index |= 0x80000000u;
}
}
void PS2Runtime::clearLLBit(R5900Context *ctx)
{
// LL/SC reservation is tracked separately from COP0 Status.
ctx->llbit = 0;
ctx->lladdr = 0;
}
uint32_t PS2Runtime::alignGuestHeapValue(uint32_t value, uint32_t alignment)
{
if (alignment == 0)
{
return value;
}
const uint32_t mask = alignment - 1u;
if (value > (std::numeric_limits<uint32_t>::max() - mask))
{
return std::numeric_limits<uint32_t>::max();
}
return (value + mask) & ~mask;
}
bool PS2Runtime::isGuestHeapAlignmentValid(uint32_t alignment)
{
return alignment != 0u && (alignment & (alignment - 1u)) == 0u;
}
uint32_t PS2Runtime::normalizeGuestHeapAlignment(uint32_t alignment)
{
if (!isGuestHeapAlignmentValid(alignment))
{
return kGuestHeapDefaultAlignment;
}
return std::max(alignment, kGuestHeapDefaultAlignment);
}
uint32_t PS2Runtime::clampGuestHeapBase(uint32_t guestBase) const
{
uint32_t normalized = guestBase;
if (normalized >= PS2_RAM_SIZE)
{
normalized &= PS2_RAM_MASK;
}
const uint32_t hardLimit = std::min(kGuestHeapHardLimit, PS2_RAM_SIZE);
return std::min(normalized, hardLimit);
}
uint32_t PS2Runtime::clampGuestHeapLimit(uint32_t guestLimit) const
{
const uint32_t hardLimit = std::min(kGuestHeapHardLimit, PS2_RAM_SIZE);
if (guestLimit == 0u || guestLimit > hardLimit)
{
return hardLimit;
}
return guestLimit;
}
void PS2Runtime::resetGuestHeapLocked(uint32_t guestBase, uint32_t guestLimit)
{
uint32_t base = alignGuestHeapValue(clampGuestHeapBase(guestBase), kGuestHeapDefaultAlignment);
uint32_t limit = clampGuestHeapLimit(guestLimit);
if (base == 0u)
{
const uint32_t fallbackBase = (m_guestHeapSuggestedBase != 0u) ? m_guestHeapSuggestedBase : kGuestHeapDefaultBase;
base = alignGuestHeapValue(clampGuestHeapBase(fallbackBase), kGuestHeapDefaultAlignment);
}
if (limit <= base)
{
base = alignGuestHeapValue(clampGuestHeapBase(m_guestHeapSuggestedBase), kGuestHeapDefaultAlignment);
limit = clampGuestHeapLimit(0u);
}
if (limit <= base)
{
base = 0u;
limit = 0u;
}
m_guestHeapBlocks.clear();
if (limit > base)
{
m_guestHeapBlocks.push_back({base, limit - base, true});
}
m_guestHeapBase = base;
m_guestHeapEnd = base;
m_guestHeapLimit = limit;
m_guestHeapConfigured = true;
}
void PS2Runtime::ensureGuestHeapInitializedLocked()
{
if (m_guestHeapConfigured)
{
return;
}
const uint32_t suggested = (m_guestHeapSuggestedBase == 0u) ? kGuestHeapDefaultBase : m_guestHeapSuggestedBase;
resetGuestHeapLocked(suggested, clampGuestHeapLimit(0u));
}
int32_t PS2Runtime::findGuestHeapBlockIndexLocked(uint32_t guestAddr) const
{
const uint32_t normalizedAddr = guestAddr & PS2_RAM_MASK;
for (size_t i = 0; i < m_guestHeapBlocks.size(); ++i)
{
const GuestHeapBlock &block = m_guestHeapBlocks[i];
if (!block.free && block.addr == normalizedAddr)
{
return static_cast<int32_t>(i);
}
}
return -1;
}
uint32_t PS2Runtime::allocateGuestBlockLocked(uint32_t size, uint32_t alignment)
{
if (size == 0u)
{
return 0u;
}
const uint32_t normalizedAlignment = normalizeGuestHeapAlignment(alignment);
if (size > (std::numeric_limits<uint32_t>::max() - (kGuestHeapDefaultAlignment - 1u)))
{
return 0u;
}
const uint32_t allocSize = alignGuestHeapValue(size, kGuestHeapDefaultAlignment);
if (allocSize == 0u)
{
return 0u;
}
for (size_t i = 0; i < m_guestHeapBlocks.size(); ++i)
{
const GuestHeapBlock block = m_guestHeapBlocks[i];
if (!block.free)
{
continue;
}
const uint64_t blockStart = block.addr;
const uint64_t blockEnd = blockStart + static_cast<uint64_t>(block.size);
const uint32_t alignedAddr = alignGuestHeapValue(block.addr, normalizedAlignment);
if (alignedAddr < block.addr)
{
continue;
}
const uint64_t alignedStart = alignedAddr;
if (alignedStart > blockEnd)
{
continue;
}
const uint64_t allocEnd = alignedStart + static_cast<uint64_t>(allocSize);
if (allocEnd > blockEnd)
{
continue;
}
const uint32_t prefixSize = static_cast<uint32_t>(alignedStart - blockStart);
const uint32_t suffixSize = static_cast<uint32_t>(blockEnd - allocEnd);
std::vector<GuestHeapBlock> replacement;
replacement.reserve(3);
if (prefixSize > 0u)
{
replacement.push_back({block.addr, prefixSize, true});
}
replacement.push_back({alignedAddr, allocSize, false});
if (suffixSize > 0u)
{
replacement.push_back({static_cast<uint32_t>(allocEnd), suffixSize, true});
}
m_guestHeapBlocks.erase(m_guestHeapBlocks.begin() + static_cast<std::ptrdiff_t>(i));
m_guestHeapBlocks.insert(m_guestHeapBlocks.begin() + static_cast<std::ptrdiff_t>(i),
replacement.begin(),
replacement.end());
m_guestHeapEnd = std::max(m_guestHeapEnd, static_cast<uint32_t>(allocEnd));
return alignedAddr;
}
return 0u;
}
void PS2Runtime::coalesceGuestHeapLocked()
{
if (m_guestHeapBlocks.empty())
{
return;
}
size_t i = 1;
while (i < m_guestHeapBlocks.size())
{
GuestHeapBlock &prev = m_guestHeapBlocks[i - 1];
GuestHeapBlock &curr = m_guestHeapBlocks[i];
const uint64_t prevEnd = static_cast<uint64_t>(prev.addr) + static_cast<uint64_t>(prev.size);
if (prev.free && curr.free && prevEnd == curr.addr)
{
prev.size += curr.size;
m_guestHeapBlocks.erase(m_guestHeapBlocks.begin() + static_cast<std::ptrdiff_t>(i));
continue;
}
++i;
}
}
void PS2Runtime::freeGuestBlockLocked(uint32_t guestAddr)
{
const int32_t index = findGuestHeapBlockIndexLocked(guestAddr);
if (index < 0)
{
return;
}
m_guestHeapBlocks[static_cast<size_t>(index)].free = true;
coalesceGuestHeapLocked();
}
void PS2Runtime::configureGuestHeap(uint32_t guestBase, uint32_t guestLimit)
{
std::lock_guard<std::mutex> lock(m_guestHeapMutex);
uint32_t normalizedBase = alignGuestHeapValue(clampGuestHeapBase(guestBase), kGuestHeapDefaultAlignment);
if (normalizedBase == 0u)
{
normalizedBase = (m_guestHeapSuggestedBase != 0u) ? m_guestHeapSuggestedBase : kGuestHeapDefaultBase;
}
m_guestHeapSuggestedBase = normalizedBase;
resetGuestHeapLocked(normalizedBase, guestLimit);
}
uint32_t PS2Runtime::guestMalloc(uint32_t size, uint32_t alignment)
{
std::lock_guard<std::mutex> lock(m_guestHeapMutex);
ensureGuestHeapInitializedLocked();
return allocateGuestBlockLocked(size, alignment);
}
uint32_t PS2Runtime::guestCalloc(uint32_t count, uint32_t size, uint32_t alignment)
{
if (count == 0u || size == 0u)
{
return 0u;
}
if (count > (std::numeric_limits<uint32_t>::max() / size))
{
return 0u;
}
const uint32_t totalSize = count * size;
const uint32_t guestAddr = guestMalloc(totalSize, alignment);
if (guestAddr != 0u)
{
uint8_t *rdram = m_memory.getRDRAM();
if (rdram)
{
uint32_t physAddr = guestAddr & PS2_RAM_MASK;
if (physAddr + totalSize <= PS2_RAM_SIZE)
std::memset(rdram + physAddr, 0, totalSize);
}
}
return guestAddr;
}
uint32_t PS2Runtime::guestRealloc(uint32_t guestAddr, uint32_t newSize, uint32_t alignment)
{
if (guestAddr == 0u)
{
return guestMalloc(newSize, alignment);
}
if (newSize == 0u)
{
guestFree(guestAddr);
return 0u;
}
if (newSize > (std::numeric_limits<uint32_t>::max() - (kGuestHeapDefaultAlignment - 1u)))
{
return 0u;
}
const uint32_t normalizedAlignment = normalizeGuestHeapAlignment(alignment);
const uint32_t requestedSize = alignGuestHeapValue(newSize, kGuestHeapDefaultAlignment);
std::lock_guard<std::mutex> lock(m_guestHeapMutex);
ensureGuestHeapInitializedLocked();
const int32_t index = findGuestHeapBlockIndexLocked(guestAddr);
if (index < 0)
{
return 0u;
}
const size_t blockIndex = static_cast<size_t>(index);
const uint32_t oldAddr = m_guestHeapBlocks[blockIndex].addr;
const uint32_t oldSize = m_guestHeapBlocks[blockIndex].size;
if (requestedSize <= oldSize)
{
if (requestedSize < oldSize)
{
const uint32_t tailAddr = oldAddr + requestedSize;
const uint32_t tailSize = oldSize - requestedSize;
m_guestHeapBlocks[blockIndex].size = requestedSize;
m_guestHeapBlocks.insert(m_guestHeapBlocks.begin() + static_cast<std::ptrdiff_t>(blockIndex + 1u),
GuestHeapBlock{tailAddr, tailSize, true});
coalesceGuestHeapLocked();
}
return oldAddr;
}
if (blockIndex + 1u < m_guestHeapBlocks.size())
{
GuestHeapBlock &next = m_guestHeapBlocks[blockIndex + 1u];
const uint64_t blockEnd = static_cast<uint64_t>(m_guestHeapBlocks[blockIndex].addr) +
static_cast<uint64_t>(m_guestHeapBlocks[blockIndex].size);
if (next.free && blockEnd == next.addr)
{
const uint64_t combined = static_cast<uint64_t>(m_guestHeapBlocks[blockIndex].size) +
static_cast<uint64_t>(next.size);
if (combined >= requestedSize)
{
const uint32_t extraNeeded = requestedSize - m_guestHeapBlocks[blockIndex].size;
m_guestHeapBlocks[blockIndex].size = requestedSize;
if (next.size == extraNeeded)
{
m_guestHeapBlocks.erase(m_guestHeapBlocks.begin() + static_cast<std::ptrdiff_t>(blockIndex + 1u));
}
else
{
next.addr += extraNeeded;
next.size -= extraNeeded;
}
m_guestHeapEnd = std::max(m_guestHeapEnd, oldAddr + requestedSize);
return oldAddr;
}
}
}
const uint32_t newAddr = allocateGuestBlockLocked(newSize, normalizedAlignment);
if (newAddr == 0u)
{
return 0u;
}
uint8_t *rdram = m_memory.getRDRAM();
if (rdram)
{
const uint32_t copyBytes = std::min(oldSize, newSize);
uint32_t dstPhys = newAddr & PS2_RAM_MASK;
uint32_t srcPhys = oldAddr & PS2_RAM_MASK;
if (dstPhys + copyBytes <= PS2_RAM_SIZE && srcPhys + copyBytes <= PS2_RAM_SIZE)
std::memmove(rdram + dstPhys, rdram + srcPhys, copyBytes);
}
freeGuestBlockLocked(oldAddr);
return newAddr;
}
void PS2Runtime::guestFree(uint32_t guestAddr)
{
if (guestAddr == 0u)
{
return;
}
std::lock_guard<std::mutex> lock(m_guestHeapMutex);
ensureGuestHeapInitializedLocked();
freeGuestBlockLocked(guestAddr);
}
uint32_t PS2Runtime::guestHeapBase() const
{
std::lock_guard<std::mutex> lock(m_guestHeapMutex);
return m_guestHeapConfigured ? m_guestHeapBase : m_guestHeapSuggestedBase;
}
uint32_t PS2Runtime::guestHeapEnd() const
{
std::lock_guard<std::mutex> lock(m_guestHeapMutex);
return m_guestHeapConfigured ? m_guestHeapEnd : m_guestHeapSuggestedBase;
}
void PS2Runtime::dispatchLoop(uint8_t *rdram, R5900Context *ctx)
{
uint32_t lastPc = std::numeric_limits<uint32_t>::max();
uint32_t samePcCount = 0;
constexpr uint32_t kSamePcYieldInterval = 0x4000u;
while (!isStopRequested())
{
const uint32_t pc = ctx->pc;
if (pc == lastPc)
{
++samePcCount;
if ((samePcCount % kSamePcYieldInterval) == 0u)
{
std::cout << "CPU is doing some work at PC 0x" << std::hex << pc << ". PC not updating." << std::endl;
std::this_thread::yield();
}
}
else
{
samePcCount = 0;
lastPc = pc;
}
m_debugPc.store(pc, std::memory_order_relaxed);
m_debugRa.store(static_cast<uint32_t>(_mm_extract_epi32(ctx->r[31], 0)), std::memory_order_relaxed);
m_debugSp.store(static_cast<uint32_t>(_mm_extract_epi32(ctx->r[29], 0)), std::memory_order_relaxed);
m_debugGp.store(static_cast<uint32_t>(_mm_extract_epi32(ctx->r[28], 0)), std::memory_order_relaxed);
RecompiledFunction fn = lookupFunction(pc);
const uint32_t dispatchedPc = pc;
const uint32_t dispatchedRa = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[31], 0));
fn(rdram, ctx, this);
if (ctx->pc == 0u)
{
const uint32_t ra = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[31], 0));
const uint32_t sp = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[29], 0));
const uint32_t gp = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[28], 0));
std::cerr << "[dispatch:pc-zero] from=0x" << std::hex << dispatchedPc
<< " fromRa=0x" << dispatchedRa
<< " ra=0x" << ra
<< " sp=0x" << sp
<< " gp=0x" << gp
<< " trace=" << formatDispatchHistory()
<< std::dec << std::endl;
// PC=0 means this guest thread returned (usually via jr $ra with RA=0).
// Do not request a global runtime stop here: other guest threads may still run.
break;
}
}
}
uint8_t PS2Runtime::Load8(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr)
{
try
{
return m_memory.read8(vaddr);
}
catch (const std::exception &)
{
SignalException(ctx, EXCEPTION_ADDRESS_ERROR_LOAD);
return 0;
}
}
uint16_t PS2Runtime::Load16(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr)
{
try
{
return m_memory.read16(vaddr);
}
catch (const std::exception &)
{
SignalException(ctx, EXCEPTION_ADDRESS_ERROR_LOAD);
return 0;
}
}
uint32_t PS2Runtime::Load32(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr)
{
try
{
return m_memory.read32(vaddr);
}
catch (const std::exception &)
{
SignalException(ctx, EXCEPTION_ADDRESS_ERROR_LOAD);
return 0;
}
}
uint64_t PS2Runtime::Load64(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr)
{
try
{
return m_memory.read64(vaddr);
}
catch (const std::exception &)
{
SignalException(ctx, EXCEPTION_ADDRESS_ERROR_LOAD);
return 0;
}
}
__m128i PS2Runtime::Load128(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr)
{
try
{
return m_memory.read128(vaddr);
}
catch (const std::exception &)
{
SignalException(ctx, EXCEPTION_ADDRESS_ERROR_LOAD);
return _mm_setzero_si128();
}
}
void PS2Runtime::Store8(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr, uint8_t value)
{
ps2TraceGuestWrite(rdram, vaddr, 1u, value, 0u, "WRITE8", ctx);
try
{
m_memory.write8(vaddr, value);
}
catch (const std::exception &)
{
SignalException(ctx, EXCEPTION_ADDRESS_ERROR_STORE);
}
}
void PS2Runtime::Store16(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr, uint16_t value)
{
ps2TraceGuestWrite(rdram, vaddr, 2u, value, 0u, "WRITE16", ctx);
try
{
m_memory.write16(vaddr, value);
}
catch (const std::exception &)
{
SignalException(ctx, EXCEPTION_ADDRESS_ERROR_STORE);
}
}
void PS2Runtime::Store32(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr, uint32_t value)
{
ps2TraceGuestWrite(rdram, vaddr, 4u, value, 0u, "WRITE32", ctx);
try
{
m_memory.write32(vaddr, value);
}
catch (const std::exception &)
{
SignalException(ctx, EXCEPTION_ADDRESS_ERROR_STORE);
}
}
void PS2Runtime::Store64(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr, uint64_t value)
{
ps2TraceGuestWrite(rdram, vaddr, 8u, value, 0u, "WRITE64", ctx);
try
{
m_memory.write64(vaddr, value);
}
catch (const std::exception &)
{
SignalException(ctx, EXCEPTION_ADDRESS_ERROR_STORE);
}
}
void PS2Runtime::Store128(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr, __m128i value)
{
alignas(16) uint64_t _parts[2];
_mm_storeu_si128(reinterpret_cast<__m128i *>(_parts), value);
ps2TraceGuestWrite(rdram, vaddr, 16u, _parts[0], _parts[1], "WRITE128", ctx);
try
{
m_memory.write128(vaddr, value);
}
catch (const std::exception &)
{
SignalException(ctx, EXCEPTION_ADDRESS_ERROR_STORE);
}
}
void PS2Runtime::requestStop()
{
m_stopRequested.store(true, std::memory_order_relaxed);
ps2_syscalls::notifyRuntimeStop();
}
bool PS2Runtime::isStopRequested() const
{
return m_stopRequested.load(std::memory_order_relaxed);
}
void PS2Runtime::HandleIntegerOverflow(R5900Context *ctx)
{
raiseCop0Exception(ctx, EXCEPTION_INTEGER_OVERFLOW);
}
void PS2Runtime::run()
{
m_stopRequested.store(false, std::memory_order_relaxed);
ps2_stubs::resetGsSyncVCallbackState();
m_cpuContext.r[4] = _mm_setzero_si128();
m_cpuContext.r[5] = _mm_setzero_si128();
m_cpuContext.r[29] = _mm_set_epi64x(0, static_cast<int64_t>(PS2_RAM_SIZE - 0x10u));
m_debugPc.store(m_cpuContext.pc, std::memory_order_relaxed);
m_debugRa.store(static_cast<uint32_t>(_mm_extract_epi32(m_cpuContext.r[31], 0)), std::memory_order_relaxed);
m_debugSp.store(static_cast<uint32_t>(_mm_extract_epi32(m_cpuContext.r[29], 0)), std::memory_order_relaxed);
m_debugGp.store(static_cast<uint32_t>(_mm_extract_epi32(m_cpuContext.r[28], 0)), std::memory_order_relaxed);
std::cout << "Starting execution at address 0x" << std::hex << m_cpuContext.pc << std::dec << std::endl;
// A blank image to use as a framebuffer
Image blank = GenImageColor(FB_WIDTH, FB_HEIGHT, BLANK);
Texture2D frameTex = LoadTextureFromImage(blank);
UnloadImage(blank);
g_activeThreads.store(1, std::memory_order_relaxed);
std::atomic<bool> gameThreadFinished{false};
std::thread gameThread([&]()
{
ThreadNaming::SetCurrentThreadName("GameThread");
try
{
dispatchLoop(m_memory.getRDRAM(), &m_cpuContext);
uint32_t pc = m_debugPc.load(std::memory_order_relaxed);
std::cout << "Game thread returned. PC=0x" << std::hex << pc
<< " RA=0x" << static_cast<uint32_t>(_mm_extract_epi32(m_cpuContext.r[31], 0)) << std::dec << std::endl;
}
catch (const std::exception &e)
{
std::cerr << "Error during program execution: " << e.what() << std::endl;
}
catch (...)
{
std::cerr << "Error during program execution: unknown exception" << std::endl;
}
g_activeThreads.fetch_sub(1, std::memory_order_relaxed);
gameThreadFinished.store(true, std::memory_order_release); });
uint64_t tick = 0;
while (!isStopRequested() && g_activeThreads.load(std::memory_order_relaxed) > 0)
{
tick++;
ps2_stubs::dispatchGsSyncVCallback(m_memory.getRDRAM(), this);
if ((tick % 120) == 0)
{
uint64_t curDma = m_memory.dmaStartCount();
uint64_t curGif = m_memory.gifCopyCount();
uint64_t curGs = m_memory.gsWriteCount();
uint64_t curVif = m_memory.vifWriteCount();
const GSRegisters &gs = m_memory.gs();
const uint32_t dbgPc = m_debugPc.load(std::memory_order_relaxed);
const uint32_t dbgRa = m_debugRa.load(std::memory_order_relaxed);
const uint32_t dbgSp = m_debugSp.load(std::memory_order_relaxed);
const uint32_t dbgGp = m_debugGp.load(std::memory_order_relaxed);
const int activeThreads = g_activeThreads.load(std::memory_order_relaxed);
constexpr uint32_t kSndTransTypeAddr = 0x01E0E1C0u;
constexpr uint32_t kSndTransBankAddr = 0x01E0E1C8u;
constexpr uint32_t kSndTransLevelAddr = 0x01E0E1B8u;
constexpr uint32_t kSndGetAdrsAddr = 0x01E212D8u;
constexpr uint32_t kSndStatusMirrorAddr = 0x01E213C0u;
constexpr uint32_t kSndSeCheckAddr = 0x01E0EF10u;
constexpr uint32_t kSndMidiCheckAddr = 0x01E0EF20u;
const uint32_t sndTransType = readGuestU32Wrapped(m_memory.getRDRAM(), kSndTransTypeAddr);
const uint32_t sndTransLevel = readGuestU32Wrapped(m_memory.getRDRAM(), kSndTransLevelAddr);
const uint32_t sndTransBank = readGuestU32Wrapped(m_memory.getRDRAM(), kSndTransBankAddr);
const uint32_t sndGetAdrs = readGuestU32Wrapped(m_memory.getRDRAM(), kSndGetAdrsAddr);
auto readGuestS16 = [&](uint32_t addr) -> int32_t
{
const uint8_t *rdram = m_memory.getRDRAM();
if (!rdram)
{
return 0;
}
const uint16_t raw = static_cast<uint16_t>(
static_cast<uint16_t>(rdram[(addr + 0u) & PS2_RAM_MASK]) |
(static_cast<uint16_t>(rdram[(addr + 1u) & PS2_RAM_MASK]) << 8));
return static_cast<int16_t>(raw);
};
const int32_t sndMirrorMidi0 = readGuestS16(kSndStatusMirrorAddr + 0x1Eu);
const int32_t sndMirrorSe0 = readGuestS16(kSndStatusMirrorAddr + 0x26u);
int32_t sndBankMidiCheck = 0;
int32_t sndBankSeCheck = 0;
if (sndTransBank < 4u)
{
sndBankMidiCheck = readGuestS16(kSndMidiCheckAddr + (sndTransBank * 2u));
}
if (sndTransBank < 5u)
{
sndBankSeCheck = readGuestS16(kSndSeCheckAddr + (sndTransBank * 2u));
}
std::cout << "[run:tick] tick=" << tick
<< " pc=0x" << std::hex << dbgPc
<< " ra=0x" << dbgRa
<< " sp=0x" << dbgSp
<< " gp=0x" << dbgGp
<< " dispfb1=0x" << gs.dispfb1
<< " display1=0x" << gs.display1
<< std::dec
<< " activeThreads=" << activeThreads
<< " dma=" << curDma
<< " gif=" << curGif
<< " gsw=" << curGs
<< " vif=" << curVif
<< " sndType=" << sndTransType
<< " sndLvl=" << sndTransLevel
<< " sndBank=" << sndTransBank
<< " getAdrs=0x" << std::hex << sndGetAdrs << std::dec
<< " sndMirrorMidi0=" << sndMirrorMidi0
<< " sndMirrorSe0=" << sndMirrorSe0
<< " sndChkMidi=" << sndBankMidiCheck
<< " sndChkSe=" << sndBankSeCheck
<< std::endl;
}
UploadFrame(frameTex, this);
BeginDrawing();
ClearBackground(BLACK);
DrawTexture(frameTex, 0, 0, WHITE);
EndDrawing();
if (WindowShouldClose())
{
std::cout << "[run] window close requested, breaking out of loop" << std::endl;
requestStop();
break;
}
}
requestStop();
const auto joinDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
while (!gameThreadFinished.load(std::memory_order_acquire) &&
std::chrono::steady_clock::now() < joinDeadline)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
if (gameThread.joinable())
{
if (gameThreadFinished.load(std::memory_order_acquire))
{
gameThread.join();
}
else
{
std::cerr << "[run] game thread did not stop within timeout; detaching" << std::endl;
gameThread.detach();
}
}
const auto workerDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(1000);
while (g_activeThreads.load(std::memory_order_relaxed) > 0 &&
std::chrono::steady_clock::now() < workerDeadline)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
if (g_activeThreads.load(std::memory_order_relaxed) > 0)
{
requestStop();
const auto finalWorkerDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(1000);
while (g_activeThreads.load(std::memory_order_relaxed) > 0 &&
std::chrono::steady_clock::now() < finalWorkerDeadline)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
UnloadTexture(frameTex);
CloseWindow();
const int remainingThreads = g_activeThreads.load(std::memory_order_relaxed);
std::cout << "[run] exiting loop, activeThreads=" << remainingThreads << std::endl;
if (remainingThreads > 0)
{
std::cerr << "[run] warning: " << remainingThreads
<< " guest worker thread(s) still active during shutdown." << std::endl;
}
}